Encryption strategy:
Generate random 256-bit encryption key (K_s).
For every PII value in payload:
1. Pad plaintext with PKCS#7 padding.
2. Generate random 128-bit Initialization Vector (IV).
3. Encrypt padded plaintext with AES-256-CBC Cipher generated with key K_s and IV to get ciphertext.
4. Append IV to cipher text and Base64 encode to get payload value.
5. Assign payload value to corresponding key in payload.
6. Encrypt K_s using RSA-OAEP with hash function SHA-256 and public RSA key to get K_enc.
7. Assign K_enc to session_key in payload.
I'm trying to implement the above encryption strategy in node js using crypto module, but I'm missing something... I'm stuck on this on the past 2 days... Can someone please help me figure out what I'm missing here?
My implementation of encryption script so far below:
const crypto = require('crypto'),
_ = require('lodash');
async function encryptPayload(dataToEncrypt, password) {
if (dataToEncrypt.constructor !== String) {
dataToEncrypt = JSON.stringify(dataToEncrypt);
}
let bufferKey = Buffer.from(password, 'hex');
const iv = crypto.randomBytes(16); // should this be crypto.randomBytes(32).toString('hex')?
let cipherKey = crypto.createCipheriv('aes-256-cbc', bufferKey, iv);
cipherKey.setAutoPadding(true);
let encryptedPayload = cipherKey.update(dataToEncrypt, 'utf8', 'base64');
// encryptedPayload += cipherKey.final('base64');
// return encryptedPayload + iv.toString('base64');
encryptedPayload = cipherKey.final()
let tempBuffer = Buffer.concat([encryptedPayload, iv]);
return tempBuffer.toString('base64');
}
async function encryptDataMultipleKeys(payload, publicKey, keysToEncrypt = []) {
if (!payload) {
return payload;
}
let password = crypto.randomBytes(32).toString('hex'); //uuid.v4();
console.log("The password is " + password + " \n");
let pendingPromisesArray = [], correspondingKeyNameArray = [];
for (const key of keysToEncrypt) {
let value = _.get(payload, key);
if (!value) {
continue;
}
//value = await encryptPayload(value, password);
pendingPromisesArray.push(encryptPayload(value, password));
correspondingKeyNameArray.push(key);
}
let promisesValueArray = await Promise.all(pendingPromisesArray);
let encryptedPayload = {}
for (let index = 0; index < correspondingKeyNameArray.length; index++) {
let key = correspondingKeyNameArray[index];
let value = promisesValueArray[index];
if (!value || !key) {
continue;
}
_.set(encryptedPayload, key, value);
//encryptedPayload[key] = value;
}
//REF: https://nodejs.org/api/crypto.html#crypto_crypto_publicencrypt_key_buffer
let encryptedPasswordBuffer = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}, Buffer.from(password, 'hex'));
let encryptedPassword = encryptedPasswordBuffer.toString('base64');
encryptedPayload.session_key = encryptedPassword
return encryptedPayload;
}
async function encryptPIIFields(payload) {
let fieldsToEncrypt = [
'applicant.ssn', 'applicant.date_of_birth', 'applicant.first_name', 'applicant.last_name',
'applicant.email_address', 'applicant.phone_number', 'applicant.income',
'applicant.address.line_1', 'applicant.address.line_2', 'applicant.address.city',
'applicant.address.country', 'applicant.address.state', 'applicant.address.zipcode'
];
let publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYsdy+gGrdzvG5F9BYLl\nVwFwCfyCzeLQ7Vmvu+wvyoDrwvMXSfLnZfg7NsZMyPc3OVt8EeRvGLzrXvxtSWKG\n+mKBC7xEzb/LM8MoHQhXlgZ7L1nofBpAs74zEFXZNGHw5SnWXTuQ3Yym0u8hkYDZ\noqDJRgrczjXdbrqDVeB3GIvpMZMU9OkTFRmZZGMLVS3P3LIswyxfdxuMvU9dBBtP\nj3wofaLuxNWA384xBZYNV7AcWzOOHR3j3Iw7KfplgVawlpm4zXhBwFrKE44g0g5z\n4vL2N1eJs/OgaAMUYUM4kuZIW1fqFGB9cRAJpbjCO9d3dnvz4sPBWXchzZVjyzXh\njwIDAQAB\n-----END PUBLIC KEY-----\n";
payload = await encryptDataMultipleKeys(payload, publicKey, fieldsToEncrypt);
return payload
}
let data = {
"applicant": {
"address": {
"line_1": "732484THSTREETss",
"city": "TACOMA",
"country": "US",
"state": "WA",
"zipcode": "98498"
},
"income": 1000,
"date_of_birth": "1938-09-09",
"email_address": "faa4#mail.com",
"first_name": "WILLIAM",
"last_name": "SCALICI",
"phone_number": "7327474747",
"ssn": "987452343"
}
}
encryptPIIFields(data).then((encryptedData) => {
console.log(JSON.stringify(encryptedData)); //eslint-disable-line
process.exit(0);
}, (err) => {
console.log(err); //eslint-disable-line
process.exit(1);
});
Decryption script:
const crypto = require('crypto'),
_ = require('lodash');
async function decryptDataMultipleKeys(payload, privateKey, keysToDecrypt) {
if (!payload) {
return payload;
}
let decryptedPasswordBuffer = crypto.privateDecrypt({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256"
}, Buffer.from(payload.session_key, 'base64'));
let password = decryptedPasswordBuffer.toString('hex');
console.log("password: " + password);
let decryptedPayload = {};
for (const key of keysToDecrypt) {
let value = _.get(payload, key);
if (!value) {
continue;
}
let encryptedDataBuffer = Buffer.from(value, 'base64');
let bufferData = encryptedDataBuffer.slice(0, 16);
let bufferIv = encryptedDataBuffer.slice(16, 32);
let cipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(password, 'hex'), bufferIv);
cipher.setAutoPadding(true);
let decryptedValue = cipher.update(bufferData, undefined, 'utf8');
decryptedValue += cipher.final('utf8');
_.set(decryptedPayload, key, decryptedValue);
}
return decryptedPayload;
}
async function decryptPIIFields(payload) {
let fieldsToDecrypt = [
'applicant.ssn', 'applicant.date_of_birth', 'applicant.first_name', 'applicant.last_name',
'applicant.email_address', 'applicant.phone_number', 'applicant.income',
'applicant.address.line_1', 'applicant.address.line_2', 'applicant.address.city',
'applicant.address.country', 'applicant.address.state', 'applicant.address.zipcode'
];
let privateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEArYsdy+gGrdzvG5F9BYLlVwFwCfyCzeLQ7Vmvu+wvyoDrwvMX\nSfLnZfg7NsZMyPc3OVt8EeRvGLzrXvxtSWKG+mKBC7xEzb/LM8MoHQhXlgZ7L1no\nfBpAs74zEFXZNGHw5SnWXTuQ3Yym0u8hkYDZoqDJRgrczjXdbrqDVeB3GIvpMZMU\n9OkTFRmZZGMLVS3P3LIswyxfdxuMvU9dBBtPj3wofaLuxNWA384xBZYNV7AcWzOO\nHR3j3Iw7KfplgVawlpm4zXhBwFrKE44g0g5z4vL2N1eJs/OgaAMUYUM4kuZIW1fq\nFGB9cRAJpbjCO9d3dnvz4sPBWXchzZVjyzXhjwIDAQABAoIBAQCBNy03bwrSF8fd\nUgWxvdW/Y62lceN/IxwHLhlAJksrT7S7kj7L69XJwfts/Fed5xyyU2Dc/aaO19O1\nBOTmmDsCYafOMh9UxzKo1u2eOGDmruq3xgzpoq58Zukkh5dTfn1cVDttbfWeUKTC\nOBVZfoQNqARVZ68ix06ZrLwvjBOBLSmH4l4XM8JzYtBFOntkU45ZHmPvxGfJBvYS\nhTOMvS3AvfxuEK2zW9A/vciDWVWmET0p0C22+pMahT+FSwOwYNTuP3BxQV2Aq6vY\nEc9ktr4hj0b2gGoRok/t4K4C/ufDhxRinNnFIFcPh9j39/st8kLwlkKCgii3Kpjv\ntzD4OyX5AoGBANwB77oOmbIGNdXGONTQ1aXnqpsO0tt1/ZAnZrQaNgCb6ThwLieN\nQ5tqem6GWbTtSSUuwpgFjxw5SMD8KxJihV+ySjo99SGhqssyPXyYHpMmOSEsbQhe\n0YeT4Epr6FuIBLuV0qFZJupI6jcHBKcmR0FQ2rXqCxPnfNopZizm5GnbAoGBAMnv\nOxIdpI2r2Z/+6WyQiBmwuEhd39ZKA8aoONJeoCp0MnAQvrbmr6kDfpP+aQWw6Xww\n+5GrAFgrtJ37STHPXw/lXPKDpXE573o8aDHTDB/WU0lpCVxJ6NY0sy/CArUIU7Pz\ntQiB11PrZZ6UDyiSmXoYzUHkR1I44EjF2/lnZlddAoGBALvx44s8Qcw1RfQzfAVB\nyeIKwFHqHfNhHpXxMumUoqFuj5OpMaSUJzczhRe6KhRHyP68rXwU86aWwTIrudfg\n1jNkKckLeMecRj2D08cGZMgsFQ3j19kYt0Js72RkPoFC91gQq3kuofHvDDaqBi2M\no76GhfB12bTNQnlUeHbPYD2VAoGALZ7kg4U65d7LPcBDUAmfFd6842yB41G5ZKog\nnDZQjQbPVk4SKBQZ318wu5Kge26qcSpHy3MMkt7c4UwiDyTAX0D8LLXdLKVgGweG\nqqr5dD/hdRZLzRPNjIc/bCyym9+TuXX3kkJzOTxXKupcOlhUYCc2SAqgqky7LvW0\narYXgukCgYEAjtfYSciex+Nv1GGaN7SjAozIBvrLAV0o9oo/zxhTblJpCkaM60aT\nimiT4NwkrEfB27zzguYduD0mgsq/Hg8BBkbe7FPKZ8GugZ6xlF0i02kVRzRDNlxT\n+cfqbL2vKt5FR9iFJFVWYjmvpVmvxZ/J1ybZD3MjT+YBNj/sf9DvclM=\n-----END RSA PRIVATE KEY-----\n";
payload = await decryptDataMultipleKeys(payload, privateKey, fieldsToDecrypt);
return payload
}
let data = {"applicant":{"ssn":"YR8BUBk+xrpQm5gHkCfrIXMFGjGJGLS192mVgcupF6U=","date_of_birth":"+ujL7mv/IZMALdFiL92Z0LACrVhb/lmzcwx8l89sIcs=","first_name":"l8nAmcQkIm8OctcaFq9t4q5TN2brkf4MTfdQ7K19PMw=","last_name":"yOqZpZjueZu10q0z3P4cTN2m5BP7ug4CqypumfzjbUc=","email_address":"2CftSOnWqRCINRF9ZK5QYTSP6TdpTUEpEanJE6PAhUQ=","phone_number":"cEQV5cbYJveBkn3XWqzCw2x9a8P2ZcEjiMX5+ezhdQc=","income":"TpM/4zOiTpCZ8to8jjjngJDLRcrDKOP8C2UVRYh9Wgs=","address":{"line_1":"MYzvsUFBl+Oav1aDOxqvjimpv8YW4g2hSjZChfOeri4=","city":"/3m9bvk1auwNgyNTJ2gtx1B0+gYxKQYy/VBThyuqrr0=","country":"H8GZ9rP+EAw9KdeVvNbPFtPyUBtU9NrCxXrQ0GMTltg=","state":"g7nshQ6rNrbsPq1vJd5vnBh/0HNjasfgN8Mhy59FW/U=","zipcode":"X5MGNTPA/Rh2Fxb8GOLUBwHx9ex8RGGrRM+RA7Wf8MU="}},"session_key":"CDfUI+12UzezVpp/7/9jbWXJ7AmR5jTcV5r9JsyIPinxZO2nEra05t8uL3lOotyE23ymr1e3Ia8mF7huReIbTma25I7p01+eBjKBR9Zv5NHV72is44wmJqXu5dB1fOiJFF7xBjUzZ5zClgBMsFNr025yc4dtDKQxPcj0xGPvQKmUbbbwTvq7TrSS0rDZrjcGLsxlpIXua1damYp+n6Jw9XjLyN4OTyiV2JtiOq7vnRMEYsdTr4hibVhtFwkDFqCrg7Y9tnvgLocg2lMwEOu/iF7QDA5UlAUyiFU+U0WThasVjPCNikoRi2FC2u/T/EAtmG9drWuohxX2DUvyKgm/bA=="}
decryptPIIFields(data).then((decryptedData) => {
console.log(JSON.stringify(decryptedData)); //eslint-disable-line
process.exit(0);
}, (err) => {
console.log(err); //eslint-disable-line
process.exit(1);
});
I have a feeling that I'm messing something in the part where I append the IV to the encrypted payload... Need some enlightenment here.
EDIT: I have added a script to decrypt the same. I'm unable to successfully decrypt only certain cases.
For example, I can decrypt if the value of line_1 is "732484THSTREETs", but can't decrypt if the value is "732484THSTREETss"... I'm getting the following the error while decrypting the latter
Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
at Decipheriv.final (internal/crypto/cipher.js:172:29)
at decryptDataMultipleKeys (/Users/pavithran/off/payment-service/oaep-decrypt.js:29:30)
at decryptPIIFields (/Users/pavithran/off/payment-service/oaep-decrypt.js:43:19)
at Object.<anonymous> (/Users/pavithran/off/payment-service/oaep-decrypt.js:48:1)
at Module._compile (internal/modules/cjs/loader.js:1158:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
at Module.load (internal/modules/cjs/loader.js:1002:32)
at Function.Module._load (internal/modules/cjs/loader.js:901:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
at internal/main/run_main_module.js:18:47 {
library: 'digital envelope routines',
function: 'EVP_DecryptFinal_ex',
reason: 'bad decrypt',
code: 'ERR_OSSL_EVP_BAD_DECRYPT'
}
The problem is in both the symmetric encryption (wrong usage of update and final) and the symmetric decryption (wrong separation of the ciphertext). In detail the current version does not work for the following reasons:
In the symmetric encryption only the final part is considered. But of course the preceding update statement must be considered as well, which has to be stored in a Buffer for the subsequent concatenation, i.e. the third argument (the encoding) must be removed. Furthermore the IV is usually placed before (and not after) the ciphertext. The latter is not a bug, but it is still useful to follow conventions. All in all, therefore, for symmetric encryption it must be:
...
let encryptedPayloadUpd = cipherKey.update(dataToEncrypt, 'utf8'); // store in buffer
let encryptedPayloadFin = cipherKey.final()
let tempBuffer = Buffer.concat([iv, encryptedPayloadUpd, encryptedPayloadFin]); // consider encryptedPayloadUpd, place IV before ciphertext
return tempBuffer.toString('base64');
...
which produces e.g. the following ciphertext:
{"applicant":{"ssn":"zFbx9fiBSu47bMiAP7whaG+fkOBrCu+CWBzfYjPcV14=","date_of_birth":"K/GzpKNIDY4Bb0MJpNfvv/wE3iUBP31y5OS1t8LTEJg=","first_name":"HbVtwcy4wVV5n7JLpt87IhX27JiLn9ewaqj08EXw8Ss=","last_name":"D5lqNNYywt88MOSlMcZQY6oTLuntTYzFvOy1op7PhjY=","email_address":"hNBSep2jzczUiBm0M7iGTZcPo3GZVScOgKzjd+t3uYA=","phone_number":"0l4PgCW12WFb1jv9lfOftHngQlE8BWsbqi/HHdcmjhk=","income":"nu16KkULL/xyBgKQjxAn//Q34fdA0kAOMS+AJTYXh4k=","address":{"line_1":"ce2BBt+Qbpe8KpJR81zaqQh7CSF3WXni6snLYZYGPuHknR3qBCY2fLdKvgMl8D2E","city":"01eVK0h7zGOSnL8I4aQ+CICSQV1t7bU470/S1HY5ZmY=","country":"XHjNTEc8ZapnuBSgLgg2YIZ9fIc7m8hH/j/nULL1UZo=","state":"17m0tTQQaT8c4y+XXVQsz8tfjIDGrOh2tBMTAcH+5PY=","zipcode":"ygjxgvF3B0HAnvtpys5s7bDMABvg6IcJDKJAIMNuLjk="}},"session_key":"jEqblsQ5ZbGDmZBlzZgXZWAxtQptL+9FL2WKvMQHL5PdTDwez1XKMl6aAKHRoMjb3oH0GDw941ICGL99WHW+nxJaanxqV9mlU9NDBE84T/fdrov/YAS5NDb5CD20ZFT8YL+/QC3ldf4VvJlzLy18EvSgt1nPYUZ6WEfdpNs6YckxtV4NAQ1wNiB/zQ07RUUfIegdNE9vn828TjOqxTUDKkwtZiyKKtaIetWS9LnCSDh7PXEnWyAcHZ19WRTZimvoMuqPUjotChzCjNrwTEkoOp/XzPN3NhG/7nxxw9vFNSP0Gy6jPHXUBiJ9sMPkg99TZCk9+2hWGdMiuP4JHpvk4g=="}
For the symmetric decryption it is assumed that the ciphertext is only one block (16 bytes for AES) large, which is generally not true. Any plaintext consisting of more than 1 block will generate a larger ciphertext (even a 1 block plaintext generates a 2 block ciphertext because of the PKCS7 padding used). For the symmetric decryption (with the order IV, ciphertext) it must therefore be:
...
let encryptedDataBuffer = Buffer.from(value, 'base64');
let bufferIv = encryptedDataBuffer.slice(0, 16); // consider order (IV, ciphertext)
let bufferData = encryptedDataBuffer.slice(16); // consider complete ciphertext
...
With this the above ciphertext can be decrypted:
{"applicant":{"ssn":"987452343","date_of_birth":"1938-09-09","first_name":"WILLIAM","last_name":"SCALICI","email_address":"faa4#mail.com","phone_number":"7327474747","income":"1000","address":{"line_1":"732484THSTREETss","city":"TACOMA","country":"US","state":"WA","zipcode":"98498"}}}
Please note: The encryption and Base64 encoding in encryptPayload of the posted code in the question has been changed relative to the original post. Before the change ciphertext and IV were each Base64 encoded and then concatenated. This is unusual, as Base64 encoding generally occurs after concatenation. But this is not a bug as long as the decryption is implemented consistently. In contrast, the code after the change did not work, as explained in detail above. The posted code snippets in this answer implement the usual scheme: concatenation of IV and ciphertext in this order, followed by Base64 encoding.
Related
I am trying to encrypt and decrypt values using node inbuild module crypto. I have followed this tutorial to encrypt the data. They haven't to gave any sample code to decrypt. When I try to use other tutorial code to decrypt the data. It not working out. Please help me out,
Code
const crypto = require('crypto');
// Difining algorithm
const algorithm = 'aes-256-cbc';
// Defining key
const key = crypto.randomBytes(32);
// Defining iv
const iv = crypto.randomBytes(16);
// An encrypt function
function encrypt(text) {
// Creating Cipheriv with its parameter
let cipher = crypto.createCipheriv(
'aes-256-cbc', Buffer.from(key), iv);
// Updating text
let encrypted = cipher.update(text);
// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()]);
// Returning iv and encrypted data
return encrypted.toString('hex');
}
var op = encrypt("Hi Hello"); //c9103b8439f8f1412e7c98cef5fa09a1
Since you havent provided the code for decryption, Cant help you what is actually wrong you doing, apart from that you can do this to get decrypted code:
const crypto = require('crypto')
// Defining key
const key = crypto.randomBytes(32)
// Defining iv
const iv = crypto.randomBytes(16)
// An encrypt function
function encrypt(text) {
// Creating Cipheriv with its parameter
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv)
// Updating text
let encrypted = cipher.update(text)
// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()])
// Returning iv and encrypted data
return encrypted.toString('hex')
}
var op = encrypt('Hi Hello')
console.log(op)
function decrypt(data) {
// Creating Decipheriv with its parameter
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv)
// Updating text
const decryptedText = decipher.update(data, 'hex', 'utf8')
const finalText = decryptedText + decipher.final('utf8')
return finalText
}
var decrptedData = decrypt(op)
console.log(decrptedData)
When I first tested it both the encrypt and decrypt using the aes-256-ctr algorithm worked fine but after I posted it to IPFS and returned it it gave me the following error.
TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined
It all looks up to par for me.
Here is the encryption.js file.
const crypto = require('crypto');
const algorithm = 'aes-256-ctr';
const secretKey = 'vOH6sdmpNWjRRIqCc7rdxs01lwHzfr33';
const iv = crypto.randomBytes(16);
//const file = fs.readFileSync("test.txt");
//console.log(secretKey.length);
const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return {
iv: iv.toString('hex'),
content: encrypted.toString('hex')
};
};
const decrypt = (hash) => {
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrypted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')),
decipher.final()]);
return decrypted.toString();
};
It seems to work for me -- but only if you pass the entire object returned by encrypt into decrypt. If you just pass in the hash without the iv, you get that error.
I am using the inbuilt crypto module, and been frustrated for many hours trying to figure out why decipher.update returns a function and not the deciphered text itself.
code:
const file = path.join(__dirname, '../secret.txt');
const fileIV = path.join(__dirname, '../iv.txt');
const at = path.join(__dirname, '../at.txt')
var secret = fs.readFileSync(file, 'utf-8');
const algorithm = 'aes-256-gcm';
var text = 'default'
var encrypted = secret;
const iv = crypto.randomBytes(16);
encrypt(plainText, key, iv) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
return { encrypted: Buffer.concat([cipher.update(plainText), cipher.final()]), authTag: cipher.getAuthTag() }
}
decrypt(encrypted, key, iv, authTag) {
const decipher = crypto.createDecipheriv(algorithm, key, iv).setAuthTag(authTag);
console.log('this worked decrypt');
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
}
SignUp(pass)
{
console.log(pass);
var pair = ec.genKeyPair();
text = pair.getPrivate.toString('hex');
const key = crypto.scryptSync(pass, 'baethrowssalt', 32);
console.log(`The key is:${key}`);
const {encrypted, authTag} = this.encrypt(text, key, iv);
console.log('encrypted: ',encrypted.toString('hex'));
const decrypted = this.decrypt(encrypted, key, iv, authTag);
console.log('Decrypted:', decrypted.toString('utf-8'));
return console.log(`Close and reopen your app to integrate your wallet securely`);
}
in console it prints this when I print out the decrypted result of the private key I initially tried encrypting with scrypt:
Decrypted: function getPrivate(enc) {
if (enc === 'hex')
return this.priv.toString(16, 2);
else
return this.priv;
}
why is
decrypt(encrypted, key, iv, authTag) {
const decipher = crypto.createDecipheriv(algorithm, key, iv).setAuthTag(authTag);
console.log('this worked decrypt');
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
}
not giving me the text in its deciphered form? Additionally, how can I obtain it since I am clearly doing something wrong. Any help would really be appreciated.
The result of the decryption is exactly the same as the plaintext you encrypted!
You can easily verify this by outputting the plaintext, i.e. the contents of text, in SignUp() in the console before encrypting it:
var text = pair.getPrivate.toString('hex');
console.log('Initial plaintext:', text); // Initial plaintext: function getPrivate(enc) {...
The reason for the unexpected content of text is that you simply forgot the pair of parentheses after getPrivate, it should be:
var text = pair.getPrivate().toString('hex');
console.log('Initial plaintext:', text); // Initial plaintext: <the hex encoded private key>
Then the decryption provides the expected result.
It is probably because "decrypted.toString('utf-8')" is not executing the function but turning it into a string to show in the console...
I believe you have to do something like:
let decryptedResult = decrypted.toString('utf-8'); console.log('Decrypted:', decryptedResult.toString('utf-8'));
or but not sure
console.log('Decrypted:', (decrypted).toString('utf-8'));
I want to decrypt a message on the client-side(react.js) using Web Crypto API which is encrypted on the back-end(node.js), however I ran into a weird problem and don't have any idea what is wrong(I also checked this)
node.js
function encrypt(message){
const KEY = crypto.randomBytes(32)
const IV = crypto.randomBytes(16)
const ALGORITHM = 'aes-256-gcm';
const cipher = crypto.createCipheriv(ALGORITHM, KEY, IV);
let encrypted = cipher.update(message, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag()
let output = {
encrypted,
KEY: KEY.toString('hex'),
IV: KEY.toString('hex'),
TAG: tag.toString('hex'),
}
return output;
}
react.js
function decrypt() {
let KEY = hexStringToArrayBuffer(data.KEY);
let IV = hexStringToArrayBuffer(data.IV);
let encrypted = hexStringToArrayBuffer(data.encrypted);
let TAG = hexStringToArrayBuffer(data.TAG);
window.crypto.subtle.importKey('raw', KEY, 'AES-GCM', true, ['decrypt']).then((importedKey)=>{
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: IV,
},
importedKey,
encrypted
).then((plaintext)=>{
console.log('plainText: ', plaintext);
})
})
function hexStringToArrayBuffer(hexString) {
hexString = hexString.replace(/^0x/, '');
if (hexString.length % 2 != 0) {
console.log('WARNING: expecting an even number of characters in the hexString');
}
var bad = hexString.match(/[G-Z\s]/i);
if (bad) {
console.log('WARNING: found non-hex characters', bad);
}
var pairs = hexString.match(/[\dA-F]{2}/gi);
var integers = pairs.map(function(s) {
return parseInt(s, 16);
});
var array = new Uint8Array(integers);
return array.buffer;
}
Encryption in back-end is done without any error, however when want to decrypt the message on the client-side, the browser(chrome) gives this error: DOMException: The provided data is too small and when I run the program on firefox browser it gives me this error: DOMException: The operation failed for an operation-specific reason. It's so unclear!!
By the way what's the usage of athentication tag in AES-GCM is it necessary for decryption on the client-side?
GCM is authenticated encryption. The authentication tag is required for decryption. It is used to check the authenticity of the ciphertext and only when this is confirmed decryption is performed.
Since the tag is not applied in your WebCrypto Code, authentication and therefore decryption fail.
WebCrypto expects that the tag is appended to the ciphertext: ciphertext | tag.
The data in the code below was created using your NodeJS code (please note that there is a bug in the NodeJS code: instead of the IV, the key is stored in output):
decrypt();
function decrypt() {
let KEY = hexStringToArrayBuffer('684aa9b1bb4630f802c5c0dd1428403a2224c98126c1892bec0de00b65cc42ba');
let IV = hexStringToArrayBuffer('775a446e052b185c05716dd1955343bb');
let encryptedHex = 'a196a7426a9b1ee64c2258c1575702cf66999a9c42290a77ab2ff30037e5901243170fd19c0092eed4f1f8';
let TAGHex = '14c03526e18502e4c963f6055ec1e9c0';
let encrypted = hexStringToArrayBuffer(encryptedHex + TAGHex)
window.crypto.subtle.importKey(
'raw',
KEY,
'AES-GCM',
true,
['decrypt']
).then((importedKey)=> {
window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: IV,
},
importedKey,
encrypted
).then((plaintext)=>{
console.log('plainText: ', ab2str(plaintext));
});
});
}
function hexStringToArrayBuffer(hexString) {
hexString = hexString.replace(/^0x/, '');
if (hexString.length % 2 != 0) {
console.log('WARNING: expecting an even number of characters in the hexString');
}
var bad = hexString.match(/[G-Z\s]/i);
if (bad) {
console.log('WARNING: found non-hex characters', bad);
}
var pairs = hexString.match(/[\dA-F]{2}/gi);
var integers = pairs.map(function(s) {
return parseInt(s, 16);
});
var array = new Uint8Array(integers);
return array.buffer;
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
For a specific application I need to symmetrically encrypt on my .NET server and decrypt in the browser.
I'm generally free to choose the algorithm, so I tried AES-GCM as that has a better built-in API on .NET and is also supported by crypto.subtle.
I don't get it to work though, I'm stumped at getting an unhelpful exception from the call to crypto.subtle.decrypt, which contains no message on Chrome and says "The operation failed for an operation-specific reason" on Firefox.
The decryption code is (also here in codesandbox):
import "./styles.css";
import { Base64 } from "js-base64";
let nonce = Base64.toUint8Array("o/YcD/yZVU2egcGd");
async function importKey() {
const keyData = Base64.toUint8Array("3NraMtQP10qKGL3HLloObA==");
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM" },
true,
["decrypt", "encrypt"]
);
return key;
}
var cypherText = Base64.toUint8Array("Is+l7cojlfbuU3vUN0gWMw==");
async function decrypt() {
const key = await importKey();
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce },
key,
cypherText
);
} catch (ex) {
console.error("Error: " + ex.message);
}
}
async function work() {
const decrypted = await decrypt();
const result = new TextDecoder().decode(decrypted);
document.getElementById("app").innerText = result;
}
work();
Not entirely sure if what .NET calls nonce is what JS calls iv.
In any case, the catch handler is always reached.
For comparison, the .NET code to generate the cypher text is (also here as a LINQPad query):
AesGcm.NonceByteSizes.Dump();
AesGcm.TagByteSizes.Dump();
var key = Guid.Parse("32dadadc-0fd4-4ad7-8a18-bdc72e5a0e6c")
.ToByteArray()
.ToArray();
var nonce = Guid.Parse("0f1cf6a3-99fc-4d55-9e81-c19d09003e9b")
.ToByteArray()
.Take(12)
.ToArray();
Convert.ToBase64String(key).Dump("key");
var aes = new AesGcm(key);
Convert.ToBase64String(nonce).Dump("nonce");
var text = Encoding.UTF8.GetBytes("Hello, world 123");
text.Length.Dump("cypher text size");
var buffer = new Byte[text.Length];
var tag = new Byte[16];
aes.Encrypt(nonce, text, buffer, tag, null);
String.Join(" ", from b in buffer select b.ToString("d")).Dump("cypher text");
Convert.ToBase64String(buffer).Dump("cypher text");
var text2 = new Byte[text.Length];
aes.Decrypt(nonce, buffer, tag, text2, null);
Encoding.UTF8.GetString(text2).Dump("check");
In the .NET code, ciphertext and tag are processed separately, while in the JavaScript code, both must be processed concatenated: ciphertext | tag.
The authentication tag generated in the .NET code isn't applied in the JavaScript code at all, which alone prevents the decryption.
Furthermore, I can't reproduce the ciphertext used in the JavaScript code with the .NET code. Key and nonce, however, can be reproduced. When I run the .NET code I get the following data (Base64 encoded):
nonce: o/YcD/yZVU2egcGd
key: 3NraMtQP10qKGL3HLloObA==
ciphertext: 1dupqLQFLXe31Pq48udCFw==
tag: kfMFJS+cy4VoDuFX1t7Reg==
If the correct ciphertext is used in the JavaScript code, and ciphertext and tag are concatenated, then the decryption is successful:
// Concatenate ciphertext and tag!
const ciphertext = Base64.toUint8Array("1dupqLQFLXe31Pq48udCFw==");
const tag = Base64.toUint8Array("kfMFJS+cy4VoDuFX1t7Reg==");
const ciphertextTag = new Uint8Array(ciphertext.length + tag.length);
ciphertextTag.set(ciphertext);
ciphertextTag.set(tag, ciphertext.length);
let nonce = Base64.toUint8Array("o/YcD/yZVU2egcGd");
async function importKey() {
const keyData = Base64.toUint8Array("3NraMtQP10qKGL3HLloObA==");
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM" },
true,
["decrypt", "encrypt"]
);
return key;
}
async function decrypt() {
const key = await importKey();
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce },
key,
ciphertextTag // Use the concatenated data!
);
} catch (ex) {
console.error("Error: " + ex.message);
}
}
async function work() {
const decrypted = await decrypt();
const result = new TextDecoder().decode(decrypted);
console.log(result);
}
work();
<script src="https://cdn.jsdelivr.net/npm/js-base64#3.2.4/base64.min.js"></script>
Note that for security reasons a key / nonce pair may only be used once (see GCM / Security). Usually a fresh, random nonce is created for each encryption. Since the nonce isn't secret, it's usually placed before the ciphertext: nonce | ciphertext | tag. This is sent to the recipient, who separates the nonce (and depending on the API, the tag) and thus has all the information needed for decryption.