This is an automated email from the ASF dual-hosted git repository.
sodonnell pushed a commit to branch HDDS-13323-sts
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/HDDS-13323-sts by this push:
new c63f444a461 HDDS-13961. [STS] Encrypt secretAccessKey in session token
(#9344)
c63f444a461 is described below
commit c63f444a46148cc05fbdb402b5b67e4cc3317172
Author: fmorg-git <[email protected]>
AuthorDate: Tue Dec 2 08:24:18 2025 -0800
HDDS-13961. [STS] Encrypt secretAccessKey in session token (#9344)
Co-authored-by: Fabian Morgan <[email protected]>
---
hadoop-ozone/ozone-manager/pom.xml | 4 +
.../request/s3/security/S3AssumeRoleRequest.java | 14 +-
.../hadoop/ozone/security/STSTokenEncryption.java | 205 +++++++++++++++++++++
.../hadoop/ozone/security/STSTokenIdentifier.java | 79 ++++++--
.../ozone/security/STSTokenSecretManager.java | 1 -
.../ozone/security/TestSTSTokenEncryption.java | 200 ++++++++++++++++++++
.../ozone/security/TestSTSTokenIdentifier.java | 37 +++-
.../ozone/security/TestSTSTokenSecretManager.java | 2 +
8 files changed, 524 insertions(+), 18 deletions(-)
diff --git a/hadoop-ozone/ozone-manager/pom.xml
b/hadoop-ozone/ozone-manager/pom.xml
index 923b1c02cbe..d1e1be0798b 100644
--- a/hadoop-ozone/ozone-manager/pom.xml
+++ b/hadoop-ozone/ozone-manager/pom.xml
@@ -209,6 +209,10 @@
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
+ </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
index 31f71204dc4..9d092eaba01 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/request/s3/security/S3AssumeRoleRequest.java
@@ -42,7 +42,19 @@
*/
public class S3AssumeRoleRequest extends OMClientRequest {
- private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+ private static final SecureRandom SECURE_RANDOM;
+
+ static {
+ SecureRandom secureRandom;
+ try {
+ // Prefer non-blocking native PRNG where available
+ secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking");
+ } catch (Exception e) {
+ // Fallback to default SecureRandom implementation
+ secureRandom = new SecureRandom();
+ }
+ SECURE_RANDOM = secureRandom;
+ }
private static final int MIN_TOKEN_EXPIRATION_SECONDS = 900; // 15
minutes in seconds
private static final int MAX_TOKEN_EXPIRATION_SECONDS = 43200; // 12 hours
in seconds
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenEncryption.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenEncryption.java
new file mode 100644
index 00000000000..ef03da982b0
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenEncryption.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hadoop.ozone.security;
+
+import com.google.common.base.Preconditions;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.hadoop.hdds.annotation.InterfaceAudience;
+import org.apache.hadoop.hdds.annotation.InterfaceStability;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
+import org.bouncycastle.crypto.params.HKDFParameters;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+/**
+ * Utility class for encrypting and decrypting sensitive data in STS tokens.
+ * Uses HKDF to derive an AES encryption key from the SCM ManagedSecretKey,
+ * then uses AES-GCM for authenticated encryption.
+ */
[email protected]
[email protected]
+public final class STSTokenEncryption {
+
+ // HKDF parameters
+ private static final byte[] HKDF_INFO =
"STS-TOKEN-ENCRYPTION".getBytes(StandardCharsets.UTF_8);
+ private static final int HKDF_SALT_LENGTH = 16; // 128 bits
+ private static final int AES_KEY_LENGTH = 32; // 256 bits
+
+ // AES-GCM parameters
+ private static final int GCM_IV_LENGTH = 12; // 96 bits
+ private static final int GCM_AUTHENTICATION_TAG_LENGTH_IN_BITS = 128;
+ private static final String AES_ALGORITHM = "AES";
+ private static final String AES_CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
+
+ private static final SecureRandom SECURE_RANDOM;
+ private static final BouncyCastleProvider BC_PROVIDER = new
BouncyCastleProvider();
+
+ private STSTokenEncryption() {
+ }
+
+ static {
+ SecureRandom secureRandom;
+ try {
+ // Prefer non-blocking native PRNG where available
+ secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking");
+ } catch (Exception e) {
+ // Fallback to default SecureRandom implementation
+ secureRandom = new SecureRandom();
+ }
+ SECURE_RANDOM = secureRandom;
+ }
+
+ /**
+ * Encrypt sensitive data using AES-GCM with a key derived from the secret
key via HKDF,
+ * binding the provided AAD to the authentication tag.
+ *
+ * @param plaintext the sensitive data to encrypt
+ * @param secretKeyBytes the secret key bytes from ManagedSecretKey
+ * @param aad additional authenticated data to bind
+ * @return base64-encoded encrypted data with Salt and IV prepended
+ * @throws STSTokenEncryptionException if encryption fails
+ */
+ public static String encrypt(String plaintext, byte[] secretKeyBytes, byte[]
aad) throws STSTokenEncryptionException {
+ Preconditions.checkArgument(
+ secretKeyBytes != null && secretKeyBytes.length > 0, "The
secretKeyBytes must not be null nor empty");
+ Preconditions.checkArgument(aad != null && aad.length > 0, "The aad must
not be null nor empty");
+ // Don't encrypt null/empty strings
+ if (plaintext == null || plaintext.isEmpty()) {
+ return plaintext;
+ }
+
+ byte[] aesKey;
+ byte[] iv;
+ byte[] salt;
+ try {
+ // Generate random salt
+ salt = new byte[HKDF_SALT_LENGTH];
+ SECURE_RANDOM.nextBytes(salt);
+
+ // Derive AES key using HKDF with random salt
+ aesKey = deriveKey(secretKeyBytes, salt);
+
+ // Generate random IV
+ iv = new byte[GCM_IV_LENGTH];
+ SECURE_RANDOM.nextBytes(iv);
+
+ // Initialize AES-GCM cipher
+ final Cipher cipher = Cipher.getInstance(AES_CIPHER_TRANSFORMATION,
BC_PROVIDER);
+ final GCMParameterSpec spec = new
GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH_IN_BITS, iv);
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey,
AES_ALGORITHM), spec);
+ cipher.updateAAD(aad);
+
+ // Encrypt the plaintext
+ final byte[] ciphertext =
cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
+
+ // Combine salt, IV and ciphertext
+ final byte[] result = org.bouncycastle.util.Arrays.concatenate(salt, iv,
ciphertext);
+
+ return Base64.getEncoder().encodeToString(result);
+ } catch (Exception e) {
+ throw new STSTokenEncryptionException("Failed to encrypt sensitive
data", e);
+ }
+ }
+
+ /**
+ * Decrypt sensitive data using AES-GCM with a key derived from the secret
key via HKDF,
+ * verifying the provided AAD bound to the authentication tag.
+ *
+ * @param encryptedData base64-encoded encrypted data with Salt and
IV prepended
+ * @param secretKeyBytes the secret key bytes from ManagedSecretKey
+ * @param aad additional authenticated data to verify
+ * @return decrypted plaintext
+ * @throws STSTokenEncryptionException if decryption fails
+ */
+ public static String decrypt(String encryptedData, byte[] secretKeyBytes,
byte[] aad)
+ throws STSTokenEncryptionException {
+ Preconditions.checkArgument(
+ secretKeyBytes != null && secretKeyBytes.length > 0, "The
secretKeyBytes must not be null nor empty");
+ Preconditions.checkArgument(aad != null && aad.length > 0, "The aad must
not be null nor empty");
+ // Don't decrypt null/empty strings
+ if (encryptedData == null || encryptedData.isEmpty()) {
+ return encryptedData;
+ }
+
+ byte[] aesKey;
+ try {
+ // Decode base64
+ final byte[] data = Base64.getDecoder().decode(encryptedData);
+
+ if (data.length < HKDF_SALT_LENGTH + GCM_IV_LENGTH) {
+ throw new STSTokenEncryptionException("Invalid encrypted data");
+ }
+
+ // Extract salt, IV and ciphertext
+ final byte[] salt = new byte[HKDF_SALT_LENGTH];
+ final byte[] iv = new byte[GCM_IV_LENGTH];
+ final byte[] ciphertext = new byte[data.length - HKDF_SALT_LENGTH -
GCM_IV_LENGTH];
+
+ System.arraycopy(data, 0, salt, 0, HKDF_SALT_LENGTH);
+ System.arraycopy(data, HKDF_SALT_LENGTH, iv, 0, GCM_IV_LENGTH);
+ System.arraycopy(data, HKDF_SALT_LENGTH + GCM_IV_LENGTH, ciphertext, 0,
ciphertext.length);
+
+ // Derive AES key using HKDF with extracted salt
+ aesKey = deriveKey(secretKeyBytes, salt);
+
+ // Initialize AES-GCM cipher
+ final Cipher cipher = Cipher.getInstance(AES_CIPHER_TRANSFORMATION,
BC_PROVIDER);
+ final GCMParameterSpec spec = new
GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH_IN_BITS, iv);
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey,
AES_ALGORITHM), spec);
+ cipher.updateAAD(aad);
+
+ // Decrypt the ciphertext
+ final byte[] output = cipher.doFinal(ciphertext);
+
+ return new String(output, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new STSTokenEncryptionException("Failed to decrypt sensitive
data", e);
+ }
+ }
+
+ /**
+ * Derive AES key using HKDF-SHA256.
+ */
+ private static byte[] deriveKey(byte[] secretKeyBytes, byte[] salt) {
+ final HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());
+ hkdf.init(new HKDFParameters(secretKeyBytes, salt, HKDF_INFO));
+
+ final byte[] aesKey = new byte[AES_KEY_LENGTH];
+ hkdf.generateBytes(aesKey, 0, AES_KEY_LENGTH);
+ return aesKey;
+ }
+
+ /**
+ * Exception thrown when encryption/decryption operations fail.
+ */
+ public static class STSTokenEncryptionException extends Exception {
+ public STSTokenEncryptionException(String message) {
+ super(message);
+ }
+
+ public STSTokenEncryptionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
+
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
index 1f2e8d300ae..1ba4b7186f2 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenIdentifier.java
@@ -23,6 +23,7 @@
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
@@ -112,21 +113,24 @@ public void readFields(DataInput in) throws IOException {
* Convert this identifier to protobuf format.
*/
public OMTokenProto toProtoBuf() {
- final OMTokenProto.Builder builder = OMTokenProto.newBuilder()
+ Preconditions.checkArgument(this.encryptionKey != null, "The encryption
key must not be null");
+
+ final OMTokenProto.Builder builder = OMTokenProto.newBuilder();
+ // Note: secretKeyId must be set before attempting to decrypt
secretAccessKey
+ if (getSecretKeyId() != null) {
+ builder.setSecretKeyId(getSecretKeyId().toString());
+ }
+
+ builder
.setType(OMTokenProto.Type.S3_STS_TOKEN)
.setMaxDate(getExpiry().toEpochMilli())
.setOwner(getOwnerId() != null ? getOwnerId() : "")
.setAccessKeyId(getOwnerId() != null ? getOwnerId() : "")
.setOriginalAccessKeyId(originalAccessKeyId != null ?
originalAccessKeyId : "")
.setRoleArn(roleArn != null ? roleArn : "")
- // TODO sts - encrypt secret access key in a future PR
- .setSecretAccessKey(secretAccessKey != null ? secretAccessKey : "")
+ .setSecretAccessKey(secretAccessKey != null ?
encryptSensitiveField(secretAccessKey) : "")
.setSessionPolicy(sessionPolicy != null ? sessionPolicy : "");
- if (getSecretKeyId() != null) {
- builder.setSecretKeyId(getSecretKeyId().toString());
- }
-
return builder.build();
}
@@ -137,6 +141,7 @@ public void fromProtoBuf(OMTokenProto token) throws
IOException {
Preconditions.checkArgument(
token.getType() == OMTokenProto.Type.S3_STS_TOKEN,
"Invalid token type for STSTokenIdentifier: " + token.getType());
+ Preconditions.checkArgument(this.encryptionKey != null, "The encryption
key must not be null");
setOwnerId(token.getOwner());
setExpiry(Instant.ofEpochMilli(token.getMaxDate()));
@@ -147,11 +152,6 @@ public void fromProtoBuf(OMTokenProto token) throws
IOException {
if (token.hasRoleArn()) {
this.roleArn = token.getRoleArn();
}
- if (token.hasSecretAccessKey()) {
- // TODO sts - decrypt secret access key in a future PR
- this.secretAccessKey = token.getSecretAccessKey();
- }
-
if (token.hasSecretKeyId()) {
try {
setSecretKeyId(UUID.fromString(token.getSecretKeyId()));
@@ -161,12 +161,63 @@ public void fromProtoBuf(OMTokenProto token) throws
IOException {
"Invalid secretKeyId format in STS token: " +
token.getSecretKeyId(), e);
}
}
+ // Note: secretKeyId must be set before attempting to decrypt
secretAccessKey
+ if (token.hasSecretAccessKey()) {
+ this.secretAccessKey = decryptSensitiveField(token.getSecretAccessKey());
+ }
if (token.hasSessionPolicy()) {
this.sessionPolicy = token.getSessionPolicy();
}
}
+ /**
+ * Encrypt a sensitive field using the configured encryption key.
+ */
+ private String encryptSensitiveField(String value) {
+ if (encryptionKey == null) {
+ throw new IllegalStateException("Encryption key must be set before
encrypting sensitive fields");
+ }
+
+ try {
+ final byte[] aad = computeAadBytes();
+ return STSTokenEncryption.encrypt(value, encryptionKey, aad);
+ } catch (STSTokenEncryption.STSTokenEncryptionException e) {
+ throw new RuntimeException("Token encryption failed", e);
+ }
+ }
+
+ /**
+ * Decrypt a sensitive field using the configured encryption key.
+ */
+ private String decryptSensitiveField(String encryptedValue) {
+ if (encryptionKey == null) {
+ throw new IllegalStateException("Encryption key must be set before
decrypting sensitive fields");
+ }
+
+ try {
+ final byte[] aad = computeAadBytes();
+ return STSTokenEncryption.decrypt(encryptedValue, encryptionKey, aad);
+ } catch (STSTokenEncryption.STSTokenEncryptionException e) {
+ throw new RuntimeException("Token decryption failed", e);
+ }
+ }
+
+ /**
+ * Compute additional authenticated data to bind token context to encryption.
+ * Includes token type, ownerId, expiry millis, and secretKeyId.
+ */
+ private byte[] computeAadBytes() {
+ final StringBuilder stringBuilder = new StringBuilder("v1|S3_STS_TOKEN|");
+ stringBuilder.append(getOwnerId());
+ stringBuilder.append('|');
+ stringBuilder.append(getExpiry().toEpochMilli());
+ stringBuilder.append('|');
+ stringBuilder.append(getSecretKeyId().toString());
+ final String aad = stringBuilder.toString();
+ return aad.getBytes(StandardCharsets.UTF_8);
+ }
+
public String getRoleArn() {
return roleArn;
}
@@ -193,6 +244,10 @@ public String getSessionPolicy() {
return sessionPolicy;
}
+ public void setEncryptionKey(byte[] encryptionKey) {
+ this.encryptionKey = encryptionKey.clone();
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
index b418beea4c3..598a5a71675 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/security/STSTokenSecretManager.java
@@ -72,7 +72,6 @@ public String createSTSTokenString(String tempAccessKeyId,
String originalAccess
// Note - the encryptionKey will NOT be encoded in the token. When
generateToken() is called, it eventually calls
// the write() method in STSTokenIdentifier which calls toProtoBuf(), and
the encryptionKey is not
// serialized there.
- // TODO sts - use the encryptionKey in a future PR to encrypt/decrypt the
secretAccessKey
final STSTokenIdentifier identifier = new STSTokenIdentifier(
tempAccessKeyId, originalAccessKeyId, roleArn, expiration,
secretAccessKey, sessionPolicy, encryptionKey);
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java
new file mode 100644
index 00000000000..1eb880f9dd0
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenEncryption.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hadoop.ozone.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.UUID;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
+import
org.apache.hadoop.ozone.security.STSTokenEncryption.STSTokenEncryptionException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for STS token encryption functionality.
+ */
+public class TestSTSTokenEncryption {
+
+ // These must match the constants in STSTokenEncryption.
+ private static final int HKDF_SALT_LENGTH = 16; // 128 bits
+
+ private static SecretKey sharedSecretKey;
+
+ @BeforeAll
+ public static void setUpClass() {
+ final byte[] keyBytes =
"01234567890123456789012345678901".getBytes(StandardCharsets.US_ASCII);
+ sharedSecretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
+ }
+
+ @Test
+ public void testEncryptDecryptRoundTrip() throws Exception {
+ final byte[] keyBytes = sharedSecretKey.getEncoded();
+
+ final String originalSecret = "mySecretAccessKey123456";
+ final byte[] aad = "test-aad".getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt the secret
+ final String encrypted = STSTokenEncryption.encrypt(originalSecret,
keyBytes, aad);
+ assertNotNull(encrypted);
+ assertNotEquals(originalSecret, encrypted);
+
+ // Decrypt the secret
+ final String decrypted = STSTokenEncryption.decrypt(encrypted, keyBytes,
aad);
+ assertEquals(originalSecret, decrypted);
+ }
+
+ @Test
+ public void testSTSTokenIdentifierEncryption() throws Exception {
+ final byte[] keyBytes = sharedSecretKey.getEncoded();
+
+ final String tempAccessKeyId = "ASIA123TEMPKEY";
+ final String originalAccessKeyId = "AKIA123ORIGINAL";
+ final String roleArn = "arn:aws:iam::123456789012:role/TestRole";
+ final String secretAccessKey = "mySecretAccessKey123456";
+ // Use millisecond precision to match serialization format
+ final Instant expiry =
Instant.ofEpochMilli(Instant.now().plusSeconds(3600).toEpochMilli());
+ final String sessionPolicy = "test-session-policy";
+
+ // Create token identifier with encryption
+ final STSTokenIdentifier tokenId = new STSTokenIdentifier(
+ tempAccessKeyId, originalAccessKeyId, roleArn, expiry,
secretAccessKey, sessionPolicy, keyBytes);
+ tokenId.setSecretKeyId(UUID.randomUUID());
+
+ // Convert to protobuf
+ final OzoneManagerProtocolProtos.OMTokenProto omTokenProto =
tokenId.toProtoBuf();
+ assertNotEquals(secretAccessKey, omTokenProto.getSecretAccessKey()); //
ensure secretAccessKey is encrypted
+ final byte[] protobufBytes = omTokenProto.toByteArray();
+
+ // Create new token identifier from protobuf with decryption key
+ final STSTokenIdentifier decodedTokenId = new STSTokenIdentifier();
+ decodedTokenId.setEncryptionKey(keyBytes);
+ decodedTokenId.readFromByteArray(protobufBytes);
+
+ // Verify all fields are correctly decrypted
+ assertEquals(tempAccessKeyId, decodedTokenId.getTempAccessKeyId());
+ assertEquals(originalAccessKeyId, decodedTokenId.getOriginalAccessKeyId());
+ assertEquals(roleArn, decodedTokenId.getRoleArn());
+ assertEquals(secretAccessKey, decodedTokenId.getSecretAccessKey());
+ assertEquals(expiry, decodedTokenId.getExpiry());
+ }
+
+ @Test
+ public void testDecryptionWithWrongKey() throws Exception {
+ // Generate two different keys
+ final KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA256");
+ keyGen.init(256);
+ final SecretKey key1 = keyGen.generateKey();
+ final SecretKey key2 = keyGen.generateKey();
+
+ final String originalSecret = "mySecretAccessKey123456";
+ final byte[] aad = "key-aad".getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt with key1
+ final String encrypted = STSTokenEncryption.encrypt(originalSecret,
key1.getEncoded(), aad);
+
+ // Try to decrypt with key2 - should fail
+ assertThrows(
+ STSTokenEncryptionException.class, () ->
STSTokenEncryption.decrypt(encrypted, key2.getEncoded(), aad));
+ }
+
+ @Test
+ public void testDecryptionFailsWhenCiphertextIsCorrupted() throws Exception {
+ final byte[] keyBytes = sharedSecretKey.getEncoded();
+
+ final String originalSecret = "mySecretAccessKey123456";
+ final byte[] aad = "ciphertext-aad".getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt the secret
+ final String encrypted = STSTokenEncryption.encrypt(originalSecret,
keyBytes, aad);
+ final byte[] data = Base64.getDecoder().decode(encrypted);
+
+ // Corrupt the last byte of the ciphertext segment
+ data[data.length - 1] ^= 0x01;
+
+ final String tampered = Base64.getEncoder().encodeToString(data);
+
+ // Decryption must fail with corrupted ciphertext
+ assertThrows(
+ STSTokenEncryptionException.class,
+ () -> STSTokenEncryption.decrypt(tampered, keyBytes, aad));
+ }
+
+ @Test
+ public void testDecryptionFailsWhenSaltIsCorrupted() throws Exception {
+ final byte[] keyBytes = sharedSecretKey.getEncoded();
+
+ final String originalSecret = "mySecretAccessKey123456";
+ final byte[] aad = "salt-aad".getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt the secret
+ final String encrypted = STSTokenEncryption.encrypt(originalSecret,
keyBytes, aad);
+ final byte[] data = Base64.getDecoder().decode(encrypted);
+
+ // Corrupt the first byte of the salt segment
+ data[0] ^= 0x01;
+
+ final String tampered = Base64.getEncoder().encodeToString(data);
+
+ // Decryption must fail with corrupted salt (derives wrong AES key)
+ assertThrows(STSTokenEncryptionException.class, () ->
STSTokenEncryption.decrypt(tampered, keyBytes, aad));
+ }
+
+ @Test
+ public void testDecryptionFailsWhenIvIsCorrupted() throws Exception {
+ final byte[] keyBytes = sharedSecretKey.getEncoded();
+
+ final String originalSecret = "mySecretAccessKey123456";
+ final byte[] aad = "iv-aad".getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt the secret
+ final String encrypted = STSTokenEncryption.encrypt(originalSecret,
keyBytes, aad);
+ final byte[] data = Base64.getDecoder().decode(encrypted);
+
+ // Corrupt the first byte of the IV segment
+ data[HKDF_SALT_LENGTH] ^= 0x01;
+
+ final String tampered = Base64.getEncoder().encodeToString(data);
+
+ // Decryption must fail with corrupted IV
+ assertThrows(STSTokenEncryptionException.class, () ->
STSTokenEncryption.decrypt(tampered, keyBytes, aad));
+ }
+
+ @Test
+ public void testDecryptionFailsWhenAadIsModified() throws Exception {
+ final byte[] keyBytes = sharedSecretKey.getEncoded();
+
+ final String originalSecret = "mySecretAccessKey123456";
+ final byte[] aadOriginal = "aad-original".getBytes(StandardCharsets.UTF_8);
+ final byte[] aadModified = "aad-modified".getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt with original AAD
+ final String encrypted = STSTokenEncryption.encrypt(originalSecret,
keyBytes, aadOriginal);
+
+ // Decrypt with modified AAD - authentication must fail
+ assertThrows(STSTokenEncryptionException.class, () ->
STSTokenEncryption.decrypt(encrypted, keyBytes, aadModified));
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
index ada9c756104..549d473a49d 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenIdentifier.java
@@ -70,11 +70,12 @@ public void testProtoBufRoundTrip() throws IOException {
assertThat(proto.getMaxDate()).isEqualTo(expiry.toEpochMilli());
assertThat(proto.getOriginalAccessKeyId()).isEqualTo("origAccess");
assertThat(proto.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/RoleY");
- assertThat(proto.getSecretAccessKey()).isEqualTo("secretKey");
+ assertThat(proto.getSecretAccessKey()).isNotEqualTo("secretKey"); //
must be encrypted
assertThat(proto.getSessionPolicy()).isEqualTo("sessionPolicy");
assertThat(proto.getSecretKeyId()).isEqualTo(secretKeyId.toString());
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
+ parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
parsedTokenIdentifier.fromProtoBuf(proto);
assertThat(parsedTokenIdentifier.getOwnerId()).isEqualTo("tempAccess");
@@ -111,11 +112,14 @@ public void testProtobufRoundTripWithNullSessionPolicy()
throws IOException {
final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
"tempAccess", "origAccess", "arn:aws:iam::123456789012:role/RoleX",
expiry, "secretKey", null, ENCRYPTION_KEY);
+ final UUID secretKeyId = UUID.randomUUID();
+ stsTokenIdentifier.setSecretKeyId(secretKeyId);
final OMTokenProto proto = stsTokenIdentifier.toProtoBuf();
assertThat(proto.getSessionPolicy()).isEmpty();
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
+ parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
parsedTokenIdentifier.fromProtoBuf(proto);
assertThat(parsedTokenIdentifier.getSessionPolicy()).isEmpty();
@@ -127,11 +131,14 @@ public void testProtobufRoundTripWithEmptySessionPolicy()
throws IOException {
final STSTokenIdentifier stsTokenIdentifier = new STSTokenIdentifier(
"tempAccess", "origAccess", "arn:aws:iam::123456789012:role/RoleZ",
expiry, "secretKey", "", ENCRYPTION_KEY);
+ final UUID secretKeyId = UUID.randomUUID();
+ stsTokenIdentifier.setSecretKeyId(secretKeyId);
final OMTokenProto proto = stsTokenIdentifier.toProtoBuf();
assertThat(proto.getSessionPolicy()).isEmpty();
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
+ parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
parsedTokenIdentifier.fromProtoBuf(proto);
assertThat(parsedTokenIdentifier.getSessionPolicy()).isEmpty();
@@ -173,6 +180,7 @@ public void testWriteToAndReadFromByteArray() throws
Exception {
final byte[] bytes = baos.toByteArray();
final STSTokenIdentifier parsedTokenIdentifier = new STSTokenIdentifier();
+ parsedTokenIdentifier.setEncryptionKey(ENCRYPTION_KEY);
parsedTokenIdentifier.readFromByteArray(bytes);
assertThat(parsedTokenIdentifier).isEqualTo(originalTokenIdentifier);
@@ -207,8 +215,19 @@ public void
testWriteToAndReadFromByteArrayWithDifferentSecretKeyIds() throws Ex
anotherTokenIdentifier.write(out);
}
- // The byte arrays should be different due to different secret key IDs
+ // The byte arrays will not be the same because the encrypted
secretAccessKey cipher for each will differ.
+ // However, the STSTokenIdentifier derived from each byte array should
also not be the same.
assertThat(baos1.toByteArray()).isNotEqualTo(baos2.toByteArray());
+ final byte[] byteArr1 = baos1.toByteArray();
+ final byte[] byteArr2 = baos2.toByteArray();
+ assertThat(byteArr1).isNotEqualTo(byteArr2);
+ final STSTokenIdentifier tokenFromByteArr1 = new STSTokenIdentifier();
+ tokenFromByteArr1.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr1.readFromByteArray(byteArr1);
+ final STSTokenIdentifier tokenFromByteArr2 = new STSTokenIdentifier();
+ tokenFromByteArr2.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr2.readFromByteArray(byteArr2);
+ assertThat(tokenFromByteArr1).isNotEqualTo(tokenFromByteArr2);
}
@Test
@@ -236,8 +255,18 @@ public void
testWriteToAndReadFromByteArrayWithSameSecretKeyIds() throws Excepti
anotherTokenIdentifier.write(out);
}
- // The byte arrays should be the same since they have the same contents
- assertThat(baos1.toByteArray()).isEqualTo(baos2.toByteArray());
+ // The byte arrays should not be the same because the encrypted
secretAccessKey cipher for each will differ.
+ // However, the STSTokenIdentifier derived from each byte array should be
the same.
+ final byte[] byteArr1 = baos1.toByteArray();
+ final byte[] byteArr2 = baos2.toByteArray();
+ assertThat(byteArr1).isNotEqualTo(byteArr2);
+ final STSTokenIdentifier tokenFromByteArr1 = new STSTokenIdentifier();
+ tokenFromByteArr1.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr1.readFromByteArray(byteArr1);
+ final STSTokenIdentifier tokenFromByteArr2 = new STSTokenIdentifier();
+ tokenFromByteArr2.setEncryptionKey(ENCRYPTION_KEY);
+ tokenFromByteArr2.readFromByteArray(byteArr2);
+ assertThat(tokenFromByteArr1).isEqualTo(tokenFromByteArr2);
}
@Test
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
index f35fc902460..5eb7868c4a2 100644
---
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/security/TestSTSTokenSecretManager.java
@@ -86,6 +86,7 @@ public void testCreateSTSTokenStringContainsCorrectFields()
throws IOException {
// Verify the token identifier fields
final STSTokenIdentifier identifier = new STSTokenIdentifier();
+ identifier.setEncryptionKey(sharedSecretKey.getEncoded());
identifier.readFromByteArray(token.getIdentifier());
final Instant afterCreation = Instant.now();
final Instant expiration = identifier.getExpiry();
@@ -113,6 +114,7 @@ public void testCreateSTSTokenStringWithNullSessionPolicy()
throws IOException {
token.decodeFromUrlString(tokenString);
final STSTokenIdentifier identifier = new STSTokenIdentifier();
+ identifier.setEncryptionKey(sharedSecretKey.getEncoded());
identifier.readFromByteArray(token.getIdentifier());
assertTrue(identifier.getSessionPolicy().isEmpty());
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]