最近移植一个 flask 项目,里面使用了 werkzeug 进行加密。为了达到无缝转换,所以需要用 java 实现。
封面《ソーサレス*アライヴ!~the World’s End Fallen Star~》

前言

最近移植实验室的一个 flask 项目。其中使用了 werkzeug 中的 generate_password_hash, check_password_hash 两个函数来加密密码和验证密码。为了两个后端之间的无缝衔接,需要使用两个后端加密验证结果一样,因此便有了本文。

网上解决方案

这种问题我认为还是比较常见的,先在网上搜了一下。在 csdn 上找到了如下代码。按照博主所说,此代码能够无缝衔接,然而在验证过程时发现加密结果不一样。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Random;

/**
* PBKDF2_SHA256加密验证算法
*
* @author 慌途L
*/

public class Pbkdf2Sha256 {

private static final Logger logger = LoggerFactory.getLogger(Pbkdf2Sha256.class);

/**
* 盐的长度
*/
public static final int SALT_BYTE_SIZE = 16;

/**
* 生成密文的长度(例:64 * 4,密文长度为64)
*/
public static final int HASH_BIT_SIZE = 64 * 4;

/**
* 迭代次数(默认迭代次数为 2000)
*/
private static final Integer DEFAULT_ITERATIONS = 2000;

/**
* 算法名称
*/
private static final String algorithm = "PBKDF2&SHA256";

/**
* 获取密文
* @param password 密码明文
* @param salt 加盐
* @param iterations 迭代次数
* @return
*/
public static String getEncodedHash(String password, String salt, int iterations) {
// Returns only the last part of whole encoded password
SecretKeyFactory keyFactory = null;
try {
keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
} catch (NoSuchAlgorithmException e) {
logger.error("Could NOT retrieve PBKDF2WithHmacSHA256 algorithm", e);
}
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt.getBytes(Charset.forName("UTF-8")), iterations, HASH_BIT_SIZE);
SecretKey secret = null;
try {
secret = keyFactory.generateSecret(keySpec);
} catch (InvalidKeySpecException e) {
logger.error("Could NOT generate secret key", e);
}

//使用Base64进行转码密文
// byte[] rawHash = secret.getEncoded();
// byte[] hashBase64 = Base64.getEncoder().encode(rawHash);
// return new String(hashBase64);

//使用十六进制密文
return toHex(secret.getEncoded());
}

/**
* 十六进制字符串转二进制字符串
* @param hex 十六进制字符串
* @return
*/
private static byte[] fromHex(String hex) {
byte[] binary = new byte[hex.length() / 2];
for (int i = 0; i < binary.length; i++) {
binary[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return binary;
}

/**
* 二进制字符串转十六进制字符串
* @param array 二进制数组
* @return
*/
private static String toHex(byte[] array) {
BigInteger bi = new BigInteger(1, array);
String hex = bi.toString(16);
int paddingLength = (array.length * 2) - hex.length();
if (paddingLength > 0)
return String.format("%0" + paddingLength + "d", 0) + hex;
else
return hex;
}

/**
* 密文加盐 (获取‘SALT_BYTE_SIZE’长度的盐值)
* @return
*/
public static String getsalt() {
//盐值使用ASCII表的数字加大小写字母组成
int length = SALT_BYTE_SIZE;
Random rand = new Random();
char[] rs = new char[length];
for (int i = 0; i < length; i++) {
int t = rand.nextInt(3);
if (t == 0) {
rs[i] = (char) (rand.nextInt(10) + 48);
} else if (t == 1) {
rs[i] = (char) (rand.nextInt(26) + 65);
} else {
rs[i] = (char) (rand.nextInt(26) + 97);
}
}
return new String(rs);
}

/**
* 获取密文
* 默认迭代次数:2000
* @param password 明文密码
* @return
*/
public static String encode(String password) {
return encode(password, getsalt());
}

/**
* 获取密文
* @param password 明文密码
* @param iterations 迭代次数
* @return
*/
public static String encode(String password, int iterations) {
return encode(password, getsalt(), iterations);
}

/**
* 获取密文
* 默认迭代次数:2000
* @param password 明文密码
* @param salt 盐值
* @return
*/
public static String encode(String password, String salt) {
return encode(password, salt, DEFAULT_ITERATIONS);
}

/**
* 最终返回的整串密文
*
* 注:此方法返回密文字符串组成:算法名称+迭代次数+盐值+密文
* 不需要的直接用getEncodedHash方法返回的密文
*
* @param password 密码明文
* @param salt 加盐
* @param iterations 迭代次数
* @return
*/
public static String encode(String password, String salt, int iterations) {
// returns hashed password, along with algorithm, number of iterations and salt
String hash = getEncodedHash(password, salt, iterations);
return String.format("%s$%d$%s$%s", algorithm, iterations, salt, hash);
}

/**
* 验证密码
* @param password 明文
* @param hashedPassword 密文
* @return
*/
public static boolean verification(String password, String hashedPassword) {
//hashedPassword = 算法名称+迭代次数+盐值+密文;
String[] parts = hashedPassword.split("\\$");
if (parts.length != 4) {
return false;
}
//解析得到迭代次数和盐值进行盐值
Integer iterations = Integer.parseInt(parts[1]);
String salt = parts[2];
String hash = encode(password, salt, iterations);
return hash.equals(hashedPassword);
}
}

修改

这里先贴一段加密前后的代码和 python 的调用入口

1
2
3
4
5
# 加密前后的代码
# raw password
123456
# encoded password
pbkdf2:sha256:260000$hxymrVhMaA4CszrW$460d382eef1ba3fe27e34520ae4a0f9e3ab7b4b6c6bdb26133f771d7b57e9450
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
# python 调用入口
def generate_password_hash(
password: str, method: str = "pbkdf2:sha256", salt_length: int = 16
) -> str:
"""Hash a password with the given method and salt with a string of
the given length. The format of the string returned includes the method
that was used so that :func:`check_password_hash` can check the hash.

The format for the hashed string looks like this::

method$salt$hash

This method can **not** generate unsalted passwords but it is possible
to set param method='plain' in order to enforce plaintext passwords.
If a salt is used, hmac is used internally to salt the password.

If PBKDF2 is wanted it can be enabled by setting the method to
``pbkdf2:method:iterations`` where iterations is optional::

pbkdf2:sha256:80000$salt$hash
pbkdf2:sha256$salt$hash

:param password: the password to hash.
:param method: the hash method to use (one that hashlib supports). Can
optionally be in the format ``pbkdf2:method:iterations``
to enable PBKDF2.
:param salt_length: the length of the salt in letters.
"""
salt = gen_salt(salt_length) if method != "plain" else ""
h, actual_method = _hash_internal(method, salt, password)
return f"{actual_method}${salt}${h}"

根据网上的代码和 python 的注释可以得知,加密后数据分为算法 pbkdf2:sha256、迭代次数 260000、盐 hxymrVhMaA4CszrW 和加密结果 460d382eef1ba3fe27e34520ae4a0f9e3ab7b4b6c6bdb26133f771d7b57e9450 四段。
因此对上述代码的以下部分进行修改,主要修改迭代次数,算法名,拼接过程和验证过程

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
/**
* 迭代次数(默认迭代次数为 260000)
*/
private static final Integer DEFAULT_ITERATIONS = 260000;
/**
* 算法名称
*/
private static final String algorithm = "pbkdf2:sha256";
/**
* 最终返回的整串密文
*
* 注:此方法返回密文字符串组成:算法名称+迭代次数+盐值+密文 不需要的直接用getEncodedHash方法返回的密文
*
* @param password 密码明文
* @param salt 加盐
* @param iterations 迭代次数
* @return
*/
public static String encode(String password, String salt, int iterations) {
// returns hashed password, along with algorithm, number of iterations and salt
String hash = getEncodedHash(password, salt, iterations);
return String.format("%s:%d$%s$%s", algorithm, iterations, salt, hash);
}
/**
* 验证密码
*
* @param password 明文
* @param hashedPassword 密文
* @return
*/
public static boolean verification(String password, String hashedPassword) {
// hashedPassword = 算法名称+迭代次数+盐值+密文;
String[] parts = hashedPassword.split("\\$");
if (parts.length != 3) {
return false;
}
String[] parts2 = parts[0].split(":");
if (parts2.length != 3) {
return false;
}
// 解析得到迭代次数和盐值进行盐值
Integer iterations = Integer.parseInt(parts2[2]);
String salt = parts[1];
String hash = encode(password, salt, iterations);
return hash.equals(hashedPassword);
}

完整代码

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package com.zju.manager_svr.util;

import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Random;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import lombok.extern.slf4j.Slf4j;

/**
* pbkdf2_sha256 from:
* modify from https://blog.csdn.net/qq_25112523/article/details/84308134
*/
@Slf4j
public class HashUtil {

/**
* 盐的长度
*/
public static final int SALT_BYTE_SIZE = 16;

/**
* 生成密文的长度(例:64 * 4,密文长度为64)
*/
public static final int HASH_BIT_SIZE = 64 * 4;

/**
* 迭代次数(默认迭代次数为 260000)
*/
private static final Integer DEFAULT_ITERATIONS = 260000;

/**
* 算法名称
*/
private static final String algorithm = "pbkdf2:sha256";

/**
* 获取密文
*
* @param password 密码明文
* @param salt 加盐
* @param iterations 迭代次数
* @return
*/
public static String getEncodedHash(String password, String salt, int iterations) {
// Returns only the last part of whole encoded password
SecretKeyFactory keyFactory = null;
try {
keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
} catch (NoSuchAlgorithmException e) {
log.error("Could NOT retrieve PBKDF2WithHmacSHA256 algorithm", e);
}
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt.getBytes(Charset.forName("UTF-8")), iterations,
HASH_BIT_SIZE);
SecretKey secret = null;
try {
secret = keyFactory.generateSecret(keySpec);
} catch (InvalidKeySpecException e) {
log.error("Could NOT generate secret key", e);
}

// 使用Base64进行转码密文
// byte[] rawHash = secret.getEncoded();
// byte[] hashBase64 = Base64.getEncoder().encode(rawHash);
// return new String(hashBase64);

// 使用十六进制密文
return toHex(secret.getEncoded());
}

/**
* 十六进制字符串转二进制字符串
*
* @param hex 十六进制字符串
* @return
*/
private static byte[] fromHex(String hex) {
byte[] binary = new byte[hex.length() / 2];
for (int i = 0; i < binary.length; i++) {
binary[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return binary;
}

/**
* 二进制字符串转十六进制字符串
*
* @param array 二进制数组
* @return
*/
private static String toHex(byte[] array) {
BigInteger bi = new BigInteger(1, array);
String hex = bi.toString(16);
int paddingLength = (array.length * 2) - hex.length();
if (paddingLength > 0)
return String.format("%0" + paddingLength + "d", 0) + hex;
else
return hex;
}

/**
* 密文加盐 (获取‘SALT_BYTE_SIZE’长度的盐值)
*
* @return
*/
public static String getsalt() {
// 盐值使用ASCII表的数字加大小写字母组成
int length = SALT_BYTE_SIZE;
Random rand = new Random();
char[] rs = new char[length];
for (int i = 0; i < length; i++) {
int t = rand.nextInt(3);
if (t == 0) {
rs[i] = (char) (rand.nextInt(10) + 48);
} else if (t == 1) {
rs[i] = (char) (rand.nextInt(26) + 65);
} else {
rs[i] = (char) (rand.nextInt(26) + 97);
}
}
return new String(rs);
}

/**
* 获取密文 默认迭代次数:260000
*
* @param password 明文密码
* @return
*/
public static String encode(String password) {
return encode(password, getsalt());
}

/**
* 获取密文
*
* @param password 明文密码
* @param iterations 迭代次数
* @return
*/
public static String encode(String password, int iterations) {
return encode(password, getsalt(), iterations);
}

/**
* 获取密文 默认迭代次数:260000
*
* @param password 明文密码
* @param salt 盐值
* @return
*/
public static String encode(String password, String salt) {
return encode(password, salt, DEFAULT_ITERATIONS);
}

/**
* 最终返回的整串密文
*
* 注:此方法返回密文字符串组成:算法名称+迭代次数+盐值+密文 不需要的直接用getEncodedHash方法返回的密文
*
* @param password 密码明文
* @param salt 加盐
* @param iterations 迭代次数
* @return
*/
public static String encode(String password, String salt, int iterations) {
// returns hashed password, along with algorithm, number of iterations and salt
String hash = getEncodedHash(password, salt, iterations);
return String.format("%s:%d$%s$%s", algorithm, iterations, salt, hash);
}

/**
* 验证密码
*
* @param password 明文
* @param hashedPassword 密文
* @return
*/
public static boolean verification(String password, String hashedPassword) {
// hashedPassword = 算法名称+迭代次数+盐值+密文;
String[] parts = hashedPassword.split("\\$");
if (parts.length != 3) {
return false;
}
String[] parts2 = parts[0].split(":");
if (parts2.length != 3) {
return false;
}
// 解析得到迭代次数和盐值进行盐值
Integer iterations = Integer.parseInt(parts2[2]);
String salt = parts[1];
String hash = encode(password, salt, iterations);
return hash.equals(hashedPassword);
}
}

验证

编写测试类对算法进行测试,验证结果是否一样。加密后的密码由 python 端提供,测试结果都通过说明加密结果一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zju.manager_svr;

import static org.junit.jupiter.api.Assertions.assertTrue;

import com.zju.manager_svr.util.HashUtil;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class HashUtilTest {

@ParameterizedTest
@CsvSource({
"123456,pbkdf2:sha256:260000$hxymrVhMaA4CszrW$460d382eef1ba3fe27e34520ae4a0f9e3ab7b4b6c6bdb26133f771d7b57e9450",
"string,pbkdf2:sha256:260000$ygNNi7PGWBbb6QT1$82c85a39863313c75d6da0921f22d19ba501df387e9a7bdc30535ab4942109e9" })
public void passwordCheckTest(String password, String expected) {
assertTrue(HashUtil.verification(password, expected));
}
}

后记

本次移植过程中,密码加密一致大概是最麻烦的问题之一。在此解决这个问题以便日后翻阅。

参考

pbkdf2&sha256 加密验证算法 | 密码加密