基于口令的密码 基于口令的密码(Password Based Encryption, PBE) 是一种根据口令生成密钥,并使用该密钥进行加密的方法。加密和解密使用同一个密钥(即使用对称密码算法),PKCS #5 (RFC 2898) 规范描述了其实现细节,广泛使用的密码工具都对其提供了实现,如 Java 的 javax.crypto
包,密码软件 PGP
, openssl 等。
PBE 的意义 要确保消息的机密性? —-> 使用密钥 (CEK) 进行加密
如何确保密钥的机密性?—-> 用另一个密钥 (KEY) 对密钥进行加密
如何确保另一个密钥 (KEY) 的机密性? —-> 继续使用第三个密钥?死循环 ?
使用口令和盐来生成密钥 (KEK) 吧,盐可以和加密后的密钥(CEK)一起保存在磁盘上,密钥(KEK)就可以丢掉了
口令就记在脑子里吧
如果不保存一般使用的密钥,靠人类记忆没有规律,冗长的密钥是十分困难的。
PBE 的加密与解密
* PBE 的加密过程
PBE 加密
生成 KEK, 使用伪随机数生成器生成 盐 (salt) ,盐就是一个随机数,用于防御字典攻击,将盐与用户输入的口令一起作为单向散列函数的输入,最终得到的散列值就是 KEK, 用来对密钥进行加密的密钥
利用伪随机数生成器再生成会话密钥 CEK, 用 KEK 对 CEK 进行加密,并和盐保存在安全的地方。KEK 就可以丢弃了,因为只需要盐和口令就可以重建 KEK
使用会话密钥对消息进行加密
PBE 解密
重建 KEK ,利用保存的盐和用户再次输入的口令作为散列函数的输入,得到 KEK
利用 KEK 解密之前加密的 CEK, 得到会话密钥 CEK
使用 CEK 对密文进行解密
盐的作用
盐是用来防御字典攻击的,字典攻击简单来说,就是攻击者提前准备好一组可能的口令,并计算好它们的摘要,当它们窃取加密后的会话密钥后,通过将准备好的候选摘要 KEK,尝试进行破解。主要是大量的用户使用了字典的词语来设置他们的密码,所以给了攻击者机会。当使用了盐后,KEK 的可能数量会随之增大,事先生成候选的 KEK 数量会变得很大,增加了破译的难度。
拉伸
生成 KEK 时,多次使用单向散列函数可以提高安全性,即将输出再次作为单向散列函数的输入,反复多次,一般建议最少 1000 次,目的也是增加攻击者破译的难度。这种多次迭代的方法称为拉伸 (stretching)
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 enter aes-128-cbc encryption password: Verifying - enter aes-128-cbc encryption password: salt=3631C9DA382CE487 key=1004087F17C38D06C8B24CA69175984C iv =5FFD9210A708073E5AB718800F9958E6 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;public class PBECryptTest { public static void main (String[] args) { byte [] plainText = "hello" .getBytes(StandardCharsets.UTF_8); char [] password = "12345678" .toCharArray(); byte [] salt = HexUtils.fromHexString("3631C9DA382CE487" ); int iter = 1000 ; int keyLength = 128 ; byte [] iv = HexUtils.fromHexString("5FFD9210A708073E5AB718800F9958E6" ); byte [] key = derivePbkdf2Key(password, salt, iter, keyLength); byte [] cipherText1 = encryptTest(plainText, key, iv); opensslOutput(salt, cipherText1); byte [] cipherText2 = pbeEncryptTest(plainText, password, salt, iter, iv); opensslOutput(salt, cipherText2); } 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)); } @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