Reputation: 732
I'm trying to implement encrypt/decrypt functions using aes-128-gcm as provided by node crypto. From my understanding, gcm encrypts the ciphertext but also hashes it and provides this as an 'authentication tag'. However, I keep getting the error: "Unsupported state or unable to authenticate data".
I'm not sure if this is an error in my code - looking at the encrypted ciphertext and auth tag, the one being fetched by the decrypt function is the same as the one produced by the encrypt function.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final();
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes,key,iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text);
decryptedData += decipher.final();
}
The error is being thrown by decipher.final().
Upvotes: 12
Views: 29172
Reputation: 1
I know this is quite old, but came across when trying to connect a NodeJS backend with iOS Swift developed app. The answers did not worked, but give me the hint to create this GIST. See a detailed implementation
iOS Swift uses CryptoKit with AES.GCM and packs (data, IV and authTag). Apple's SealedBox documentation
Then you need to play around to pack/unpack IV, cipher and authTag in a way you can send to Apple's CryptoKit in a "sealed box".
const buffer = require('buffer');
const myCrypto = require('crypto');
const aes256gcm = (key) => {
const ALGO = 'aes-256-gcm';
const encryptToSealedBox = (clearText) => {
// do the usual encription process
const iv = Buffer.from(myCrypto.randomBytes(12), 'utf8');
const cipher = myCrypto.createCipheriv(ALGO, key, iv);
let enc = cipher.update(clearText, 'utf8', 'base64');
enc += cipher.final('base64');
let tag = cipher.getAuthTag();
// create the SealedBox data structure
let uiArray = [Buffer.from(enc, 'base64'), tag];
let finalCipher = iv.toString('base64') + Buffer.concat(uiArray).toString('base64');
return finalCipher;
};
const decryptFromSealedBox = (b64EncodedCipher) => {
// data received as base64, so, unpack it and make Buffers of IV & AuthTag as required
// might this code be optimized?... sure, let me know if so... I did it this way to test & worked.. and I understand what's going on
// get IV
const iv = b64EncodedCipher.substring(0,16);
const ivBuff = Buffer.from(iv, 'base64');
// get encripted text + authTag
const cipherBlob = b64EncodedCipher.substring(16);
const cipherBuff = Buffer.from(cipherBlob, 'base64');
const cipherTextBuff = cipherBuff.subarray(0, cipherBuff.length-16);
const tagBuff = cipherBuff.subarray(cipherBuff.length-16);
const cipherText = cipherTextBuff.toString('base64');
const decipher = myCrypto.createDecipheriv(ALGO, key, ivBuff);
decipher.setAuthTag(tagBuff);
let str = decipher.update(cipherText, 'base64', 'utf8');
str += decipher.final('utf8');
return str;
};
return {
encryptToSealedBox,
decryptFromSealedBox
};
};
//test it
const KEY = Buffer.from(myCrypto.randomBytes(32), 'utf8');
const aesCipher = aes256gcm(KEY);
const textToTest = 'I wanna try GCM encryption';
const textEncrypted = aesCipher.encryptToSealedBox(textToTest);
const clearFinalText = aesCipher.decryptFromSealedBox(textEncrypted);
console.log(clearFinalText);
Upvotes: 0
Reputation: 1092
In case if someone still tries to get a working example of encryption and decryption process.
I've left some comments that should be taken into consideration.
import * as crypto from 'crypto';
const textToEncode = 'some secret text'; // utf-16
const algo = 'aes-128-gcm';
// Key bytes length depends on algorithm being used:
// 'aes-128-gcm' = 16 bytes
// 'aes-192-gcm' = 24 bytes
// 'aes-256-gcm' = 32 bytes
const key = crypto.randomBytes(16);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algo, key, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(textToEncode, 'utf-8')),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
console.info('Value encrypted', {
valueToEncrypt: textToEncode,
encryptedValue: encrypted.toString('hex'),
authTag: authTag.toString('hex'),
});
// It's important to use the same authTag and IV that were used during encoding
const decipher = crypto.createDecipheriv(algo, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
console.info('Value decrypted', {
valueToDecrypt: encrypted.toString('hex'),
decryptedValue: decrypted.toString('utf-8'),
});
Upvotes: 23
Reputation: 732
I managed to fix this: the issue was that I wasn't specifying an encoding type for cipher.final() and I was returning it within a String, so it wasn't returning a Buffer object, which decipher.final() was expecting.
To fix, I add 'utf-8' to 'hex' encoding parameters within my cipher.update and cipher.final, and vice versa in decipher.
Edited to add code example - note this is from 2018, so may be outdated now.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext, 'utf-8', 'hex'),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final('hex');
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes, key, iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text, 'hex', 'utf-8');
decryptedData += decipher.final('utf-8');
}
Upvotes: 11