В этой статье обсуждается создание ключей PBE на основе пароля.
Сначала напоминание о более ранних пунктах — как правило, вы должны, когда это возможно, использовать ключ PBE в качестве главного ключа, который используется исключительно для разблокировки рабочего ключа. Это имеет три основных преимущества:
- Вы можете иметь несколько паролей, например, восстановленный ключ восстановления,
- Вы можете изменить свой пароль без необходимости перешифровывать все,
- Вы можете изменить свой рабочий ключ без необходимости менять пароль.
Я буду обсуждать использование рабочих ключей в шифровании базы данных в следующей статье.
Генерация ключа шифрования на основе пароля с помощью PBKDF2WithHmacSHA1
В прошлом у Java не было стандартного способа создания ключа PBE. Отдельные провайдеры криптографии предоставили свои собственные генераторы, но это был болезненный процесс, чтобы идентифицировать и использовать генератор, который соответствует вашему шифру.
Это изменилось в Java 7. Теперь существует стандартный алгоритм генерации ключей, который доступен во всех реализациях JCE. Это определенно может быть использовано для производства ключей AES. Я видел пример его использования для создания ключа произвольной длины, но я не смог продублировать это поведение — это может быть нестандартное расширение.
Этот алгоритм принимает четыре параметра. Первая длина ключа — используйте 128 для ключей AES. Другие возможные значения: 192 и 256 бит. Второе число итераций. Ваш Wi-Fi-маршрутизатор использует 4096 итераций, но многие сейчас рекомендуют не менее 10 000 итераций.
Третий параметр — «соль». Маршрутизатор Wi-Fi использует SSID, многие сайты используют небольшой файл, и я расскажу о другом подходе ниже. Соль должна быть достаточно большой, чтобы энтропия превышала длину ключа. Например, если вам нужен 128-битный ключ, вы должны иметь (как минимум) 128 бит случайных двоичных данных или около 22 случайных буквенно-цифровых символов.
Последний параметр — пароль. Опять же энтропия должна быть больше, чем длина ключа. В веб-приложении пароль часто предоставляется сервером приложений через JNDI.
Наконец, нам нужен и ключ шифрования, и IV, а не просто ключ шифрования. Отсутствие IV или использование слабого — одна из самых распространенных ошибок, совершаемых людьми, незнакомыми с криптографией. (См .: Не использовать случайный вектор инициализации в режиме цепочки блоков шифрования [owasp.org].) Один из распространенных подходов — генерировать случайную соль и добавлять ее к зашифрованному тексту для использования во время расшифровки, но я расскажу о другом подходе, который использует пароль и соль.
Теперь код. Сначала мы увидим, как создать ключ шифрования и IV из пароля и соли. (Мы обсудим соль через минуту.)
|
01
02
03
04
05
06
07
08
09
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
|
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); } }} |
Мы могли бы просто загрузить файл, содержащий случайные двоичные данные, но использование шифра Фейстеля позволяет смешивать энтропию из двух источников.
|
01
02
03
04
05
06
07
08
09
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
|
/** * 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; } |
Практическая реализация с использованием как пакета ресурсов (который виден в пути к классам), так и строки, загруженной из файловой системы или базы данных:
|
01
02
03
04
05
06
07
08
09
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
|
/** * 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; } |
Наконец, мы можем увидеть это на практике в двух тестовых методах:
|
01
02
03
04
05
06
07
08
09
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
|
/** * 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.