数组转字符串 Arrays.toString(bytes) 将 byte 类型数组(byte[])转换成一个字符串表示形式

Hex编码

hex编码是一种用16个字符表示任意二进制数据的方法,是一种编码,而不是加密

1
2
3
用0-9 a-f 16个字符表示

每个十六进制字符代表4bit,也就是每2个十六进制字符代表一个字节

java实现

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String name = "你好";
byte[] bytes = name.getBytes(StandardCharsets.UTF_8);
// utf8编码下一个汉字是3个字节
System.out.println(bytes.length);
String encode = HexBin.encode(bytes);
System.out.println(encode);
}

HexBin.encode源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static public String encode(byte[] binaryData) {
if (binaryData == null)
return null;
int lengthData = binaryData.length;
int lengthEncode = lengthData * 2;
char[] encodedData = new char[lengthEncode];
int temp;
for (int i = 0; i < lengthData; i++) {
temp = binaryData[i];
if (temp < 0)
temp += 256;
encodedData[i*2] = lookUpHexAlphabet[temp >> 4];
encodedData[i*2+1] = lookUpHexAlphabet[temp & 0xf];
}
return new String(encodedData);
}

可以看到lookUpHexAlphabet就是个码表,如果把这段代码抠出来,然后换一个自定义的码表也行‘

Android实现

首先在模块的build.gradle文件中引入依赖

1
2
3
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
}

代码中

1
2
3
4
5
6
7
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("aaaaa","test");
ByteString byteString = ByteString.of("你好".getBytes());
System.out.println(byteString.hex());
}

Base64编码

是一种用64个字符表示任意二进制数据的方法

每个base64编码之后的数据中的一位等于原始数据中的6bit

Android实现

1
2
3
4
5
6
7
8
9
10
11
12
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("aaaaa","test");
ByteString byteString = ByteString.of("你好".getBytes());
System.out.println(byteString.base64());

// 方法2
Base64.getEncoder().encodeToString("你好".getBytes());
// 方法3
android.util.Base64.encodeToString("你好".getBytes(),0);
}

可以hook对应的方法,打印堆栈,来定位关键函数

消息摘要算法

算法特点

1
2
3
4
5
6
7
消息摘要算法、单向散列函数、哈希函数

不同长度输入,产生固定长度输出

散列后的密文不可逆且结果唯一

一般用于校验数据完整性,签名算法一般会把源数据和签名后的值一起提交到服务端

常见算法

MD5,sha1,sha256,sha512…

MD5

Java实现

1
2
3
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update("test".getBytes());
byte[] digest = md5.digest();

加密后的字节数据可以编码成hex或者base64

碰到加salt的md5的话,可以输入空的值,然后把加密后的结果丢去查

Android实现

1
2
3
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 这里的digest是重载方法
byte[] digest = md5.digest("test".getBytes());

SHA算法

Java实现

    MessageDigest md5 = MessageDigest.getInstance("SHA-1");
    md5.update("test".getBytes());
    byte[] digest = md5.digest();

MAC算法

与MD5和SHA的区别是多个一个密钥,密钥可以随机给

Java实现

1
2
3
4
5
SecretKeySpec secretKeySpec = new SecretKeySpec("a12346".getBytes(),"HmacSHA1");
Mac mac = Mac.getInstance(secretKeySpec.getAlgorithm());
mac.init(secretKeySpec);
mac.update("test".getBytes());
mac.doFinal();

对称加密算法

可逆、加解密密钥一样、密钥有位数要求

RC4、DES、3DES、AES

DES算法

Android实现

ECB模式

1
2
3
4
5
6
7
8
9
10
11
// getInstance不指定模式的话,默认是DES/ECB/PKCS5Padding
SecretKeySpec secretKeySpec = new SecretKeySpec("a12346".getBytes(),"DES");
Cipher des = Cipher.getInstance("DES");
des.init(Cipher.ENCRYPT_MODE, secretKeySpec);
des.doFinal("test".getBytes());

//解密
SecretKeySpec secretKeySpec = new SecretKeySpec("a12346".getBytes(),"DES");
Cipher des = Cipher.getInstance("DES");
des.init(Cipher.DECRYPT_MODE, secretKeySpec);
des.doFinal(cipherTextBytes);

