微博-密码算法

基于口令的密码

基于口令的密码(Password Based Encryption, PBE) 是一种根据口令生成密钥,并使用该密钥进行加密的方法。加密和解密使用同一个密钥(即使用对称密码算法),PKCS #5 (RFC 2898) 规范描述了其实现细节,广泛使用的密码工具都对其提供了实现,如 Java 的 javax.crypto 包,密码软件 PGP, openssl 等。

PBE 的意义

要确保消息的机密性? —-> 使用密钥 (CEK) 进行加密

如何确保密钥的机密性?—-> 用另一个密钥 (KEY) 对密钥进行加密

如何确保另一个密钥 (KEY) 的机密性? —-> 继续使用第三个密钥?死循环 ?

使用口令和盐来生成密钥 (KEK) 吧,盐可以和加密后的密钥(CEK)一起保存在磁盘上,密钥(KEK)就可以丢掉了

口令就记在脑子里吧

如果不保存一般使用的密钥,靠人类记忆没有规律,冗长的密钥是十分困难的。

PBE 的加密与解密

pbe

* PBE 的加密过程

PBE 加密

  1. 生成 KEK, 使用伪随机数生成器生成 盐 (salt) ,盐就是一个随机数,用于防御字典攻击,将盐与用户输入的口令一起作为单向散列函数的输入,最终得到的散列值就是 KEK, 用来对密钥进行加密的密钥
  2. 利用伪随机数生成器再生成会话密钥 CEK, 用 KEK 对 CEK 进行加密,并和盐保存在安全的地方。KEK 就可以丢弃了,因为只需要盐和口令就可以重建 KEK
  3. 使用会话密钥对消息进行加密

PBE 解密

  1. 重建 KEK ,利用保存的盐和用户再次输入的口令作为散列函数的输入,得到 KEK
  2. 利用 KEK 解密之前加密的 CEK, 得到会话密钥 CEK
  3. 使用 CEK 对密文进行解密

盐的作用

盐是用来防御字典攻击的,字典攻击简单来说,就是攻击者提前准备好一组可能的口令,并计算好它们的摘要,当它们窃取加密后的会话密钥后,通过将准备好的候选摘要 KEK,尝试进行破解。主要是大量的用户使用了字典的词语来设置他们的密码,所以给了攻击者机会。当使用了盐后,KEK 的可能数量会随之增大,事先生成候选的 KEK 数量会变得很大,增加了破译的难度。

拉伸

生成 KEK 时,多次使用单向散列函数可以提高安全性,即将输出再次作为单向散列函数的输入,反复多次,一般建议最少 1000 次,目的也是增加攻击者破译的难度。这种多次迭代的方法称为拉伸 (stretching)

passwordEncryption

PKCS #5

RFC 8018 (Obsoletes 2898) Password-Based Cryptography Specification Version 2.1中详细描述了 PBE 的实现步骤。

PBKDF

PBKDF 即 Password Based Key Derivation Functions, 基于口令的密钥生成函数,仅包含密钥生成的定义,不包含加密和解密的过程。

PBKDF1

密钥生成过程中应用的是一个单向散列函数(hash function),如 MD2, MD5,SHA-1等, 与 PKCS #5 v1.5 中的定义兼容, 受限于单向散列函数输出的长度,生成密钥的长度也受到限制,只出于兼容性的需求使用。

PBKDF2

密钥过程中应用的是一个伪随机数生成函数(pseudorandom function), 如 HMAC with SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, and SHA-512/256,推荐在新的应用中使用

PBES

PBES 即 Password Based Encryption Schemes, 即 PBE 的策略,其实就是 PBKDF 与加密和解密过程相结合,完成密钥的生成,对消息的加密和解密。

PBES1 PBKDF1 与对称分组密码的结合

PBES2 PBKDF2 与对称分组密码的结合

How to encrypt user passwords

jasypt-How to encrypt user passwords

使用 openssl 与 JCA 进行实践

使用 openssl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
guo@DESKTOP-4L69AND:/mnt/e/learning-dir/shell-learning$ echo -n 'hello' | openssl enc -aes-128-cbc -e -base64 -pbkdf2 -iter 1000 -S 3631C9DA382CE487 -p

