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

Reply via email to