CBC模式,需要iv向量

1
2
3
4
5
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("test".getBytes());

DES加密是每8个字节一组,不足8的倍数就填充,不同的填充方式填充的数据不同

ECB模式是每8个分组,对每组进行加密,得到每组加密之后的结果,不同组之间不影响

CBC模式有iv向量,iv会和第一个分组进行异或,然后再加密得到cipher1,然后cipher1再和第二个分组进行异或得到cipher2…以此类推

注意密钥,iv这些传入的时候实际上都是字节数组,不一定要用字符串.getBytes()方法去传参

1
2
byte[] desKeyBytes = new byte[]{1,2,3,4,5,6,7,8};
SecretKeySpec desKey = new SecretKeySpec(desKeyBytes,"DES");

3DES算法

又叫DESede算法

1
2
3
4
SecretKeySpec secretKeySpec = new SecretKeySpec("123456781234567812345678".getBytes(),"DESede");
Cipher des = Cipher.getInstance("DESede");
des.init(Cipher.ENCRYPT_MODE, secretKeySpec);
des.doFinal("test".getBytes());

AES算法

1
2
3
4
5
SecretKeySpec secretKeySpec = new SecretKeySpec("1234567890abcdef".getBytes(),"AES");
AlgorithParameterSpec iv = new AlgorithParameterSpec("1234567890abcdef".getBytes());
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
aes.init(1, key, iv);
aes.doFinal("test".getBytes());

填充方式

对称加密算法中,如果使用NoPadding,加密的明文长度必须等于分组长度的倍数,否则报错

如果使用PKCS5Padding,填充1-1个分组长度范围内的字节数

非对称加密算法

典型就是RSA

需要生产一个密钥对,包含公钥和私钥,公钥加密私钥解密,或者私钥加密公钥解密

单次加密长度有限制,公钥无法推导出私钥

密钥格式有PKCS1和PKCS8(这是私钥格式,Java里是PKCS8格式)

RSA_base64

私钥的格式

pkcs1格式开头 BEGIN RSA PRIVATE KEY

pkcs8格式开头 BEGIN PRIVATE KEY

RSA密钥的解析(这里指的是密钥是base64格式的)

1
2
3
4
5
6
7
8
9
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");
PublicKey privateKey = KeyFactory.generatePrivate(keySpec);

RSA加解密

