本文讨论了创建基于密码的加密PBE密钥。
首先提醒您以前的要点–通常,在实际操作中,应将PBE密钥用作主密钥,该主密钥仅用于解锁工作密钥。 这具有三个主要优点:
- 您可以有多个密码,例如,托管的恢复密钥,
- 您无需更改密码即可更改密码,
- 您可以更改工作密钥,而不必强行更改密码。
我将在以后的文章中讨论在数据库加密中使用工作密钥。
使用PBKDF2WithHmacSHA1的基于密码的加密密钥生成
Java过去没有创建PBE密钥的标准方法。 各个密码提供者提供了自己的生成器,但是识别和使用与您的密码匹配的生成器是一个痛苦的过程。
Java 7对此进行了更改。现在,所有JCE实现中都提供了一种标准的密钥生成算法。 它绝对可以用于生成AES密钥。 我已经看到了一个示例,该示例用于生成任意长度的密钥,但是我无法复制该行为–这可能是非标准扩展。
该算法采用四个参数。 第一个是密钥长度– AES密钥使用128。 其他可能的值是192和256位。 第二个是迭代次数。 您的wifi路由器使用4096次迭代,但是现在很多人建议至少进行10,000次迭代。
第三个参数是“盐”。 wifi路由器使用SSID,许多站点使用一个小文件,下面我将讨论另一种方法。 盐应足够大,以使熵大于密钥长度。 例如,如果要使用128位密钥,则应该(至少)具有128位随机二进制数据或大约22个随机字母数字字符。
最后一个参数是密码。 同样,熵应大于密钥长度。 在Webapp中,密码通常是由应用服务器通过JNDI提供的。
最后,我们既需要密码密钥,又需要IV,而不仅仅是密码密钥。 缺乏IV或使用较弱的IV是不熟悉密码学的人最常见的错误之一。 (请参阅: 不使用具有密码块链接模式的随机初始化矢量 [owasp.org]。)一种常见的方法是生成随机盐,并将其添加到密文中以供解密期间使用,但是我将讨论另一种使用密码和盐。
现在的代码。 首先,我们了解如何从密码和盐创建密码密钥和IV。 (我们待会儿讨论盐。)
public class PbkTest {
private static final Provider bc = new BouncyCastleProvider();
private static final ResourceBundle BUNDLE = ResourceBundle
.getBundle(PbkTest.class.getName());
private SecretKey cipherKey;
private AlgorithmParameterSpec ivSpec;
/**
* Create secret key and IV from password.
*
* Implementation note: I've believe I've seen other code that can extract
* the random bits for the IV directly from the PBEKeySpec but I haven't
* been able to duplicate it. It might have been a BouncyCastle extension.
*
* @throws Exception
*/
public void createKeyAndIv(char[] password) throws SecurityException,
NoSuchAlgorithmException, InvalidKeySpecException {
final String algorithm = "PBKDF2WithHmacSHA1";
final SecretKeyFactory factory = SecretKeyFactory
.getInstance(algorithm);
final int derivedKeyLength = 128;
final int iterations = 10000;
// create salt
final byte[][] salt = feistelSha1Hash(createSalt(), 1000);
// create cipher key
final PBEKeySpec cipherSpec = new PBEKeySpec(password, salt[0],
iterations, derivedKeyLength);
cipherKey = factory.generateSecret(cipherSpec);
cipherSpec.clearPassword();
// create IV. This is just one of many approaches. You do
// not want to use the same salt used in creating the PBEKey.
try {
final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", bc);
cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(
salt[1], 0, 16));
ivSpec = new IvParameterSpec(cipher.doFinal(salt[1], 4, 16));
} catch (NoSuchPaddingException e) {
throw new SecurityException("unable to create IV", e);
} catch (InvalidAlgorithmParameterException e) {
throw new SecurityException("unable to create IV", e);
} catch (InvalidKeyException e) {
throw new SecurityException("unable to create IV", e);
} catch (BadPaddingException e) {
throw new SecurityException("unable to create IV", e);
} catch (IllegalBlockSizeException e) {
throw new SecurityException("unable to create IV", e);
}
}
}
我们可以简单地加载包含随机二进制数据的文件,但是使用Feistel密码可以使我们混合来自两个来源的熵。
/**
* Create salt. Two values are provided to support creation of both a cipher
* key and IV from a single password.
*
* The 'left' salt is pulled from a file outside of the app context. this
* makes it much harder for a compromised app to obtain or modify this
* value. You could read it as classloader resource but that's not really
* different from the properties file used below. Another possibility is to
* load it from a read-only value in a database, ideally one with a
* different schema than the rest of the application. (It could even be an
* in-memory database such as H2 that contains nothing but keying material,
* again initialized from a file outside of the app context.)
*
* The 'right' salt is pulled from a properties file. It is possible to use
* a base64-encoded value but administration is a lot easier if we just take
* an arbitrary string and hash it ourselves. At a minimum it should be a
* random mix-cased string of at least (120/5 = 24) characters.
*
* The generated salts are equally strong.
*
* Implementation note: since this is for demonstration purposes a static
* string in used in place of reading an external file.
*/
public byte[][] createSalt() throws NoSuchAlgorithmException {
final MessageDigest digest = MessageDigest.getInstance("SHA1");
final byte[] left = new byte[20]; // fall back to all zeroes
final byte[] right = new byte[20]; // fall back to all zeroes
// load value from file or database.
// note: we use fixed value for demonstration purposes.
final String leftValue = "this string should be read from file or database";
if (leftValue != null) {
System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0,
left.length);
digest.reset();
}
// load value from resource bundle.
final String rightValue = BUNDLE.getString("salt");
if (rightValue != null) {
System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0,
right.length);
digest.reset();
}
final byte[][] salt = feistelSha1Hash(new byte[][] { left, right },
1000);
return salt;
}
使用资源束(在类路径中可见)和从文件系统或数据库加载的字符串的实际实现是:
/**
* Create salt. Two values are provided to support creation of both a cipher
* key and IV from a single password.
*
* The 'left' salt is pulled from a file outside of the app context. this
* makes it much harder for a compromised app to obtain or modify this
* value. You could read it as classloader resource but that's not really
* different from the properties file used below. Another possibility is to
* load it from a read-only value in a database, ideally one with a
* different schema than the rest of the application. (It could even be an
* in-memory database such as H2 that contains nothing but keying material,
* again initialized from a file outside of the app context.)
*
* The 'right' salt is pulled from a properties file. It is possible to use
* a base64-encoded value but administration is a lot easier if we just take
* an arbitrary string and hash it ourselves. At a minimum it should be a
* random mix-cased string of at least (120/5 = 24) characters.
*
* The generated salts are equally strong.
*
* Implementation note: since this is for demonstration purposes a static
* string in used in place of reading an external file.
*/
public byte[][] createSalt() throws NoSuchAlgorithmException {
final MessageDigest digest = MessageDigest.getInstance("SHA1");
final byte[] left = new byte[20]; // fall back to all zeroes
final byte[] right = new byte[20]; // fall back to all zeroes
// load value from file or database.
// note: we use fixed value for demonstration purposes.
final String leftValue = "this string should be read from file or database";
if (leftValue != null) {
System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0,
left.length);
digest.reset();
}
// load value from resource bundle.
final String rightValue = BUNDLE.getString("salt");
if (rightValue != null) {
System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0,
right.length);
digest.reset();
}
final byte[][] salt = feistelSha1Hash(new byte[][] { left, right },
1000);
return salt;
}
最后,我们可以通过两种测试方法在实践中看到它:
/**
* Obtain password. Architectually we'll want good "separation of concerns"
* and we should get the cipher key and IV from a separate place than where
* we use it.
*
* This is a unit test so the password is stored in a properties file. In
* practice we'll want to get it from JNDI from an appserver, or at least a
* file outside of the appserver's directory.
*
* @throws Exception
*/
@Before
public void setup() throws Exception {
createKeyAndIv(BUNDLE.getString("password").toCharArray());
}
/**
* Test encryption.
*
* @throws Exception
*/
@Test
public void testEncryption() throws Exception {
String plaintext = BUNDLE.getString("plaintext");
Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
byte[] actual = cipher.doFinal(plaintext.getBytes());
assertEquals(BUNDLE.getString("ciphertext"),
new String(Base64.encode(actual), Charset.forName("UTF-8")));
}
/**
* Test decryption.
*
* @throws Exception
*/
@Test
public void testEncryptionAndDecryption() throws Exception {
String ciphertext = BUNDLE.getString("ciphertext");
Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
byte[] actual = cipher.doFinal(Base64.decode(ciphertext));
assertEquals(BUNDLE.getString("plaintext"),
new String(actual, Charset.forName("UTF-8")));
}
- 完整的源代码可从http://code.google.com/p/invariant-properties-blog/source/browse/pbekey获取 。
- 另请参阅: NIST SP 800-132,基于密码的密钥派生建议 ,第5.3节。
- 另请参阅: http : //stackoverflow.com/questions/2465690/pbkdf2-hmac-sha1/2465884#2465884 ,有关创建WPA2网络主密钥的讨论。
翻译自: https://www.javacodegeeks.com/2013/10/creating-password-based-encryption-keys.html