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]

Reply via email to