This is an automated email from the ASF dual-hosted git repository.
shenghang pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 6af6ff4238 [Feature][Transform-V2] Support AES_GCM algorithm in
FieldEncrypt (#10603)
6af6ff4238 is described below
commit 6af6ff4238c9c64ae059f6e67e5fe018c11f783a
Author: dy102 <[email protected]>
AuthorDate: Wed Mar 18 10:04:29 2026 +0900
[Feature][Transform-V2] Support AES_GCM algorithm in FieldEncrypt (#10603)
---
docs/en/transforms/encrypt.md | 13 ++-
docs/zh/transforms/encrypt.md | 14 ++-
.../field_decrypt_transform_multi_table.conf | 1 +
.../field_encrypt_transform_multi_table.conf | 1 +
.../transform/encrypt/FieldEncryptTransform.java | 6 +-
.../encrypt/FieldEncryptTransformConfig.java | 7 +-
.../encrypt/encryptor/AbstractAesEncryptor.java | 55 +++++++++
.../encrypt/encryptor/AesCbcEncryptor.java | 38 +-----
.../{AesCbcEncryptor.java => AesGcmEncryptor.java} | 61 +++-------
.../encrypt/FieldEncryptTransformTest.java | 9 +-
.../encrypt/encryptor/AesGcmEncryptorTest.java | 127 +++++++++++++++++++++
11 files changed, 235 insertions(+), 97 deletions(-)
diff --git a/docs/en/transforms/encrypt.md b/docs/en/transforms/encrypt.md
index cfcd479d85..780fb6d27c 100644
--- a/docs/en/transforms/encrypt.md
+++ b/docs/en/transforms/encrypt.md
@@ -11,20 +11,27 @@ The Encrypt transform plugin is used to encrypt or decrypt
specified fields in r
| name | type | required | default value | description
|
|-------------|--------|----------|---------------|-----------------------------------|
| `fields` | Array | Yes | - | List of fields to
encrypt/decrypt |
-| `algorithm` | String | No | `AES_CBC` | Encryption algorithm
|
+| `algorithm` | String | No | `AES_GCM` | Encryption algorithm
|
| `key` | String | Yes | - | Base64-encoded encryption
key |
| `mode` | String | No | `ENCRYPT` | `ENCRYPT`or `DECRYPT`
|
### algorithm [string]
Encryption algorithm used by this transform.
-Currently, only `AES_CBC` is supported.
+
+Supported values:
+- `AES_GCM`: default, AES in GCM mode with authentication tag
+- `AES_CBC`: AES in CBC mode with PKCS5 padding
+
+`AES_GCM` provides authenticated encryption and is recommended for better
security.
+
+If not specified, `AES_GCM` is used by default.
### key [string]
The encryption key must be provided in Base64-encoded format.
Make sure the key length matches the requirements of the selected algorithm.
-For `AES_CBC`, valid key lengths are 16, 24, or 32 bytes (corresponding to
AES-128, AES-192, or AES-256).
+For both `AES_GCM` and `AES_CBC`, valid key lengths are 16, 24, or 32 bytes
(corresponding to AES-128, AES-192, or AES-256).
**Example**
- `base64:AAAAAAAAAAAAAAAAAAAAAA==`
diff --git a/docs/zh/transforms/encrypt.md b/docs/zh/transforms/encrypt.md
index 5600f6fb13..a4d64360da 100644
--- a/docs/zh/transforms/encrypt.md
+++ b/docs/zh/transforms/encrypt.md
@@ -17,16 +17,22 @@ Encrypt Transform 插件用于使用对称加密算法,对记录中指定的
### algorithm [string]
-本 Transform 使用的加密算法。
-目前仅支持 `AES_CBC`。
+用于指定该 transform 所使用的加密算法。
+
+支持的值:
+- `AES_GCM`:默认值。采用 GCM 模式并包含认证标签(Authentication Tag)的 AES 加密。
+- `AES_CBC`:采用 CBC 模式及 PKCS5 填充(Padding)的 AES 加密。
+
+`AES_GCM` 提供认证加密(Authenticated Encryption),安全性更高,推荐使用。
+
+如果未明确指定,系统将默认使用 `AES_GCM`。
### key [string]
加密密钥必须以 Base64 编码格式提供。
请确保密钥长度符合所选加密算法的要求。
-对于 `AES_CBC`,支持的密钥长度为 16、24 或 32 字节
-(分别对应 AES-128、AES-192 和 AES-256)。
+对于 `AES_GCM` 和 `AES_CBC`,支持的密钥长度为 16、24 或 32 字节 (分别对应 AES-128、AES-192 和
AES-256)。
**示例**
diff --git
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_decrypt_transform_multi_table.conf
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_decrypt_transform_multi_table.conf
index 5fdb1c28c8..76e7c7957b 100644
---
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_decrypt_transform_multi_table.conf
+++
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_decrypt_transform_multi_table.conf
@@ -107,6 +107,7 @@ transform {
table_path = "test.abc"
fields = ["name", "address"]
key = "base64:AAAAAAAAAAAAAAAAAAAAAA=="
+ algorithm = "AES_CBC"
mode = "DECRYPT"
}
]
diff --git
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_encrypt_transform_multi_table.conf
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_encrypt_transform_multi_table.conf
index 3bb1b9a7c8..7250a14484 100644
---
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_encrypt_transform_multi_table.conf
+++
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/field_encrypt_transform_multi_table.conf
@@ -107,6 +107,7 @@ transform {
table_path = "test.abc"
fields = ["name", "address"]
key = "base64:AAAAAAAAAAAAAAAAAAAAAA=="
+ algorithm = "AES_CBC"
mode = "ENCRYPT"
}
]
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransform.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransform.java
index ee599d5b49..19b870eb59 100644
---
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransform.java
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransform.java
@@ -17,8 +17,6 @@
package org.apache.seatunnel.transform.encrypt;
-import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
-
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.table.catalog.CatalogTable;
import org.apache.seatunnel.api.table.catalog.Column;
@@ -109,9 +107,7 @@ public class FieldEncryptTransform extends
AbstractCatalogSupportMapTransform {
"Field length exceeds the maximum limit of " +
maxFieldLength);
}
- if (StringUtils.isNotBlank(value)) {
- outputRow.setField(index, action.apply(value));
- }
+ outputRow.setField(index, action.apply(value));
}
return outputRow;
}
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformConfig.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformConfig.java
index 584f6490ed..fb07e5828c 100644
---
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformConfig.java
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformConfig.java
@@ -19,7 +19,7 @@ package org.apache.seatunnel.transform.encrypt;
import org.apache.seatunnel.api.configuration.Option;
import org.apache.seatunnel.api.configuration.Options;
-import org.apache.seatunnel.transform.encrypt.encryptor.AesCbcEncryptor;
+import org.apache.seatunnel.transform.encrypt.encryptor.AesGcmEncryptor;
import java.util.List;
@@ -33,8 +33,9 @@ public class FieldEncryptTransformConfig {
public static final Option<String> ALGORITHM =
Options.key("algorithm")
.stringType()
- .defaultValue(AesCbcEncryptor.IDENTIFIER)
- .withDescription("The encryption algorithm, support
AES_CBC.");
+ .defaultValue(AesGcmEncryptor.IDENTIFIER)
+ .withDescription(
+ "The encryption algorithm, Supported values:
AES_CBC (default), AES_GCM");
public static final Option<String> KEY =
Options.key("key").stringType().noDefaultValue().withDescription("The
encryption key.");
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AbstractAesEncryptor.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AbstractAesEncryptor.java
new file mode 100644
index 0000000000..efa94208bb
--- /dev/null
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AbstractAesEncryptor.java
@@ -0,0 +1,55 @@
+/*
+ * 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.seatunnel.transform.encrypt.encryptor;
+
+import org.apache.seatunnel.common.exception.CommonError;
+
+import javax.crypto.spec.SecretKeySpec;
+
+import java.util.Base64;
+
+public abstract class AbstractAesEncryptor implements Encryptor {
+ protected SecretKeySpec buildAesKey(String key) {
+ if (key == null || key.trim().isEmpty()) {
+ throw CommonError.illegalArgument(key, "Encryption key cannot be
null or empty");
+ }
+
+ String base64 = key;
+ if (key.startsWith("base64:")) {
+ base64 = key.substring("base64:".length());
+ }
+ base64 = base64.trim();
+
+ byte[] keyBytes;
+ try {
+ keyBytes = Base64.getDecoder().decode(base64);
+ } catch (IllegalArgumentException e) {
+ throw CommonError.illegalArgument(key, "Invalid Base64 encoding in
encryption key");
+ }
+
+ if (!(keyBytes.length == 16 || keyBytes.length == 24 ||
keyBytes.length == 32)) {
+ throw CommonError.illegalArgument(
+ key,
+ "Invalid AES key length: "
+ + keyBytes.length
+ + ". Expected 16, 24, or 32 bytes");
+ }
+
+ return new SecretKeySpec(keyBytes, "AES");
+ }
+}
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
index 33b137d5ed..5c04d8469b 100644
---
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
@@ -31,7 +31,7 @@ import java.security.SecureRandom;
import java.util.Base64;
@AutoService(Encryptor.class)
-public class AesCbcEncryptor implements Encryptor {
+public class AesCbcEncryptor extends AbstractAesEncryptor {
public static final String IDENTIFIER = "AES_CBC";
private static final int IV_SIZE = 16;
@@ -63,7 +63,7 @@ public class AesCbcEncryptor implements Encryptor {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
encrypted =
cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
- throw TransformCommonError.encryptionError("plaintext length:" +
plainText.length(), e);
+ throw TransformCommonError.encryptionError("Encryption failed", e);
}
byte[] encryptedWithIv = new byte[IV_SIZE + encrypted.length];
@@ -78,7 +78,7 @@ public class AesCbcEncryptor implements Encryptor {
byte[] decoded = Base64.getDecoder().decode(cipherText);
byte[] iv = new byte[IV_SIZE];
if (decoded.length < IV_SIZE) {
- throw CommonError.illegalArgument(cipherText, "Invalid encrypted
value");
+ throw CommonError.illegalArgument(cipherText, "Invalid encrypted
value (too short)");
}
byte[] encrypted = new byte[decoded.length - IV_SIZE];
@@ -93,39 +93,9 @@ public class AesCbcEncryptor implements Encryptor {
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
original = cipher.doFinal(encrypted);
} catch (Exception e) {
- throw TransformCommonError.encryptionError(
- "ciphertext length:" + cipherText.length(), e);
+ throw TransformCommonError.encryptionError("Decryption failed", e);
}
return new String(original, StandardCharsets.UTF_8);
}
-
- private SecretKeySpec buildAesKey(String key) {
- if (key == null || key.trim().isEmpty()) {
- throw CommonError.illegalArgument(key, "Encryption key cannot be
null or empty");
- }
-
- String base64 = key;
- if (key.startsWith("base64:")) {
- base64 = key.substring("base64:".length());
- }
- base64 = base64.trim();
-
- byte[] keyBytes;
- try {
- keyBytes = Base64.getDecoder().decode(base64);
- } catch (IllegalArgumentException e) {
- throw CommonError.illegalArgument(key, "Invalid Base64 encoding in
encryption key");
- }
-
- if (!(keyBytes.length == 16 || keyBytes.length == 24 ||
keyBytes.length == 32)) {
- throw CommonError.illegalArgument(
- key,
- "Invalid AES key length: "
- + keyBytes.length
- + ". Expected 16, 24, or 32 bytes");
- }
-
- return new SecretKeySpec(keyBytes, "AES");
- }
}
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesGcmEncryptor.java
similarity index 62%
copy from
seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
copy to
seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesGcmEncryptor.java
index 33b137d5ed..4980af24a7 100644
---
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesCbcEncryptor.java
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/encryptor/AesGcmEncryptor.java
@@ -23,7 +23,7 @@ import
org.apache.seatunnel.transform.exception.TransformCommonError;
import com.google.auto.service.AutoService;
import javax.crypto.Cipher;
-import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
@@ -31,12 +31,14 @@ import java.security.SecureRandom;
import java.util.Base64;
@AutoService(Encryptor.class)
-public class AesCbcEncryptor implements Encryptor {
- public static final String IDENTIFIER = "AES_CBC";
+public class AesGcmEncryptor extends AbstractAesEncryptor {
+ public static final String IDENTIFIER = "AES_GCM";
+
+ private static final int IV_SIZE = 12;
+ private static final int TAG_BIT_LENGTH = 128;
- private static final int IV_SIZE = 16;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
- private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
+ private static final String ALGORITHM = "AES/GCM/NoPadding";
private SecretKeySpec keySpec;
@@ -55,15 +57,15 @@ public class AesCbcEncryptor implements Encryptor {
byte[] iv = new byte[IV_SIZE];
SECURE_RANDOM.nextBytes(iv);
- IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
byte[] encrypted;
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
- cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, spec);
encrypted =
cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
- throw TransformCommonError.encryptionError("plaintext length:" +
plainText.length(), e);
+ throw TransformCommonError.encryptionError("Encryption failed", e);
}
byte[] encryptedWithIv = new byte[IV_SIZE + encrypted.length];
@@ -76,56 +78,29 @@ public class AesCbcEncryptor implements Encryptor {
@Override
public String decrypt(String cipherText) {
byte[] decoded = Base64.getDecoder().decode(cipherText);
- byte[] iv = new byte[IV_SIZE];
- if (decoded.length < IV_SIZE) {
- throw CommonError.illegalArgument(cipherText, "Invalid encrypted
value");
+
+ if (decoded.length < IV_SIZE + (TAG_BIT_LENGTH / 8)) {
+ throw CommonError.illegalArgument(cipherText, "Invalid encrypted
value (too short)");
}
+
+ byte[] iv = new byte[IV_SIZE];
byte[] encrypted = new byte[decoded.length - IV_SIZE];
System.arraycopy(decoded, 0, iv, 0, IV_SIZE);
System.arraycopy(decoded, IV_SIZE, encrypted, 0, encrypted.length);
- IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_LENGTH, iv);
byte[] original;
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
- cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, spec);
original = cipher.doFinal(encrypted);
} catch (Exception e) {
throw TransformCommonError.encryptionError(
- "ciphertext length:" + cipherText.length(), e);
+ "Decryption failed (possible tampering or wrong key)", e);
}
return new String(original, StandardCharsets.UTF_8);
}
-
- private SecretKeySpec buildAesKey(String key) {
- if (key == null || key.trim().isEmpty()) {
- throw CommonError.illegalArgument(key, "Encryption key cannot be
null or empty");
- }
-
- String base64 = key;
- if (key.startsWith("base64:")) {
- base64 = key.substring("base64:".length());
- }
- base64 = base64.trim();
-
- byte[] keyBytes;
- try {
- keyBytes = Base64.getDecoder().decode(base64);
- } catch (IllegalArgumentException e) {
- throw CommonError.illegalArgument(key, "Invalid Base64 encoding in
encryption key");
- }
-
- if (!(keyBytes.length == 16 || keyBytes.length == 24 ||
keyBytes.length == 32)) {
- throw CommonError.illegalArgument(
- key,
- "Invalid AES key length: "
- + keyBytes.length
- + ". Expected 16, 24, or 32 bytes");
- }
-
- return new SecretKeySpec(keyBytes, "AES");
- }
}
diff --git
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformTest.java
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformTest.java
index 3f85c822c2..0b6dc1ce95 100644
---
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformTest.java
+++
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformTest.java
@@ -34,12 +34,14 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class FieldEncryptTransformTest {
- public static final String KEY = "base64:AAAAAAAAAAAAAAAAAAAAAA==";
+ public static final String KEY =
+ "base64:" +
Base64.getEncoder().encodeToString("0123456789abcdef".getBytes());
private static CatalogTable catalogTable;
private static Object[] values;
private static Object[] original;
@@ -157,10 +159,7 @@ class FieldEncryptTransformTest {
Object[] valuesWithEmpty = new Object[] {"value1", "", " ",
"value4", "value5"};
SeaTunnelRow input = new SeaTunnelRow(valuesWithEmpty);
- SeaTunnelRow output = fieldEncryptTransform.transformRow(input);
-
- Assertions.assertEquals("", output.getField(1));
- Assertions.assertEquals(" ", output.getField(2));
+ Assertions.assertDoesNotThrow(() ->
fieldEncryptTransform.transformRow(input));
}
@Test
diff --git
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/encryptor/AesGcmEncryptorTest.java
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/encryptor/AesGcmEncryptorTest.java
new file mode 100644
index 0000000000..c2582c7265
--- /dev/null
+++
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/encrypt/encryptor/AesGcmEncryptorTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.seatunnel.transform.encrypt.encryptor;
+
+import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Base64;
+
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AesGcmEncryptorTest {
+
+ private AesGcmEncryptor encryptor;
+
+ private static final String TEST_KEY =
+ "base64:" +
Base64.getEncoder().encodeToString("1234567890123456".getBytes());
+
+ @BeforeEach
+ void setUp() {
+ encryptor = new AesGcmEncryptor();
+ encryptor.init(TEST_KEY);
+ }
+
+ @Test
+ void testEncryptAndDecrypt() {
+ String plain = "test-text";
+
+ String cipher = encryptor.encrypt(plain);
+ String decrypted = encryptor.decrypt(cipher);
+
+ assertEquals(plain, decrypted);
+ }
+
+ @Test
+ void testEncryptProducesDifferentCipherText() {
+ String plain = "same-text";
+
+ String cipher1 = encryptor.encrypt(plain);
+ String cipher2 = encryptor.encrypt(plain);
+
+ // GCM uses random IV so ciphertext should differ
+ assertNotEquals(cipher1, cipher2);
+ }
+
+ @Test
+ void testDecryptTamperedCipherText() {
+ String plain = "secure-text";
+
+ String cipher = encryptor.encrypt(plain);
+
+ byte[] decoded = Base64.getDecoder().decode(cipher);
+
+ // tamper with ciphertext
+ decoded[decoded.length - 1] ^= 1;
+
+ String tampered = Base64.getEncoder().encodeToString(decoded);
+
+ assertThrows(SeaTunnelRuntimeException.class, () ->
encryptor.decrypt(tampered));
+ }
+
+ @Test
+ void testInvalidCipherTextTooShort() {
+ String invalid = Base64.getEncoder().encodeToString(new byte[5]);
+
+ SeaTunnelRuntimeException ex =
+ assertThrows(SeaTunnelRuntimeException.class, () ->
encryptor.decrypt(invalid));
+
+ assertTrue(ex.getMessage().contains("Invalid encrypted value (too
short)"));
+ }
+
+ @Test
+ void testDecryptWithWrongKey() {
+ String plain = "hello";
+
+ String cipher = encryptor.encrypt(plain);
+
+ AesGcmEncryptor another = new AesGcmEncryptor();
+
+ String otherKey =
+ "base64:" +
Base64.getEncoder().encodeToString("abcdefabcdefabcd".getBytes());
+
+ another.init(otherKey);
+
+ SeaTunnelRuntimeException ex =
+ assertThrows(SeaTunnelRuntimeException.class, () ->
another.decrypt(cipher));
+ assertTrue(ex.getMessage().contains("Decryption failed (possible
tampering or wrong key)"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", " ", "\t", "\n"})
+ void testEmptyOrWhitespaceString(String plain) {
+ String cipher = encryptor.encrypt(plain);
+ String decrypt = encryptor.decrypt(cipher);
+
+ assertEquals(plain, decrypt);
+ }
+
+ @Test
+ void testSupportAlgorithm() {
+ assertTrue(encryptor.support(AesGcmEncryptor.IDENTIFIER));
+ assertFalse(encryptor.support(AesCbcEncryptor.IDENTIFIER));
+ }
+}