Skip to content

Commit 16d8aaf

Browse files
authored
Strengthen encryption for elasticsearch-keystore tool to AES 256 (elastic#119749)
The latest format version (7) of KeyStoreWrapper will be using a 256 bits key for the AES cipher. Closes #ES-10419
1 parent b2f290f commit 16d8aaf

File tree

4 files changed

+63
-32
lines changed

4 files changed

+63
-32
lines changed

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

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import static org.hamcrest.Matchers.equalTo;
5959
import static org.hamcrest.Matchers.hasSize;
6060
import static org.hamcrest.Matchers.instanceOf;
61+
import static org.hamcrest.Matchers.is;
6162
import static org.hamcrest.Matchers.notNullValue;
6263

6364
public class KeyStoreWrapperTests extends ESTestCase {
@@ -436,17 +437,8 @@ public void testStringAndFileDistinction() throws Exception {
436437
public void testLegacyV3() throws GeneralSecurityException, IOException {
437438
assumeFalse("Cannot open unprotected keystore on FIPS JVM", inFipsJvm());
438439
final Path configDir = createTempDir();
439-
final Path keystore = configDir.resolve("elasticsearch.keystore");
440-
try (
441-
InputStream is = KeyStoreWrapperTests.class.getResourceAsStream("/format-v3-elasticsearch.keystore");
442-
OutputStream os = Files.newOutputStream(keystore)
443-
) {
444-
final byte[] buffer = new byte[4096];
445-
int readBytes;
446-
while ((readBytes = is.read(buffer)) > 0) {
447-
os.write(buffer, 0, readBytes);
448-
}
449-
}
440+
copyKeyStoreFromResourceToConfigDir(configDir, "/format-v3-elasticsearch.keystore");
441+
450442
final KeyStoreWrapper wrapper = KeyStoreWrapper.load(configDir);
451443
assertNotNull(wrapper);
452444
wrapper.decrypt(new char[0]);
@@ -460,9 +452,31 @@ public void testLegacyV3() throws GeneralSecurityException, IOException {
460452

461453
public void testLegacyV5() throws GeneralSecurityException, IOException {
462454
final Path configDir = createTempDir();
455+
copyKeyStoreFromResourceToConfigDir(configDir, "/format-v5-with-password-elasticsearch.keystore");
456+
457+
final KeyStoreWrapper wrapper = KeyStoreWrapper.load(configDir);
458+
assertNotNull(wrapper);
459+
wrapper.decrypt("keystorepassword".toCharArray());
460+
assertThat(wrapper.getFormatVersion(), equalTo(5));
461+
assertThat(wrapper.getSettingNames(), equalTo(Set.of("keystore.seed")));
462+
}
463+
464+
public void testLegacyV6() throws GeneralSecurityException, IOException {
465+
final Path configDir = createTempDir();
466+
copyKeyStoreFromResourceToConfigDir(configDir, "/format-v6-elasticsearch.keystore");
467+
468+
final KeyStoreWrapper wrapper = KeyStoreWrapper.load(configDir);
469+
assertNotNull(wrapper);
470+
wrapper.decrypt("keystorepassword".toCharArray());
471+
assertThat(wrapper.getFormatVersion(), equalTo(6));
472+
assertThat(wrapper.getSettingNames(), equalTo(Set.of("keystore.seed", "string")));
473+
assertThat(wrapper.getString("string"), equalTo("value"));
474+
}
475+
476+
private void copyKeyStoreFromResourceToConfigDir(Path configDir, String name) throws IOException {
463477
final Path keystore = configDir.resolve("elasticsearch.keystore");
464478
try (
465-
InputStream is = KeyStoreWrapperTests.class.getResourceAsStream("/format-v5-with-password-elasticsearch.keystore");
479+
InputStream is = KeyStoreWrapperTests.class.getResourceAsStream(name); //
466480
OutputStream os = Files.newOutputStream(keystore)
467481
) {
468482
final byte[] buffer = new byte[4096];
@@ -471,11 +485,6 @@ public void testLegacyV5() throws GeneralSecurityException, IOException {
471485
os.write(buffer, 0, readBytes);
472486
}
473487
}
474-
final KeyStoreWrapper wrapper = KeyStoreWrapper.load(configDir);
475-
assertNotNull(wrapper);
476-
wrapper.decrypt("keystorepassword".toCharArray());
477-
assertThat(wrapper.getFormatVersion(), equalTo(5));
478-
assertThat(wrapper.getSettingNames(), equalTo(Set.of("keystore.seed")));
479488
}
480489

481490
public void testSerializationNewlyCreated() throws Exception {
@@ -487,6 +496,7 @@ public void testSerializationNewlyCreated() throws Exception {
487496
wrapper.writeTo(out);
488497
final KeyStoreWrapper fromStream = new KeyStoreWrapper(out.bytes().streamInput());
489498

499+
assertThat(fromStream.getFormatVersion(), is(KeyStoreWrapper.CURRENT_VERSION));
490500
assertThat(fromStream.getSettingNames(), hasSize(2));
491501
assertThat(fromStream.getSettingNames(), containsInAnyOrder("string_setting", "keystore.seed"));
492502

Binary file not shown.

docs/changelog/119749.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 119749
2+
summary: Strengthen encryption for elasticsearch-keystore tool to AES 256
3+
area: Infra/CLI
4+
type: enhancement
5+
issues: []

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

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,17 @@ public void writeTo(StreamOutput out) throws IOException {
120120
/** The version where lucene directory API changed from BE to LE. */
121121
public static final int LE_VERSION = 5;
122122
public static final int HIGHER_KDF_ITERATION_COUNT_VERSION = 6;
123-
public static final int CURRENT_VERSION = HIGHER_KDF_ITERATION_COUNT_VERSION;
123+
public static final int CIPHER_KEY_BITS_256_VERSION = 7;
124+
public static final int CURRENT_VERSION = CIPHER_KEY_BITS_256_VERSION;
124125

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

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

131-
/**
132-
* The number of bits for the cipher key.
133-
*
134-
* Note: The Oracle JDK 8 ships with a limited JCE policy that restricts key length for AES to 128 bits.
135-
* This can be increased to 256 bits once minimum java 9 is the minimum java version.
136-
* See http://www.oracle.com/technetwork/java/javase/terms/readme/jdk9-readme-3852447.html#jce
137-
* */
138-
private static final int CIPHER_KEY_BITS = 128;
132+
/** The number of bits for the cipher key (256 bits are supported as of Java 9).*/
133+
private static final int CIPHER_KEY_BITS = 256;
139134

140135
/** The number of bits for the GCM tag. */
141136
private static final int GCM_TAG_BITS = 128;
@@ -156,6 +151,7 @@ public void writeTo(StreamOutput out) throws IOException {
156151
// 4: remove distinction between string/files, ES 6.8/7.1
157152
// 5: Lucene directory API changed to LE, ES 8.0
158153
// 6: increase KDF iteration count, ES 8.14
154+
// 7: increase cipher key length to 256 bits, ES 9.0
159155

160156
/** The metadata format version used to read the current keystore wrapper. */
161157
private final int formatVersion;
@@ -318,8 +314,9 @@ public boolean hasPassword() {
318314
return hasPassword;
319315
}
320316

321-
private static Cipher createCipher(int opmode, char[] password, byte[] salt, byte[] iv, int kdfIters) throws GeneralSecurityException {
322-
PBEKeySpec keySpec = new PBEKeySpec(password, salt, kdfIters, CIPHER_KEY_BITS);
317+
private static Cipher createCipher(int opmode, char[] password, byte[] salt, byte[] iv, int kdfIters, int cipherKeyBits)
318+
throws GeneralSecurityException {
319+
PBEKeySpec keySpec = new PBEKeySpec(password, salt, kdfIters, cipherKeyBits);
323320
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KDF_ALGO);
324321
SecretKey secretKey;
325322
try {
@@ -343,6 +340,11 @@ private static int getKdfIterationCountForVersion(int formatVersion) {
343340
return formatVersion < HIGHER_KDF_ITERATION_COUNT_VERSION ? 10000 : KDF_ITERS;
344341
}
345342

343+
private static int getCipherKeyBitsForVersion(int formatVersion) {
344+
// cipher key length was increased in version 7; it was 128 bits in previous versions
345+
return formatVersion < CIPHER_KEY_BITS_256_VERSION ? 128 : CIPHER_KEY_BITS;
346+
}
347+
346348
/**
347349
* Decrypts the underlying keystore data.
348350
*
@@ -371,7 +373,14 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
371373
throw new SecurityException("Keystore has been corrupted or tampered with", e);
372374
}
373375

374-
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, password, salt, iv, getKdfIterationCountForVersion(formatVersion));
376+
Cipher cipher = createCipher(
377+
Cipher.DECRYPT_MODE,
378+
password,
379+
salt,
380+
iv,
381+
getKdfIterationCountForVersion(formatVersion),
382+
getCipherKeyBitsForVersion(formatVersion)
383+
);
375384
try (
376385
ByteArrayInputStream bytesStream = new ByteArrayInputStream(encryptedBytes);
377386
CipherInputStream cipherStream = new CipherInputStream(bytesStream, cipher);
@@ -409,11 +418,12 @@ private static byte[] readByteArray(DataInput input) throws IOException {
409418
}
410419

411420
/** Encrypt the keystore entries and return the encrypted data. */
412-
private byte[] encrypt(char[] password, byte[] salt, byte[] iv, int kdfIterationCount) throws GeneralSecurityException, IOException {
421+
private byte[] encrypt(char[] password, byte[] salt, byte[] iv, int kdfIterationCount, int cipherKeyBits)
422+
throws GeneralSecurityException, IOException {
413423
assert isLoaded();
414424

415425
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
416-
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv, kdfIterationCount);
426+
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv, kdfIterationCount, cipherKeyBits);
417427
try (
418428
CipherOutputStream cipherStream = new CipherOutputStream(bytes, cipher);
419429
DataOutputStream output = new DataOutputStream(cipherStream)
@@ -456,7 +466,13 @@ public synchronized void save(Path configDir, char[] password, boolean preserveP
456466
byte[] iv = new byte[12];
457467
random.nextBytes(iv);
458468
// encrypted data
459-
byte[] encryptedBytes = encrypt(password, salt, iv, getKdfIterationCountForVersion(CURRENT_VERSION));
469+
byte[] encryptedBytes = encrypt(
470+
password,
471+
salt,
472+
iv,
473+
getKdfIterationCountForVersion(CURRENT_VERSION),
474+
getCipherKeyBitsForVersion(CURRENT_VERSION)
475+
);
460476

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

0 commit comments

Comments
 (0)