Skip to content

Increase KDF iteration count in KeyStoreWrapper #107107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,26 @@ public void testLegacyV3() throws GeneralSecurityException, IOException {
assertThat(toByteArray(wrapper.getFile("file_setting")), equalTo("file_value".getBytes(StandardCharsets.UTF_8)));
}

public void testLegacyV5() throws GeneralSecurityException, IOException {
final Path configDir = createTempDir();
final Path keystore = configDir.resolve("elasticsearch.keystore");
try (
InputStream is = KeyStoreWrapperTests.class.getResourceAsStream("/format-v5-with-password-elasticsearch.keystore");
OutputStream os = Files.newOutputStream(keystore)
) {
final byte[] buffer = new byte[4096];
int readBytes;
while ((readBytes = is.read(buffer)) > 0) {
os.write(buffer, 0, readBytes);
}
}
final KeyStoreWrapper wrapper = KeyStoreWrapper.load(configDir);
assertNotNull(wrapper);
wrapper.decrypt("keystorepassword".toCharArray());
assertThat(wrapper.getFormatVersion(), equalTo(5));
assertThat(wrapper.getSettingNames(), equalTo(Set.of("keystore.seed")));
}

public void testSerializationNewlyCreated() throws Exception {
final KeyStoreWrapper wrapper = KeyStoreWrapper.create();
wrapper.setString("string_setting", "string_value".toCharArray());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.cli.ProcessInfo;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.settings.KeyStoreWrapper;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.env.Environment;

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

public void testKeystoreUpgradeV5() throws Exception {
assertKeystoreUpgradeWithPassword("/format-v5-with-password-elasticsearch.keystore", KeyStoreWrapper.LE_VERSION);
}

private void assertKeystoreUpgrade(String file, int version) throws Exception {
assumeFalse("Cannot open unprotected keystore on FIPS JVM", inFipsJvm());
assertKeystoreUpgrade(file, version, null);
}

private void assertKeystoreUpgradeWithPassword(String file, int version) throws Exception {
assertKeystoreUpgrade(file, version, "keystorepassword");
}

private void assertKeystoreUpgrade(String file, int version, @Nullable String password) throws Exception {
final Path keystore = KeyStoreWrapper.keystorePath(env.configFile());
try (InputStream is = KeyStoreWrapperTests.class.getResourceAsStream(file); OutputStream os = Files.newOutputStream(keystore)) {
is.transferTo(os);
Expand All @@ -56,11 +69,17 @@ private void assertKeystoreUpgrade(String file, int version) throws Exception {
assertNotNull(beforeUpgrade);
assertThat(beforeUpgrade.getFormatVersion(), equalTo(version));
}
if (password != null) {
terminal.addSecretInput(password);
terminal.addSecretInput(password);
}
execute();
terminal.reset();

try (KeyStoreWrapper afterUpgrade = KeyStoreWrapper.load(env.configFile())) {
assertNotNull(afterUpgrade);
assertThat(afterUpgrade.getFormatVersion(), equalTo(KeyStoreWrapper.CURRENT_VERSION));
afterUpgrade.decrypt(new char[0]);
afterUpgrade.decrypt(password != null ? password.toCharArray() : new char[0]);
assertThat(afterUpgrade.getSettingNames(), hasItem(KeyStoreWrapper.SEED_SETTING.getKey()));
}
}
Expand All @@ -69,5 +88,4 @@ public void testKeystoreDoesNotExist() {
final UserException e = expectThrows(UserException.class, this::execute);
assertThat(e, hasToString(containsString("keystore not found at [" + KeyStoreWrapper.keystorePath(env.configFile()) + "]")));
}

}
Binary file not shown.
5 changes: 5 additions & 0 deletions docs/changelog/107107.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 107107
summary: Increase KDF iteration count in `KeyStoreWrapper`
area: Infra/CLI
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,18 @@ public void writeTo(StreamOutput out) throws IOException {

/** The oldest metadata format version that can be read. */
private static final int MIN_FORMAT_VERSION = 3;
/** Legacy versions of the metadata written before the keystore data. */
public static final int V2_VERSION = 2;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused, so I'm removing it.

public static final int V3_VERSION = 3;
public static final int V4_VERSION = 4;
/** The version where lucene directory API changed from BE to LE. */
public static final int LE_VERSION = 5;
public static final int CURRENT_VERSION = LE_VERSION;
public static final int HIGHER_KDF_ITERATION_COUNT_VERSION = 6;
public static final int CURRENT_VERSION = HIGHER_KDF_ITERATION_COUNT_VERSION;

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

/** The number of iterations to derive the cipher key. */
private static final int KDF_ITERS = 10000;
private static final int KDF_ITERS = 210000;

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

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

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

private static int getKdfIterationCountForVersion(int formatVersion) {
// iteration count was increased in version 6; it was 10,000 in previous versions
return formatVersion < HIGHER_KDF_ITERATION_COUNT_VERSION ? 10000 : KDF_ITERS;
}

/**
* Decrypts the underlying keystore data.
*
Expand Down Expand Up @@ -365,7 +370,7 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
throw new SecurityException("Keystore has been corrupted or tampered with", e);
}

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

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

ByteArrayOutputStream bytes = new ByteArrayOutputStream();
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv);
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv, kdfIterationCount);
try (
CipherOutputStream cipherStream = new CipherOutputStream(bytes, cipher);
DataOutputStream output = new DataOutputStream(cipherStream)
Expand Down Expand Up @@ -450,7 +455,7 @@ public synchronized void save(Path configDir, char[] password, boolean preserveP
byte[] iv = new byte[12];
random.nextBytes(iv);
// encrypted data
byte[] encryptedBytes = encrypt(password, salt, iv);
byte[] encryptedBytes = encrypt(password, salt, iv, getKdfIterationCountForVersion(CURRENT_VERSION));

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