04 密碼學

語言: CN / TW / HK

theme: channing-cyan highlight: a11y-light


為什麼要去看密碼學

  • 客戶端傳送給伺服器的資料包中,有些引數不知道來源,可能是隨機生成、標準演算法加密的、自寫演算法加密的
  • 安卓中,標準演算法加密通常會出現在Java、so(C/C++)、JS中
  • Java有現成的系統API呼叫,開發者想要使用這些API,必須使用固定的方法名去訪問。這些API也就是我們需要學習的內容。
  • C/C++沒有現成的系統API呼叫,開發者要麼自己去實現演算法,要麼呼叫別人寫好的模組,演算法的執行不依賴系統API,因此方法名可以混淆。你要做的就是根據各種標準演算法的特徵,去識別是否標準演算法。這些演算法的特徵,就是我們需要學習的內容。
  • JS也沒有現成的系統API呼叫,開發者一般使用CryptoJS、jsencrypt等第三方加密庫來加密。

密碼學裡面要看哪些內容

  • 訊息摘要演算法(雜湊函式、雜湊函式):MD5、SHA、MAC

  • 對稱加密演算法:DES、3DES、AES

  • 非對稱加密演算法:RSA
  • 數字簽名演算法:MD5withRSA、SHA1withRSA、SHA256withRSA

加密方式

1 Hex編碼

什麼是hex編碼

txt hex編碼是一種用16個字元表示任意二進位制資料的方法

Hex編碼的特點

a) 用0-9 a-f 16個字元表示。
b) 每個十六進位制字元代表4bit, 也就是2個十六進位制字元代表一個位元組。
c) 在實際應用中,比如金鑰初始化,一定要分清楚傳進去的金鑰是哪種編碼的,採用對應的方式解析,才能得到正確的結果
d) 程式設計中很多問題,需要從位元組甚至二進位制位的角度去考慮,才能明白

Hex編碼Java實現

``` java import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;

import java.security.MessageDigest; import java.security.NoSuchAlgorithmException;

public class HexEncode { public static void main(String[] args) { MessageDigest md5 = null; try { // md5 = MessageDigest.getInstance("MD5"); // md5.update("a12345678".getBytes()); // md5.update("a12345678".getBytes(), 1, 3); // byte[] result1 = md5.digest(); // System.out.println(HexBin.encode(result1)); String data = "Andy"; String salt = "a12345678"; byte[] result2 = MessageDigest.getInstance("SHA-256").digest((data + salt).getBytes()); String hexStr = HexBin.encode(result2); System.out.println(hexStr); //System.out.println(Base64.getEncoder().encodeToString(hexStr.getBytes())); //System.out.println(Base64.getEncoder().encodeToString(result2));

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }


}

} ```

2 Base64編碼

1. 什麼是Base64

Base64是一種用64個字元表示任意二進位制資料的方法
是一種編碼,而非加密
A-Z a-z 0-9 + / =

2. Base64的應用

RSA金鑰、加密後的密文、圖片等資料中,會有一些不可見字元,
直接轉成文字傳輸的話,會有亂碼、資料錯誤、資料丟失等情況出現,就可以使用Base64編碼

3. Base64的程式碼實現和碼錶

//java.util.Base64 碼錶 Android8.0以上可用
Base64.getEncoder().encodeToString("Andy".getBytes())
//android.util.Base64 碼錶
Base64.encodeToString

//okio.ByteString
build.gradle
dependencies {
    api 'com.squareup.okhttp3:okhttp:3.10.0'
}
ByteString byteString = ByteString.of("100".getBytes());
byteString.base64();    //碼錶 okio.Base64 encode

4. Base64碼錶的妙用

為了傳輸資料安全,通常會對Base64資料進行URL編碼,或者會把+和/替換成-和_

5. Base64編碼細節

每個Base64字元代表原資料中的6bit
Base64編碼後的字元數,是4的倍數
編碼的位元組數是3的倍數時,不需要填充

6. Base64編碼的特點

a) Base64編碼是編碼,不是壓縮,編碼後只會增加位元組數
b) 演算法可逆, 解碼很方便, 不用於私密資訊通訊
c) 標準的Base64每行為76個字元,行末新增換行符
d) 加密後的字串只有65種字元, 不可列印字元也可傳輸
e) 在Java層可以通過hook對應方法名來快速定位關鍵程式碼
f) 在so層可以通過輸入輸出的資料和碼錶來確定演算法

image.png

image.png

訊息摘要演算法

1. 演算法特點

a) 訊息摘要演算法/單向雜湊函式/雜湊函式
b) 不同長度的輸入,產生固定長度的輸出
c) 雜湊後的密文不可逆
d) 雜湊後的結果唯一
e) 雜湊碰撞
f) 一般用於校驗資料完整性、簽名sign
由於密文不可逆,所以服務端也無法解密
想要驗證,就需要跟前端一樣的方式去重新簽名一遍
簽名演算法一般會把源資料和簽名後的值一起提交到服務端
要保證在簽名時候的資料和提交上去的源資料一致

