This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit de1850a6e88f2e0c1682b8b6c72efdfafcba85f2
Author: Logic <[email protected]>
AuthorDate: Wed Aug 26 12:19:06 2026 +0800

    feat: authenticate stored secret payloads
---
 .../org/apache/hertzbeat/common/util/AesUtil.java  | 78 ++++++++++++++--------
 .../apache/hertzbeat/common/util/AesUtilTest.java  | 21 ++++--
 2 files changed, 69 insertions(+), 30 deletions(-)

diff --git 
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java
 
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java
index b9b63579a5..81a65af8ad 100644
--- 
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java
+++ 
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/util/AesUtil.java
@@ -26,6 +26,7 @@ import java.util.Base64;
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
 import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.spec.GCMParameterSpec;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 import lombok.extern.slf4j.Slf4j;
@@ -45,13 +46,21 @@ public final class AesUtil {
     /**
      * Default algorithm
      */
-    private static final String ALGORITHM_STR = "AES/CBC/PKCS5Padding";
+    private static final String CBC_ALGORITHM_STR = "AES/CBC/PKCS5Padding";
 
     private static final String AES = "AES";
 
-    private static final byte[] PAYLOAD_HEADER = {'H', 'B', 'A', '2'};
+    private static final String AUTHENTICATED_ALGORITHM_STR = 
"AES/GCM/NoPadding";
 
-    private static final int IV_LENGTH = 16;
+    private static final byte[] CBC_PAYLOAD_HEADER = {'H', 'B', 'A', '2'};
+
+    private static final byte[] AUTHENTICATED_PAYLOAD_HEADER = {'H', 'B', 'A', 
'3'};
+
+    private static final int CBC_IV_LENGTH = 16;
+
+    private static final int GCM_NONCE_LENGTH = 12;
+
+    private static final int GCM_TAG_LENGTH_BITS = 128;
 
     private static final SecureRandom SECURE_RANDOM = new SecureRandom();
 
@@ -84,7 +93,7 @@ public final class AesUtil {
     }
 
     /**
-     * Encrypted plaintext aes cbc mode
+     * Encrypt plaintext in the authenticated payload format.
      *
      * @param content content
      * @param encryptKey secretKey
@@ -92,21 +101,19 @@ public final class AesUtil {
      */
     public static String aesEncode(String content, String encryptKey) {
         try {
-            // todo consider not init cipher every time and test performance
             SecretKeySpec keySpec = new 
SecretKeySpec(encryptKey.getBytes(StandardCharsets.UTF_8), AES);
-            // cipher based on the algorithm AES
-            Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
-            byte[] initializationVector = new byte[IV_LENGTH];
-            SECURE_RANDOM.nextBytes(initializationVector);
-            // init cipher Encrypt_mode or Decrypt_mode operation, the second 
parameter is the KEY used
-            cipher.init(Cipher.ENCRYPT_MODE, keySpec, new 
IvParameterSpec(initializationVector));
+            Cipher cipher = Cipher.getInstance(AUTHENTICATED_ALGORITHM_STR);
+            byte[] nonce = new byte[GCM_NONCE_LENGTH];
+            SECURE_RANDOM.nextBytes(nonce);
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec, new 
GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
+            cipher.updateAAD(AUTHENTICATED_PAYLOAD_HEADER);
             // get content bytes, must utf-8
             byte[] byteEncode = content.getBytes(StandardCharsets.UTF_8);
             // encode content to byte array
             byte[] byteAes = cipher.doFinal(byteEncode);
-            byte[] payload = ByteBuffer.allocate(PAYLOAD_HEADER.length + 
initializationVector.length + byteAes.length)
-                    .put(PAYLOAD_HEADER)
-                    .put(initializationVector)
+            byte[] payload = 
ByteBuffer.allocate(AUTHENTICATED_PAYLOAD_HEADER.length + nonce.length + 
byteAes.length)
+                    .put(AUTHENTICATED_PAYLOAD_HEADER)
+                    .put(nonce)
                     .put(byteAes)
                     .array();
             // base64 encode content
@@ -149,33 +156,52 @@ public final class AesUtil {
     
     private static byte[] getBytes(final String content, final String 
decryptKey) throws Exception {
         SecretKeySpec keySpec = new 
SecretKeySpec(decryptKey.getBytes(StandardCharsets.UTF_8), AES);
-        // cipher based on the algorithm AES
-        Cipher cipher = Cipher.getInstance(ALGORITHM_STR);
         byte[] bytesContent = Base64.getDecoder().decode(content);
+        if (hasPayloadHeader(bytesContent, AUTHENTICATED_PAYLOAD_HEADER)) {
+            return decryptAuthenticatedPayload(bytesContent, keySpec);
+        }
+        return decryptCbcPayload(bytesContent, decryptKey, keySpec);
+    }
+
+    private static byte[] decryptAuthenticatedPayload(byte[] payload, 
SecretKeySpec keySpec) throws Exception {
+        int encryptedOffset = AUTHENTICATED_PAYLOAD_HEADER.length + 
GCM_NONCE_LENGTH;
+        int minimumPayloadLength = encryptedOffset + GCM_TAG_LENGTH_BITS / 
Byte.SIZE;
+        if (payload.length < minimumPayloadLength) {
+            throw new IllegalArgumentException("Invalid authenticated 
encrypted payload");
+        }
+        byte[] nonce = Arrays.copyOfRange(
+                payload, AUTHENTICATED_PAYLOAD_HEADER.length, encryptedOffset);
+        Cipher cipher = Cipher.getInstance(AUTHENTICATED_ALGORITHM_STR);
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, new 
GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
+        cipher.updateAAD(AUTHENTICATED_PAYLOAD_HEADER);
+        return cipher.doFinal(payload, encryptedOffset, payload.length - 
encryptedOffset);
+    }
+
+    private static byte[] decryptCbcPayload(byte[] bytesContent, String 
decryptKey, SecretKeySpec keySpec)
+            throws Exception {
         byte[] initializationVector;
-        if (hasPayloadHeader(bytesContent)) {
-            if (bytesContent.length <= PAYLOAD_HEADER.length + IV_LENGTH) {
+        if (hasPayloadHeader(bytesContent, CBC_PAYLOAD_HEADER)) {
+            if (bytesContent.length <= CBC_PAYLOAD_HEADER.length + 
CBC_IV_LENGTH) {
                 throw new IllegalArgumentException("Invalid encrypted 
payload");
             }
             initializationVector = Arrays.copyOfRange(
-                    bytesContent, PAYLOAD_HEADER.length, PAYLOAD_HEADER.length 
+ IV_LENGTH);
+                    bytesContent, CBC_PAYLOAD_HEADER.length, 
CBC_PAYLOAD_HEADER.length + CBC_IV_LENGTH);
             bytesContent = Arrays.copyOfRange(
-                    bytesContent, PAYLOAD_HEADER.length + IV_LENGTH, 
bytesContent.length);
+                    bytesContent, CBC_PAYLOAD_HEADER.length + CBC_IV_LENGTH, 
bytesContent.length);
         } else {
             initializationVector = decryptKey.getBytes(StandardCharsets.UTF_8);
         }
-        // init cipher Encrypt_mode or Decrypt_mode operation, the second 
parameter is the KEY used
+        Cipher cipher = Cipher.getInstance(CBC_ALGORITHM_STR);
         cipher.init(Cipher.DECRYPT_MODE, keySpec, new 
IvParameterSpec(initializationVector));
-        // decode content to byte array
         return cipher.doFinal(bytesContent);
     }
 
-    private static boolean hasPayloadHeader(byte[] payload) {
-        if (payload.length < PAYLOAD_HEADER.length) {
+    private static boolean hasPayloadHeader(byte[] payload, byte[] 
expectedHeader) {
+        if (payload.length < expectedHeader.length) {
             return false;
         }
-        for (int index = 0; index < PAYLOAD_HEADER.length; index++) {
-            if (payload[index] != PAYLOAD_HEADER[index]) {
+        for (int index = 0; index < expectedHeader.length; index++) {
+            if (payload[index] != expectedHeader[index]) {
                 return false;
             }
         }
diff --git 
a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java
 
b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java
index 7ff913701c..6e033660a2 100644
--- 
a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java
+++ 
b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/AesUtilTest.java
@@ -53,12 +53,12 @@ class AesUtilTest {
 
         byte[] encryptedPayload = Base64.getDecoder().decode(encryptedText);
         byte[] secondEncryptedPayload = 
Base64.getDecoder().decode(secondEncryptedText);
-        assertArrayEquals(new byte[] {'H', 'B', 'A', '2'}, 
Arrays.copyOfRange(encryptedPayload, 0, 4));
+        assertArrayEquals(new byte[] {'H', 'B', 'A', '3'}, 
Arrays.copyOfRange(encryptedPayload, 0, 4));
         assertFalse(Arrays.equals(
-                VALID_KEY.getBytes(StandardCharsets.UTF_8), 
Arrays.copyOfRange(encryptedPayload, 4, 20)));
+                VALID_KEY.getBytes(StandardCharsets.UTF_8), 
Arrays.copyOfRange(encryptedPayload, 4, 16)));
         assertFalse(Arrays.equals(
-                Arrays.copyOfRange(encryptedPayload, 4, 20),
-                Arrays.copyOfRange(secondEncryptedPayload, 4, 20)));
+                Arrays.copyOfRange(encryptedPayload, 4, 16),
+                Arrays.copyOfRange(secondEncryptedPayload, 4, 16)));
 
         String decryptedText = aesDecode(encryptedText, VALID_KEY);
         assertEquals(originalText, decryptedText);
@@ -72,6 +72,15 @@ class AesUtilTest {
         assertTrue(isCiphertext(legacyCiphertext, VALID_KEY));
     }
 
+    @Test
+    void testRandomIvCbcCiphertextCanBeDecoded() {
+        String randomIvCbcCiphertext =
+                
"SEJBMgABAgMEBQYHCAkKCwwNDg9SvG5xbeL65ozxm7IHkzrexBLPr0EyDlh+5BupKDWZ/Q==";
+
+        assertEquals("This is a secret message", 
aesDecode(randomIvCbcCiphertext, VALID_KEY));
+        assertTrue(isCiphertext(randomIvCbcCiphertext, VALID_KEY));
+    }
+
     @Test
     void testAesDecode() {
         // Test with invalid key
@@ -111,6 +120,10 @@ class AesUtilTest {
         encryptedText = aesEncode(originalText, VALID_KEY);
         String invalidKey = "6543210987654321";
         assertFalse(isCiphertext(encryptedText, invalidKey));
+
+        byte[] tamperedPayload = Base64.getDecoder().decode(encryptedText);
+        tamperedPayload[tamperedPayload.length - 1] ^= 1;
+        
assertFalse(isCiphertext(Base64.getEncoder().encodeToString(tamperedPayload), 
VALID_KEY));
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to