Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.File;
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} that uses a {@link KeyStore}.
* <p>
* 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<String> 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<String> keyEntryAliases, int defaultKeyNumber,
@Nonnull String cipherName, @Nonnull SecureRandom secureRandom) {
this.keyStore = keyStore;
this.keyEntryPassword = keyEntryPassword;
this.keyEntryAliases = List.copyOf(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 {

Check warning on line 104 in fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java#L104

Reduce this class from 111 lines to the maximum allowed 25 or externalize it in a public class https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3606%2FMMcM%2Fserializer-key-store-key-manager%3AHEAD&id=D215506DDFC27C13FB0B548741559D36
@Nullable
String keyStoreFileName;
@Nullable
String keyStorePassword;
@Nullable
String keyEntryPassword;
@Nullable
String defaultKeyEntryAlias;
@Nullable
List<String> 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<String> getKeyEntryAliases() {
return keyEntryAliases;
}

public void setKeyEntryAliases(@Nonnull List<String> 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(new File(keyStoreFileName), 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());

Check warning on line 205 in fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java

View check run for this annotation

fdb.teamscale.io / Teamscale | Findings

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/KeyStoreSerializationKeyManager.java#L205

Move the declaration of `keyEntryProtection` closer to the code that uses it https://fdb.teamscale.io/findings/details/foundationdb-fdb-record-layer?t=FORK_MR%2F3606%2FMMcM%2Fserializer-key-store-key-manager%3AHEAD&id=14463DE4312506DB60CF36E05FAB8930
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably good to move this closer

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then something else is too far away, right? Right now, it is constructing the arguments in the order in which they are passed to the constructor.

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) {
if (keyEntryAliases.isEmpty()) {
throw new RecordCoreArgumentException("need at least one key alias");
}
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -241,6 +242,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.
*/
Expand Down Expand Up @@ -496,6 +502,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<String>)entry.getValue());
break;
default:
Expand Down Expand Up @@ -549,6 +556,7 @@ private static Map<Name, List<OptionContract>> 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 OrderedCollectionContract<>(TypeContract.stringType())));
data.put(Name.ENCRYPTION_KEY_PASSWORD, List.of(TypeContract.nullableStringType()));
data.put(Name.COMPRESS_WHEN_SERIALIZING, List.of(TypeContract.booleanType()));

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> the type parameter of the collection
*/
public class OrderedCollectionContract<T> extends CollectionContract<T> {
public OrderedCollectionContract(@Nonnull TypeContract<T> 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.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading