在平时的Android开发中使用到加密的地方很多,比如:
1.登陆请求的加密
一般我们客户端登陆会带上服务器生成的Sessionid,如果服务器的Redis中存在这个Sessionid,就判断是合法的客户端;但是如果这个Sessionid被第三方截获,模拟请求,就会产生很大的风险。如果这时候引入Token,客户端对Token加一些其它参数组合,然后使用MD5进行加密生成签名,然后将Sessionid和签名一起发送给服务器;服务器将Sessionid作为Key取出Token,使用约定好的规则通过MD5加密得到一个签名,如果两个签名相同,那就判断是合法得客户端。
这就产生了另一个问题,Token从何而来,不用想,肯定是服务器发送给客户端,既然是通过网络发送,而且Token是维持用户登陆状态的关键数据,那这个Token还是得进行加密,这时候进行如下操作
* 客户端向服务器发送一个空请求,服务器使用RSA算法生成一对私钥和公钥,并将公钥返回给客户端
* 客户端收到公钥后,来加密用户密码;同时自己也使用RSA算法产生一对私钥和公钥,最后将自己产生的公钥、用户名、密码等一些参数发送给服务器
* 服务器收到请求,使用第一步产生的私钥对密码进行解密,如果正确,就认为是合法用户,然后生成Sessionid和Token,并使用收到的客户端公钥对Token进行加密,最后将Sessionid和加密后的Token返回给客户端
* 客户端收到Token后,使用自己的私钥进行解密,得到真正的Token,以后就用以Token+其它参数生成的签名加Sessionid与服务器进行通信
* 服务器收到Sessionid作为Key取出Token,使用约定好的规则通过MD5加密得到一个签名,如果两个签名相同,那就判断是合法的客户端,然后就可以快乐的交流了
这样一个完整登陆加密请求就走完了
2.通信明文加密
* 客户端通过AES算法利用Token产生一个密钥,然后对消息明文进行加密发送到服务器
* 服务器也用这个Token产生一个相同的密钥,然后用密钥去解密这段消息,反之同理
至于为什么使用RSA算法加密小段Token数据,而用AES算法加密明文;因为RSA算法计算量大,速度较慢,但是安全系数高,所以用来加密小段数据;但是AES算法计算量小,速度快,所以用来加密大段的消息明文。
还有很多其它地方用到加密,比如使用MD5,SHA-1来加密文件、密码;所以对这些加密算法的总结是很有必要的
1.BASE64
其实它不算是加密算法,本质上是一种将二进制数据转成文本数据的方案,计算流程是
对于非二进制数据,是先将其转换成二进制形式,然后每连续6比特(2的6次方=64)计算其十进制值,根据该值在A--Z,a--z,0--9,+,/ 这64个字符中找到对应的字符, 最终得到一个文本字符串,即转成了BASE码
base64编码表
码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | A | 8 | I | 16 | Q | 24 | Y | 32 | g | 40 | o | 48 | w | 56 | 4 |
1 | B | 9 | J | 17 | R | 25 | Z | 33 | h | 41 | p | 49 | x | 57 | 5 |
2 | C | 10 | K | 18 | S | 26 | a | 34 | i | 42 | q | 50 | y | 58 | 6 |
3 | D | 11 | L | 19 | T | 27 | b | 35 | j | 43 | r | 51 | z | 59 | 7 |
4 | E | 12 | M | 20 | U | 28 | c | 36 | k | 44 | s | 52 | 0 | 60 | 8 |
5 | F | 13 | N | 21 | V | 29 | d | 37 | l | 45 | t | 53 | 1 | 61 | 9 |
6 | G | 14 | O | 22 | W | 30 | e | 38 | m | 46 | u | 54 | 2 | 62 | + |
7 | H | 15 | P | 23 | X | 31 | f | 39 | n | 47 | v | 55 | 3 | 63 | / |
码值表示可打印字符的索引。
eq:我们来计算下Boy这个单词的Base64值
根据上面说的计算方法,对于非二进制数据,先转成二进制,问题来了怎么将Boy这个单词转成二进制呢?
在学校计算机课学习的时候应该知道计算机识别字母是找到对应的ASCII码,再转成二进制数据,没错,根据Ascii码进行转,至于ASCII是什么,这里就不进行详述了
我们将Boy的二进制数据排列在一起
再将这些二进制数据每连续6bit(Base64最小单位是6个bit)转成10进制
再把这4个数去base64编码表找到对应的可打印字符,可以得到Qm95,即将Boy单词经过Base64转码后得到的是Qm95。
看到上面的转换可以知道,三个字节正好能转成Base64里的4个字节,也就是将我们的输入每三个字节一组进行转换。
如果我们的输入不是三个字节的倍数呢,例如Boys,多了一个字节,即bit数是32个,这时候就需要进行凑数了,我们补充两个空字节来达到6个字节,这样bit数就是48个,这样就能正好转成48/6=8个Base64字节了。
再转成对应的10进制
那后面的0怎么办呢,在Base64编码规则里使用=表示000000这种情况,这里就需要两个=了,转码后就是Qm95cw==
其实我们可以得到一个规律,就是用四个Base64中的字节表示我们输入的三个字节,我们输入要么一个,要么两个,要么是三的倍数,所以最后转码后的=的数量最多2个。
原理知道了那怎么在平时开发中用呢,如下
/**
* 对文件进行Base64解码
* @param desFile,将文件解码到何处
* @param encodedString
* @return
*/
public static void decodeFile(File desFile,String encodedString) {
FileOutputStream fos = null;
try {
byte[] decodeBytes = Base64.decode(encodedString.getBytes(), Base64.DEFAULT);
fos = new FileOutputStream(desFile);
fos.write(decodeBytes);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 对文件进行Base64编码
* @param file
* @return
*/
public static String encodeFfile(File file) {
String encodedString = "";
FileInputStream inputFile = null;
try {
inputFile = new FileInputStream(file);
byte[] buffer = new byte[(int) file.length()];
inputFile.read(buffer);
inputFile.close();
encodedString = Base64.encodeToString(buffer, Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
}
return encodedString;
}
/**
* BASE64解码
* @param key
* @return
* @throws Exception
*/
public static byte[] decodeToBytes(String key) {
return Base64.decode(key, Base64.NO_WRAP);
}
public static String decodeToString(String key) {
String decodedString =new String(Base64.decode(key,Base64.DEFAULT));
return decodedString;
}
/**
* BASE64编码
* @param key
* @return
* @throws Exception
*/
public static String encodeByte(byte[] key) {
byte[] res = Base64.encode(key, Base64.NO_WRAP);
String resStr =new String(res);
return resStr;
}
public static String encodeString(String key) {
String encodedString = Base64.encodeToString(key.getBytes(), Base64.DEFAULT);
return encodedString;
}
像平时在进行Http请求的时候,传输一个byte数组,那我们可以用Base64先进行转码;或者将byte数据转码后保存到数据库里;其实也就是把一些不方便网络传输或者保存的二进制数据转成可打印字符。
2.MD5
中文名为消息摘要算法第五版,这种加密算法是单向加密实现,不可逆的加密,输出一个128位的消息摘要,有如下特点:
1.任意长度的数据得到的MD5值长度是固定的
2.一般数据得到MD5值计算比较简单
3.对源数据进行最小的改变,得到的MD5值变动都会非常大
4.想找到一个与源数据相同的MD5值是非常困难的
代码实现如下
/**
* 对字符串多次MD5加密
* @param string
* @param times
* @return
*/
public static String md5(String string, int times) {
if (TextUtils.isEmpty(string)) {
return "";
}
String md5 = md5(string);
for (int i = 0; i < times - 1; i++) {
md5 = md5(md5);
}
return md5(md5);
}
/**\
* MD5加盐
加盐的方式也是多种多样
string+key(盐值key)然后进行MD5加密
用string明文的hashcode作为盐,然后进行MD5加密
随机生成一串字符串作为盐,然后进行MD5加密
* @param string
* @param slat
* @return
*/
public static String md5(String string, String slat) {
if (TextUtils.isEmpty(string)) {
return "";
}
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");//获取信息摘要对象
byte[] bytes = md5.digest((string + slat).getBytes());//信息摘要对象对字节数组进行摘要,得到摘要字节数组
String result = "";
for (byte b : bytes) {
String temp = Integer.toHexString(b & 0xff);//把摘要数组中的每一个字节转换成16进制,并拼在一起就得到了MD5值
if (temp.length() == 1) {
temp = "0" + temp;
}
result += temp;
}
return result;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
/*
* 计算字符串MD5值
*/
public static String md5(String string) {
if (TextUtils.isEmpty(string)) {
return "";
}
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
byte[] bytes = md5.digest(string.getBytes());
String result = "";
for (byte b : bytes) {
String temp = Integer.toHexString(b & 0xff);
if (temp.length() == 1) {
temp = "0" + temp;
}
result += temp;
}
return result;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return "";
}
/**
* 计算文件的MD5值
* @param file
* @return
*/
public static String md5(File file) {
if (file == null || !file.isFile() || !file.exists()) {
return "";
}
FileInputStream in = null;
String result = "";
byte buffer[] = new byte[8192];
int len;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer)) != -1) {
md5.update(buffer, 0, len);
}
byte[] bytes = md5.digest();
for (byte b : bytes) {
String temp = Integer.toHexString(b & 0xff);
if (temp.length() == 1) {
temp = "0" + temp;
}
result += temp;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(null!=in){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
3.AES
该算法是对称加密算法,最需要注意的是加密解密中使用的KEY生成的密钥,加密解密都需要这个,这就跟一把锁跟钥匙一样,使用钥匙把锁锁住,那就需要这把钥匙再把锁打开。
在我们平时开发中用到的还是比较多的,上面说的网络传输中加密明文消息;还有我们保存一些数据在手机本地的时候也可以用这个加密,像一些缓存数据,需要持久化保存的信息。
使用如下
/*
* 加密
*/
public static String encrypt(String key, String cleartext) {
if (TextUtils.isEmpty(cleartext)) {
return cleartext;
}
try {
byte[] result = encrypt(key, cleartext.getBytes());
return BASE64Util.encodeByte(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*
* 加密
*/
private static byte[] encrypt(String key, byte[] clear) throws Exception {
byte[] raw = getRawKey(key.getBytes());
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
/**
* 对密钥进行处理
* @param seed
* @return
* @throws Exception
*/
private static byte[] getRawKey(byte[] seed) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance(AES);
//for android
SecureRandom sr = null;
// 在4.2以上版本中,SecureRandom获取方式发生了改变
if (android.os.Build.VERSION.SDK_INT >= 17) {
sr = SecureRandom.getInstance(SHA1PRNG, "Crypto");
} else {
sr = SecureRandom.getInstance(SHA1PRNG);
}
// for Java
// secureRandom = SecureRandom.getInstance(SHA1PRNG);
sr.setSeed(seed);
kgen.init(128, sr); //256 bits or 128 bits,192bits
//AES中128位密钥版本有10个加密循环,192比特密钥版本有12个加密循环,256比特密钥版本则有14个加密循环。
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
return raw;
}
第一个方法传入的key很重要,解密的时候也是用这个key生成密钥解密。第三个方法就是用来生成密钥的
解密如下
/*
* 解密
*/
public static String decrypt(String key, String encrypted) {
if (TextUtils.isEmpty(encrypted)) {
return encrypted;
}
try {
byte[] enc = BASE64Util.decodeToBytes(encrypted);
byte[] result = decrypt(key, enc);
return new String(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/*
* 解密
*/
private static byte[] decrypt(String key, byte[] encrypted) throws Exception {
byte[] raw = getRawKey(key.getBytes());
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
不过Android7.0以后不再支持Crypto了,修改如下
/**
* password的长度,必须为128或192或256bits.也就是16或24或32byte
* @Description TODO()
* @author Mangoer
* @return
* @parame
*/
private byte[] encrypt7(String content, String password) throws Exception {
// 创建AES秘钥
SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES/CBC/PKCS5PADDING");
// 创建密码器
Cipher cipher = Cipher.getInstance("AES");
// 初始化加密器
cipher.init(Cipher.ENCRYPT_MODE, key);
// 加密
return cipher.doFinal(content.getBytes("UTF-8"));
}
private byte[] decrypt7(byte[] content, String password) throws Exception {
// 创建AES秘钥
SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES/CBC/PKCS5PADDING");
// 创建密码器
Cipher cipher = Cipher.getInstance("AES");
// 初始化解密器
cipher.init(Cipher.DECRYPT_MODE, key);
// 解密
return cipher.doFinal(content);
}
4.RSA
RSA是目前最有影响力的公钥加密算法,能够抵抗目前位置绝大多数密码攻击,RSA的安全是基于大数分解的难度,其公钥和私钥是一对大素数,从一个公钥和密文恢复出明文的难度,等价于分解两个大素数之积,这是公认的数学难题。这个原理决定了它的安全性很高,不过RSA的加解密的速度比较耗时,消耗性能,像那些高并发的服务器,更加承受不了,所以只能加密那些小段数据。
RSA是不对称加密算法,在加密解密的时候使用不同的密钥操作。一般是公钥给服务器去加密数据,传输到客户端后使用自己的密钥进行解密。
使用如下
/**
* 随机生成RSA密钥对
*
* @param keyLength 密钥长度,范围:512~2048
* 小于1024长度的密钥已经被证实是不安全的,通常设置为1024或者2048,建议2048
* @return
*/
public static KeyPair generateRSAKeyPair(int keyLength) {
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSA);
kpg.initialize(keyLength);
return kpg.genKeyPair();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
然后再用公钥加密
/**
* 用公钥对字符串进行加密
*
* @param data 原文
*/
public static byte[] encryptByPublicKey(byte[] data, byte[] publicKey) throws Exception {
// 得到公钥
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey);
KeyFactory kf = KeyFactory.getInstance(RSA);
PublicKey keyPublic = kf.generatePublic(keySpec);
// 加密数据
Cipher cp = Cipher.getInstance(ECB_PKCS1_PADDING);
cp.init(Cipher.ENCRYPT_MODE, keyPublic);
return cp.doFinal(data);
}
这个入参是公钥的byte数据
kpg.genKeyPair().getPublic().getEncoded()
然后使用是私钥解密
/**
* 使用私钥进行解密
*/
public static byte[] decryptByPrivateKey(byte[] encrypted, byte[] privateKey) throws Exception {
// 得到私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory kf = KeyFactory.getInstance(RSA);
PrivateKey keyPrivate = kf.generatePrivate(keySpec);
// 解密数据
Cipher cp = Cipher.getInstance(ECB_PKCS1_PADDING);
cp.init(Cipher.DECRYPT_MODE, keyPrivate);
byte[] arr = cp.doFinal(encrypted);
return arr;
}
至于在Android端加密,服务端解密不了的问题,是因为Android的api和java的api有点不同,可参考这篇文章点击打开链接