I am trying to create an application in NodeJS (Electron) as a cross platform Desktop App. This will pair with the mobile app developed using SWIFT on iOS. As part of sharing data, it is encrypted using AES-256-GCM algorithm. I have the following encrypt and decrypt methods in SWIFT:
//listItems is an array of the following structure:
// - id: Int, title: String, data: String, ldate: String
func encrypt(listItems: [ListItem], pass: String) -> String{
let encoder = JSONEncoder()
do{
let data = try encoder.encode(listItems)
let key = SymmetricKey(data: SHA256.hash(data: pass.data(using: .utf8)!))
let iv = AES.GCM.Nonce()
let sealedBox = try AES.GCM.seal(data, using: key, nonce: iv)
return sealedBox.combined?.base64EncodedData()
}catch{
fatalError("Couldn't encrypt data\(error)")
}
}
func decrypt(data: Data, pass: String) -> [ListItem]{
do{
let key = SymmetricKey(data: SHA256.hash(data: pass.data(using: .utf8)!))
let mySealedBox = try AES.GCM.SealedBox(combined: Data(base64Encoded: data)!)
let content = try AES.GCM.open(mySealedBox, using: key)
return load(content)
}catch{
fatalError("Couldn't encrypt data\(error)")
}
}
func load<T: Decodable>(_ data: Data) -> T{
do{
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}catch{
fatalError("Could not parse the data")
}
}
For NodeJS, I have the below functions:
const crypto = require('crypto')
module.exports = {
encryptData(data,password){
let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest('hex').slice(0,32).toLowerCase();
let iv = crypto.pseudoRandomBytes(12);
iv = Buffer.from('mBj0tzBUxDFmix1T', 'base64');
let cipher = crypto.createCipheriv('aes-256-gcm', password_hash, iv);
let encryptedData = Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex'), 'hex');
console.log(' --------------- ENC BEGIN ---------------');
console.log(`IV Length: ${iv.length}`);
//console.log(`IV Base64 Length: ${iv.toString('base64').length}`);
console.log(iv.toString('base64'));
//console.log(`AuthTag Length: ${cipher.getAuthTag().length}`);
//console.log(`AuthTag Base64 Length: ${cipher.getAuthTag().toString('base64').length}`);
console.log(cipher.getAuthTag().toString('base64'));
//console.log(`Encrypted Data Length: ${encryptedData.length}`)
//console.log(`Encrypted Data Base64 Length: ${encryptedData.toString('base64').length}`)
console.log(encryptedData.toString('base64'));
console.log(' --------------- ENC END ---------------');
console.log(Buffer.concat([cipher.getAuthTag(), encryptedData]).toString('base64'));
console.log(Buffer.concat([encryptedData, cipher.getAuthTag()]).toString('base64'));
//let encryptedBuffer = Buffer.concat([iv, cipher.getAuthTag(), encryptedData]);
return iv.toString('base64') + cipher.getAuthTag().toString('base64') + encryptedData.toString('base64');
},
decryptData(data,password){
let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest('hex').slice(0,32).toLowerCase();
//let combinerBuffer = Buffer.from(data, 'base64');
//let iv = combinerBuffer.slice(0,16);
let iv = Buffer.from(data.slice(0,16), 'base64');
console.log(' --------------- DEC BEGIN ---------------');
console.log(iv.toString('base64'));
let at = Buffer.from(data.slice(16,32), 'base64');
console.log(at.toString('base64'));
let enc_buffer = Buffer.from(data.slice(32), 'base64');
console.log(enc_buffer.toString('base64'));
console.log(' --------------- DEC END ---------------');
let deciper = crypto.createDecipheriv('aes-256-gcm', password_hash, iv);
deciper.setAuthTag(at)
let dec_buf = deciper.update(enc_buffer, 'utf8') + deciper.final('utf8');
return dec_buf.toString('utf8');
}
}
The encrypted by SWIFT cannot be decrypted by NodeJS. While decrypting the data, i get the error:
Unsupported state or unable to authenticate data
I have a similar code in Java also which works well with the SWIFT generated encrypted data but the NodeJS one does not work at all. The main question is how do I get the AAD and AuthTag from the combined encrypted text generated by SWIFT. In Java, I just need to extract the first 16 bytes of IV and the rest as Cipher Text which includes Auth Tag as well. However, in NodeJS i need to manually extract the pass on the AuthTag. I tried breaking up the combined data from SWIFT as:
- IV (16 bytes) + CipherText + Tag (16 bytes) and also as:
- IV (16 bytes) + Tag (16 bytes) + CipherText
Both of these do not work and generate the same error as above.
Below is the Java Code:
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
class CryptoTest{
public static void main(String[] args){
try{
String enc = Crypto.encrypt("This is data", "password");
String dec = Crypto.decrypt(enc,"password");
System.out.println(enc);
System.out.println(dec);
}catch(Exception ex){
System.out.println("ERROR");
}
}
private static class Crypto {
private static final int GCM_TAG_LENGTH = 16;
private static final int GCM_IV_LENGTH = 12;
private static final String ALGORITHM = "AES_256/GCM/NoPadding";
private static final String ALGORITHM_SHORT_NAME = "AES";
private static final String HASH_ALGORITHM = "SHA-256";
public static String encrypt(String plaintext, String password) throws Exception
{
//Generate the key from password
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
byte[] key = md.digest(password.getBytes(StandardCharsets.UTF_8));
SecureRandom sr = new SecureRandom(password.getBytes(StandardCharsets.UTF_8));
byte[] iv = new byte[GCM_IV_LENGTH];
// sr.nextBytes(iv);
iv = Base64.getDecoder().decode("mBj0tzBUxDFmix1T");
// Get Cipher Instance
Cipher cipher = Cipher.getInstance(ALGORITHM);
// Create SecretKeySpec
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM_SHORT_NAME);
// Create GCMParameterSpec
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
// Initialize Cipher for ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
// Perform Encryption
byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
//Return the IV and CipherText as Base64 encoded and appended strings
System.out.println(Base64.getEncoder().encodeToString(iv));
System.out.println(Base64.getEncoder().encodeToString(cipherText));
return Base64.getEncoder().encodeToString(iv)+Base64.getEncoder().encodeToString(cipherText);
}
public static String decrypt(String sourceText, String password) throws Exception
{
//Get the IV from cipherText
byte[] iv = Base64.getDecoder().decode(sourceText.substring(0,16));
//Get the reminder of cipherText after the iv
byte[] cipherText = Base64.getDecoder().decode(sourceText.substring(16));
//Generate the key from password
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
byte[] key = md.digest(password.getBytes(StandardCharsets.UTF_8));
// Get Cipher Instance
Cipher cipher = Cipher.getInstance(ALGORITHM);
// Create SecretKeySpec
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM_SHORT_NAME);
// Create GCMParameterSpec
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
// Initialize Cipher for DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
// Perform Decryption
byte[] decryptedText = cipher.doFinal(cipherText);
return new String(decryptedText);
}
}
}
Data: This is data
Key: password
IV: mBj0tzBUxDFmix1T
Java code generates the following:
Encrypted Text: UgNY3VAwNU07iEqU1Jq3m3Q+p6bDCZg6UI0h8w==
NodeJS code generates the following:
AuthTag: BXQ0vZH4HBBpGb7Y7R9iJw==
Encrypted Text: O+wuVJB06JO6rPrc
From what I have read, Java will generate the encrypted text with the AuthTag included in it. I tried concatenating the AuthTag to Encrypted Text but the output never equals the one generated by Java.
The Java encrypted text however can be decrypted by the SWIFT CryptoKit code without any issues.
Buffer.concat([encryptedData, cipher.getAuthTag()]).toString('base64')
returnsO+wuVJB06JO6rPrcBXQ0vZH4HBBpGb7Y7R9iJw==
whereas the Java program returnsUgNY3VAwNU07iEqU1Jq3m3Q+p6bDCZg6UI0h8w==
. Both of these use the same IV and KEY so ideally both should return the same encrypted text. - Amit Bajaj