From bc6541eb5a0f1a2e8a898b69efbfb56bd3a1f0b2 Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Wed, 17 Sep 2025 08:13:32 -0700 Subject: [PATCH 1/4] Add a key manager using a key store and convert key loading to use this for existing tests. --- .../KeyStoreSerializationKeyManager.java | 231 ++++++++++++++++++ .../recordlayer/storage/StoreConfig.java | 63 ++--- 2 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java new file mode 100644 index 0000000000..b0c0904909 --- /dev/null +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java @@ -0,0 +1,231 @@ +/* + * KeyStoreSerializationKeyManager.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.record.provider.common; + +import com.apple.foundationdb.annotation.API; +import com.apple.foundationdb.record.RecordCoreArgumentException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.List; +import java.util.Random; + + +/** + * A {@link SerializationKeyManager} to uses a {@link KeyStore}. + *

+ * Key numbers are mapped to and from key names, which are looked up in the store. + */ +@API(API.Status.EXPERIMENTAL) +public class KeyStoreSerializationKeyManager implements SerializationKeyManager { + @Nonnull + private final KeyStore keyStore; + @Nonnull + private final KeyStore.ProtectionParameter keyEntryPassword; + @Nonnull + private final List keyEntryAliases; + private final int defaultKeyNumber; + @Nonnull + private final String cipherName; + @Nonnull + private final SecureRandom secureRandom; + + protected KeyStoreSerializationKeyManager(@Nonnull KeyStore keyStore, @Nonnull KeyStore.ProtectionParameter keyEntryPassword, + @Nonnull List keyEntryAliases, int defaultKeyNumber, + @Nonnull String cipherName, @Nonnull SecureRandom secureRandom) { + this.keyStore = keyStore; + this.keyEntryPassword = keyEntryPassword; + this.keyEntryAliases = keyEntryAliases; + this.defaultKeyNumber = defaultKeyNumber; + this.cipherName = cipherName; + this.secureRandom = secureRandom; + } + + @Override + public int getSerializationKey() { + return defaultKeyNumber; + } + + @Override + public Key getKey(int keyNumber) { + if (keyNumber < 0 || keyNumber >= keyEntryAliases.size()) { + throw new RecordSerializationException("key number out of range"); + } + final String keyEntryAlias = keyEntryAliases.get(keyNumber); + final KeyStore.SecretKeyEntry entry; + try { + entry = (KeyStore.SecretKeyEntry)keyStore.getEntry(keyEntryAlias, keyEntryPassword); + } catch (GeneralSecurityException ex) { + throw new RecordSerializationException("cannot load key", ex); + } + return entry.getSecretKey(); + } + + @Override + public String getCipher(int keyNumber) { + return cipherName; + } + + @Override + public Random getRandom(int keyNumber) { + return secureRandom; + } + + @Nonnull + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + @Nullable + String keyStoreFileName; + @Nullable + String keyStorePassword; + @Nullable + String keyEntryPassword; + @Nullable + String defaultKeyEntryAlias; + @Nullable + List keyEntryAliases; + @Nonnull + String cipherName = CipherPool.DEFAULT_CIPHER; + @Nullable + SecureRandom secureRandom; + + protected Builder() { + } + + @Nullable + public String getKeyStoreFileName() { + return keyStoreFileName; + } + + public void setKeyStoreFileName(@Nonnull String keyStoreFileName) { + this.keyStoreFileName = keyStoreFileName; + } + + @Nullable + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(@Nonnull String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + @Nullable + public String getKeyEntryPassword() { + return keyEntryPassword; + } + + public void setKeyEntryPassword(@Nonnull String keyEntryPassword) { + this.keyEntryPassword = keyEntryPassword; + } + + @Nullable + public String getDefaultKeyEntryAlias() { + return defaultKeyEntryAlias; + } + + public void setDefaultKeyEntryAlias(@Nonnull String defaultKeyEntryAlias) { + this.defaultKeyEntryAlias = defaultKeyEntryAlias; + } + + @Nullable + public List getKeyEntryAliases() { + return keyEntryAliases; + } + + public void setKeyEntryAliases(@Nonnull List keyEntryAliases) { + this.keyEntryAliases = keyEntryAliases; + } + + @Nonnull + public String getCipherName() { + return cipherName; + } + + public void setCipherName(@Nonnull String cipherName) { + this.cipherName = cipherName; + } + + @Nullable + public SecureRandom getSecureRandom() { + return secureRandom; + } + + public void setSecureRandom(@Nonnull SecureRandom secureRandom) { + this.secureRandom = secureRandom; + } + + @Nonnull + public KeyStoreSerializationKeyManager build() { + if (keyStoreFileName == null) { + throw new RecordCoreArgumentException("must specify key store file name"); + } + if (keyStorePassword == null) { + keyStorePassword = ""; + } + final KeyStore keyStore; + try { + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + try (FileInputStream fis = new FileInputStream(keyStoreFileName)) { + keyStore.load(fis, keyStorePassword.toCharArray()); + } + } catch (FileNotFoundException ex) { + throw new RecordCoreArgumentException("Key store not found", ex); + } catch (GeneralSecurityException | IOException ex) { + throw new RecordCoreArgumentException("Key store loading failed", ex); + } + if (keyEntryPassword == null) { + keyEntryPassword = keyStorePassword; + } + final KeyStore.ProtectionParameter keyEntryProtection = new KeyStore.PasswordProtection(keyEntryPassword.toCharArray()); + if (keyEntryAliases == null && defaultKeyEntryAlias == null) { + throw new RecordCoreArgumentException("must specify key alias list or single default alias"); + } + final int defaultKeyNumber; + if (keyEntryAliases == null) { + keyEntryAliases = List.of(defaultKeyEntryAlias); + defaultKeyNumber = 0; + } else if (defaultKeyEntryAlias == null) { + defaultKeyNumber = keyEntryAliases.size() - 1; + } else { + defaultKeyNumber = keyEntryAliases.indexOf(defaultKeyEntryAlias); + if (defaultKeyNumber < 0) { + throw new RecordCoreArgumentException("default key alias not in key alias list"); + } + } + if (secureRandom == null) { + secureRandom = new SecureRandom(); + } + return new KeyStoreSerializationKeyManager(keyStore, keyEntryProtection, keyEntryAliases, defaultKeyNumber, + cipherName, secureRandom); + } + } +} diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java index ea7d446189..f841723bf8 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java @@ -24,7 +24,9 @@ import com.apple.foundationdb.record.RecordCoreException; import com.apple.foundationdb.record.RecordMetaDataProvider; import com.apple.foundationdb.record.metadata.MetaDataException; +import com.apple.foundationdb.record.provider.common.KeyStoreSerializationKeyManager; import com.apple.foundationdb.record.provider.common.RecordSerializer; +import com.apple.foundationdb.record.provider.common.SerializationKeyManager; import com.apple.foundationdb.record.provider.common.TransformedRecordSerializer; import com.apple.foundationdb.record.provider.common.TransformedRecordSerializerJCE; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; @@ -41,13 +43,9 @@ import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil; import com.google.protobuf.Message; -import javax.crypto.SecretKey; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.net.URI; -import java.security.GeneralSecurityException; -import java.security.KeyStore; import java.util.zip.Deflater; @API(API.Status.EXPERIMENTAL) @@ -127,11 +125,12 @@ public static StoreConfig create(RecordLayerConfig recordLayerConfig, return new StoreConfig(recordLayerConfig, schemaName, schemaPath, metaDataProvider, serializer); } - static RecordSerializer serializerFromOptions(Options options) throws RelationalException { + @Nonnull + static RecordSerializer serializerFromOptions(@Nonnull Options options) throws RelationalException { final boolean encrypted = options.getOption(Options.Name.ENCRYPT_WHEN_SERIALIZING); final boolean compressed = options.getOption(Options.Name.COMPRESS_WHEN_SERIALIZING); - final String keyStoreFile = options.getOption(Options.Name.ENCRYPTION_KEY_STORE); - if (!encrypted && compressed && keyStoreFile == null) { + final SerializationKeyManager keyManager = keyManagerFromOptions(options); + if (!encrypted && compressed && keyManager == null) { return DEFAULT_RELATIONAL_SERIALIZER; } final TransformedRecordSerializerJCE.Builder serializerBuilder = TransformedRecordSerializerJCE.newDefaultBuilder() @@ -139,30 +138,34 @@ static RecordSerializer serializerFromOptions(Options options) throws R .setCompressWhenSerializing(compressed) .setCompressionLevel(Deflater.DEFAULT_COMPRESSION) .setWriteValidationRatio(0.0); - if (keyStoreFile != null) { - final SecretKey key; - try { - final String keyEntryAlias = options.getOption(Options.Name.ENCRYPTION_KEY_ENTRY); - if (keyEntryAlias == null) { - throw new RelationalException("Key entry not specified", ErrorCode.UNSUPPORTED_OPERATION); - } - final String keyPassword = options.getOption(Options.Name.ENCRYPTION_KEY_PASSWORD); - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (FileInputStream fis = new FileInputStream(keyStoreFile)) { - keystore.load(fis, keyPassword.toCharArray()); - } - KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(keyPassword.toCharArray()); - KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry)keystore.getEntry(keyEntryAlias, protParam); - key = entry.getSecretKey(); - } catch (FileNotFoundException ex) { - throw new RelationalException("Key store not found", ErrorCode.UNSUPPORTED_OPERATION, ex); - } catch (GeneralSecurityException | IOException ex) { - throw new RelationalException("Key loading failed", ErrorCode.UNSUPPORTED_OPERATION, ex); - } - serializerBuilder.setEncryptionKey(key); + if (keyManager != null) { + serializerBuilder.setKeyManager(keyManager); } else if (encrypted) { throw new RelationalException("Key store not specified", ErrorCode.UNSUPPORTED_OPERATION); } return serializerBuilder.build(); } + + @Nullable + static SerializationKeyManager keyManagerFromOptions(@Nonnull Options options) throws RelationalException { + final String keyStoreFileName = options.getOption(Options.Name.ENCRYPTION_KEY_STORE); + if (keyStoreFileName == null) { + return null; + } + final KeyStoreSerializationKeyManager.Builder builder = KeyStoreSerializationKeyManager.newBuilder(); + builder.setKeyStoreFileName(keyStoreFileName); + final String defaultKeyEntryAlias = options.getOption(Options.Name.ENCRYPTION_KEY_ENTRY); + if (defaultKeyEntryAlias != null) { + builder.setDefaultKeyEntryAlias(defaultKeyEntryAlias); + } + final String keyStorePassword = options.getOption(Options.Name.ENCRYPTION_KEY_PASSWORD); + if (keyStorePassword != null) { + builder.setKeyStorePassword(keyStorePassword); + } + try { + return builder.build(); + } catch (RecordCoreException ex) { + throw new RelationalException("problem with encryption options", ErrorCode.UNSUPPORTED_OPERATION, ex); + } + } } From 93cfbf2fa880e8b2a7bad60b1cbd1d578e0feab5 Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Wed, 17 Sep 2025 11:13:54 -0700 Subject: [PATCH 2/4] Add another option to specify a list of multiple keys. Add a new test block using it showing reading with different keys, restricted to the current version, as because old versions do not understand the new option. --- .../foundationdb/relational/api/Options.java | 7 ++++++ .../relational/utils/OptionsTestHelper.java | 1 + .../recordlayer/storage/StoreConfig.java | 5 +++++ .../relational/jdbc/TypeConversion.java | 8 +++++++ .../proto/grpc/relational/jdbc/v1/jdbc.proto | 1 + .../resources/serialization-options.yamsql | 22 +++++++++++++++++++ 6 files changed, 44 insertions(+) diff --git a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java index 69a37404f7..cefc720460 100644 --- a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java +++ b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java @@ -241,6 +241,11 @@ public enum Name { */ ENCRYPTION_KEY_ENTRY, + /** + * All the key store entries available as encryption keys. + */ + ENCRYPTION_KEY_ENTRY_LIST, + /** * The integrity key of the key store and the encryption key of the key entry. */ @@ -496,6 +501,7 @@ public static Properties toProperties(final Options options) { prop = bytesToHex(((Continuation)entry.getValue()).serialize()); break; case DISABLED_PLANNER_RULES: + case ENCRYPTION_KEY_ENTRY_LIST: prop = String.join(",", (Collection)entry.getValue()); break; default: @@ -549,6 +555,7 @@ private static Map> makeContracts() { data.put(Name.ENCRYPT_WHEN_SERIALIZING, List.of(TypeContract.booleanType())); data.put(Name.ENCRYPTION_KEY_STORE, List.of(TypeContract.nullableStringType())); data.put(Name.ENCRYPTION_KEY_ENTRY, List.of(TypeContract.nullableStringType())); + data.put(Name.ENCRYPTION_KEY_ENTRY_LIST, List.of(new CollectionContract<>(TypeContract.stringType()))); data.put(Name.ENCRYPTION_KEY_PASSWORD, List.of(TypeContract.nullableStringType())); data.put(Name.COMPRESS_WHEN_SERIALIZING, List.of(TypeContract.booleanType())); diff --git a/fdb-relational-api/src/testFixtures/java/com/apple/foundationdb/relational/utils/OptionsTestHelper.java b/fdb-relational-api/src/testFixtures/java/com/apple/foundationdb/relational/utils/OptionsTestHelper.java index fceed5ffc1..2442a7cfcf 100644 --- a/fdb-relational-api/src/testFixtures/java/com/apple/foundationdb/relational/utils/OptionsTestHelper.java +++ b/fdb-relational-api/src/testFixtures/java/com/apple/foundationdb/relational/utils/OptionsTestHelper.java @@ -62,6 +62,7 @@ public static Options nonDefaultOptions() throws SQLException { builder = builder.withOption(Options.Name.ENCRYPT_WHEN_SERIALIZING, true); builder = builder.withOption(Options.Name.ENCRYPTION_KEY_STORE, "secrets.ks"); builder = builder.withOption(Options.Name.ENCRYPTION_KEY_ENTRY, "mykey"); + builder = builder.withOption(Options.Name.ENCRYPTION_KEY_ENTRY_LIST, List.of("mykey", "anotherkey")); builder = builder.withOption(Options.Name.ENCRYPTION_KEY_PASSWORD, "mypass"); builder = builder.withOption(Options.Name.COMPRESS_WHEN_SERIALIZING, false); Options options = builder.build(); diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java index f841723bf8..291db2671a 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/storage/StoreConfig.java @@ -46,6 +46,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.net.URI; +import java.util.List; import java.util.zip.Deflater; @API(API.Status.EXPERIMENTAL) @@ -158,6 +159,10 @@ static SerializationKeyManager keyManagerFromOptions(@Nonnull Options options) t if (defaultKeyEntryAlias != null) { builder.setDefaultKeyEntryAlias(defaultKeyEntryAlias); } + final List keyEntryAliases = options.getOption(Options.Name.ENCRYPTION_KEY_ENTRY_LIST); + if (keyEntryAliases != null) { + builder.setKeyEntryAliases(keyEntryAliases); + } final String keyStorePassword = options.getOption(Options.Name.ENCRYPTION_KEY_PASSWORD); if (keyStorePassword != null) { builder.setKeyStorePassword(keyStorePassword); diff --git a/fdb-relational-grpc/src/main/java/com/apple/foundationdb/relational/jdbc/TypeConversion.java b/fdb-relational-grpc/src/main/java/com/apple/foundationdb/relational/jdbc/TypeConversion.java index 5eff8e61bc..cf6f9d56b5 100644 --- a/fdb-relational-grpc/src/main/java/com/apple/foundationdb/relational/jdbc/TypeConversion.java +++ b/fdb-relational-grpc/src/main/java/com/apple/foundationdb/relational/jdbc/TypeConversion.java @@ -692,6 +692,11 @@ static com.apple.foundationdb.relational.jdbc.grpc.v1.Options.Builder toProtobuf builder.setEncryptionKeyEntry((String)entry.getValue()); } break; + case ENCRYPTION_KEY_ENTRY_LIST: + for (String rule : (List)entry.getValue()) { + builder.addEncryptionKeyEntryList(rule); + } + break; case ENCRYPTION_KEY_PASSWORD: if (Options.isNull(entry.getValue())) { builder.clearEncryptionKeyPassword(); @@ -813,6 +818,9 @@ public static Options fromProtobuf(com.apple.foundationdb.relational.jdbc.grpc.v if (protoOptions.hasEncryptionKeyEntry()) { builder.withOption(Options.Name.ENCRYPTION_KEY_ENTRY, protoOptions.getEncryptionKeyEntry()); } + if (protoOptions.getEncryptionKeyEntryListCount() > 0) { + builder.withOption(Options.Name.ENCRYPTION_KEY_ENTRY_LIST, protoOptions.getEncryptionKeyEntryListList()); + } if (protoOptions.hasEncryptionKeyPassword()) { builder.withOption(Options.Name.ENCRYPTION_KEY_PASSWORD, protoOptions.getEncryptionKeyPassword()); } diff --git a/fdb-relational-grpc/src/main/proto/grpc/relational/jdbc/v1/jdbc.proto b/fdb-relational-grpc/src/main/proto/grpc/relational/jdbc/v1/jdbc.proto index f91aa7fd6c..342c9b3104 100644 --- a/fdb-relational-grpc/src/main/proto/grpc/relational/jdbc/v1/jdbc.proto +++ b/fdb-relational-grpc/src/main/proto/grpc/relational/jdbc/v1/jdbc.proto @@ -152,6 +152,7 @@ message Options { optional string encryption_key_entry = 29; optional string encryption_key_password = 30; optional bool compress_when_serializing = 31; + repeated string encryption_key_entry_list = 32; } // Request that is part of a transactional (stateful) exchange diff --git a/yaml-tests/src/test/resources/serialization-options.yamsql b/yaml-tests/src/test/resources/serialization-options.yamsql index 81030eae5b..ea696b05a9 100644 --- a/yaml-tests/src/test/resources/serialization-options.yamsql +++ b/yaml-tests/src/test/resources/serialization-options.yamsql @@ -151,4 +151,26 @@ test_block: {S: 'uvw'}, {S: 'ghi'}, {S: 'rst'}] +--- +test_block: + name: multiple-keys + preset: single_repetition_ordered + options: + supported_version: !current_version + connection_options: + ENCRYPTION_KEY_ENTRY_LIST: [key-1, key-2] + ENCRYPTION_KEY_ENTRY: key-2 + tests: + - + - query: INSERT INTO t VALUES (7, 'jkl', null) + - count: 1 + - + - query: SELECT * FROM t + - unorderedResult: [{ID: 1, S: 'abc', PAD: '________________________________'}, + {ID: 2, S: 'def', PAD: '________________________________'}, + {ID: 3, S: 'xyz', PAD: !null }, + {ID: 4, S: 'uvw', PAD: !null }, + {ID: 5, S: 'ghi', PAD: '********************************'}, + {ID: 6, S: 'rst', PAD: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}, + {ID: 7, S: 'jkl', PAD: !null }] ... From 23e81e36c13d2e953b7901c6c19eedb94f06e43a Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Wed, 24 Sep 2025 16:04:09 -0700 Subject: [PATCH 3/4] Update fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java Co-authored-by: Scott Dugas --- .../record/provider/common/KeyStoreSerializationKeyManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java index b0c0904909..59403bc29d 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java @@ -37,7 +37,7 @@ /** - * A {@link SerializationKeyManager} to uses a {@link KeyStore}. + * A {@link SerializationKeyManager} that uses a {@link KeyStore}. *

* Key numbers are mapped to and from key names, which are looked up in the store. */ From 3342add63e40ef73878c3f0d1f5f473111e39528 Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Wed, 24 Sep 2025 16:21:47 -0700 Subject: [PATCH 4/4] Address review comments --- .../KeyStoreSerializationKeyManager.java | 12 ++--- .../foundationdb/relational/api/Options.java | 3 +- .../options/OrderedCollectionContract.java | 48 +++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/options/OrderedCollectionContract.java diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java index 59403bc29d..71e86ae257 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java @@ -25,7 +25,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.io.FileInputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.security.GeneralSecurityException; @@ -60,7 +60,7 @@ protected KeyStoreSerializationKeyManager(@Nonnull KeyStore keyStore, @Nonnull K @Nonnull String cipherName, @Nonnull SecureRandom secureRandom) { this.keyStore = keyStore; this.keyEntryPassword = keyEntryPassword; - this.keyEntryAliases = keyEntryAliases; + this.keyEntryAliases = List.copyOf(keyEntryAliases); this.defaultKeyNumber = defaultKeyNumber; this.cipherName = cipherName; this.secureRandom = secureRandom; @@ -193,10 +193,7 @@ public KeyStoreSerializationKeyManager build() { } final KeyStore keyStore; try { - keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (FileInputStream fis = new FileInputStream(keyStoreFileName)) { - keyStore.load(fis, keyStorePassword.toCharArray()); - } + keyStore = KeyStore.getInstance(new File(keyStoreFileName), keyStorePassword.toCharArray()); } catch (FileNotFoundException ex) { throw new RecordCoreArgumentException("Key store not found", ex); } catch (GeneralSecurityException | IOException ex) { @@ -214,6 +211,9 @@ public KeyStoreSerializationKeyManager build() { keyEntryAliases = List.of(defaultKeyEntryAlias); defaultKeyNumber = 0; } else if (defaultKeyEntryAlias == null) { + if (keyEntryAliases.isEmpty()) { + throw new RecordCoreArgumentException("need at least one key alias"); + } defaultKeyNumber = keyEntryAliases.size() - 1; } else { defaultKeyNumber = keyEntryAliases.indexOf(defaultKeyEntryAlias); diff --git a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java index cefc720460..a14b06e8fc 100644 --- a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java +++ b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/Options.java @@ -25,6 +25,7 @@ import com.apple.foundationdb.relational.api.options.CollectionContract; import com.apple.foundationdb.relational.api.options.OptionContract; import com.apple.foundationdb.relational.api.options.OptionContractWithConversion; +import com.apple.foundationdb.relational.api.options.OrderedCollectionContract; import com.apple.foundationdb.relational.api.options.RangeContract; import com.apple.foundationdb.relational.api.options.TypeContract; import com.google.common.annotations.VisibleForTesting; @@ -555,7 +556,7 @@ private static Map> makeContracts() { data.put(Name.ENCRYPT_WHEN_SERIALIZING, List.of(TypeContract.booleanType())); data.put(Name.ENCRYPTION_KEY_STORE, List.of(TypeContract.nullableStringType())); data.put(Name.ENCRYPTION_KEY_ENTRY, List.of(TypeContract.nullableStringType())); - data.put(Name.ENCRYPTION_KEY_ENTRY_LIST, List.of(new CollectionContract<>(TypeContract.stringType()))); + data.put(Name.ENCRYPTION_KEY_ENTRY_LIST, List.of(new OrderedCollectionContract<>(TypeContract.stringType()))); data.put(Name.ENCRYPTION_KEY_PASSWORD, List.of(TypeContract.nullableStringType())); data.put(Name.COMPRESS_WHEN_SERIALIZING, List.of(TypeContract.booleanType())); diff --git a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/options/OrderedCollectionContract.java b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/options/OrderedCollectionContract.java new file mode 100644 index 0000000000..9dad9ec32a --- /dev/null +++ b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/options/OrderedCollectionContract.java @@ -0,0 +1,48 @@ +/* + * OrderedCollectionTypeContract.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2025 Apple Inc. and the FoundationDB project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.apple.foundationdb.relational.api.options; + +import com.apple.foundationdb.relational.api.Options; +import com.apple.foundationdb.relational.api.exceptions.ErrorCode; + +import javax.annotation.Nonnull; +import java.sql.SQLException; +import java.util.List; + +/** + * Ordered ({@code List}) version of {@link CollectionContract}. + * @param the type parameter of the collection + */ +public class OrderedCollectionContract extends CollectionContract { + public OrderedCollectionContract(@Nonnull TypeContract elementContract) { + super(elementContract); + } + + @Override + public void validate(final Options.Name name, final Object value) throws SQLException { + if (!(value instanceof List)) { + throw new SQLException("Option " + name + " should be of a list type instead of " + value.getClass().getName(), ErrorCode.INVALID_PARAMETER.getErrorCode()); + } + super.validate(name, value); + } + + // No need to override fromString, as that already returns a list in value element order. +}