From fd082c0b27dc128a31a34736a1a6c61b19738dfc Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Thu, 21 Aug 2025 17:34:38 -0700 Subject: [PATCH 1/7] Propagate options from connection into database. --- .../foundationdb/relational/api/Options.java | 4 ++++ .../recordlayer/AbstractDatabase.java | 17 ++++++++++++++++- .../EmbeddedRelationalConnection.java | 3 ++- .../recordlayer/RecordLayerDatabase.java | 5 +---- .../catalog/TransactionBoundDatabase.java | 5 +---- .../BackingLocatableResolverStoreTest.java | 2 +- 6 files changed, 25 insertions(+), 11 deletions(-) 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 210ce835b8..1b11daaa4f 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 @@ -291,6 +291,10 @@ public T getOption(Name name) { } } + public Options withOption(@Nonnull Name name, Object value) throws SQLException { + return builder().fromOptions(this).withOption(name, value).build(); + } + public Options withChild(@Nonnull Options childOptions) throws SQLException { return Options.combine(this, childOptions); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractDatabase.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractDatabase.java index 959911bab4..2a49364c9e 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractDatabase.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/AbstractDatabase.java @@ -21,6 +21,7 @@ package com.apple.foundationdb.relational.recordlayer; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; +import com.apple.foundationdb.relational.api.Options; import com.apple.foundationdb.relational.api.Transaction; import com.apple.foundationdb.relational.api.TransactionManager; import com.apple.foundationdb.relational.api.catalog.RelationalDatabase; @@ -34,6 +35,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.net.URI; +import java.sql.SQLException; import java.util.HashMap; import java.util.Map; @@ -49,13 +51,17 @@ public abstract class AbstractDatabase implements RelationalDatabase { final Map schemas = new HashMap<>(); @Nullable private final RelationalPlanCache planCache; + @Nonnull + protected Options options; public AbstractDatabase(@Nonnull final MetadataOperationsFactory metadataOperationsFactory, @Nonnull DdlQueryFactory ddlQueryFactory, - @Nullable RelationalPlanCache planCache) { + @Nullable RelationalPlanCache planCache, + @Nonnull Options options) { this.metadataOperationsFactory = metadataOperationsFactory; this.ddlQueryFactory = ddlQueryFactory; this.planCache = planCache; + this.options = options; } protected void setConnection(@Nonnull EmbeddedRelationalConnection conn) { @@ -119,4 +125,13 @@ public DdlQueryFactory getDdlQueryFactory() { public RelationalPlanCache getPlanCache() { return planCache; } + + @Nonnull + public Options getOptions() { + return options; + } + + public void setOption(@Nonnull Options.Name name, Object value) throws SQLException { + options = options.withOption(name, value); + } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java index 382a50f025..879285e3b6 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/EmbeddedRelationalConnection.java @@ -415,7 +415,8 @@ public Options getOptions() { @Override public void setOption(Options.Name name, Object value) throws SQLException { - options = Options.builder().fromOptions(options).withOption(name, value).build(); + options = options.withOption(name, value); + frl.setOption(name, value); } @Override diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java index 823af4e2a7..6f616a61cb 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java @@ -53,8 +53,6 @@ public class RecordLayerDatabase extends AbstractDatabase { private final RecordLayerConfig recordLayerConfig; private final RelationalKeyspaceProvider.RelationalDatabasePath databasePath; - @Nonnull - private final Options options; @Nullable private final String defaultSchema; @@ -83,14 +81,13 @@ public RecordLayerDatabase(FdbConnection fdbDb, @Nullable RelationalPlanCache planCache, @Nullable String defaultSchema, @Nonnull Options options) { - super(metadataOperationsFactory, ddlQueryFactory, planCache); + super(metadataOperationsFactory, ddlQueryFactory, planCache, options); this.fdbDb = fdbDb; this.metaDataStore = new CachedMetaDataStore(metaDataStore); this.storeCatalog = storeCatalog; this.recordLayerConfig = config; this.databasePath = databasePath; this.defaultSchema = defaultSchema; - this.options = options; } @Override diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/TransactionBoundDatabase.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/TransactionBoundDatabase.java index 61c67af37a..fef19589e3 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/TransactionBoundDatabase.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/catalog/TransactionBoundDatabase.java @@ -65,8 +65,6 @@ public class TransactionBoundDatabase extends AbstractDatabase { BackingStore store; URI uri; - @Nonnull - final Options options; private static final MetadataOperationsFactory onlyTemporaryFunctionOperationsFactory = new AbstractMetadataOperationsFactory() { @Nonnull @@ -85,9 +83,8 @@ public ConstantAction getDropTemporaryFunctionConstantAction(final boolean throw }; public TransactionBoundDatabase(URI uri, @Nonnull Options options, @Nullable RelationalPlanCache planCache) { - super(onlyTemporaryFunctionOperationsFactory, NoOpQueryFactory.INSTANCE, planCache); + super(onlyTemporaryFunctionOperationsFactory, NoOpQueryFactory.INSTANCE, planCache, options); this.uri = uri; - this.options = options; } @Override diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/storage/BackingLocatableResolverStoreTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/storage/BackingLocatableResolverStoreTest.java index 0c9276ed34..e7908c2083 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/storage/BackingLocatableResolverStoreTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/storage/BackingLocatableResolverStoreTest.java @@ -131,7 +131,7 @@ public TransactionBoundLocatableResolverDatabase(@Nonnull URI dbPath, @Nonnull StorageCluster cluster, @Nonnull LocatableResolver resolver, @Nonnull StoreCatalog catalog) { - super(NoOpMetadataOperationsFactory.INSTANCE, NoOpQueryFactory.INSTANCE, null); + super(NoOpMetadataOperationsFactory.INSTANCE, NoOpQueryFactory.INSTANCE, null, Options.NONE); this.dbPath = dbPath; this.transactionManager = cluster.getTransactionManager(); this.resolver = resolver; From 396777be964cf3d7e2032f4d07e3424631dc3dcc Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Thu, 21 Aug 2025 17:44:38 -0700 Subject: [PATCH 2/7] Move serializer from RecordLayerConfig to StoreConfig --- .../recordlayer/RecordLayerConfig.java | 18 ++------------ .../recordlayer/RecordLayerDatabase.java | 2 +- ...RecordLayerCreateSchemaConstantAction.java | 3 ++- ...ecordLayerSetStoreStateConstantAction.java | 3 ++- .../recordlayer/storage/StoreConfig.java | 24 +++++++++++++++---- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java index 4180a19ab1..a393bf7101 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java @@ -24,15 +24,14 @@ import com.apple.foundationdb.record.IndexState; import com.apple.foundationdb.record.provider.common.RecordSerializer; -import com.apple.foundationdb.record.provider.common.TransformedRecordSerializer; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.provider.foundationdb.FormatVersion; +import com.apple.foundationdb.relational.recordlayer.storage.StoreConfig; import com.google.protobuf.Message; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.zip.Deflater; /** * Holder object for RecordLayer-specific stuff that isn't directly tied to an actual FDB StorageCluster. @@ -40,20 +39,11 @@ @API(API.Status.EXPERIMENTAL) public final class RecordLayerConfig { private final FDBRecordStoreBase.UserVersionChecker userVersionChecker; - private final RecordSerializer serializer; private final FormatVersion formatVersion; private final Map indexStateMap; - private static final RecordSerializer DEFAULT_RELATIONAL_SERIALIZER = TransformedRecordSerializer.newDefaultBuilder() - .setEncryptWhenSerializing(false) - .setCompressWhenSerializing(true) - .setCompressionLevel(Deflater.DEFAULT_COMPRESSION) - .setWriteValidationRatio(0.0) - .build(); - private RecordLayerConfig(RecordLayerConfigBuilder builder) { this.userVersionChecker = builder.userVersionChecker; - this.serializer = builder.serializer; this.formatVersion = builder.formatVersion; this.indexStateMap = builder.indexStateMap; } @@ -62,10 +52,6 @@ public FDBRecordStoreBase.UserVersionChecker getUserVersionChecker() { return userVersionChecker; } - public RecordSerializer getSerializer() { - return serializer; - } - public FormatVersion getFormatVersion() { return formatVersion; } @@ -86,7 +72,7 @@ public static class RecordLayerConfigBuilder { public RecordLayerConfigBuilder() { this.userVersionChecker = (oldUserVersion, oldMetaDataVersion, metaData) -> CompletableFuture.completedFuture(oldUserVersion); - this.serializer = DEFAULT_RELATIONAL_SERIALIZER; + this.serializer = StoreConfig.DEFAULT_RELATIONAL_SERIALIZER; this.formatVersion = FormatVersion.getDefaultFormatVersion(); this.indexStateMap = Map.of(); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java index 6f616a61cb..cca2dbb521 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerDatabase.java @@ -126,7 +126,7 @@ public void close() throws RelationalException { } BackingRecordStore loadStore(@Nonnull Transaction txn, @Nonnull String schemaName, @Nonnull FDBRecordStoreBase.StoreExistenceCheck existenceCheck) throws RelationalException { - StoreConfig storeConfig = StoreConfig.create(recordLayerConfig, schemaName, databasePath, metaDataStore, txn); + StoreConfig storeConfig = StoreConfig.create(recordLayerConfig, schemaName, databasePath, metaDataStore, txn, options); return BackingRecordStore.load(txn, storeConfig, existenceCheck); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerCreateSchemaConstantAction.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerCreateSchemaConstantAction.java index da7a296eb0..3cac6dd320 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerCreateSchemaConstantAction.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerCreateSchemaConstantAction.java @@ -38,6 +38,7 @@ import com.apple.foundationdb.relational.recordlayer.RecordLayerConfig; import com.apple.foundationdb.relational.recordlayer.RelationalKeyspaceProvider; import com.apple.foundationdb.relational.recordlayer.catalog.CatalogMetaDataProvider; +import com.apple.foundationdb.relational.recordlayer.storage.StoreConfig; import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil; import java.net.URI; @@ -101,7 +102,7 @@ public void execute(Transaction txn) throws RelationalException { try { FDBRecordStore.newBuilder() .setKeySpacePath(databasePath) - .setSerializer(rlConfig.getSerializer()) + .setSerializer(StoreConfig.DEFAULT_RELATIONAL_SERIALIZER) .setMetaDataProvider(new CatalogMetaDataProvider(catalog, dbUri, schemaName, txn)) .setUserVersionChecker(rlConfig.getUserVersionChecker()) .setFormatVersion(rlConfig.getFormatVersion()) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerSetStoreStateConstantAction.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerSetStoreStateConstantAction.java index dfb80510b0..ed4e4ea685 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerSetStoreStateConstantAction.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/ddl/RecordLayerSetStoreStateConstantAction.java @@ -36,6 +36,7 @@ import com.apple.foundationdb.relational.recordlayer.RecordLayerConfig; import com.apple.foundationdb.relational.recordlayer.RelationalKeyspaceProvider; import com.apple.foundationdb.relational.recordlayer.catalog.CatalogMetaDataProvider; +import com.apple.foundationdb.relational.recordlayer.storage.StoreConfig; import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil; import java.net.URI; @@ -69,7 +70,7 @@ public void execute(Transaction txn) throws RelationalException { FDBRecordStore recordStore = FDBRecordStore.newBuilder() .setKeySpacePath(databasePath) - .setSerializer(rlConfig.getSerializer()) + .setSerializer(StoreConfig.DEFAULT_RELATIONAL_SERIALIZER) .setMetaDataProvider(new CatalogMetaDataProvider(catalog, dbUri, schemaName, txn)) .setContext(txn.unwrap(FDBRecordContext.class)) .open(); 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 19ce6ce190..58bcfc1141 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 @@ -26,10 +26,12 @@ import com.apple.foundationdb.record.RecordMetaDataProvider; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.provider.common.RecordSerializer; +import com.apple.foundationdb.record.provider.common.TransformedRecordSerializer; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.provider.foundationdb.FormatVersion; import com.apple.foundationdb.record.provider.foundationdb.keyspace.KeySpacePath; import com.apple.foundationdb.record.provider.foundationdb.keyspace.NoSuchDirectoryException; +import com.apple.foundationdb.relational.api.Options; import com.apple.foundationdb.relational.api.Transaction; import com.apple.foundationdb.relational.api.exceptions.ErrorCode; import com.apple.foundationdb.relational.api.exceptions.RelationalException; @@ -41,22 +43,32 @@ import com.google.protobuf.Message; import java.net.URI; +import java.util.zip.Deflater; @API(API.Status.EXPERIMENTAL) public final class StoreConfig { + public static final RecordSerializer DEFAULT_RELATIONAL_SERIALIZER = TransformedRecordSerializer.newDefaultBuilder() + .setEncryptWhenSerializing(false) + .setCompressWhenSerializing(true) + .setCompressionLevel(Deflater.DEFAULT_COMPRESSION) + .setWriteValidationRatio(0.0) + .build(); private final RecordLayerConfig recordLayerConfig; private final String schemaName; private final KeySpacePath storePath; private final RecordMetaDataProvider metaDataProvider; + private final RecordSerializer serializer; private StoreConfig(RecordLayerConfig recordLayerConfig, String schemaName, KeySpacePath storePath, - RecordMetaDataProvider metaDataProvider) { + RecordMetaDataProvider metaDataProvider, + RecordSerializer serializer) { this.recordLayerConfig = recordLayerConfig; this.schemaName = schemaName; this.storePath = storePath; this.metaDataProvider = metaDataProvider; + this.serializer = serializer; } public String getSchemaName() { @@ -72,7 +84,7 @@ public RecordMetaDataProvider getMetaDataProvider() { } public RecordSerializer getSerializer() { - return recordLayerConfig.getSerializer(); + return serializer; } public FDBRecordStoreBase.UserVersionChecker getUserVersionChecker() { @@ -87,7 +99,8 @@ public static StoreConfig create(RecordLayerConfig recordLayerConfig, String schemaName, RelationalKeyspaceProvider.RelationalDatabasePath databasePath, RecordMetaDataStore metaDataStore, - Transaction transaction) throws RelationalException { + Transaction transaction, + Options options) throws RelationalException { //TODO(bfines) error handling if this store doesn't exist RelationalKeyspaceProvider.RelationalSchemaPath schemaPath; @@ -104,6 +117,9 @@ public static StoreConfig create(RecordLayerConfig recordLayerConfig, URI dbUri = databasePath.toUri(); RecordMetaDataProvider metaDataProvider = metaDataStore.loadMetaData(transaction, dbUri, schemaName); - return new StoreConfig(recordLayerConfig, schemaName, schemaPath, metaDataProvider); + RecordSerializer serializer = DEFAULT_RELATIONAL_SERIALIZER; + // TODO: Get from options. + + return new StoreConfig(recordLayerConfig, schemaName, schemaPath, metaDataProvider, serializer); } } From 448a651cf6a15d61a262bcb03d1639dbb7ecdc88 Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Thu, 21 Aug 2025 12:43:06 -0700 Subject: [PATCH 3/7] Add some more options related to encryption. --- .../foundationdb/relational/api/Options.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 1b11daaa4f..0588462b0e 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 @@ -221,7 +221,23 @@ public enum Name { * operations interacting with FDB. * Scope: Engine */ - ASYNC_OPERATIONS_TIMEOUT_MILLIS + ASYNC_OPERATIONS_TIMEOUT_MILLIS, + + /** + * A boolean indicating whether to encrypt records when saving and decrypt when loading. + */ + ENCRYPT_WHEN_SERIALIZING, + + /** + * An AES encryption key in Base64. + */ + ENCRYPTION_KEY, + + /** + * A text password to be used to generate an encryption key. + * Since a fixed salt is used, this is not secure at all. + */ + ENCRYPTION_PASSWORD, } public enum IndexFetchMethod { @@ -258,6 +274,7 @@ public enum IndexFetchMethod { builder.put(Name.CASE_SENSITIVE_IDENTIFIERS, false); builder.put(Name.CONTINUATIONS_CONTAIN_COMPILED_STATEMENTS, true); builder.put(Name.ASYNC_OPERATIONS_TIMEOUT_MILLIS, 10_000L); + builder.put(Name.ENCRYPT_WHEN_SERIALIZING, false); OPTIONS_DEFAULT_VALUES = builder.build(); } @@ -429,6 +446,9 @@ private static Map> makeContracts() { data.put(Name.VALID_PLAN_HASH_MODES, List.of(TypeContract.stringType())); data.put(Name.CONTINUATIONS_CONTAIN_COMPILED_STATEMENTS, List.of(TypeContract.booleanType())); data.put(Name.ASYNC_OPERATIONS_TIMEOUT_MILLIS, List.of(TypeContract.longType(), RangeContract.of(0L, Long.MAX_VALUE))); + data.put(Name.ENCRYPT_WHEN_SERIALIZING, List.of(TypeContract.booleanType())); + data.put(Name.ENCRYPTION_KEY, List.of(TypeContract.stringType())); + data.put(Name.ENCRYPTION_PASSWORD, List.of(TypeContract.stringType())); return Collections.unmodifiableMap(data); } From 1ff759c67fe66213061837df6aa11a941afe98a3 Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Thu, 21 Aug 2025 18:07:19 -0700 Subject: [PATCH 4/7] Use those options to generate encryption keys --- .../recordlayer/storage/StoreConfig.java | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) 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 58bcfc1141..5211aa89e1 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 @@ -21,12 +21,12 @@ package com.apple.foundationdb.relational.recordlayer.storage; import com.apple.foundationdb.annotation.API; - 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.RecordSerializer; import com.apple.foundationdb.record.provider.common.TransformedRecordSerializer; +import com.apple.foundationdb.record.provider.common.TransformedRecordSerializerJCE; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.provider.foundationdb.FormatVersion; import com.apple.foundationdb.record.provider.foundationdb.keyspace.KeySpacePath; @@ -39,10 +39,19 @@ import com.apple.foundationdb.relational.recordlayer.RelationalKeyspaceProvider; import com.apple.foundationdb.relational.recordlayer.catalog.RecordMetaDataStore; import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil; - import com.google.protobuf.Message; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; import java.util.zip.Deflater; @API(API.Status.EXPERIMENTAL) @@ -117,9 +126,53 @@ public static StoreConfig create(RecordLayerConfig recordLayerConfig, URI dbUri = databasePath.toUri(); RecordMetaDataProvider metaDataProvider = metaDataStore.loadMetaData(transaction, dbUri, schemaName); - RecordSerializer serializer = DEFAULT_RELATIONAL_SERIALIZER; - // TODO: Get from options. + RecordSerializer serializer = serializerFromOptions(options); return new StoreConfig(recordLayerConfig, schemaName, schemaPath, metaDataProvider, serializer); } + + static RecordSerializer serializerFromOptions(Options options) throws RelationalException { + final boolean encrypted = options.getOption(Options.Name.ENCRYPT_WHEN_SERIALIZING); + if (!encrypted) { + return DEFAULT_RELATIONAL_SERIALIZER; + } + final SecretKey key; + final String keyBase64 = options.getOption(Options.Name.ENCRYPTION_KEY); + if (keyBase64 != null) { + key = new SecretKeySpec(Base64.getDecoder().decode(keyBase64), "AES"); + } else { + // TODO: Is there a way to make this only available in YAML test framework? + final String keyPassword = options.getOption(Options.Name.ENCRYPTION_PASSWORD); + if (keyPassword != null) { + SecretKeyFactory kdf; + try { + kdf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + } catch (NoSuchAlgorithmException ex) { + throw new RelationalException("KDF not found", ErrorCode.UNSUPPORTED_OPERATION, ex); + } + KeySpec ks = new PBEKeySpec(keyPassword.toCharArray(), "YAML-salt".getBytes(StandardCharsets.UTF_8), 1, 128); + try { + key = new SecretKeySpec(kdf.generateSecret(ks).getEncoded(), "AES"); + } catch (InvalidKeySpecException ex) { + throw new RelationalException("Key derivation failed", ErrorCode.UNSUPPORTED_OPERATION, ex); + } + } else { + KeyGenerator keyGen; + try { + keyGen = KeyGenerator.getInstance("AES"); + } catch (NoSuchAlgorithmException ex) { + throw new RelationalException("Key generator not found", ErrorCode.UNSUPPORTED_OPERATION, ex); + } + keyGen.init(128); + key = keyGen.generateKey(); + } + } + return TransformedRecordSerializerJCE.newDefaultBuilder() + .setEncryptWhenSerializing(true) + .setEncryptionKey(key) + .setCompressWhenSerializing(true) + .setCompressionLevel(Deflater.DEFAULT_COMPRESSION) + .setWriteValidationRatio(0.0) + .build(); + } } From ccc924e6d00e3122cb1179cc94a0eb0de009970c Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Thu, 21 Aug 2025 18:15:51 -0700 Subject: [PATCH 5/7] Add a YAML test of that. --- .../src/test/java/YamlIntegrationTests.java | 5 ++ .../src/test/resources/encrypted.yamsql | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 yaml-tests/src/test/resources/encrypted.yamsql diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 3fc2000074..0e330c86bc 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -310,4 +310,9 @@ public void literalExtractionTests(YamlTest.Runner runner) throws Exception { public void caseSensitivityTest(YamlTest.Runner runner) throws Exception { runner.runYamsql("case-sensitivity.yamsql"); } + + @TestTemplate + public void encryptedTest(YamlTest.Runner runner) throws Exception { + runner.runYamsql("encrypted.yamsql"); + } } diff --git a/yaml-tests/src/test/resources/encrypted.yamsql b/yaml-tests/src/test/resources/encrypted.yamsql new file mode 100644 index 0000000000..d17a53110b --- /dev/null +++ b/yaml-tests/src/test/resources/encrypted.yamsql @@ -0,0 +1,67 @@ +# +# encrypted.yamsql +# +# 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. +--- +options: + supported_version: 4.5.6.0 + connection_options: + ENCRYPT_WHEN_SERIALIZING: true + ENCRYPTION_PASSWORD: SQL +--- +schema_template: + create table t(id bigint, s string, primary key(id)) +--- +setup: + steps: + - query: INSERT INTO t + VALUES (1, 'abc'), + (2, 'def') +--- +test_block: + name: read-encrypted + options: + connection_options: + ENCRYPT_WHEN_SERIALIZING: true + ENCRYPTION_PASSWORD: SQL + tests: + - + - query: SELECT * FROM t + - unorderedResult: [{ID: 1, S: 'abc'}, + {ID: 2, S: 'def'}] +--- +test_block: + name: read-unencrypted + options: + connection_options: + ENCRYPT_WHEN_SERIALIZING: false + tests: + - + - query: SELECT * FROM t + - error: "25F01" +--- +test_block: + name: read-wrong-key + options: + connection_options: + ENCRYPT_WHEN_SERIALIZING: false + ENCRYPTION_PASSWORD: NoSQL + tests: + - + - query: SELECT * FROM t + - error: "25F01" +... From 2a047fa528131231fc03c35541604c5fce0e1795 Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Tue, 2 Sep 2025 10:50:18 -0700 Subject: [PATCH 6/7] Update yaml-tests/src/test/resources/encrypted.yamsql Co-authored-by: ohadzeliger <70664918+ohadzeliger@users.noreply.github.com> --- yaml-tests/src/test/resources/encrypted.yamsql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yaml-tests/src/test/resources/encrypted.yamsql b/yaml-tests/src/test/resources/encrypted.yamsql index d17a53110b..6374b733bd 100644 --- a/yaml-tests/src/test/resources/encrypted.yamsql +++ b/yaml-tests/src/test/resources/encrypted.yamsql @@ -18,7 +18,7 @@ # limitations under the License. --- options: - supported_version: 4.5.6.0 + supported_version: !current_version connection_options: ENCRYPT_WHEN_SERIALIZING: true ENCRYPTION_PASSWORD: SQL From e0cb62d7f3052255f57f41ce397c9fc8c9b2e0eb Mon Sep 17 00:00:00 2001 From: Mike McMahon Date: Tue, 2 Sep 2025 10:48:52 -0700 Subject: [PATCH 7/7] Switch to using a key store file, as this makes it more clear what is a test-only configuration. --- .../foundationdb/relational/api/Options.java | 20 ++++-- .../recordlayer/RecordLayerConfig.java | 7 -- .../recordlayer/storage/StoreConfig.java | 62 +++++++----------- yaml-tests/src/test/resources/encrypted.p12 | Bin 0 -> 698 bytes .../src/test/resources/encrypted.yamsql | 11 ++-- 5 files changed, 44 insertions(+), 56 deletions(-) create mode 100644 yaml-tests/src/test/resources/encrypted.p12 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 0588462b0e..41c61372b1 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 @@ -229,15 +229,19 @@ public enum Name { ENCRYPT_WHEN_SERIALIZING, /** - * An AES encryption key in Base64. + * The key store file containing the encryption key to use. */ - ENCRYPTION_KEY, + ENCRYPTION_KEY_STORE, /** - * A text password to be used to generate an encryption key. - * Since a fixed salt is used, this is not secure at all. + * The key store entry containing the encryption key to use. */ - ENCRYPTION_PASSWORD, + ENCRYPTION_KEY_ENTRY, + + /** + * The integrity key of the key store and the encryption key of the key entry. + */ + ENCRYPTION_KEY_PASSWORD, } public enum IndexFetchMethod { @@ -275,6 +279,7 @@ public enum IndexFetchMethod { builder.put(Name.CONTINUATIONS_CONTAIN_COMPILED_STATEMENTS, true); builder.put(Name.ASYNC_OPERATIONS_TIMEOUT_MILLIS, 10_000L); builder.put(Name.ENCRYPT_WHEN_SERIALIZING, false); + builder.put(Name.ENCRYPTION_KEY_PASSWORD, ""); OPTIONS_DEFAULT_VALUES = builder.build(); } @@ -447,8 +452,9 @@ private static Map> makeContracts() { data.put(Name.CONTINUATIONS_CONTAIN_COMPILED_STATEMENTS, List.of(TypeContract.booleanType())); data.put(Name.ASYNC_OPERATIONS_TIMEOUT_MILLIS, List.of(TypeContract.longType(), RangeContract.of(0L, Long.MAX_VALUE))); data.put(Name.ENCRYPT_WHEN_SERIALIZING, List.of(TypeContract.booleanType())); - data.put(Name.ENCRYPTION_KEY, List.of(TypeContract.stringType())); - data.put(Name.ENCRYPTION_PASSWORD, List.of(TypeContract.stringType())); + data.put(Name.ENCRYPTION_KEY_STORE, List.of(TypeContract.stringType())); + data.put(Name.ENCRYPTION_KEY_ENTRY, List.of(TypeContract.stringType())); + data.put(Name.ENCRYPTION_KEY_PASSWORD, List.of(TypeContract.stringType())); return Collections.unmodifiableMap(data); } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java index a393bf7101..a9146b83e8 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/RecordLayerConfig.java @@ -21,14 +21,9 @@ package com.apple.foundationdb.relational.recordlayer; import com.apple.foundationdb.annotation.API; - import com.apple.foundationdb.record.IndexState; -import com.apple.foundationdb.record.provider.common.RecordSerializer; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; - import com.apple.foundationdb.record.provider.foundationdb.FormatVersion; -import com.apple.foundationdb.relational.recordlayer.storage.StoreConfig; -import com.google.protobuf.Message; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -66,13 +61,11 @@ public static RecordLayerConfig getDefault() { public static class RecordLayerConfigBuilder { private FDBRecordStoreBase.UserVersionChecker userVersionChecker; - private final RecordSerializer serializer; private FormatVersion formatVersion; private Map indexStateMap; public RecordLayerConfigBuilder() { this.userVersionChecker = (oldUserVersion, oldMetaDataVersion, metaData) -> CompletableFuture.completedFuture(oldUserVersion); - this.serializer = StoreConfig.DEFAULT_RELATIONAL_SERIALIZER; this.formatVersion = FormatVersion.getDefaultFormatVersion(); this.indexStateMap = Map.of(); } 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 5211aa89e1..d9e8f06da2 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 @@ -41,17 +41,13 @@ import com.apple.foundationdb.relational.recordlayer.util.ExceptionUtil; import com.google.protobuf.Message; -import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Base64; +import java.security.GeneralSecurityException; +import java.security.KeyStore; import java.util.zip.Deflater; @API(API.Status.EXPERIMENTAL) @@ -137,35 +133,27 @@ static RecordSerializer serializerFromOptions(Options options) throws R return DEFAULT_RELATIONAL_SERIALIZER; } final SecretKey key; - final String keyBase64 = options.getOption(Options.Name.ENCRYPTION_KEY); - if (keyBase64 != null) { - key = new SecretKeySpec(Base64.getDecoder().decode(keyBase64), "AES"); - } else { - // TODO: Is there a way to make this only available in YAML test framework? - final String keyPassword = options.getOption(Options.Name.ENCRYPTION_PASSWORD); - if (keyPassword != null) { - SecretKeyFactory kdf; - try { - kdf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - } catch (NoSuchAlgorithmException ex) { - throw new RelationalException("KDF not found", ErrorCode.UNSUPPORTED_OPERATION, ex); - } - KeySpec ks = new PBEKeySpec(keyPassword.toCharArray(), "YAML-salt".getBytes(StandardCharsets.UTF_8), 1, 128); - try { - key = new SecretKeySpec(kdf.generateSecret(ks).getEncoded(), "AES"); - } catch (InvalidKeySpecException ex) { - throw new RelationalException("Key derivation failed", ErrorCode.UNSUPPORTED_OPERATION, ex); - } - } else { - KeyGenerator keyGen; - try { - keyGen = KeyGenerator.getInstance("AES"); - } catch (NoSuchAlgorithmException ex) { - throw new RelationalException("Key generator not found", ErrorCode.UNSUPPORTED_OPERATION, ex); - } - keyGen.init(128); - key = keyGen.generateKey(); + try { + final String keyStoreFile = options.getOption(Options.Name.ENCRYPTION_KEY_STORE); + if (keyStoreFile == null) { + throw new RelationalException("Key store not specified", ErrorCode.UNSUPPORTED_OPERATION); + } + 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); } return TransformedRecordSerializerJCE.newDefaultBuilder() .setEncryptWhenSerializing(true) diff --git a/yaml-tests/src/test/resources/encrypted.p12 b/yaml-tests/src/test/resources/encrypted.p12 new file mode 100644 index 0000000000000000000000000000000000000000..4776e1d38cc882925861f42f2e89812dab0fb159 GIT binary patch literal 698 zcmXqLV%o;U$ZXKWl)%QR)#lOmotKfFaX}MPAWIXIFHqPMh+R>o%z#oxKw&+DCPra4 zZm2FEE=JY`jfV{y43ag{+D+!R(`gGd7l1eeD^n?>Zt%AK{Ysizdv zFJ89$epgd$m#)o6CMIzq-?eWGs8gFM;q5ozysa;CVweZ8zx)cU2xe`o*%OIdv;B!J^eY zRahf#Sz20e&)ywR!oRRMFj_mz35`3Te|Ck$R{!*pX&)9mzKV&Kj*H+AyRs;Vjuku>^-L|^6*L&zF;xJ8!Seq#wMnQ2EGP{2E6dN zV`5}wU{N?3yK=*6ccbv>9_{Ywmrbu6y8U-i+|95biiOARrDlr0XAv=a%Iv)Oroic` Tzpq>Gd$;Yzp=n2cfdT*ke~{tG literal 0 HcmV?d00001 diff --git a/yaml-tests/src/test/resources/encrypted.yamsql b/yaml-tests/src/test/resources/encrypted.yamsql index 6374b733bd..b980c4e3b7 100644 --- a/yaml-tests/src/test/resources/encrypted.yamsql +++ b/yaml-tests/src/test/resources/encrypted.yamsql @@ -18,10 +18,12 @@ # limitations under the License. --- options: - supported_version: !current_version + supported_version: !current_version connection_options: ENCRYPT_WHEN_SERIALIZING: true - ENCRYPTION_PASSWORD: SQL + ENCRYPTION_KEY_STORE: src/test/resources/encrypted.p12 + ENCRYPTION_KEY_ENTRY: key-1 + ENCRYPTION_KEY_PASSWORD: YAML+SQL --- schema_template: create table t(id bigint, s string, primary key(id)) @@ -37,7 +39,6 @@ test_block: options: connection_options: ENCRYPT_WHEN_SERIALIZING: true - ENCRYPTION_PASSWORD: SQL tests: - - query: SELECT * FROM t @@ -58,8 +59,8 @@ test_block: name: read-wrong-key options: connection_options: - ENCRYPT_WHEN_SERIALIZING: false - ENCRYPTION_PASSWORD: NoSQL + ENCRYPT_WHEN_SERIALIZING: true + ENCRYPTION_KEY_ENTRY: key-2 tests: - - query: SELECT * FROM t