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.
This commit is contained in:
Tim Vernum 2018-05-15 09:57:34 +10:00 committed by GitHub
parent 15790e1b56
commit 6517ac98eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 43 additions and 14 deletions

View File

@ -158,6 +158,7 @@ public class KeyStoreWrapper implements SecureSettings {
/** The decrypted secret data. See {@link #decrypt(char[])}. */
private final SetOnce<Map<String, Entry>> entries = new SetOnce<>();
private volatile boolean closed;
private KeyStoreWrapper(int formatVersion, boolean hasPassword, byte[] dataBytes) {
this.formatVersion = formatVersion;
@ -448,8 +449,8 @@ public class KeyStoreWrapper implements SecureSettings {
}
/** Write the keystore to the given config directory. */
public void save(Path configDir, char[] password) throws Exception {
assert isLoaded();
public synchronized void save(Path configDir, char[] password) throws Exception {
ensureOpen();
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
// write to tmp file first, then overwrite
@ -500,16 +501,22 @@ public class KeyStoreWrapper implements SecureSettings {
}
}
/**
* It is possible to retrieve the setting names even if the keystore is closed.
* This allows {@link SecureSetting} to correctly determine that a entry exists even though it cannot be read. Thus attempting to
* read a secure setting after the keystore is closed will generate a "keystore is closed" exception rather than using the fallback
* setting.
*/
@Override
public Set<String> getSettingNames() {
assert isLoaded();
assert entries.get() != null : "Keystore is not loaded";
return entries.get().keySet();
}
// TODO: make settings accessible only to code that registered the setting
@Override
public SecureString getString(String setting) {
assert isLoaded();
public synchronized SecureString getString(String setting) {
ensureOpen();
Entry entry = entries.get().get(setting);
if (entry == null || entry.type != EntryType.STRING) {
throw new IllegalArgumentException("Secret setting " + setting + " is not a string");
@ -520,13 +527,12 @@ public class KeyStoreWrapper implements SecureSettings {
}
@Override
public InputStream getFile(String setting) {
assert isLoaded();
public synchronized InputStream getFile(String setting) {
ensureOpen();
Entry entry = entries.get().get(setting);
if (entry == null || entry.type != EntryType.FILE) {
throw new IllegalArgumentException("Secret setting " + setting + " is not a file");
}
return new ByteArrayInputStream(entry.bytes);
}
@ -543,8 +549,8 @@ public class KeyStoreWrapper implements SecureSettings {
}
/** Set a string setting. */
void setString(String setting, char[] value) {
assert isLoaded();
synchronized void setString(String setting, char[] value) {
ensureOpen();
validateSettingName(setting);
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(value));
@ -556,8 +562,8 @@ public class KeyStoreWrapper implements SecureSettings {
}
/** Set a file setting. */
void setFile(String setting, byte[] bytes) {
assert isLoaded();
synchronized void setFile(String setting, byte[] bytes) {
ensureOpen();
validateSettingName(setting);
Entry oldEntry = entries.get().put(setting, new Entry(EntryType.FILE, Arrays.copyOf(bytes, bytes.length)));
@ -568,15 +574,23 @@ public class KeyStoreWrapper implements SecureSettings {
/** Remove the given setting from the keystore. */
void remove(String setting) {
assert isLoaded();
ensureOpen();
Entry oldEntry = entries.get().remove(setting);
if (oldEntry != null) {
Arrays.fill(oldEntry.bytes, (byte)0);
}
}
private void ensureOpen() {
if (closed) {
throw new IllegalStateException("Keystore is closed");
}
assert isLoaded() : "Keystore is not loaded";
}
@Override
public void close() {
public synchronized void close() {
this.closed = true;
for (Entry entry : entries.get().values()) {
Arrays.fill(entry.bytes, (byte)0);
}

View File

@ -48,11 +48,13 @@ import org.elasticsearch.common.Randomness;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.instanceOf;
public class KeyStoreWrapperTests extends ESTestCase {
@ -97,6 +99,19 @@ public class KeyStoreWrapperTests extends ESTestCase {
assertTrue(keystore.getSettingNames().contains(KeyStoreWrapper.SEED_SETTING.getKey()));
}
public void testCannotReadStringFromClosedKeystore() throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.create();
assertThat(keystore.getSettingNames(), Matchers.hasItem(KeyStoreWrapper.SEED_SETTING.getKey()));
assertThat(keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()), notNullValue());
keystore.close();
assertThat(keystore.getSettingNames(), Matchers.hasItem(KeyStoreWrapper.SEED_SETTING.getKey()));
final IllegalStateException exception = expectThrows(IllegalStateException.class,
() -> keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()));
assertThat(exception.getMessage(), containsString("closed"));
}
public void testUpgradeNoop() throws Exception {
KeyStoreWrapper keystore = KeyStoreWrapper.create();
SecureString seed = keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey());