From b5703031c316680b637183ac0dd0f4520f9e1ebd Mon Sep 17 00:00:00 2001 From: Slash Date: Tue, 2 Jun 2026 13:35:47 +0530 Subject: [PATCH] fix: use random nonce per call in AES-GCM onboarding signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the static IV (first 12 bytes of the key) with a fresh SecureRandom-generated 12-byte nonce for every encryption call. The old approach caused AES-GCM nonce reuse: an attacker who collects two onboarding_signature values from partner URLs can XOR the ciphertexts to cancel the keystream and forge a valid signature for any submerchant ID without knowing the partner secret key (NIST SP 800-38D §8.3 Forbidden Attack). New output format: hex(iv[12] || ciphertext || tag[16]) The receiver reads the first 24 hex chars as the IV before decrypting. Reported via HackerOne #3754503 (ISS-2528895), SLA: 2026-06-05. Co-authored-by: ankitdas13 --- src/main/java/com/razorpay/Utils.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/razorpay/Utils.java b/src/main/java/com/razorpay/Utils.java index 0beb2469..7cae81fd 100644 --- a/src/main/java/com/razorpay/Utils.java +++ b/src/main/java/com/razorpay/Utils.java @@ -1,6 +1,7 @@ package com.razorpay; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.Mac; @@ -61,13 +62,25 @@ public static String encrypt(String dataToEncrypt, String secret) throws Razorpa try { byte[] keyBytes = secret.substring(0, 16).getBytes(StandardCharsets.UTF_8); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + + // Generate a fresh random 12-byte nonce per call (fixes AES-GCM nonce reuse). + // A static IV derived from the key allows keystream recovery and tag forgery + // (NIST SP 800-38D §8.3 Forbidden Attack) using only two captured ciphertexts. byte[] iv = new byte[12]; - System.arraycopy(keyBytes, 0, iv, 0, 12); + new SecureRandom().nextBytes(iv); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); + // In Java, doFinal() appends the 16-byte GCM auth tag to the ciphertext. byte[] encryptedData = cipher.doFinal(dataToEncrypt.getBytes(StandardCharsets.UTF_8)); - return bytesToHex(encryptedData); + + // Output format: iv (12 bytes) || ciphertext || tag (16 bytes), hex-encoded. + // Receiver must read the first 24 hex chars as the IV before decrypting. + byte[] combined = new byte[iv.length + encryptedData.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(encryptedData, 0, combined, iv.length, encryptedData.length); + return bytesToHex(combined); } catch (Exception e) { throw new RazorpayException(e.getMessage());