Featured image of post 密码学

密码学

Hex编码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
1. 什么是Hex编码
Hex编码是一种用16个字符表示任意二进制数据的方法
是一种编码,而非加密
  字符编码 ASCII、UTF-8、GBK
  USC2编码、URL编码,其实与Hex编码差不多

2. Hex编码的代码实现和码表
build.gradle
dependencies {
    api 'com.squareup.okhttp3:okhttp:3.10.0'
}
//okio.ByteString
ByteString byteString = ByteString.of("100".getBytes());
byteString.hex();

3. Hex编码表的妙用
4. Hex编码特点
a) 用0-9 a-f 16个字符表示。
b) 每个十六进制字符代表4bit, 也就是2个十六进制字符代表一个字节。
c) 在实际应用中,比如密钥初始化,一定要分清楚传进去的密钥是哪种编码的,采用对应	方式解析,才能得到正确的结果
d) 编程中很多问题,需要从字节甚至二进制位的角度去考虑,才能明白

Base64编码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
1. 什么是Base64
Base64是一种用64个字符表示任意二进制数据的方法
是一种编码,而非加密
A-Z a-z 0-9 + / =

2. Base64的应用
RSA密钥、加密后的密文、图片等数据中,会有一些不可见字符,直接转成文本传输的话,会有乱码、数据错误、数据丢失等情况出现,就可以使用Base64编码

3. Base64的代码实现和码表
//java.util.Base64 码表 Android8.0以上可用
Base64.getEncoder().encodeToString("小肩膀".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层可以通过输入输出的数据和码表来确定算法

消息摘要

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
1. 算法特点
a) 消息摘要算法/单向散列函数/哈希函数
b) 不同长度的输入,产生固定长度的输出
c) 散列后的密文不可逆
d) 散列后的结果唯一
e) 哈希碰撞
f) 一般用于校验数据完整性、签名sign
由于密文不可逆,所以服务端也无法解密
想要验证,就需要跟前端一样的方式去重新签名一遍
签名算法一般会把源数据和签名后的值一起提交到服务端
要保证在签名时候的数据和提交上去的源数据一致

2. 常见算法
MD5、SHA1、SHA256、SHA512、HmacMD5、HmacSHA1、HmacSHA256、HmacSHA512
RIPEMD160、HmacRIPEMD160、PBKDF2、EvpKDF

image-20260203212649496

MD5

1
2
3
4
5
6
7
8
1. MD5的Java实现
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update("xiaojianbang".getBytes());
md5.digest();

2. 加密后的字节数组可以编码成Hex、Base64
3. 没有任何输入,也能计算hash值
4. 碰到加salt的MD5,可以直接输入空的值,得到结果去CMD5查询一下,有可能就得到salt

SHA

1
2
3
4
5
6
7
1. SHA的Java实现
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update("xiaojianbang".getBytes());
sha1.digest();

2. 加密后的字节数组可以编码成Hex、Base64
3. 没有任何输入,也能计算hash值

MAC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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("xiaojianbang".getBytes());
mac.doFinal();
3. 加密后的字节数组可以编码成Hex、Base64
4. 没有任何输入,也能计算hash值
5. MAC算法通杀hook

image-20260203215649529

对称加密算法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
DES加解密的Java实现
//DESKeySpec desKeySpec = new DESKeySpec("12345678".getBytes());
//SecretKeyFactory key = SecretKeyFactory.getInstance("DES");
//SecretKey secretKey = key.generateSecret(desKeySpec);
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.doFinal("xiaojianbang".getBytes());

1. 对称加密算法里使用NOPadding加密的明文必须等于分组长度倍数否则报错
2. 没有指明加密模式和填充方式表示使用默认的DES/ECB/PKCS5Padding
3. 加密后的字节数组可以编码成HexBase64
4. 要复现一个对称加密算法需要得到明文keyivmodepadding
5. 明文keyiv需要注意解析方式而且不一定是字符串形式
6. 如果加密模式是ECB则不需要加iv加了的话会报错

7. ECB模式和CBC模式的区别
8. 如果使用PKCS5Padding会对加密的明文填充1字节-1个分组的长度
9. DES算法明文按64位进行分组加密
10. 如果明文中有两个分组的内容相同ECB会得到完全一样的密文CBC不会
11. 加密算法的结果通常与明文等长或者更长如果变短了那可能是gzipprotobuf消息摘要算法

DESede

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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());

1. DESede实际上是先进行DES加密再进行DES解密再进行DES加密

2. DESede的密钥24个字节第1个8字节密钥用于DES加密之后类推

3. 如果DESede的3个8字节密钥相同则加密结果与DES一致

4. 为了保证DESede的安全性一般前2个或者3个8字节密钥都不一致

5. DESede的其他特征与DES一致

image-20260204140716583

AES

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
1. 根据密钥长度不同分为AES128AES192AES256

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. DES3DESAES算法通杀hook

非对称加密算法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
典型算法:RSA

1. 需要生成一个密钥对,包含公钥和私钥,密钥不是随便写的
密钥对生成 http://web.chacuo.net/netrsakeypair

