Android開發-使用MD5 AES RSA BASE64 加密解密 比如登入通訊Token加密
在平時的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有點不同,可參考這篇文章點選開啟連結