2. 常見演算法

MD5、SHA1、SHA256、SHA512、HmacMD5、HmacSHA1、HmacSHA256、HmacSHA512
RIPEMD160、HmacRIPEMD160、PBKDF2、EvpKDF

image.png

MD5

image.png

1. MD5的Java實現

MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update("Andy".getBytes());
md5.digest();

2 演算法特點

1. 加密後的位元組陣列可以編碼成Hex、Base64
2. 沒有任何輸入,也能計算hash值
3. 碰到加salt的MD5,可以直接輸入空的值,得到結果去CMD5查詢一下,有可能就得到salt

SHA

image.png

1. SHA的Java實現

MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update("Andy".getBytes());
sha1.digest();

2 SHA演算法的特點

1 加密後的位元組陣列可以編碼成Hex、Base64
2 沒有任何輸入,也能計算hash值

MAC

image.png

1. MAC演算法與MD和SHA的區別是多了一個金鑰,金鑰可以隨機給
2. MAC的Java實現
SecretKeySpec secretKeySpec = new SecretKeySpec("a12345678".getBytes(),"HmacSHA1");
Mac mac = Mac.getInstance(secretKeySpec.getAlgorithm());
mac.init(secretKeySpec);
mac.update("Andy".getBytes());
mac.doFinal();
3. 加密後的位元組陣列可以編碼成Hex、Base64
4. 沒有任何輸入,也能計算hash值

MD5,HEX,MAC通殺演算法

https://note.youdao.com/s/CJT8frzb

對稱加密演算法

1. 加密/解密的過程可逆的演算法,叫做加密演算法
2. 加密/解密使用相同的金鑰,叫做對稱加密演算法
3. 對稱加密演算法的金鑰可以隨機給,但是有位數要求
4. 對稱加密演算法的輸入資料沒有長度要求,加密速度快
5. 各演算法的金鑰長度
RC4 金鑰長度1-256位元組
DES 金鑰長度8位元組
3DES/DESede/TripleDES 金鑰長度24位元組
AES 金鑰長度16、24、32位元組
根據金鑰長度不同AES又分為AES-128、AES-192、AES-256
6. 對稱加密分類
a) 序列加密/流加密: 以位元組流的方式,依次加密(解密)明文(密文)中的每一個位元組
   RC4 
b) 分組加密: 將明文訊息分組(每組有多個位元組),逐組進行加密
   DES、3DES、AES

DES

image.png

DES演算法實現

```java import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;

import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64;

public class DESEncode { public static void main(String[] args) throws NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { // DES加密 SecretKeySpec desKey = new SecretKeySpec("12345678".getBytes(), "DES"); IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes()); // 長度按照對應演算法的分組長度 Cipher des = Cipher.getInstance("DES/CBC/PKCS5Padding"); des.init(Cipher.ENCRYPT_MODE, desKey, ivParameterSpec); //des.update("1234xiaojianbang1234".getBytes()); byte[] result3 = des.doFinal("xiaojianxiaojian".getBytes()); String hexStr = HexBin.encode(result3); // 加密結果為hex編碼 System.out.println(hexStr); // 加密結果為base64編碼 System.out.println(Base64.getEncoder().encodeToString(result3));

        // 將加密結果解碼為字串
        des.init(Cipher.DECRYPT_MODE, desKey, ivParameterSpec);
        byte[] result4 = des.doFinal(result3);
        System.out.println(new String(result4));
}

} ```

DES演算法特點

1. 對稱加密演算法裡,使用NOPadding,加密的明文必須等於分組長度倍數,否則報錯
2. 沒有指明加密模式和填充方式,表示使用預設的DES/ECB/PKCS5Padding
3. 加密後的位元組陣列可以編碼成Hex、Base64
4. 要復現一個對稱加密演算法,需要得到明文、key、iv、mode、padding
5. 明文、key、iv需要注意解析方式,而且不一定是字串形式
6. 如果加密模式是ECB,則不需要加iv,加了的話會報錯

7. ECB模式和CBC模式的區別
8. 如果使用PKCS5Padding,會對加密的明文填充1位元組-1個分組的長度
9. DES演算法明文按64位進行分組加密
10. 如果明文中有兩個分組的內容相同,ECB會得到完全一樣的密文,CBC不會
11. 加密演算法的結果通常與明文等長或者更長,如果變短了,那可能是gzip、protobuf、訊息摘要演算法

DESede

image.png

DESede演算法實現

java DESede加解密的Java實現 //DESedeKeySpec desedeKey = new DESedeKeySpec("123456781234567812345678".getBytes()); //SecretKeyFactory key = SecretKeyFactory.getInstance("DESede"); //SecretKey secretKey = key.generateSecret(desKeySpec); SecretKeySpec secretKeySpec = new SecretKeySpec("123456781234567812345678".getBytes(), "DESede"); IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes()); Cipher desede = Cipher.getInstance("DESede/CBC/PKCS5Padding"); desede.init(Cipher.ENCRYPT_MODE, desedeKey, ivParameterSpec); desede.doFinal("xiaojianbang".getBytes());

