Java加密學習筆記第一版
Java加密學習筆記第一版
寫在前面,今天自己做個leetcode就做了一個hour, 學個加密,半天看不懂,想了好久,覺得自己都沒有成套的知識體系,老是在快餐式的學習,沒有計劃的學習,今天學這個,明天學哪個,技術能力不但沒有提高,反而程式碼能力都有所下降,這樣如何準備即將到來的實習。暑假就是資料結構和java,打好基礎,做好筆記,不要去想別的東西了,希望你有所沉澱。
1. 編碼演算法
1.1 必須掌握的編碼知識
這是為了解決計算機如何表示一個char這個問題。計算機底層只能儲存二進位制,所以我們做了一個對映表,把二進位制表示的數字去對映不同的字元。一開始用一個位元組來表示不同的字元,最高位不使用,所以實際上就可以表示2**7 -1 = 127個字元。這種表示方法就叫做ascii碼。如果說,有的國家字元數多於127個咋辦呢? 於是各個國家有了不同的表示方式,中國採用gb2312。後來為了統一,頒佈了一個固定位元組數的編碼方式unicode編碼,java中採用了這種方式,佔2個位元組。後來又出現了可變長編碼utf-8編碼,長度可以變化,更加節省位元組。
StandardCharsets.US_ASCII // java中表示ascii碼
StandardCharsets.UTF_8 // java中表示utf-8編碼
byte[] bytes = new byte[]{1,2,34,5};
String str = new String(bytes, StandardCharsets.UTF_8); // byte陣列轉String,指定utf-8編碼
byte[] bytes = "hell0".getBytes(StandardCharsets.UTF_8); // String轉byte[],指定utf-8編碼
有些時候,我們在進行編碼轉換的時候,常常出現亂碼,前段時間的爬蟲專案,當將位元組流轉換成字元的時候,老是亂碼。有些時候讀檔案也會亂碼。這個時候應該優先設定utf-8編碼。有的時候,伺服器只支援ascii碼,只能用ascii碼了。
1.2. 常見的編碼演算法
URL編碼演算法
// url編碼 // url 需要編碼,有些伺服器只認識ascii // 編碼規則 /** * 26個英文小寫字母和它的大寫字母以及- _ . * 不變 * 如果是其他字元,先轉換為UTF-8編碼,然後對每個位元組以%XX表示 */ // url編碼器 String encoded = URLEncoder.encode("hhf-_.*;!付清", StandardCharsets.UTF_8); System.out.println(encoded); // url解碼器 String decodeString = URLDecoder.decode(encoded,StandardCharsets.UTF_8); System.out.println(decodeString);
Base64編碼演算法
public class Base64Test {
public static void main(String[] args) {
/**
* Base64編碼是對二進位制資料進行編碼,表示成文字格式。
* Base64編碼可以把任意長度的二進位制資料變為純文字,
* 且只包含A~Z、a~z、0~9、+、/、=這些字元。
* 它的原理是把3位元組的二進位制資料按6bit一組,
* 用4個int整數表示,然後查表,把int整數用索引對應到字元,
* 得到編碼後的字串
* 因為6位整數的範圍總是0~63,所以,
* 能用64個字元表示:字元A~Z對應索引0~25,
* 字元a~z對應索引26~51,字元0~9對應索引52~61,
* 最後兩個索引62、63分別用字元+和/表示
*/
byte[] bytes = new byte[]{1,3,4,3,2};
// 編碼
String base64encoded = Base64.getEncoder().encodeToString(bytes);
System.out.println(base64encoded);
// 不是三的倍數,用等號補上,結果用withoutPadding去掉
System.out.println(Base64.getEncoder().withoutPadding().encodeToString(bytes));
// 解碼
byte[] output = Base64.getDecoder().decode(base64encoded);
System.out.println(Arrays.toString(output));
// 應用場景
/**
Base64編碼的目的是把二進位制資料變成文字格式,
這樣在很多文字中就可以處理二進位制資料。例如,
電子郵件協議就是文字協議,如果要在電子郵件中新增一個二進位制檔案,
就可以用Base64編碼,然後以文字的形式傳送
*/
// 缺點
/**
* 位元組數必須是三的倍數
*/
}
}
2. hash演算法
2.1 hash演算法介紹
-
雜湊演算法(Hash)又稱摘要演算法(Digest),
它的作用是:對任意一組輸入資料進行計算,
得到一個固定長度的輸出摘要 -
相同的輸入一定得到相同的輸出; 不同的輸入大概率得到不同的輸出。-
-
一個安全的雜湊演算法必須滿足:
碰撞概率低;
不能猜測輸出。不能通過hash值猜測對應的原來的字串。也就是說是單向的,相當於單向加密。 -
常見的hash演算法
MD5 128 bits 16 bytes(容易被破譯,專案裡最好不要用)
SHA-1 160 bits 20 bytes
RipeMD-160 160 bits 20 bytes
SHA-256 256 bits 32 bytes
SHA-512 512 bits 64 bytes (這個長些,安全些)
2.2 應用場景
-
資料登入模組設計。密碼的儲存常常使用hash演算法,而不是直接儲存明文。我在想要是發生了hash衝突咋辦,錯誤的password也可以登入。這個應該概率極小吧。
-
防止篡改。
-
補充知識:
// 啥叫彩虹表攻擊,如何預防 /** * 黑客獲得資料庫密碼錶時,如果你的密碼錶採用的是md5加密,一般來說,我們 * 可以大量窮舉密碼明文以獲得密碼明文。但是黑客不會那麼笨,他們會預先準備一個密碼 * 字典,列出常用的密碼和對應的md5的加密值。如果使用者使用了常用密碼,那麼很快就可以查到 * 對應的明文了。畢竟hashMap查詢很快的。但是如果使用者不使用常用密碼,黑客會實現準備一個彩虹表。 * 怎麼解決: 加鹽儲存,就是給每個摘要附上一個隨機值 */
彩虹表資料: https://blog.csdn.net/whatday/article/details/88527936
實現一個簡單的彩虹表生成器
2.3 demo
public static void main(String[] args) throws Exception {
byte[] bytes = "World".getBytes(StandardCharsets.US_ASCII);
MessageDigest messageDigest = MessageDigest.getInstance("MD5");// 演算法名稱
messageDigest.update(new byte[]{1, 4, 2, 1});
messageDigest.update(bytes); // 更新摘要
byte[] res = messageDigest.digest();
; // 重置為初始化狀態
// 轉化為String
BigInteger bigInteger = new BigInteger(1,res);
String str = bigInteger.toString();
}
2.4 外部類庫, java核心庫沒有RipeMD160這個演算法
/**
* BouncyCastle
* 首先去官網下載,idea設定好classpath
*/
public static void main(String[] args) throws Exception{
// 註冊BouncyCastle:
Security.addProvider(new BouncyCastleProvider());
// 按名稱正常呼叫:
MessageDigest md = MessageDigest.getInstance("RipeMD160");
md.update("HelloWorld".getBytes("UTF-8"));
byte[] result = md.digest();
System.out.println(new BigInteger(1, result).toString(16)); // 16進位制的字串
}
2.5 基於鹽值的演算法之Hmac
demo
public static void main(String[] args) throws Exception {
// 生成隨機鹽值
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
Mac mac = Mac.getInstance("HmacMD5");
// 列印隨機生成的key:
byte[] skey = key.getEncoded();
mac.init(key);
mac.update("HelloWorld".getBytes(StandardCharsets.UTF_8));
byte[] result = mac.doFinal();
// 驗證過程
System.out.println(isCorrectPassWorld("HelloWorld", result, skey));
System.out.println(isCorrectPassWorld("HelloWorls", result, skey));
}
static boolean isCorrectPassWorld(String info, byte[] result , byte[] salt) throws Exception{
Mac mac = Mac.getInstance("HmacMD5");
SecretKey key = new SecretKeySpec(salt, "HmacMD5");
mac.init(key);
mac.update(info.getBytes(StandardCharsets.UTF_8));
return Arrays.equals(mac.doFinal(), result);
// 陣列的equals方法是Object的equals方法,陣列也是一個物件
// Arrays.equals()比較的是陣列的值
}
3. 對稱加密
3.1 對稱加密介紹
-
用一個密碼加密,同一個密碼解密。常用的WinZIP和WinRAR對壓縮包的加密和解密,就是使用對稱加密演算法
-
常用對稱加密演算法
演算法 金鑰長度bites 工作模式 填充模式 DES 56/64 ECB/CBC/PCBC/CTR/... NoPadding/PKCS5Padding/... AES 128/192/256 ECB/CBC/PCBC/CTR/... NoPadding/PKCS5Padding/PKCS7Padding/... IDEA 128 ECB PKCS5Padding/PKCS7Padding/...
-
最後注意,DES演算法由於金鑰過短,可以在短時間內被暴力破解,所以現在已經不安全了
AES演算法是目前應用最廣泛的加密演算法,金鑰的長度固定 -
demo
public static void main(String[] args) throws GeneralSecurityException { byte[] encrypt = null; byte[] decrypt = null; Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKey key = new SecretKeySpec("password11111111".getBytes(StandardCharsets.UTF_8), "AES"); SecretKey errorKey = new SecretKeySpec("password12313211".getBytes(StandardCharsets.UTF_8), "AES"); // 加密encrypt cipher.init(Cipher.ENCRYPT_MODE, key); // 加密模式 cipher.update("fsdfsafsdfsads".getBytes(StandardCharsets.UTF_8)); // 16byte encrypt = cipher.doFinal(); // 解密decrypt cipher.init(Cipher.DECRYPT_MODE, errorKey); try { decrypt = cipher.doFinal(encrypt); System.out.println(new String(decrypt, StandardCharsets.UTF_8)); } catch (BadPaddingException e) { System.out.println("code error"); }
3.2 口令加密演算法介紹,解決的是任意長度口令的對稱加密問題
public static void main(String[] args) throws Exception {
// 把BouncyCastle作為Provider新增到java.security:
Security.addProvider(new BouncyCastleProvider());
// 原文:
String message = "Hello, world!";
// 加密口令:
String password = "hello12345";
// 16 bytes隨機Salt:
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);
System.out.printf("salt: %032x\n", new BigInteger(1, salt));
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(password, salt, data);
System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(password, salt, encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.DECRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
4.非對稱加密
4.1 金鑰交換演算法DH
-
DH演算法是一個金鑰協商演算法,雙方最終協商出一個共同的金鑰,而這個金鑰不會通過網路傳輸。
-
如果我們把a看成甲的私鑰,A看成甲的公鑰,b看成乙的私鑰,B看成乙的公鑰,DH演算法的本質就是雙方各自生成自己的私鑰和公鑰,私鑰僅對自己可見,然後交換公鑰,並根據自己的私鑰和對方的公鑰,生成最終的金鑰secretKey,DH演算法通過數學定律保證了雙方各自計算出的secretKey是相同的
4.2 非對稱加密介紹
非對稱加密基於DH演算法,是一種加密和解密所需要的金鑰不同的演算法。每個人維護一個公鑰和私鑰。加密的時候,主要有兩種方式。
-
一是獲得接收方的公鑰,利用對方的公鑰和自己的私鑰加密資訊。然後傳送給對方,對方獲得傳送方的公鑰,和自己的私鑰生成金鑰解密。這樣看來,解密就好像是利用自己的私鑰解密,加密利用自己的公鑰的加密。
-
第二種是利用自己的私鑰加密,然後傳送出去。我們知道,這個資訊可以用傳送方的公鑰解密,那麼任何人都可以去解密,因為傳送方的公鑰是公開的。這樣可以防止傳送方抵賴。
-
demo
class Person { String name; // 私鑰: PrivateKey sk; // 公鑰: PublicKey pk; public Person(String name) throws GeneralSecurityException { this.name = name; // 生成公鑰/私鑰對: KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA"); kpGen.initialize(1024); KeyPair kp = kpGen.generateKeyPair(); this.sk = kp.getPrivate(); this.pk = kp.getPublic(); } // 把私鑰匯出為位元組 public byte[] getPrivateKey() { return this.sk.getEncoded(); } // 把公鑰匯出為位元組 public byte[] getPublicKey() { return this.pk.getEncoded(); } // 用公鑰加密: public byte[] encrypt(byte[] message) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, this.pk); return cipher.doFinal(message); } // 用私鑰解密: public byte[] decrypt(byte[] input) throws GeneralSecurityException { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, this.sk); return cipher.doFinal(input); } } class Tool { static PrivateKey convertToPrivate(byte[] prikeyData) throws InvalidKeySpecException, NoSuchAlgorithmException { KeyFactory kf = KeyFactory.getInstance("RSA"); PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(prikeyData); return kf.generatePrivate(skSpec); } static PublicKey convertToPublic(byte[] pubkeyData) throws InvalidKeySpecException, NoSuchAlgorithmException { KeyFactory kf = KeyFactory.getInstance("RSA"); X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pubkeyData); return kf.generatePublic(pkSpec); } }
4.3數字簽名
私鑰加密,公鑰解密。別人都知道自己的公鑰,因此都可以開啟,所有人就都知道這個檔案是誰的了,不能抵賴。
私鑰加密得到的密文實際上就是數字簽名,要驗證這個簽名是否正確,只能用私鑰持有者的公鑰進行解密驗證。
使用數字簽名的目的是為了確認某個資訊確實是由某個傳送方傳送的,任何人都不可能偽造訊息,並且,傳送方也不能抵賴
在實際應用的時候,簽名實際上並不是針對原始訊息,而是針對原始訊息的雜湊進行簽名
demo
public static void main(String[] args) throws GeneralSecurityException {
// 生成RSA公鑰/私鑰:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
PrivateKey sk = kp.getPrivate();
PublicKey pk = kp.getPublic();
// 待簽名的訊息:
byte[] message = "Hello, I am Bob!".getBytes(StandardCharsets.UTF_8);
// 用私鑰簽名:
Signature s = Signature.getInstance("SHA1withRSA"); // SHA1hash演算法,對hash值進行簽名
s.initSign(sk);
s.update(message);
byte[] signed = s.sign();
System.out.println(String.format("signature: %x", new BigInteger(1, signed)));
// 用公鑰驗證:
Signature v = Signature.getInstance("SHA1withRSA");
v.initVerify(pk);
v.update(message);
boolean valid = v.verify(signed);
System.out.println("valid? " + valid);
}
4.4 數字證書
- 非對稱加密演算法可以對資料進行摘要演算法用來確保資料沒有被篡改加解密,簽名演算法可以確保資料完整性和抗否認性,
把這些演算法集合到一起,並搞一套完善的標準,這就是數字證書。- java內部可以自己生成證書,供開發的時候使用,上線的時候不要使用。以後遇到了再來補充筆記。
4.5 對稱加密與非對稱加密優缺點比較
-
與多對個人通訊時, 對稱加密要管理多個密碼,非對稱加密只需要管理好自己的私鑰和公鑰。
-
非對稱加密需要複雜的協商過程,所以如果通訊頻繁,那麼效率也不好。但是如果採用對稱加密的話,加密口令在網路中傳輸,也不安全。一般來說,是這樣處理:
-
/** 以HTTPS協議為例,瀏覽器和伺服器建立安全連線的步驟如下: 瀏覽器向伺服器發起請求,伺服器向瀏覽器傳送自己的數字證書; 瀏覽器用作業系統內建的Root CA來驗證伺服器的證書是否有效,如果有效,就使用該證書的公鑰加密一個隨機的AES口令併發送給伺服器;// AES口令傳輸採用非對稱加密 伺服器用自己的私鑰解密獲得AES口令,並在後續通訊中使用AES加密。 // 對稱加密 上述流程只是一種最常見的單向驗證。如果伺服器還要驗證客戶端,那麼客戶端也需要把自己的證書傳送給伺服器驗證,這種場景常見於網銀等。 */