In an application I want the java frontend should take the encrypted text from nodejs and provided a fixed SECRET_KEY
it should be able to decode it and the vice versa. Any help is appriciated.
I am using javax.crypto for java and crypto in node. What I found is there is no problem with iv as both of them use iv to create cipher from crypto, but the problem happens with tag. Java provides no prebuilt method to get the tag but nodejs has it and the encrypted string in case of node is of 16bytes where as in java it is 32bytes. I have tried merging for node and slicing the end 16bytes for java but it throws invalid tag. I have tried using updateAAD()
method of java also but no result. Please help me with this, I am stuck since last 2 days. Thank you.
JAVA encoder
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Encoder3 {
public static void main(String[] args) throws Exception {
String base64Key = "XHgxl8qs5D6sejZncSsYg7OqPIeV0uQe5I9Zh+uHLcc=";
String originalString = "Hello, NODE JS!";
// Generate random IV
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
byte[] aad = new byte[16];
new SecureRandom().nextBytes(aad);
// Generate a key from the base64 encoded key
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
SecretKey key = new SecretKeySpec(keyBytes, "AES");
// Create cipher instance
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// Generate a random IV
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(aad.length*8, iv));
// Encrypt the string
byte[] cipherText = cipher.doFinal(originalString.getBytes(StandardCharsets.UTF_8));
String encodedString = Base64.getEncoder().encodeToString(cipherText);
String ivString = Base64.getEncoder().encodeToString(iv);
System.out.println("Encoded String: " + encodedString);
System.out.println("IV String: " + ivString);
}
}
Node.js decoder
const crypto = require('crypto');
function decrypt(encodedString, iv, base64Key) {
const key = Buffer.from(base64Key, 'base64');
iv = Buffer.from(iv, 'base64');
const buffer = Buffer.from(encodedString, 'base64');
const encryptedString = buffer.slice(0, 16);
const tag = buffer.slice(16, 32);
// Create decipher instance
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
// Decrypt the string
let decrypted = decipher.update(encryptedString, 'base64', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}
// Example usage
const base64Key = 'XHgxl8qs5D6sejZncSsYg7OqPIeV0uQe5I9Zh+uHLcc=';
const encodedStringFromJava = 'gifu7S5cqzb5etwmNGManhj/Y/pxLrT8DJ9OLuXAww==';
const iv = `Mk6vC6BfFfWtDevA`;
const decodedString = decrypt(encodedStringFromJava,iv, base64Key);
console.log('Decoded String:', decodedString);
It shows
node:internal/crypto/cipher:193
const ret = this[kHandle].final();
^
Error: Unsupported state or unable to authenticate data
at Decipheriv.final (node:internal/crypto/cipher:193:29)
at decrypt (S:\My Practice Files\UI\app.js\decrypt.js:16:27)
at Object.<anonymous> (S:\My Practice Files\UI\app.js\decrypt.js:26:23)
at Module._compile (node:internal/modules/cjs/loader:1375:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1434:10)
at Module.load (node:internal/modules/cjs/loader:1206:32)
at Module._load (node:internal/modules/cjs/loader:1022:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:142:12)
at node:internal/main/run_main_module:28:49
Node.js v21.5.0
AAD IS WRONG. If you use AAD in an AEAD cipher, it must be the same at both ends (encrypter and decrypter). If you generate it as random at the encrypter and don't send it, the decrypter can never get it right; if you do send it, you're just wasting effort to accomplish nothing. Remove it.
Note that the first parameter in Java's GCMParameterSpec
is the tag length, not the AAD length. These are entirely different things.
31 IS NOT 32. As you appear to have guessed, but could have learned by looking at the documentation, Java crypto returns the tag appended to the ciphertext, and accepts it there on decryption; nodejs crypto instead returns or accepts it as a separate field. Thus you are correct that the last 16 bytes of the Java 'ciphertext' should become the nodejs tag.
But you state the Java 'ciphertext' is 32 bytes. It is not. It is 31 bytes -- 15 bytes of ciphertext and 16 bytes of tag. Thus you need to slice it as (0,15) (15,31)
or more generally as (0,-16) (-16 /* to end */)
.
If I change your Java to remove the AAD, and for test purposes ONLY* to reuse your posted IV instead of a new one, I get encodedString='gifu7S5cqznzbbRBPX0a+YOrlrs9WQFOfr1aPV4W4A==' and if I change your JS to slice correctly it correctly decrypts that encodedString.
*Caveat: this is only for test! in real use of GCM don't ever reuse IV/nonce for the same key -- that destroys security.