# 输入 password
enter aes-128-cbc encryption password:
Verifying - enter aes-128-cbc encryption password:

# Salt 盐
salt=3631C9DA382CE487

# pbkdf2 生成的密钥 secret key
key=1004087F17C38D06C8B24CA69175984C

# iv 初始化向量
iv =5FFD9210A708073E5AB718800F9958E6

# 加密后的密文 Salted__${salt}${cipherText}
U2FsdGVkX182McnaOCzkhx9oK2fDQKq6pIwPZD9892Q=

同样使用 JCA 验证

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
package org.learn.something.security.crypto;

import lombok.SneakyThrows;
import org.apache.tomcat.util.buf.HexUtils;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.spec.KeySpec;
import java.util.Base64;

/**
* @author guo
* @date 2021/4/11
*/
public class PBECryptTest {

public static void main(String[] args) {
byte[] plainText = "hello".getBytes(StandardCharsets.UTF_8);

// create key
char[] password = "12345678".toCharArray();

// salt can be acquired by Random generator
byte[] salt = HexUtils.fromHexString("3631C9DA382CE487");
int iter = 1000;
int keyLength = 128;

// init algorithm
// iv can be acquired by Random generator
byte[] iv = HexUtils.fromHexString("5FFD9210A708073E5AB718800F9958E6");

// ==============================================================================
// 1. use pbkdf2 to generate 128 bits key
// 2. use key and AES to encrypt the message
byte[] key = derivePbkdf2Key(password, salt, iter, keyLength);
byte[] cipherText1 = encryptTest(plainText, key, iv);
opensslOutput(salt, cipherText1);


// ==============================================================================
// use PBE to encrypt the message so we don't have to care about the derived key
byte[] cipherText2 = pbeEncryptTest(plainText, password, salt, iter, iv);
opensslOutput(salt, cipherText2);
}

/**
* openssl output format: Salted__${salt}${cipherText}
*
* @param salt
* @param cipherText
*/
private static void opensslOutput(byte[] salt, byte[] cipherText) {
byte[] finalBytes = new byte["Salted__".length() + salt.length + cipherText.length];
System.arraycopy("Salted__".getBytes(), 0, finalBytes, 0, "Salted__".length());
System.arraycopy(salt, 0, finalBytes, "Salted__".length(), salt.length);
System.arraycopy(cipherText, 0, finalBytes, salt.length + "Salted__".length(), cipherText.length);
System.out.println("openssl output format: " + Base64.getEncoder().encodeToString(finalBytes));
}

/**
* pbkdf2
*
* @return
*/
@SneakyThrows
private static byte[] derivePbkdf2Key(char[] password, byte[] salt, int iter, int keyLength) {
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

KeySpec pbeKeySpec = new PBEKeySpec(password, salt, iter, keyLength);
SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);

byte[] encodedKey = secretKey.getEncoded();
System.out.printf("algorithm: %s, key(hex format): %s\n", secretKey.getAlgorithm(), HexUtils.toHexString(encodedKey));
return encodedKey;
}

@SneakyThrows
public static byte[] encryptTest(byte[] plainText, byte[] key, byte[] iv) {
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");

AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance("AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
algorithmParameters.init(ivParameterSpec);

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, algorithmParameters);

return cipher.doFinal(plainText);
}

@SneakyThrows
public static byte[] pbeEncryptTest(byte[] plainText, char[] password, byte[] salt, int iter, byte[] iv) {

KeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_128");
SecretKey secretKey = factory.generateSecret(pbeKeySpec);

Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_128/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 1000, ivParameterSpec);

cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
return cipher.doFinal(plainText);
}
}
1
2
3
algorithm: PBKDF2WithHmacSHA256, key(hex format): 1004087f17c38d06c8b24ca69175984c
openssl output format: U2FsdGVkX182McnaOCzkhx9oK2fDQKq6pIwPZD9892Q=
openssl output format: U2FsdGVkX182McnaOCzkhx9oK2fDQKq6pIwPZD9892Q=

参考阅读

[1] 图解密码技术

[2] PKCS #5 rfc8018

[3] jasypt-How to encrypt user passwords

[4] openssl enc command

[5] SecretKeyFactory Algorithms

[6] 廖雪峰-口令加密算法

[7] Deriving a secret from a master key using JCE/JCA