產(chǎn)品經(jīng)理:小凌好爬,這里有個(gè)簡(jiǎn)單的需求,將用戶的敏感信息加密保存起來(lái)甥啄,需要盡快實(shí)現(xiàn)存炮。
程序猿:好,沒(méi)有問(wèn)題蜈漓,半個(gè)小時(shí)就搞定穆桂。
說(shuō)完以后,小凌就動(dòng)手起來(lái)了融虽,打開百度搜索“Java加密算法”享完,復(fù)制了如下代碼:
//加密
public static byte[] encrypt(String content, String password) {
try {
//構(gòu)造密鑰生成器,指定為AES算法
KeyGenerator kgen = KeyGenerator.getInstance("AES");
//初始化密鑰生成器有额,指定隨機(jī)源
kgen.init(128, new SecureRandom(password.getBytes()));
//產(chǎn)生原始對(duì)稱密鑰
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
//根據(jù)字節(jié)數(shù)組生成AES密鑰
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
//根據(jù)指定算法AES自成密碼器
Cipher cipher = Cipher.getInstance("AES");
byte[] byteContent = content.getBytes("utf-8");
//初始化密碼器
cipher.init(Cipher.ENCRYPT_MODE, key);
//加密
byte[] result = cipher.doFinal(byteContent);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
加密寫好了般又,哦不,是復(fù)制好了巍佑,既然有加密茴迁,那必須有解密,總不能將加密的信息直接顯示出來(lái)萤衰,解密如下:
//解密
public static byte[] decrypt(byte[] content, String password) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] result = cipher.doFinal(content);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
加密和解密的代碼實(shí)現(xiàn)沒(méi)有太大的不同堕义,嗯.....代碼復(fù)制好,就是這么簡(jiǎn)單.
接下來(lái)就是進(jìn)行測(cè)試了:
public static void main(String[] args) throws Exception {
String content = "linghenzeng";
String password = "12345678";
//加密
System.out.println("加密前1:" + content);
byte[] encryptResult = encrypt(content, password);
String strEncryptResult = parseByte2HexStr(encryptResult);
System.out.println("加密后1:" + strEncryptResult);
//解密
byte[] byteDecryptResult = parseHexStr2Byte(strEncryptResult);
byte[] decryptResult = decrypt(byteDecryptResult, password);
System.out.println("解密后1:" + new String(decryptResult));
}
為了加密和解密顯示正常脆栋,將加密生成的字節(jié)轉(zhuǎn)換成為十六進(jìn)制倦卖,再將十六進(jìn)制轉(zhuǎn)換為字符串洒擦,解密之前,將字符串轉(zhuǎn)換為二進(jìn)制的字節(jié)怕膛,二進(jìn)制和十六進(jìn)制的轉(zhuǎn)換函數(shù)如下:
// 二進(jìn)制轉(zhuǎn)十六進(jìn)制
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
//十六進(jìn)制轉(zhuǎn)二進(jìn)制
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2),
16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
在Window上測(cè)試一切正常熟嫩,加密解密都好使,剛好半個(gè)小時(shí)就完成了這個(gè)小需求褐捻,跟產(chǎn)品確認(rèn)下邦危,發(fā)布上線。
燃鵝舍扰,現(xiàn)實(shí)和理想存在巨大的鴻溝倦蚪,服務(wù)器報(bào)異常:
javax.crypto.BadPaddingException: Given final block not properly padded
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:966)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:824)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at test.decrypt(file.java:48)
at test.main(file.java:94)
Exception in thread "main" java.lang.NullPointerException
at java.lang.String.<init>(String.java:554)
at test.main(file.java:95)
晴天霹靂,線上差不多三千條的用戶敏感信息加密了边苹,但解密不了......陵且,將版本回滾回來(lái),聯(lián)系DBA將數(shù)據(jù)恢復(fù)个束,在DAB深厚技術(shù)基礎(chǔ)上慕购,數(shù)據(jù)恢復(fù)回來(lái)了。
遇到問(wèn)題茬底,首先是Ctrl + C沪悲、Ctrl + V,然后Enter阱表,最后在搜索出來(lái)的內(nèi)容去找出解決問(wèn)題的方法殿如,根據(jù)廣大網(wǎng)友的智慧提供的一系列方法中提煉出來(lái)的答案如下:
密鑰生成器指定的隨機(jī)源是操作系統(tǒng)本身的內(nèi)部狀態(tài)的,即SecureRandom 類在源碼上實(shí)現(xiàn)是不一樣的最爬,在windows平臺(tái)上每次生成的key都相同涉馁,但是在linux平臺(tái)上則不同。
具體的解決辦法為將如下代碼:
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
換成
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(key.getBytes());
kgen.init(128, secureRandom);
新舊代碼不同的地方是舊代碼直接將解密密鑰的字節(jié)初始化SecureRandom爱致,而新代碼則是指定“SHA1PRNG”隨機(jī)生成種子算法初始化SecureRandom烤送,然后再將解密密鑰的字節(jié)設(shè)置為隨機(jī)種子。
問(wèn)題解決了糠悯,代碼在Linux上可以正常加密解密帮坚。
但是為什么指定“SHA1PRNG”,代碼就可以在window平臺(tái)和Linux平臺(tái)上運(yùn)行互艾?難道在Linux平臺(tái)默認(rèn)指定的是其它算法试和?
在程序員界中,女程序員以為男程序員忘朝,什么都會(huì)灰署。男程序員中判帮,初級(jí)程序員以為高級(jí)程序員局嘁,什么都會(huì)溉箕。而高級(jí)程序員,每次都在網(wǎng)上苦苦查找答案悦昵。
為了更進(jìn)一步接近問(wèn)題的本質(zhì)肴茄,打算從源碼層次上尋找答案,探究SecureRandom類在不同平臺(tái)上采取的隨機(jī)種子算法有什么不同但指。
上圖是window平臺(tái)上調(diào)試的代碼截圖寡痰,以字節(jié)數(shù)組初始化的SecureRandom的構(gòu)造函數(shù)中,默認(rèn)采用的是“SHA1PRNG”算法進(jìn)行初始化對(duì)象棋凳。下圖的代碼截圖是在Linux上調(diào)試的結(jié)果:
在Linux平臺(tái)上拦坠,以字節(jié)數(shù)組初始化的SecureRandom的構(gòu)造函數(shù)中,默認(rèn)采用的是“NativePRNG”算法進(jìn)行初始化對(duì)象剩岳。
經(jīng)過(guò)一系列的分析贞滨,終于在源碼層面上找到產(chǎn)生問(wèn)題的原因了,但引發(fā)的疑問(wèn)更多了拍棕,什么是“SHA1PRNG”晓铆?什么是“NativePRNG”?它們之間有什么不同绰播?.......
在java文檔中找到了“SHA1PRNG”的解釋:
https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html
The name of the pseudo-random number generation (PRNG) algorithm supplied by the SUN provider. This algorithm uses SHA-1 as the foundation of the PRNG. It computes the SHA-1 hash over a true-random seed value concatenated with a 64-bit counter which is incremented by 1 for each operation. From the 160-bit SHA-1 output, only 64 bits are used.
翻譯為:
SUN提供的偽隨機(jī)數(shù)生成(PRNG)算法的名稱骄噪。該算法以SHA-1作為PRNG的生成函數(shù)。它通過(guò)一個(gè)真隨機(jī)種子值和一個(gè)64位計(jì)數(shù)器連接來(lái)計(jì)算SHA-1散列蠢箩,每個(gè)操作增加1链蕊。在160位SHA-1輸出中,只使用64位谬泌。
而在另外一篇博文中示弓,找到有關(guān)“NativePRNG”的信息:
https://metebalci.com/blog/everything-about-javas-securerandom/
As you expect, NativePRNG is platform specific:
1、For Solaris/Linux/MacOS, it obtains seed and random numbers from /dev/random and /dev/urandom and reads securerandom.source Security property and java.security.egd System property. The default is to obtain seed from /dev/random and obtain random numbers from /dev/urandom.
2呵萨、For Windows, NativePRNG is not implemented, but Windows native implemetation is provided using SunMSCAPI provider.
總結(jié)上面的意思:
1奏属、在Solaris/Linux/MacOS上,“NativePRNG”底層是從從/dev/random和/dev/urandom獲取種子和隨機(jī)數(shù)潮峦。默認(rèn)是從/dev/random獲取種子囱皿,從/dev/urandom獲取隨機(jī)數(shù)。
2忱嘹、在window上嘱腥,沒(méi)有實(shí)現(xiàn)“NativePRNG”,但是Windows的實(shí)現(xiàn)是使用SunMSCAPI provider提供的拘悦。
至此齿兔,所有疑問(wèn)都解決了。