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 fd1279b809..b9b63579a5 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 @@ -17,8 +17,11 @@ package org.apache.hertzbeat.common.util; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -46,6 +49,12 @@ public final class AesUtil { private static final String AES = "AES"; + private static final byte[] PAYLOAD_HEADER = {'H', 'B', 'A', '2'}; + + private static final int IV_LENGTH = 16; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + /** * Encryption key The AES encryption key is 16 bits. * If the AES encryption key is larger than 16 bits, an error message is displayed @@ -87,14 +96,21 @@ public final class AesUtil { 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(encryptKey.getBytes(StandardCharsets.UTF_8))); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(initializationVector)); // 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) + .put(byteAes) + .array(); // base64 encode content - return Base64.getEncoder().encodeToString(byteAes); + return Base64.getEncoder().encodeToString(payload); } catch (Exception e) { log.error("aes encode content error: {}", e.getMessage(), e); return content; @@ -135,13 +151,36 @@ public final class AesUtil { SecretKeySpec keySpec = new SecretKeySpec(decryptKey.getBytes(StandardCharsets.UTF_8), AES); // cipher based on the algorithm AES Cipher cipher = Cipher.getInstance(ALGORITHM_STR); - // init cipher Encrypt_mode or Decrypt_mode operation, the second parameter is the KEY used - cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(decryptKey.getBytes(StandardCharsets.UTF_8))); - // base64 decode content byte[] bytesContent = Base64.getDecoder().decode(content); + byte[] initializationVector; + if (hasPayloadHeader(bytesContent)) { + if (bytesContent.length <= PAYLOAD_HEADER.length + IV_LENGTH) { + throw new IllegalArgumentException("Invalid encrypted payload"); + } + initializationVector = Arrays.copyOfRange( + bytesContent, PAYLOAD_HEADER.length, PAYLOAD_HEADER.length + IV_LENGTH); + bytesContent = Arrays.copyOfRange( + bytesContent, PAYLOAD_HEADER.length + 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.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) { + return false; + } + for (int index = 0; index < PAYLOAD_HEADER.length; index++) { + if (payload[index] != PAYLOAD_HEADER[index]) { + return false; + } + } + return true; + } /** * Determine whether it is encrypted 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 9bff2d915c..7ff913701c 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 @@ -20,10 +20,14 @@ package org.apache.hertzbeat.common.util; import static org.apache.hertzbeat.common.util.AesUtil.aesDecode; import static org.apache.hertzbeat.common.util.AesUtil.aesEncode; import static org.apache.hertzbeat.common.util.AesUtil.isCiphertext; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; import org.junit.jupiter.api.Test; /** @@ -43,12 +47,31 @@ class AesUtilTest { void testAesEncode() { String originalText = "This is a secret message"; String encryptedText = aesEncode(originalText, VALID_KEY); + String secondEncryptedText = aesEncode(originalText, VALID_KEY); assertNotEquals(originalText, encryptedText); + assertNotEquals(encryptedText, secondEncryptedText); + + byte[] encryptedPayload = Base64.getDecoder().decode(encryptedText); + byte[] secondEncryptedPayload = Base64.getDecoder().decode(secondEncryptedText); + assertArrayEquals(new byte[] {'H', 'B', 'A', '2'}, Arrays.copyOfRange(encryptedPayload, 0, 4)); + assertFalse(Arrays.equals( + VALID_KEY.getBytes(StandardCharsets.UTF_8), Arrays.copyOfRange(encryptedPayload, 4, 20))); + assertFalse(Arrays.equals( + Arrays.copyOfRange(encryptedPayload, 4, 20), + Arrays.copyOfRange(secondEncryptedPayload, 4, 20))); String decryptedText = aesDecode(encryptedText, VALID_KEY); assertEquals(originalText, decryptedText); } + @Test + void testLegacyCiphertextCanBeDecoded() { + String legacyCiphertext = "muJNZwhxg173v6EZAXb5TK8L5XlmDE5xmc9XTryH6Qk="; + + assertEquals("This is a secret message", aesDecode(legacyCiphertext, VALID_KEY)); + assertTrue(isCiphertext(legacyCiphertext, VALID_KEY)); + } + @Test void testAesDecode() { // Test with invalid key