Skip to content

Commit 8830637

Browse files
authored
Increase KDF iteration count in KeyStoreWrapper (#107107)
This PR increases the KDF iteration count for the keystore password. Additional context in ES-8063.
1 parent 8ca6f50 commit 8830637

File tree

5 files changed

+60
-12
lines changed

5 files changed

+60
-12
lines changed

distribution/tools/keystore-cli/src/test/java/org/elasticsearch/cli/keystore/KeyStoreWrapperTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,26 @@ public void testLegacyV3() throws GeneralSecurityException, IOException {
457457
assertThat(toByteArray(wrapper.getFile("file_setting")), equalTo("file_value".getBytes(StandardCharsets.UTF_8)));
458458
}
459459

460+
public void testLegacyV5() throws GeneralSecurityException, IOException {
461+
final Path configDir = createTempDir();
462+
final Path keystore = configDir.resolve("elasticsearch.keystore");
463+
try (
464+
InputStream is = KeyStoreWrapperTests.class.getResourceAsStream("/format-v5-with-password-elasticsearch.keystore");
465+
OutputStream os = Files.newOutputStream(keystore)
466+
) {
467+
final byte[] buffer = new byte[4096];
468+
int readBytes;
469+
while ((readBytes = is.read(buffer)) > 0) {
470+
os.write(buffer, 0, readBytes);
471+
}
472+
}
473+
final KeyStoreWrapper wrapper = KeyStoreWrapper.load(configDir);
474+
assertNotNull(wrapper);
475+
wrapper.decrypt("keystorepassword".toCharArray());
476+
assertThat(wrapper.getFormatVersion(), equalTo(5));
477+
assertThat(wrapper.getSettingNames(), equalTo(Set.of("keystore.seed")));
478+
}
479+
460480
public void testSerializationNewlyCreated() throws Exception {
461481
final KeyStoreWrapper wrapper = KeyStoreWrapper.create();
462482
wrapper.setString("string_setting", "string_value".toCharArray());

distribution/tools/keystore-cli/src/test/java/org/elasticsearch/cli/keystore/UpgradeKeyStoreCommandTests.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.cli.ProcessInfo;
1515
import org.elasticsearch.cli.UserException;
1616
import org.elasticsearch.common.settings.KeyStoreWrapper;
17+
import org.elasticsearch.core.Nullable;
1718
import org.elasticsearch.env.Environment;
1819

1920
import java.io.InputStream;
@@ -46,8 +47,20 @@ public void testKeystoreUpgradeV4() throws Exception {
4647
assertKeystoreUpgrade("/format-v4-elasticsearch.keystore", KeyStoreWrapper.V4_VERSION);
4748
}
4849

50+
public void testKeystoreUpgradeV5() throws Exception {
51+
assertKeystoreUpgradeWithPassword("/format-v5-with-password-elasticsearch.keystore", KeyStoreWrapper.LE_VERSION);
52+
}
53+
4954
private void assertKeystoreUpgrade(String file, int version) throws Exception {
5055
assumeFalse("Cannot open unprotected keystore on FIPS JVM", inFipsJvm());
56+
assertKeystoreUpgrade(file, version, null);
57+
}
58+
59+
private void assertKeystoreUpgradeWithPassword(String file, int version) throws Exception {
60+
assertKeystoreUpgrade(file, version, "keystorepassword");
61+
}
62+
63+
private void assertKeystoreUpgrade(String file, int version, @Nullable String password) throws Exception {
5164
final Path keystore = KeyStoreWrapper.keystorePath(env.configFile());
5265
try (InputStream is = KeyStoreWrapperTests.class.getResourceAsStream(file); OutputStream os = Files.newOutputStream(keystore)) {
5366
is.transferTo(os);
@@ -56,11 +69,17 @@ private void assertKeystoreUpgrade(String file, int version) throws Exception {
5669
assertNotNull(beforeUpgrade);
5770
assertThat(beforeUpgrade.getFormatVersion(), equalTo(version));
5871
}
72+
if (password != null) {
73+
terminal.addSecretInput(password);
74+
terminal.addSecretInput(password);
75+
}
5976
execute();
77+
terminal.reset();
78+
6079
try (KeyStoreWrapper afterUpgrade = KeyStoreWrapper.load(env.configFile())) {
6180
assertNotNull(afterUpgrade);
6281
assertThat(afterUpgrade.getFormatVersion(), equalTo(KeyStoreWrapper.CURRENT_VERSION));
63-
afterUpgrade.decrypt(new char[0]);
82+
afterUpgrade.decrypt(password != null ? password.toCharArray() : new char[0]);
6483
assertThat(afterUpgrade.getSettingNames(), hasItem(KeyStoreWrapper.SEED_SETTING.getKey()));
6584
}
6685
}
@@ -69,5 +88,4 @@ public void testKeystoreDoesNotExist() {
6988
final UserException e = expectThrows(UserException.class, this::execute);
7089
assertThat(e, hasToString(containsString("keystore not found at [" + KeyStoreWrapper.keystorePath(env.configFile()) + "]")));
7190
}
72-
7391
}

docs/changelog/107107.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 107107
2+
summary: Increase KDF iteration count in `KeyStoreWrapper`
3+
area: Infra/CLI
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,18 @@ public void writeTo(StreamOutput out) throws IOException {
114114

115115
/** The oldest metadata format version that can be read. */
116116
private static final int MIN_FORMAT_VERSION = 3;
117-
/** Legacy versions of the metadata written before the keystore data. */
118-
public static final int V2_VERSION = 2;
119117
public static final int V3_VERSION = 3;
120118
public static final int V4_VERSION = 4;
121119
/** The version where lucene directory API changed from BE to LE. */
122120
public static final int LE_VERSION = 5;
123-
public static final int CURRENT_VERSION = LE_VERSION;
121+
public static final int HIGHER_KDF_ITERATION_COUNT_VERSION = 6;
122+
public static final int CURRENT_VERSION = HIGHER_KDF_ITERATION_COUNT_VERSION;
124123

125124
/** The algorithm used to derive the cipher key from a password. */
126125
private static final String KDF_ALGO = "PBKDF2WithHmacSHA512";
127126

128127
/** The number of iterations to derive the cipher key. */
129-
private static final int KDF_ITERS = 10000;
128+
private static final int KDF_ITERS = 210000;
130129

131130
/**
132131
* The number of bits for the cipher key.
@@ -155,6 +154,7 @@ public void writeTo(StreamOutput out) throws IOException {
155154
// 3: FIPS compliant algos, ES 6.3
156155
// 4: remove distinction between string/files, ES 6.8/7.1
157156
// 5: Lucene directory API changed to LE, ES 8.0
157+
// 6: increase KDF iteration count, ES 8.14
158158

159159
/** The metadata format version used to read the current keystore wrapper. */
160160
private final int formatVersion;
@@ -317,8 +317,8 @@ public boolean hasPassword() {
317317
return hasPassword;
318318
}
319319

320-
private static Cipher createCipher(int opmode, char[] password, byte[] salt, byte[] iv) throws GeneralSecurityException {
321-
PBEKeySpec keySpec = new PBEKeySpec(password, salt, KDF_ITERS, CIPHER_KEY_BITS);
320+
private static Cipher createCipher(int opmode, char[] password, byte[] salt, byte[] iv, int kdfIters) throws GeneralSecurityException {
321+
PBEKeySpec keySpec = new PBEKeySpec(password, salt, kdfIters, CIPHER_KEY_BITS);
322322
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KDF_ALGO);
323323
SecretKey secretKey;
324324
try {
@@ -337,6 +337,11 @@ private static Cipher createCipher(int opmode, char[] password, byte[] salt, byt
337337
return cipher;
338338
}
339339

340+
private static int getKdfIterationCountForVersion(int formatVersion) {
341+
// iteration count was increased in version 6; it was 10,000 in previous versions
342+
return formatVersion < HIGHER_KDF_ITERATION_COUNT_VERSION ? 10000 : KDF_ITERS;
343+
}
344+
340345
/**
341346
* Decrypts the underlying keystore data.
342347
*
@@ -365,7 +370,7 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
365370
throw new SecurityException("Keystore has been corrupted or tampered with", e);
366371
}
367372

368-
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, password, salt, iv);
373+
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, password, salt, iv, getKdfIterationCountForVersion(formatVersion));
369374
try (
370375
ByteArrayInputStream bytesStream = new ByteArrayInputStream(encryptedBytes);
371376
CipherInputStream cipherStream = new CipherInputStream(bytesStream, cipher);
@@ -403,11 +408,11 @@ private static byte[] readByteArray(DataInput input) throws IOException {
403408
}
404409

405410
/** Encrypt the keystore entries and return the encrypted data. */
406-
private byte[] encrypt(char[] password, byte[] salt, byte[] iv) throws GeneralSecurityException, IOException {
411+
private byte[] encrypt(char[] password, byte[] salt, byte[] iv, int kdfIterationCount) throws GeneralSecurityException, IOException {
407412
assert isLoaded();
408413

409414
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
410-
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv);
415+
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv, kdfIterationCount);
411416
try (
412417
CipherOutputStream cipherStream = new CipherOutputStream(bytes, cipher);
413418
DataOutputStream output = new DataOutputStream(cipherStream)
@@ -450,7 +455,7 @@ public synchronized void save(Path configDir, char[] password, boolean preserveP
450455
byte[] iv = new byte[12];
451456
random.nextBytes(iv);
452457
// encrypted data
453-
byte[] encryptedBytes = encrypt(password, salt, iv);
458+
byte[] encryptedBytes = encrypt(password, salt, iv, getKdfIterationCountForVersion(CURRENT_VERSION));
454459

455460
// size of data block
456461
output.writeInt(4 + salt.length + 4 + iv.length + 4 + encryptedBytes.length);

0 commit comments

Comments
 (0)