Skip to content

Commit 406a722

Browse files
Repository uses passwords again
1 parent f0ae034 commit 406a722

File tree

7 files changed

+152
-153
lines changed

7 files changed

+152
-153
lines changed

x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/AESKeyUtils.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,23 @@
99
import javax.crypto.IllegalBlockSizeException;
1010
import javax.crypto.NoSuchPaddingException;
1111
import javax.crypto.SecretKey;
12+
import javax.crypto.SecretKeyFactory;
13+
import javax.crypto.spec.PBEKeySpec;
1214
import javax.crypto.spec.SecretKeySpec;
1315
import java.nio.charset.StandardCharsets;
14-
import java.security.GeneralSecurityException;
1516
import java.security.InvalidKeyException;
1617
import java.security.Key;
1718
import java.security.NoSuchAlgorithmException;
19+
import java.security.spec.InvalidKeySpecException;
1820
import java.util.Base64;
1921

2022
public final class AESKeyUtils {
23+
public static final int KEY_LENGTH_IN_BYTES = 32; // 256-bit AES key
24+
public static final int WRAPPED_KEY_LENGTH_IN_BYTES = KEY_LENGTH_IN_BYTES + 8; // https://www.ietf.org/rfc/rfc3394.txt section 2.2
25+
// parameter for the KDF function, it's a funny and unusual iter count larger than 60k
26+
private static final int KDF_ITER = 61616;
27+
// the KDF algorithm that generate the symmetric key given the password
28+
private static final String KDF_ALGO = "PBKDF2WithHmacSHA512";
2129
// The Id of any AES SecretKey is the AES-Wrap-ciphertext of this fixed 32 byte wide array.
2230
// Key wrapping encryption is deterministic (same plaintext generates the same ciphertext)
2331
// and the probability that two different keys map the same plaintext to the same ciphertext is very small
@@ -43,6 +51,9 @@ public static SecretKey unwrap(SecretKey wrappingKey, byte[] keyToUnwrap) throws
4351
if (false == "AES".equals(wrappingKey.getAlgorithm())) {
4452
throw new IllegalArgumentException("wrappingKey argument is not an AES Key");
4553
}
54+
if (keyToUnwrap.length != WRAPPED_KEY_LENGTH_IN_BYTES) {
55+
throw new IllegalArgumentException("keyToUnwrap invalid length [" + keyToUnwrap.length + "]");
56+
}
4657
Cipher c = Cipher.getInstance("AESWrap");
4758
c.init(Cipher.UNWRAP_MODE, wrappingKey);
4859
Key unwrappedKey = c.unwrap(keyToUnwrap, "AES", Cipher.SECRET_KEY);
@@ -59,8 +70,18 @@ public static SecretKey unwrap(SecretKey wrappingKey, byte[] keyToUnwrap) throws
5970
* Moreover, the ciphertext reveals no information on the key, and the probability of collision of ciphertexts given different
6071
* keys is statistically negligible.
6172
*/
62-
public static String computeId(SecretKey secretAESKey) throws GeneralSecurityException {
73+
public static String computeId(SecretKey secretAESKey) throws IllegalBlockSizeException, InvalidKeyException,
74+
NoSuchAlgorithmException, NoSuchPaddingException {
6375
byte[] ciphertextOfKnownPlaintext = wrap(secretAESKey, new SecretKeySpec(KEY_ID_PLAINTEXT, "AES"));
6476
return new String(Base64.getUrlEncoder().withoutPadding().encode(ciphertextOfKnownPlaintext), StandardCharsets.UTF_8);
6577
}
78+
79+
public static SecretKey generatePasswordBasedKey(char[] password, byte[] salt) throws NoSuchAlgorithmException,
80+
InvalidKeySpecException {
81+
PBEKeySpec keySpec = new PBEKeySpec(password, salt, KDF_ITER, KEY_LENGTH_IN_BYTES * Byte.SIZE);
82+
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KDF_ALGO);
83+
SecretKey secretKey = keyFactory.generateSecret(keySpec);
84+
SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
85+
return secret;
86+
}
6687
}

x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepository.java

Lines changed: 74 additions & 47 deletions
Large diffs are not rendered by default.

x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryPlugin.java

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.cluster.service.ClusterService;
1414
import org.elasticsearch.common.Strings;
1515
import org.elasticsearch.common.settings.SecureSetting;
16+
import org.elasticsearch.common.settings.SecureString;
1617
import org.elasticsearch.common.settings.Setting;
1718
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
1819
import org.elasticsearch.env.Environment;
@@ -24,11 +25,6 @@
2425
import org.elasticsearch.repositories.blobstore.BlobStoreRepository;
2526
import org.elasticsearch.xpack.core.XPackPlugin;
2627