1
2
3
4
5
6
7
8
9
Cipher cipher = Cipher.getInstance("RSA/None/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bt_encrypt = cipher.doFinal(plainText.getBytes());


Cipher cipher = Cipher.getInstance("RSA/None/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 这里的encText也是byte数组,如果不是要转
byte[] bt_original = cipher.doFinal(encText);

RSA模式和填充细节

None模式与ECB模式一样

NoPadding,明文最多字节数为密钥字节数,密文与密钥等长,填充字节0,每次加密后的密文不变

pkcs1Padding,明文最大字节数为密钥字节数-11(因为要填充),密文与密钥等长,每次填充的不一样,每次加密后结果不一样

RSA密钥的转换

算法还原的时候可能要用其他的语言来实现算法

有可能手头只有PEM/base64格式的密钥,没有Hex格式的密钥,这个时候就需要完成PEM和hex格式密钥之间的转换

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

1
openssl rsa -in input.pem -text

会得到modulus,publicExponent,privateExponent,有这三个就可以实现加解密了(加密只需要modulus,publicExponent,这两个在公钥里,解密的话需要privateExponent),这里默认得到的都是十六进制的数据

有少数的modulus,publicExponent,privateExponent使用十进制表示

RSA_Hex

Hex格式下的RSA加解密实现

1
2
3
4
BigInteger N = new BigInteger(stringN, 16);
BigInteger E = new BigInteger(stringE, 16);
RSAPublicKeySpec keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = KeyFactory.generatePublic(spec);

PS:Java 的 Cipher 在 NoPadding 模式下会自动在明文前填充 0 字节

常见加密算法的结合套路

随机生成AES密钥A,A密钥用于AES加密数据,得到数据密文B,使用RSA对A密钥加密,得到密文C,提交密钥密文C和数据密文B给服务器

数字签名算法

先对数据进行消息摘要,然后对摘要结果进行RSA加密

签名

1
2
3
4
5
PrivateKey priK = getPrivateKey(str_priK);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(priK);
sig.update(data.getBytes());
sig.sign();

验证

1
2
3
4
5
PublicKey priK = getPublicKey(str_pubK);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(pubK);
sig.update(data.getBytes());
sig.verify(sign);

CryptoJS

为什么选择使用js来复现算法,因为js实现的算法,可以很方便被任何语言调用

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
CryptoJS.MD5(message);
CryptoJS.HmacMD5(message, key);
CryptoJS.SHA1(message, key);
CryptoJS.HmacSHA1(message, key);
CryptoJS.SHA256(message);
CryptoJS.HmacSHA256(message, key);
CryptoJS.SHA512(message);
CryptoJS.HmacSHA512(message, key);
CryptoJS.SHA3("testtestesss", {outputLength: 256});

//输出字符串的话
CryptoJS.MD5(message + '');
CryptoJS.MD5(message).toString();

// 其他调用方式
var hasher = CryptoJS.algo.MD5.create();
hasher.reset();
hasher.update("ttttttt");
var hash = hasher.finalize();
console.log(hash + '');


var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, "12345678");
hmacHasher.reset();
hmacHasher.update('message');
var hmac = hmacHasher.finalize();

字符串转换

因为update方法除了传字符串还能传入wordArray

wordArray就是直接运行CryptoJS.MD5(message);得到的输出

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
string转wordArray
CryptoJS.enc.Utf8.parse(utf8String);
CryptoJS.enc.Hex.parse(hexString);
CryptoJS.enc.Base64.parse(base64String);
//上面的具体用哪一个,utf8还是Hex还是Base64取决于字符串的格式
//单纯的字符串test,就用utf8,一个字符对应一个字节,如果hex编码这个test字符串之后,就不能调用utf8了,就得是Hex
例如
var md5Str = "test";
//默认是utf8解析
CryptoJS.MD5(md5Str).toString();
var md5Str = "1df10923e0908cffe";
//如果不是纯字符串格式的,需要指定解析方式
var HexBytes = CryptoJS.enc.Hex.parse(md5Str);
CryptoJS.MD5(HexBytes).toString();


wordArray转string
wordArray + '';
wordArray.toString(CryptoJS.enc.Utf8);
wordArray.toString(CryptoJS.format.Hex);
wordArray.toString(CryptoJS.format.Base64);
wordArray.toString();
CryptoJS.enc.Utf8.stringify(wordArray);
// 上面的这句就相当于Java里的new String
CryptoJS.enc.Hex.stringify(wordArray);
CryptoJS.enc.Base64.stringify(wordArray);

对称加密算法

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
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);

cfg的格式
var cfg = {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
format: CryptoJS.format.Hex
};


//demo
var CryptoJS = module.exports;
var plainTextBytes = CryptoJS.enc.Utf8.parse("xiaojianbang");
var hexKeyBytes = CryptoJS.enc.Hex.parse("0102030405060708");
var Utf8IvBytes = CryptoJS.enc.Utf8.parse("12345678");
var cfg = {
iv: Utf8IvBytes,
mode: CryptoJS.mode.CBC,
// 这里来pKcs7相当于Java里的pkcs5
padding: CryptoJS.pad.Pkcs7
// format: CryptoJS.format.Hex
};
var cipherTextObj = CryptoJS.DES.encrypt(plainTextBytes, hexKeyBytes, cfg);
// toString默认输出base64编码之后的
console.log(cipherTextObj.toString());
// 如果想输出Hex格式
console.log(cipherTextObj.toString(CryptoJS.format.Hex));

对称加密算法补充

cfg中没有传mode和padding,默认使用CBC加密模式,PKCS7填充

加密结果是wordArray对象,调用toString默认转为Base64编码密文,转hex可以使用(因为wordArray对象有ciphertext属性)

1
var hexString = wordArray.ciphertext.toString()

如果不想手动调用cipherTextObj.toString(CryptoJS.format.Hex)也行,在cfg中手动指定输出格式,不指定就是base64默认

1
format: CryptoJS.format.Hex

这里的format其实就是一个对象,当调用.toString()的时候会调用format对象中的stringify方法,然后这个方法返回toString的字符串,所以可以自定义任何的返回格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
format: {
stringify: function (data){
var e = {
ct: data.ciphertext.toString(),
miaoshu: "这是自定义的输出"
};
return JSON.stringify(e);
},
}

那么调用
var cipherTextObj = CryptoJS.DES.encrypt(plainTextBytes, hexKeyBytes, cfg);
var ciphertext = ciphertextObj.toString();
就会返回
{"ct":"xxxxxxx","miaoshu":"xxxxxxx"}

同时,再解密的时候也会调用format里的parse方法,在parse方法里就需要处理json数据,提取出里面的加密数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
format: {
stringify: function (data){
var 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;
}
}

RSA加密算法的JS实现

1
2
3
4
5
6
7
8
9
10
11
var JSEncrypt = JSEncryptExports.JSEncrypt;

function getEncrtpt(plaintext, publickey){
let jsEncrypt = new JSEncrypt();
jsEncrypt.setPublicKey(publickey);
let encrypt = jsEncrypt.encrypt(plaintext);
return encrypt;
}

var publicKeyBase64 = "xxx";
console.log(getEncrtpt("password",publicKeyBase64));

补js环境的话(因为有的js文件写出来是为了在浏览器运行的,node的webstorm下会报错),有时候要把变量定义为{},有时候是window,有时候是this,有时候是global

1
2
var navigator = {};
var window = global;

给jsencrypt加密库添加nopadding填充,无非就是在明文前面添加字节0

JS数字签名算法的使用

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
var signData = "xiaojianbang";
//PKCS1格式的密钥 前缀 -----BEGIN RSA PRIVATE KEY----- 后缀 -----END RSA PRIVATE KEY-----
//PKCS8格式的密钥 前缀 -----BEGIN PRIVATE KEY----- 后缀 -----END PRIVATE KEY-----
var privateKeyBase64 = "-----BEGIN PRIVATE KEY-----MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAPFFAfEv/zFnURo2\n" +
"ZAEZmekyIJjuBHOiDqcON8ElpzK0SJUmclG6rVX8P4kppcPB62wKdJRrPrksIUdT\n" +
"T05IRh57mgKIjSqXbUQDfTpz3WhP99Ck+eBAZkctS0M5R0lWUAqeJwK+ZHbg2rI0\n" +
"oW5jwvRycHXNxrHvdF/4K1/+XEA7AgMBAAECgYEAsGkDrYWps0bW7zKb1o4Qkojb\n" +
"etZ2HNJ+ojlsHObaJOHbPGs7JXU4bmmdTz5LfSIacAoJCciMuTqCLrPEhfmkghPq\n" +
"U2MjyjfqYdXALoP7l/vt6QmjY/g1IAsaZN9nFhyjJ2WzgOx1f7gZj4NBSvTdSj7H\n" +
"m5E24zkm+p7Qw1z6/mkCQQD7WSXAXcv2v3Vo6qi1FUlkzQgCQLFYqXNSOSPpno3y\n" +
"oohUFIkMj0bYGbVE1LzV30Rb6Z8e8yQAByw6l8RuGb2PAkEA9bwb2euyOe6CcqpE\n" +
"PNFc+7UlOJAy5epVFKHbu0aNivVpU0hsphqjIGXJGHYTspyEOLqtzILqKPZr6pru\n" +
"WvJUlQJBAJoImQUZtlyCGs7wN/G5mN/ocscGpGikd+Lk16hdHbqbdpaoexCyYYUf\n" +
"xCHpicw75mW5d2V9Ngu6WZWS2rNqnOsCQCoMK//X8sEy7KNOOyrk8DIpxtqs4eix\n" +
"dil3oK+k3OdgIsubYuvxNuR+RjCnU6uGWKGUX9TUudiUgda89/gb6xkCQFm8gD6n\n" +
"AyN+PPPKRq2M84+cAbnvjdIAY3OFHfkaoWCtEj5DR0UDuVv7jN7+re2D7id/GkAe\n" +
"FAmhvYQwwLnifrw=-----END PRIVATE KEY-----";

function doSign() {
var signature = KEYUTIL.getKey(privateKeyBase64);
var hSig = signature.signString(signData, "sha256");
return hex2b64(hSig);
}

console.log(doSign());