DESede演算法特點

1. DESede實際上是先進行DES加密,再進行DES解密,再進行DES加密

2. DESede的金鑰24個位元組,第1個8位元組金鑰用於DES加密,之後類推

3. 如果DESede的3個8位元組金鑰相同,則加密結果與DES一致

4. 為了保證DESede的安全性,一般前2個或者3個8位元組金鑰都不一致

5. DESede的其他特徵與DES一致

AES

image.png

1. 根據金鑰長度不同,分為AES128、AES192、AES256

2. AES加解密的Java實現
SecretKeySpec key = new SecretKeySpec("1234567890abcdef".getBytes(),"AES");
AlgorithmParameterSpec iv = new IvParameterSpec("1234567890abcdef".getBytes());
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
aes.init(Cipher.ENCRYPT_MODE, key, iv);
aes.doFinal("xiaojianbang1234xiaojianbang1234".getBytes());

3. AES演算法明文按128位進行分組加密,其餘特徵與DES一致
4. DES、3DES、AES演算法通殺hook
https://note.youdao.com/s/7EcfTfzx

非對稱加密演算法

典型演算法:RSA

1. 需要生成一個金鑰對,包含公鑰和私鑰,金鑰不是隨便寫的
金鑰對生成 http://web.chacuo.net/netrsakeypair

2. 公鑰加密的資料,私鑰才能解密
   私鑰加密的資料,公鑰才能解密

3. 一般公鑰是公開的,私鑰保密,私鑰包含公鑰,從公鑰無法推匯出私鑰

4. 加密處理安全,但是效能極差,單次加密長度有限制

5. RSA演算法既可用於加密解密,也可用於資料簽名

RSA_Base64

image.png

1. 私鑰的格式

pkcs1格式通常開頭是 -----BEGIN RSA PRIVATE KEY-----
pkcs8格式通常開頭是 -----BEGIN PRIVATE KEY-----
Java中的私鑰必須是pkcs8格式

2. RSA金鑰的解析

byte[] keyBytes = Base64Decoder.decodeBuffer(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);

byte[] keyBytes = Base64Decoder.decodeBuffer(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

3. RSA加解密

Cipher cipher = Cipher.getInstance("RSA/None/NOPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bt_encrypted = cipher.doFinal(bt_plaintext);

Cipher cipher = Cipher.getInstance("RSA/None/NOPadding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bt_original = cipher.doFinal(bt_encrypted);

4. RSA模式和填充細節

a) None模式與ECB模式是一致的
b) NOPadding
   明文最多位元組數為金鑰位元組數
   填充位元組0, 加密後的密文不變
   密文與金鑰等長
c) PKCS1Padding
   明文最大位元組數為金鑰位元組數-11
   每一次的填充不一樣,使得加密後的密文會變
   密文與金鑰等長
    1. 把PKCS1Padding加密後的密文,用NOPadding去解密,會怎麼樣呢?
    1. 沒有指明加密模式和填充方式,表示使用預設的RSA/ECB/NOPadding
    1. 加密後的位元組陣列可以編碼成Hex、Base64

RSA_hex

1. RSA金鑰的轉換

https://www.cnblogs.com/wyzhou/p/9738964.html

openssl rsa -pubin -in public.pem -text //以文字格式輸出公鑰內容
openssl rsa -in private.pem -text       //以文字格式輸出私鑰內容
a) 從PEM格式金鑰中提取modulus、publicExponent、privateExponent
b) modulus、publicExponent、privateExponent轉PEM格式
c) 還有極少數的modulus、publicExponent、privateExponent用十進位制表示

2. RSA金鑰的解析

BigInteger N = new BigInteger(stringN, 16); //16改10就可以把十進位制轉成大數
BigInteger E = new BigInteger(stringE, 16); 
RSAPublicKeySpec spec = new RSAPublicKeySpec(N, E);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(spec);

3. RSA_Hex加解密的方式與RSA_Base64一致

4. 多種加密演算法的常見結合套路

隨機生成AES金鑰AESKey
AESKey金鑰用於AES加密資料,得到資料密文cipherText
使用RSA對AESKey加密,得到金鑰密文cipherKey
提交金鑰密文cipherKey和資料密文cipherText給伺服器

5 關於AES和RSA加密結合

  • 先獲取AES隨機的祕鑰,通過該祕鑰將資料進行加密
  • 將AES的祕鑰進行rsa加密
  • 客戶端收到AES加密後的資料,以及RSA加密後的key,先將key通過rsa解密,之後拿著加密之後的key將資料體進行解密

  • RSA演算法通殺hook https://note.youdao.com/s/1ZcMKaQk

數字簽名演算法

image.png

1. 簽名
PrivateKey priK = getPrivateKey(str_priK);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(priK);
sig.update(data);
sig.sign();

2. 驗證
PublicKey pubK = getPublicKey(str_pubK);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(pubK);
sig.update(data);
sig.verify(sign);

3. 數字簽名演算法通殺hook