Skip to content

Commit cea86bb

Browse files
committed
Fail if reading from closed KeyStoreWrapper (#30394)
In #28255 the implementation of the elasticsearch.keystore was changed to no longer be built on top of a PKCS#12 keystore. A side effect of that change was that calling getString or getFile on a closed KeyStoreWrapper ceased to throw an exception, and would instead return a value consisting of all 0 bytes. This change restores the previous behaviour as closely as possible. It is possible to retrieve the _keys_ from a closed keystore, but any attempt to get or set the entries will throw an IllegalStateException.
1 parent 9120370 commit cea86bb

File tree

2 files changed

+45
-14
lines changed

2 files changed

+45
-14
lines changed

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

+28-14
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ private static class Entry {
157157

158158
/** The decrypted secret data. See {@link #decrypt(char[])}. */
159159
private final SetOnce<Map<String, Entry>> entries = new SetOnce<>();
160+
private volatile boolean closed;
160161

161162
private KeyStoreWrapper(int formatVersion, boolean hasPassword, byte[] dataBytes) {
162163
this.formatVersion = formatVersion;
@@ -448,8 +449,8 @@ private void decryptLegacyEntries() throws GeneralSecurityException, IOException
448449
}
449450

450451
/** Write the keystore to the given config directory. */
451-
public void save(Path configDir, char[] password) throws Exception {
452-
assert isLoaded();
452+
public synchronized void save(Path configDir, char[] password) throws Exception {
453+
ensureOpen();
453454

454455
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
455456
// write to tmp file first, then overwrite
@@ -500,16 +501,22 @@ public void save(Path configDir, char[] password) throws Exception {
500501
}
501502
}
502503

504+
/**
505+
* It is possible to retrieve the setting names even if the keystore is closed.
506+
* This allows {@link SecureSetting} to correctly determine that a entry exists even though it cannot be read. Thus attempting to
507+
* read a secure setting after the keystore is closed will generate a "keystore is closed" exception rather than using the fallback
508+
* setting.
509+
*/
503510
@Override
504511
public Set<String> getSettingNames() {
505-
assert isLoaded();
512+
assert entries.get() != null : "Keystore is not loaded";
506513
return entries.get().keySet();
507514
}
508515

509516
// TODO: make settings accessible only to code that registered the setting
510517
@Override
511-
public SecureString getString(String setting) {
512-
assert isLoaded();
518+
public synchronized SecureString getString(String setting) {
519+
ensureOpen();
513520
Entry entry = entries.get().get(setting);
514521
if (entry == null || entry.type != EntryType.STRING) {
515522
throw new IllegalArgumentException("Secret setting " + setting + " is not a string");
@@ -520,13 +527,12 @@ public SecureString getString(String setting) {
520527
}
521528

522529
@Override
523-
public InputStream getFile(String setting) {
524-
assert isLoaded();
530+
public synchronized InputStream getFile(String setting) {
531+
ensureOpen();
525532
Entry entry = entries.get().get(setting);
526533
if (entry == null || entry.type != EntryType.FILE) {
527534
throw new IllegalArgumentException("Secret setting " + setting + " is not a file");
528535
}
529-
530536
return new ByteArrayInputStream(entry.bytes);
531537
}
532538

@@ -543,8 +549,8 @@ public static void validateSettingName(String setting) {
543549
}
544550

545551
/** Set a string setting. */
546-
void setString(String setting, char[] value) {
547-
assert isLoaded();
552+
synchronized void setString(String setting, char[] value) {
553+
ensureOpen();
548554
validateSettingName(setting);
549555

550556
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(value));
@@ -556,8 +562,8 @@ void setString(String setting, char[] value) {
556562
}
557563

558564
/** Set a file setting. */
559-
void setFile(String setting, byte[] bytes) {
560-
assert isLoaded();
565+
synchronized void setFile(String setting, byte[] bytes) {
566+
ensureOpen();
561567
validateSettingName(setting);
562568

563569
Entry oldEntry = entries.get().put(setting, new Entry(EntryType.FILE, Arrays.copyOf(bytes, bytes.length)));
@@ -568,15 +574,23 @@ void setFile(String setting, byte[] bytes) {
568574

569575
/** Remove the given setting from the keystore. */
570576
void remove(String setting) {
571-
assert isLoaded();
577+
ensureOpen();
572578
Entry oldEntry = entries.get().remove(setting);
573579
if (oldEntry != null) {
574580
Arrays.fill(oldEntry.bytes, (byte)0);
575581
}
576582
}
577583

584+
private void ensureOpen() {
585+
if (closed) {
586+
throw new IllegalStateException("Keystore is closed");
587+
}
588+
assert isLoaded() : "Keystore is not loaded";
589+
}
590+
578591
@Override
579-
public void close() {
592+
public synchronized void close() {
593+
this.closed = true;
580594
for (Entry entry : entries.get().values()) {
581595
Arrays.fill(entry.bytes, (byte)0);
582596
}

server/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java

+17
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,14 @@
4545
import org.elasticsearch.bootstrap.BootstrapSettings;
4646
import org.elasticsearch.env.Environment;
4747
import org.elasticsearch.test.ESTestCase;
48+
import org.hamcrest.Matchers;
4849
import org.junit.After;
4950
import org.junit.Before;
5051

52+
import static org.hamcrest.Matchers.containsString;
5153
import static org.hamcrest.Matchers.equalTo;
54+
import static org.hamcrest.Matchers.notNullValue;
55+
import static org.hamcrest.Matchers.instanceOf;
5256

5357
public class KeyStoreWrapperTests extends ESTestCase {
5458

@@ -92,6 +96,19 @@ public void testCreate() throws Exception {
9296
assertTrue(keystore.getSettingNames().contains(KeyStoreWrapper.SEED_SETTING.getKey()));
9397
}
9498

99+
public void testCannotReadStringFromClosedKeystore() throws Exception {
100+
KeyStoreWrapper keystore = KeyStoreWrapper.create();
101+
assertThat(keystore.getSettingNames(), Matchers.hasItem(KeyStoreWrapper.SEED_SETTING.getKey()));
102+
assertThat(keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()), notNullValue());
103+
104+
keystore.close();
105+
106+
assertThat(keystore.getSettingNames(), Matchers.hasItem(KeyStoreWrapper.SEED_SETTING.getKey()));
107+
final IllegalStateException exception = expectThrows(IllegalStateException.class,
108+
() -> keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()));
109+
assertThat(exception.getMessage(), containsString("closed"));
110+
}
111+
95112
public void testUpgradeNoop() throws Exception {
96113
KeyStoreWrapper keystore = KeyStoreWrapper.create();
97114
SecureString seed = keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey());

0 commit comments

Comments
 (0)