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]
