This is an automated email from the ASF dual-hosted git repository.
yasith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/master by this push:
new ed2986f791 security: upgrade credential encryption from AES/CBC to
AES/GCM (#610)
ed2986f791 is described below
commit ed2986f79151f06d8ae0a4dc896c63f8a849468c
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Tue Mar 31 00:58:26 2026 -0400
security: upgrade credential encryption from AES/CBC to AES/GCM (#610)
* security: upgrade credential encryption from AES/CBC to AES/GCM
Replace AES/CBC/PKCS5Padding with AES/GCM/NoPadding for credential
store encryption. The old code used a static zero IV which is insecure.
GCM provides authenticated encryption with random IVs prepended to
the ciphertext.
Extracted from #556.
* security: add legacy AES/CBC fallback for transparent migration
On decrypt, try GCM first. If the auth tag fails (AEADBadTagException),
fall back to the old AES/CBC/PKCS5Padding with static zero IV. This
allows existing credentials to be read without re-encryption.
Credentials will migrate to GCM format on next update/rotation —
writes always use AES/GCM.
* test: add unit tests for GCM/CBC encryption and legacy fallback
- testFallbackDecryptsLegacyCBC: CBC-encrypted data decrypted via fallback
- testFallbackDecryptsNewGCM: GCM-encrypted data decrypted via fallback
- testGcmDecryptRejectsLegacyData: GCM decrypt throws on CBC data
* security: remove fallback, add migration script instead
Replace transparent fallback with a one-time migration script
(MigrateCredentialEncryption) that re-encrypts all CREDENTIALS rows
from legacy AES/CBC to AES/GCM. Run before deploying the GCM-only code.
Usage:
java MigrateCredentialEncryption <jdbcUrl> <dbUser> <dbPass> \
<keystorePath> <keyAlias> <keystorePass>
The script skips rows already in GCM format.
* style: spotless:apply on MigrateCredentialEncryption
* Potential fix for code scanning alert no. 129: Use of a broken or risky
cryptographic algorithm
Co-authored-by: Copilot Autofix powered by AI
<62310815+github-advanced-security[bot]@users.noreply.github.com>
* fix: make decryptLegacy public for cross-package access from migration
script
---------
Co-authored-by: Copilot Autofix powered by AI
<62310815+github-advanced-security[bot]@users.noreply.github.com>
---
.../credential/repository/db/CredentialsDAO.java | 10 +-
.../repository/db/MigrateCredentialEncryption.java | 103 +++++++++++++++++++++
.../airavata/security/util/SecurityUtil.java | 74 ++++++++-------
.../airavata/security/util/SecurityUtilTest.java | 44 +++++++--
4 files changed, 186 insertions(+), 45 deletions(-)
diff --git
a/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/CredentialsDAO.java
b/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/CredentialsDAO.java
index 44f050c384..1757d03cbe 100644
---
a/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/CredentialsDAO.java
+++
b/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/CredentialsDAO.java
@@ -414,8 +414,9 @@ public class CredentialsDAO extends ParentDAO {
try {
// decrypt the data first
if (encrypt()) {
- data = SecurityUtil.decrypt(
- this.keyStorePath, this.secretKeyAlias,
this.keyStorePasswordCallback, data);
+ var key = SecurityUtil.getSymmetricKey(
+ this.keyStorePath, this.secretKeyAlias,
this.keyStorePasswordCallback);
+ data = SecurityUtil.decrypt(data, key);
}
objectInputStream = new ObjectInputStream(new
ByteArrayInputStream(data));
@@ -464,8 +465,9 @@ public class CredentialsDAO extends ParentDAO {
if (encrypt()) {
byte[] array = byteArrayOutputStream.toByteArray();
try {
- return SecurityUtil.encrypt(
- this.keyStorePath, this.secretKeyAlias,
this.keyStorePasswordCallback, array);
+ var key = SecurityUtil.getSymmetricKey(
+ this.keyStorePath, this.secretKeyAlias,
this.keyStorePasswordCallback);
+ return SecurityUtil.encrypt(array, key);
} catch (GeneralSecurityException e) {
throw new CredentialStoreException("Error encrypting data", e);
} catch (IOException e) {
diff --git
a/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/MigrateCredentialEncryption.java
b/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/MigrateCredentialEncryption.java
new file mode 100644
index 0000000000..8d5bcefedc
--- /dev/null
+++
b/airavata-api/src/main/java/org/apache/airavata/credential/repository/db/MigrateCredentialEncryption.java
@@ -0,0 +1,103 @@
+/**
+*
+* 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.airavata.credential.repository.db;
+
+import java.security.Key;
+import java.sql.*;
+import org.apache.airavata.common.server.KeyStorePasswordCallback;
+import org.apache.airavata.security.util.SecurityUtil;
+
+/**
+ * One-time migration: re-encrypts all CREDENTIALS rows from legacy AES/CBC
+ * (static zero IV) to AES/GCM (random IV). Run before deploying the GCM-only
code.
+ *
+ * Usage: java MigrateCredentialEncryption <jdbcUrl> <dbUser> <dbPass>
<keystorePath> <keyAlias> <keystorePass>
+ */
+public class MigrateCredentialEncryption {
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 6) {
+ System.err.println(
+ "Usage: MigrateCredentialEncryption <jdbcUrl> <dbUser>
<dbPass> <keystorePath> <keyAlias> <keystorePass>");
+ System.exit(1);
+ }
+
+ String jdbcUrl = args[0], dbUser = args[1], dbPass = args[2];
+ String keystorePath = args[3], keyAlias = args[4];
+ char[] keystorePass = args[5].toCharArray();
+
+ KeyStorePasswordCallback cb = new KeyStorePasswordCallback() {
+ public char[] getStorePassword() {
+ return keystorePass;
+ }
+
+ public char[] getSecretKeyPassPhrase(String alias) {
+ return keystorePass;
+ }
+ };
+
+ Key key = SecurityUtil.getSymmetricKey(keystorePath, keyAlias, cb);
+
+ try (Connection conn = DriverManager.getConnection(jdbcUrl, dbUser,
dbPass)) {
+ conn.setAutoCommit(false);
+
+ int migrated = 0, skipped = 0;
+
+ try (PreparedStatement select =
+ conn.prepareStatement("SELECT GATEWAY_ID,
TOKEN_ID, CREDENTIAL FROM CREDENTIALS");
+ PreparedStatement update = conn.prepareStatement(
+ "UPDATE CREDENTIALS SET CREDENTIAL = ? WHERE
GATEWAY_ID = ? AND TOKEN_ID = ?")) {
+
+ ResultSet rs = select.executeQuery();
+ while (rs.next()) {
+ String gatewayId = rs.getString("GATEWAY_ID");
+ String tokenId = rs.getString("TOKEN_ID");
+ byte[] blob = rs.getBytes("CREDENTIAL");
+
+ // Try GCM first — if it works, already migrated
+ try {
+ SecurityUtil.decrypt(blob, key);
+ skipped++;
+ continue;
+ } catch (Exception ignored) {
+ // Not GCM, proceed with migration
+ }
+
+ // Decrypt with legacy CBC, re-encrypt with GCM
+ byte[] plaintext = SecurityUtil.decryptLegacy(blob, key);
+ byte[] gcmEncrypted = SecurityUtil.encrypt(plaintext, key);
+
+ update.setBytes(1, gcmEncrypted);
+ update.setString(2, gatewayId);
+ update.setString(3, tokenId);
+ update.addBatch();
+ migrated++;
+ }
+
+ if (migrated > 0) {
+ update.executeBatch();
+ }
+ }
+
+ conn.commit();
+ System.out.printf("Migration complete: %d migrated, %d already
GCM%n", migrated, skipped);
+ }
+ }
+}
diff --git
a/airavata-api/src/main/java/org/apache/airavata/security/util/SecurityUtil.java
b/airavata-api/src/main/java/org/apache/airavata/security/util/SecurityUtil.java
index 5aa1049d0f..4900a7851b 100644
---
a/airavata-api/src/main/java/org/apache/airavata/security/util/SecurityUtil.java
+++
b/airavata-api/src/main/java/org/apache/airavata/security/util/SecurityUtil.java
@@ -20,9 +20,12 @@
package org.apache.airavata.security.util;
import java.io.*;
+import java.nio.ByteBuffer;
import java.security.*;
import java.security.cert.CertificateException;
+import java.util.Arrays;
import javax.crypto.Cipher;
+import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import org.apache.airavata.common.server.KeyStorePasswordCallback;
import org.slf4j.Logger;
@@ -35,52 +38,55 @@ public class SecurityUtil {
public static final String PASSWORD_HASH_METHOD_PLAINTEXT = "PLAINTEXT";
public static final String CHARSET_ENCODING = "UTF-8";
- public static final String PADDING_MECHANISM = "AES/CBC/PKCS5Padding";
+ public static final String CIPHER_NAME = "AES/GCM/NoPadding";
+ public static final int GCM_IV_BYTES = 12; // 96 bits
+ public static final int GCM_TAG_BITS = 128;
private static final Logger logger =
LoggerFactory.getLogger(SecurityUtil.class);
- public static byte[] encryptString(
- String keyStorePath, String keyAlias, KeyStorePasswordCallback
passwordCallback, String value)
- throws GeneralSecurityException, IOException {
- return encrypt(keyStorePath, keyAlias, passwordCallback,
value.getBytes(CHARSET_ENCODING));
- }
-
- public static byte[] encrypt(
- String keyStorePath, String keyAlias, KeyStorePasswordCallback
passwordCallback, byte[] value)
- throws GeneralSecurityException, IOException {
-
- Key secretKey = getSymmetricKey(keyStorePath, keyAlias,
passwordCallback);
-
- Cipher cipher = Cipher.getInstance(PADDING_MECHANISM);
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(new
byte[16]));
- return cipher.doFinal(value);
- }
-
- private static Key getSymmetricKey(String keyStorePath, String keyAlias,
KeyStorePasswordCallback passwordCallback)
+ public static Key getSymmetricKey(String keyStorePath, String keyAlias,
KeyStorePasswordCallback passwordCallback)
throws CertificateException, NoSuchAlgorithmException,
KeyStoreException, IOException,
UnrecoverableKeyException {
KeyStore ks = SecurityUtil.loadKeyStore(keyStorePath,
passwordCallback);
return ks.getKey(keyAlias,
passwordCallback.getSecretKeyPassPhrase(keyAlias));
}
- public static byte[] decrypt(
- String keyStorePath, String keyAlias, KeyStorePasswordCallback
passwordCallback, byte[] encrypted)
- throws GeneralSecurityException, IOException {
-
- Key secretKey = getSymmetricKey(keyStorePath, keyAlias,
passwordCallback);
-
- Cipher cipher = Cipher.getInstance(PADDING_MECHANISM);
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(new
byte[16]));
-
- return cipher.doFinal(encrypted);
+ public static byte[] encrypt(byte[] data, Key key) throws
GeneralSecurityException {
+ var cipher = Cipher.getInstance(CIPHER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ var iv = cipher.getIV();
+ var encryptedData = cipher.doFinal(data);
+ return ByteBuffer.allocate(iv.length + encryptedData.length)
+ .put(iv)
+ .put(encryptedData)
+ .array();
}
- public static String decryptString(
- String keyStorePath, String keyAlias, KeyStorePasswordCallback
passwordCallback, byte[] encrypted)
- throws GeneralSecurityException, IOException {
+ public static byte[] decrypt(byte[] tag, Key key) throws
GeneralSecurityException {
+ var iv = Arrays.copyOfRange(tag, 0, GCM_IV_BYTES);
+ var encryptedData = Arrays.copyOfRange(tag, GCM_IV_BYTES, tag.length);
+ var cipher = Cipher.getInstance(CIPHER_NAME);
+ var spec = new GCMParameterSpec(GCM_TAG_BITS, iv);
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ return cipher.doFinal(encryptedData);
+ }
- byte[] decrypted = decrypt(keyStorePath, keyAlias, passwordCallback,
encrypted);
- return new String(decrypted, CHARSET_ENCODING);
+ /**
+ * Decrypt using the legacy AES/CBC/PKCS5Padding scheme with a static zero
IV.
+ * <p>
+ * WARNING: This method is intentionally limited to migration of
credentials that were
+ * encrypted in the past using AES/CBC with a fixed zero IV. Do NOT use
this method
+ * for new code or for decrypting data encrypted with any modern scheme.
All new
+ * encryption and decryption should use the AES/GCM helpers in this class
instead.
+ * </p>
+ * Used only by the migration script to read old credentials, which should
then be
+ * re-encrypted using AES/GCM.
+ */
+ @Deprecated
+ public static byte[] decryptLegacy(byte[] encrypted, Key key) throws
GeneralSecurityException {
+ var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(new
byte[16]));
+ return cipher.doFinal(encrypted);
}
public static KeyStore loadKeyStore(String keyStoreFilePath,
KeyStorePasswordCallback passwordCallback)
diff --git
a/airavata-api/src/test/java/org/apache/airavata/security/util/SecurityUtilTest.java
b/airavata-api/src/test/java/org/apache/airavata/security/util/SecurityUtilTest.java
index a8ae9d48ff..490175bc36 100644
---
a/airavata-api/src/test/java/org/apache/airavata/security/util/SecurityUtilTest.java
+++
b/airavata-api/src/test/java/org/apache/airavata/security/util/SecurityUtilTest.java
@@ -24,7 +24,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.nio.charset.StandardCharsets;
+import java.security.Key;
import java.security.KeyStore;
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
import org.apache.airavata.common.server.KeyStorePasswordCallback;
import org.junit.jupiter.api.Test;
@@ -39,12 +42,10 @@ public class SecurityUtilTest {
@Test
public void testEncryptString() throws Exception {
-
String stringToEncrypt = "Test string to encrypt";
- byte[] encrypted =
- SecurityUtil.encryptString(keyStorePath, "mykey", new
TestKeyStoreCallback(), stringToEncrypt);
-
- String decrypted = SecurityUtil.decryptString(keyStorePath, "mykey",
new TestKeyStoreCallback(), encrypted);
+ var key = SecurityUtil.getSymmetricKey(keyStorePath, "mykey", new
TestKeyStoreCallback());
+ byte[] encrypted =
SecurityUtil.encrypt(stringToEncrypt.getBytes(StandardCharsets.UTF_8), key);
+ String decrypted = new String(SecurityUtil.decrypt(encrypted, key),
StandardCharsets.UTF_8);
assertEquals(stringToEncrypt, decrypted);
}
@@ -52,8 +53,9 @@ public class SecurityUtilTest {
public void testEncryptBytes() throws Exception {
String stringToEncrypt = "Test string to encrypt";
byte[] plaintext = stringToEncrypt.getBytes(StandardCharsets.UTF_8);
- byte[] encrypted = SecurityUtil.encrypt(keyStorePath, "mykey", new
TestKeyStoreCallback(), plaintext);
- byte[] decrypted = SecurityUtil.decrypt(keyStorePath, "mykey", new
TestKeyStoreCallback(), encrypted);
+ var key = SecurityUtil.getSymmetricKey(keyStorePath, "mykey", new
TestKeyStoreCallback());
+ byte[] encrypted = SecurityUtil.encrypt(plaintext, key);
+ byte[] decrypted = SecurityUtil.decrypt(encrypted, key);
assertArrayEquals(plaintext, decrypted);
}
@@ -63,6 +65,34 @@ public class SecurityUtilTest {
assertNotNull(ks);
}
+ @Test
+ public void testLegacyDecryptRoundTrip() throws Exception {
+ byte[] plaintext = "legacy data".getBytes(StandardCharsets.UTF_8);
+ Key key = SecurityUtil.getSymmetricKey(keyStorePath, "mykey", new
TestKeyStoreCallback());
+ byte[] legacyEncrypted = legacyEncrypt(plaintext, key);
+ byte[] decrypted = SecurityUtil.decryptLegacy(legacyEncrypted, key);
+ assertArrayEquals(plaintext, decrypted);
+ }
+
+ @Test
+ public void testMigrationLegacyToGcm() throws Exception {
+ byte[] plaintext = "migrating
credential".getBytes(StandardCharsets.UTF_8);
+ Key key = SecurityUtil.getSymmetricKey(keyStorePath, "mykey", new
TestKeyStoreCallback());
+ // decrypt old format, re-encrypt as GCM, verify
+ byte[] legacyEncrypted = legacyEncrypt(plaintext, key);
+ byte[] decrypted = SecurityUtil.decryptLegacy(legacyEncrypted, key);
+ byte[] gcmEncrypted = SecurityUtil.encrypt(decrypted, key);
+ byte[] result = SecurityUtil.decrypt(gcmEncrypted, key);
+ assertArrayEquals(plaintext, result);
+ }
+
+ /** Simulate old AES/CBC encryption with static zero IV. */
+ private static byte[] legacyEncrypt(byte[] data, Key key) throws Exception
{
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(new
byte[16]));
+ return cipher.doFinal(data);
+ }
+
private static class TestKeyStoreCallback implements
KeyStorePasswordCallback {
@Override