27-
import javax.crypto.SecretKey;
28-
import javax.crypto.spec.SecretKeySpec;
29-
import java.io.IOException;
30-
import java.io.InputStream;
31-
import java.io.UncheckedIOException;
3228
import java.util.Arrays;
3329
import java.util.Collections;
3430
import java.util.HashMap;
@@ -39,12 +35,11 @@
3935
public class EncryptedRepositoryPlugin extends Plugin implements RepositoryPlugin {
4036
static final Logger logger = LogManager.getLogger(EncryptedRepositoryPlugin.class);
4137
static final String REPOSITORY_TYPE_NAME = "encrypted";
42-
static final Setting.AffixSetting<InputStream> KEY_ENCRYPTION_KEY_SETTING = Setting.affixKeySetting("repository.encrypted.",
43-
"key", key -> SecureSetting.secureFile(key, null));
38+
static final List<String> SUPPORTED_ENCRYPTED_TYPE_NAMES = Arrays.asList("fs", "gcs", "azure", "s3");
39+
static final Setting.AffixSetting<SecureString> ENCRYPTION_PASSWORD_SETTING = Setting.affixKeySetting("repository.encrypted.",
40+
"password", key -> SecureSetting.secureString(key, null));
4441
static final Setting<String> DELEGATE_TYPE_SETTING = Setting.simpleString("delegate_type", "");
45-
static final Setting<String> KEK_NAME_SETTING = Setting.simpleString("key_name", "");
46-
static final String KEK_CIPHER_ALGO = "AES";
47-
static final int KEK_LENGTH_IN_BYTES = 32; // 256-bit AES symmetric key
42+
static final Setting<String> PASSWORD_NAME_SETTING = Setting.simpleString("password_name", "");
4843

4944
// "protected" because it is overloaded for tests
5045
protected XPackLicenseState getLicenseState() { return XPackPlugin.getSharedLicenseState(); }
@@ -59,37 +54,22 @@ public EncryptedRepositoryPlugin() {
5954

6055
@Override
6156
public List<Setting<?>> getSettings() {
62-
return List.of(KEY_ENCRYPTION_KEY_SETTING);
57+
return List.of(ENCRYPTION_PASSWORD_SETTING);
6358
}
6459

6560
@Override
6661
public Map<String, Repository.Factory> getRepositories(Environment env,
6762
NamedXContentRegistry registry,
6863
ClusterService clusterService) {
69-
// store all KEKs from the keystore in memory (because the keystore is not readable later on)
70-
final Map<String, SecretKey> repositoryKEKMapBuilder = new HashMap<>();
71-
for (String KEKName : KEY_ENCRYPTION_KEY_SETTING.getNamespaces(env.settings())) {
72-
final Setting<InputStream> KEKSetting = KEY_ENCRYPTION_KEY_SETTING.getConcreteSettingForNamespace(KEKName);
73-
final SecretKey KEK;
74-
byte[] encodedKEKBytes = null;
75-
try (InputStream KEKInputStream = KEKSetting.get(env.settings())) {
76-
encodedKEKBytes = KEKInputStream.readAllBytes();
77-
if (encodedKEKBytes.length != KEK_LENGTH_IN_BYTES) {
78-
throw new IllegalArgumentException("Expected a 32 bytes (256 bit) wide AES key, but key ["
79-
+ KEKSetting.getKey() + "] is [" + encodedKEKBytes.length + "] bytes wide");
80-
}
81-
KEK = new SecretKeySpec(encodedKEKBytes, 0, KEK_LENGTH_IN_BYTES, KEK_CIPHER_ALGO);
82-
} catch (IOException e) {
83-
throw new UncheckedIOException("Exception while reading [" + KEKName + "] from the node keystore", e);
84-
} finally {
85-
if (encodedKEKBytes != null) {
86-
Arrays.fill(encodedKEKBytes, (byte) 0);
87-
}
88-
}
89-
logger.debug(() -> new ParameterizedMessage("Loaded repository AES key [" + KEKName + "] from the node keystore"));
90-
repositoryKEKMapBuilder.put(KEKName, KEK);
64+
// load all the passwords from the keystore in memory because the keystore is not readable when the repository is created
65+
final Map<String, char[]> repositoryPasswordsMapBuilder = new HashMap<>();
66+
for (String passwordName : ENCRYPTION_PASSWORD_SETTING.getNamespaces(env.settings())) {
67+
Setting<SecureString> passwordSetting = ENCRYPTION_PASSWORD_SETTING.getConcreteSettingForNamespace(passwordName);
68+
SecureString encryptionPassword = passwordSetting.get(env.settings());
69+
repositoryPasswordsMapBuilder.put(passwordName, encryptionPassword.getChars());
70+
logger.debug(() -> new ParameterizedMessage("Loaded repository password [" + passwordName + "] from the node keystore"));
9171
}
92-
final Map<String, SecretKey> repositoryKEKMap = Map.copyOf(repositoryKEKMapBuilder);
72+
final Map<String, char[]> repositoryPasswordsMap = Map.copyOf(repositoryPasswordsMapBuilder);
9373

9474
return Collections.singletonMap(REPOSITORY_TYPE_NAME, new Repository.Factory() {
9575

@@ -109,23 +89,23 @@ public Repository create(RepositoryMetaData metaData, Function<String, Repositor
10989
DELEGATE_TYPE_SETTING.getKey() + "] must not be equal to [" + REPOSITORY_TYPE_NAME + "]");
11090
}
11191
final Repository.Factory factory = typeLookup.apply(delegateType);
112-
if (null == factory) {
113-
throw new IllegalArgumentException("Unrecognized delegate repository type [" + DELEGATE_TYPE_SETTING.getKey() + "]");
92+
if (null == factory || false == SUPPORTED_ENCRYPTED_TYPE_NAMES.contains(delegateType)) {
93+
throw new IllegalArgumentException("Unsupported delegate repository type [" + DELEGATE_TYPE_SETTING.getKey() + "]");
11494
}
115-
final String repositoryKEKName = KEK_NAME_SETTING.get(metaData.settings());
116-
if (Strings.hasLength(repositoryKEKName) == false) {
117-
throw new IllegalArgumentException("Repository setting [" + KEK_NAME_SETTING.getKey() + "] must be set");
95+
final String repositoryPasswordName = PASSWORD_NAME_SETTING.get(metaData.settings());
96+
if (Strings.hasLength(repositoryPasswordName) == false) {
97+
throw new IllegalArgumentException("Repository setting [" + PASSWORD_NAME_SETTING.getKey() + "] must be set");
11898
}
119-
final SecretKey repositoryKEK = repositoryKEKMap.get(repositoryKEKName);
120-
if (repositoryKEK == null) {
99+
final char[] repositoryPassword = repositoryPasswordsMap.get(repositoryPasswordName);
100+
if (repositoryPassword == null) {
121101
throw new IllegalArgumentException("Secure setting [" +
122-
KEY_ENCRYPTION_KEY_SETTING.getConcreteSettingForNamespace(repositoryKEKName).getKey() + "] must be set");
102+
ENCRYPTION_PASSWORD_SETTING.getConcreteSettingForNamespace(repositoryPasswordName).getKey() + "] must be set");
123103
}
124104
final Repository delegatedRepository = factory.create(new RepositoryMetaData(metaData.name(), delegateType,
125105
metaData.settings()));
126106
if (false == (delegatedRepository instanceof BlobStoreRepository)
127107
|| delegatedRepository instanceof EncryptedRepository) {
128-
throw new IllegalArgumentException("Unsupported delegate type [" + DELEGATE_TYPE_SETTING.getKey() + "]");
108+
throw new IllegalArgumentException("Unsupported delegate repository type [" + DELEGATE_TYPE_SETTING.getKey() + "]");
129109
}
130110
if (false == getLicenseState().isEncryptedSnapshotAllowed()) {
131111
logger.warn("Encrypted snapshots are not allowed for the currently installed license." +
@@ -134,7 +114,7 @@ public Repository create(RepositoryMetaData metaData, Function<String, Repositor
134114
LicenseUtils.newComplianceException("encrypted snapshots"));
135115
}
136116
return new EncryptedRepository(metaData, registry, clusterService, (BlobStoreRepository) delegatedRepository,
137-
() -> getLicenseState(), repositoryKEK);
117+
() -> getLicenseState(), repositoryPassword);
138118
}
139119
});
140120
}

x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/EncryptedAzureBlobStoreRepositoryIntegTests.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.elasticsearch.repositories.azure.AzureBlobStoreRepositoryTests;
1515
import org.junit.BeforeClass;
1616

17-
import java.nio.charset.StandardCharsets;
1817
import java.util.ArrayList;
1918
import java.util.Arrays;
2019
import java.util.Collection;
@@ -52,11 +51,8 @@ protected Settings nodeSettings(int nodeOrdinal) {
5251
protected MockSecureSettings nodeSecureSettings() {
5352
MockSecureSettings secureSettings = new MockSecureSettings();
5453
for (String repositoryName : repositoryNames) {
55-
byte[] repositoryNameBytes = repositoryName.getBytes(StandardCharsets.UTF_8);
56-
byte[] repositoryKEK = new byte[32];
57-
System.arraycopy(repositoryNameBytes, 0, repositoryKEK, 0, repositoryNameBytes.length);
58-
secureSettings.setFile(EncryptedRepositoryPlugin.KEY_ENCRYPTION_KEY_SETTING.
59-
getConcreteSettingForNamespace(repositoryName).getKey(), repositoryKEK);
54+
secureSettings.setString(EncryptedRepositoryPlugin.ENCRYPTION_PASSWORD_SETTING.
55+
getConcreteSettingForNamespace(repositoryName).getKey(), repositoryName);
6056
}
6157
return secureSettings;
6258
}
@@ -81,7 +77,7 @@ protected Settings repositorySettings(String repositoryName) {
8177
return Settings.builder()
8278
.put(super.repositorySettings())
8379
.put(EncryptedRepositoryPlugin.DELEGATE_TYPE_SETTING.getKey(), "azure")
84-
.put(EncryptedRepositoryPlugin.KEK_NAME_SETTING.getKey(), repositoryName)
80+
.put(EncryptedRepositoryPlugin.PASSWORD_NAME_SETTING.getKey(), repositoryName)
8581
.build();
8682
}
8783

0 commit comments

Comments
 (0)