2. 公钥加密的数据,私钥才能解密
   私钥加密的数据,公钥才能解密

3. 一般公钥是公开的,私钥保密,私钥包含公钥,从公钥无法推导出私钥

4. 加密处理安全,但是性能极差,单次加密长度有限制

5. RSA算法既可用于加密解密,也可用于数据签名

RSA_Base64

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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);
BigInteger n = rsaPublicKey.getModulus();    // 模数
BigInteger e = rsaPublicKey.getPublicExponent(); // 公钥指数

byte[] keyBytes = Base64Decoder.decodeBuffer(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
BigInteger n = rsaPrivateKey.getModulus();           // 模数
BigInteger e = rsaPrivateKey.getPublicExponent();    // 公钥指数
BigInteger d = rsaPrivateKey.getPrivateExponent();   // 私钥指数
BigInteger p = rsaPrivateKey.getPrimeP();            // 质数p
BigInteger q = rsaPrivateKey.getPrimeQ();            // 质数q
BigInteger dp = rsaPrivateKey.getPrimeExponentP();   // dp = d mod (p-1)
BigInteger dq = rsaPrivateKey.getPrimeExponentQ();   // dq = d mod (q-1)
BigInteger qinv = rsaPrivateKey.getCrtCoefficient(); // qinv = q^(-1) mod p

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
   每一次的填充不一样使得加密后的密文会变
   密文与密钥等长

5. 把PKCS1Padding加密后的密文用NOPadding去解密会怎么样呢?
6. 没有指明加密模式和填充方式表示使用默认的RSA/ECB/NOPadding
7. 加密后的字节数组可以编码成HexBase64

image-20260204212401683

RSA_Hex

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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格式密钥中提取moduluspublicExponentprivateExponent
b) moduluspublicExponentprivateExponent转PEM格式
c) 还有极少数的moduluspublicExponentprivateExponent用十进制表示

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. RSA算法通杀hook

数字签名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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

image-20260205121302394

CryptoJS

消息摘要算法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 为什么选择使用JavaScript来复现算法
JS实现的算法可以很方便地被任何语言调用

2. CryptoJS中消息摘要算法的使用
CryptoJS.MD5(message);
CryptoJS.HmacMD5(message, key);
CryptoJS.SHA1(message);
CryptoJS.HmacSHA1(message, key);
CryptoJS.SHA256(message);
CryptoJS.HmacSHA256(message, key);
CryptoJS.SHA512(message);
CryptoJS.HmacSHA512(message, key);

//CryptoJS.SHA3('xiaojianbang', {outputLength: 256})

3. 消息摘要算法的其他调用形式
SHA256
var hasher = CryptoJS.algo.SHA256.create();
hasher.reset();
hasher.update('message');
hasher.update(wordArray);
var hash = hasher.finalize();
var hash = hasher.finalize('message');
var hash = hasher.finalize(wordArray);
HmacSHA256
var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key);
hmacHasher.reset();
hmacHasher.update('message');
hmacHasher.update(wordArray);
var hmac = hmacHasher.finalize();
var hmac = hmacHasher.finalize('message');
var hmac = hmacHasher.finalize(wordArray);

字符串解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
1. string转wordArray
CryptoJS.enc.Utf8.parse(utf8String);
CryptoJS.enc.Hex.parse(hexString);
CryptoJS.enc.Base64.parse(base64String);

2. wordArray转string
wordArray + '';
wordArray.toString();
wordArray.toString(CryptoJS.enc.Utf8);
wordArray.toString(CryptoJS.enc.Hex);
wordArray.toString(CryptoJS.enc.Base64);
CryptoJS.enc.Utf8.stringify(wordArray);
CryptoJS.enc.Hex.stringify(wordArray);
CryptoJS.enc.Base64.stringify(wordArray);

3. Hex编码转Base64编码

4. 如果函数传入的参数是string类型的数据将使用默认的Utf8.parse来解析

对称加密

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
1. CryptoJS中对称加密算法的使用
var ciphertext = CryptoJS.DES.encrypt(message, key, cfg);
var plaintext  = CryptoJS.DES.decrypt(ciphertext, key, cfg);

var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg);
var plaintext  = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg);

var ciphertext = CryptoJS.AES.encrypt(message, key, cfg);
var plaintext  = CryptoJS.AES.decrypt(ciphertext, key, cfg);

var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg);
var plaintext  = CryptoJS.RC4.decrypt(ciphertext, key, cfg);

2. cfg的详细含义
var cfg = {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
    format: CryptoJS.format.Hex
};

3. cfg中没有传mode和padding默认使用CBC的加密模式Pkcs7的填充方式
4. 加密结果是CipherParams对象调用toString默认转Base64编码的密文转hex可以使用
var hexString = wordArray.ciphertext.toString();
5. CryptoJS中提供的加密模式
    CBC ECB CFB OFB CTRGladman CTR
6. CryptoJS中提供的填充方式
    NoPadding ZeroPadding Pkcs7(Pkcs5) Iso10126 Iso97971 AnsiX923
7. 对称加密算法的解密
8. 密文/明文的自定义输出/输入(cfg中format的指定)
    format: {
        stringify: function (data){
            let e = {
                ct: data.ciphertext.toString(),
                miaoshu: "这是我们的自定义输出内容"
            };
            return JSON.stringify(e)
        },
        parse: function (data){
            let json = JSON.parse(data);
            let newVar = CryptoJS.lib.CipherParams.create({ciphertext: CryptoJS.enc.Hex.parse(json.ct)});
            return newVar
        }
    }

其他算法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
RIPEMD160

HmacRIPEMD160

PBKDF2
keySize大小设定为4/8/16 对应输出结果位数为 128/256/512
CryptoJS.PBKDF2(message, salt, {keySize: 8, iterations: 10000})

EvpKDF
CryptoJS.EvpKDF(message, salt, {keySize: 8, iterations: 10000})

JS::RSA和数字签名

1
2
3
4
5
6
7
1. jsencrypt加密库的改写

2. jsencrypt加密库的使用

3. 给jsencrypt加密库添加NOPadding填充

4. JS数字签名算法库的使用

进阶

  1. app为什么会把代码放到so中 a) C/C++语言历史悠久,有很多现成的代码可用 b) C/C++代码执行效率比Java高 c) Java代码很容易被反编译,而且反编译以后的逻辑很清晰 d) C/C++代码反编译比较困难,反编译以后的代码也与源码有很大区别。尤其是去除符号和加入混淆以后,对加解密算法识别难度较大

  2. 为什么要学密码学进阶 a) 密码学进阶主要介绍相关算法的C实现 b) so中会接触到的东西:系统库函数、加密算法、jni调用、系统调用、自定义算法 c) so去除符号和加入混淆以后,对加解密算法识别难度较大 d) so层没有标准加密库调用,程序员需要自己实现相应算法,花样繁多 e) 不存在so层算法通杀,而且有些会魔改算法中用到的常量 f) 算法为什么能改?怎么改?改了后会不会降低算法安全性? g) 减少对算法恐惧 h) 所以了解算法的细节,对so中的算法逆向来说,很有必要

MD5

MD5(明文的处理)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. C实现的MD5算法的使用

2. 对明文进行Hex编码
xiaojianbang ==> 78 69 61 6f 6a 69 61 6e 62 61 6e 67

3. 填充
2.1 把明文填充到448bit
先填充一个1,后面跟对应个数的0
2.2 附加消息长度
1) 用64bit表示消息长度
2) 00 00 00 00 00 00 00 60 转小端序 60 00 00 00 00 00 00 00
3) 如果内容过长,64个比特放不下。就取低64bit。所以MD5输入长度可以无限大,SHA3算法也是无限大,其他哈希算法不是

4. 明文最后处理成
78 69 61 6f 6a 69 61 6e 62 61 6e 67 80 ...... 60 00 00 00 00 00 00 00
明文 填充 明文长度

5. MD5输入数据无限大,不可能一起处理,需要分组
6. MD5分组长度为512bit,数据需要处理到512的倍数,因此需要填充
7. 填充位数为1-512bit,如果明文长度刚好448bit,那么就填充512bit
8. 字节序
大端字节序、小端字节序、字节序的转换
什么时候需要转换字节序
MD5算法使用的是小端字节序
9. 把处理后的明文分成16块 M1-M16,用于后续计算
512bit / 16 = 32bit
实际需要的值 内存中小端字节序
M1 78 69 61 6f 6f 61 69 78
M2 6a 69 61 6e 6e 61 69 6a
M3 62 61 6e 67 67 6e 61 62
M4 80 00 00 00 00 00 00 80
M15 60 00 00 00 00 00 00 60
M16 0 0

MD5(初始化常量)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
2. MD5的初始化常量
A: 01 23 45 67
B: 89 ab cd ef 
C: fe dc ba 98
D: 76 54 32 10

    context->state[0] = 0x67452301;
    context->state[1] = 0xEFCDAB89;
    context->state[2] = 0x98BADCFE;
    context->state[3] = 0x10325476;

MD5Transform

image-20260208211026765

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
1. MD5总共64轮,每一轮都会把旧的D直接给新的A,旧的B直接给新的C,旧的C直接给新的D,也就是每一轮只计算一个新的B
2. 图中的田代表相加
3. 图中的F函数并不是一个函数,而是由四个函数组成
4. K表里面的值由公式计算的,但体现在代码中一般都是常量
5. <<<s代表循环左移,什么是循环左移?
6. 最后把四个初始化常量不断变化后的值,拼接得到最终的摘要结果
7. 喜欢钻牛角尖的灵魂发问
a) 初始化常量为什么是这四个
b) F函数为什么是这四个
c) K表的值为什么是这些
d) 循环左移位数为什么是这些
e) 不需要纠结这些,这些变了那就不是标准MD5算法了
f) 魔改MD5通常会魔改初始化常量,K表等
g) 算法的代码实现并非只有一种写法

8. 一般不会去改循环左移位数
有安全性问题的SHA0,通过循环左移1位,解决了这个问题。这就是后来的SHA1

SHA1

MAC

DES

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
人生若只如初见
使用 Hugo 构建
主题 StackJimmy 设计