import crypto from 'crypto'
import _ from 'lodash'
// AES-128 configuration
const algorithm = 'aes-128-cbc';
/**
* Derives a key and IV from a passphrase.
* @param {string} passphrase - The passphrase to derive key and IV from.
* @returns {{key: Buffer, iv: Buffer, salt: string}} - The derived key, IV, and salt in base64 format.
*/
function deriveKeyAndIv(passphrase) {
const salt = crypto.randomBytes(16); // Generate a random salt
const keyIv = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
return {
key: keyIv.slice(0, 16), // AES-128 requires a 16-byte key
iv: keyIv.slice(16, 32), // IV is also 16 bytes long
salt: salt.toString('base64')
};
}
/**
* Encrypts a Base64-encoded string with AES-128 using a passphrase.
* @param {string} base64String - The Base64-encoded string to encrypt.
* @param {string} passphrase - The passphrase used to derive the encryption key and IV.
* @returns {string} - The encrypted string in Base64 format.
*/
export function encrypt(string, passphrase= process.env.ibanPassphrase) {
const { key, iv, salt } = deriveKeyAndIv(passphrase);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(string, 'utf8', 'base64');
encrypted += cipher.final('base64');
return `${salt}:${encrypted}`;
}
/**
* Decrypts an AES-128 encrypted Base64 string using a passphrase.
* @param {string} encryptedData - The encrypted string (Salt and encrypted text in Base64, separated by colons).
* Supports both old format (salt:iv:encrypted) and new format (salt:encrypted).
* @param {string} passphrase - The passphrase used to derive the decryption key and IV.
* @returns {string} - The decrypted Base64 string.
*/
export function decrypt(encryptedData, passphrase=process.env.ibanPassphrase) {
try
{
const parts = encryptedData.split(':');
const salt = Buffer.from(parts[0], 'base64');
const keyIv = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
const key = keyIv.slice(0, 16);
let iv, encryptedBase64;
// Handle both old format (salt:iv:encrypted) and new format (salt:encrypted)
if (parts.length === 3) {
// Old format: use stored IV (for backward compatibility)
iv = Buffer.from(parts[1], 'base64');
encryptedBase64 = parts[2];
} else {
// New format: derive IV from salt
iv = keyIv.slice(16, 32);
encryptedBase64 = parts[1];
}
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encryptedBase64, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
catch(e)
{
console.log('decryption failed',e,encryptedData)
}
}
export const encryptIbans= (config,data)=>
{
if(data.accounts && data.accounts.length>0)
{
data.accounts.forEach(account=>
{
if( account?.IBAN)
{
// no encryption
if(!process.env.ibanPassphrase || process.env.ibanPassphrase.length<12)
{
console.warn('IBAN passphrase is not set or too short. Skipping encryption.');
account.IBANshow=(account.IBAN.slice(0,account.IBAN.length-6)+'XXXXXX').replace(/(.{4})/g, `$1 `).trim()
return account;
}
const encrypted=encrypt(account.IBAN)
delete account.IBANdecrypt
account.IBANshow=(account.IBAN.slice(0,account.IBAN.length-6)+'XXXXXX').replace(/(.{4})/g, `$1 `).trim()
delete account.IBAN
account. IBANencrypted=encrypted
}
})
}
}
export const decryptAccount=(account)=>
{
if(account.IBANencrypted)
{
try{
const decrypted=decrypt(account.IBANencrypted)
if(decrypted.match(/[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}/))
return {...account,IBAN:decrypted}
else
return account
}
catch(e)
{
return account
}
}
else
return account
}
export const decryptIbans= (data)=>
{
if(data.accounts && data.accounts.length>0)
{
data.accounts=data.accounts.map(account=>
{
return decryptAccount(account)
})
}
}
export const accountsEqual=(newSet,oldSet)=>
{
// this will compare two account Sets
// the encryption is with random salt.
// so we have tro treat IBANencrypted seperately
// we have to exclude IBANencrypted to check for equality
const newAccounts=newSet?.map(a => decryptAccount(a)) ?? []
newAccounts.forEach(a=>{delete a.IBANencrypted})
// we have to exclude IBANencrypted to check for equality
const oldAccounts=oldSet?.map(a => decryptAccount(a)) ?? []
oldAccounts.forEach(a=>{delete a.IBANencrypted})
return _.isEqual(oldAccounts,newAccounts)
}