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.
Related
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));
}
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.
I have a client in JavaScript and a server in Node.JS. I'm trying to sign a simple text in client and send the signature along with publicKey to the server then server can verify the publicKey.
Anything in client-side is OK! but I'm unable to verify the signature in server-side. I think there is no need for you to read the client code but just for assurance I'll provide it too.
Client code:
let privateKey = 0;
let publicKey = 0;
let encoded = '';
let signatureAsBase64 = '';
let pemExported = ''
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
let keygen = crypto.subtle.generateKey({
name: 'RSA-PSS',
modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]),
hash: 'SHA-256'
}, true, ['sign', 'verify']);
keygen.then((value)=>{
publicKey = value.publicKey;
privateKey = value.privateKey;
let exported = crypto.subtle.exportKey('spki', publicKey);
return exported
}).then((value)=>{
console.log('successful');
const exportedAsString = ab2str(value);
const exportedAsBase64 = btoa(exportedAsString);
pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;
//signing:
encoded = new TextEncoder().encode('test');
let signing = crypto.subtle.sign({
name: "RSA-PSS",
saltLength: 32
},
privateKey,
encoded);
return signing;
}).then((signature)=>{
const signatureAsString = ab2str(signature);
signatureAsBase64 = btoa(signatureAsString);
//verifying just to be sure everything is OK:
return crypto.subtle.verify({
name: 'RSA-PSS',
saltLength: 32
},
publicKey,
signature,
encoded)
}).then((result)=>{
console.log(result);
//send information to server:
let toSend = new XMLHttpRequest();
toSend.onreadystatechange = ()=>{
console.log(this.status);
};
toSend.open("POST", "http://127.0.0.1:3000/authentication", true);
let data = {
signature: signatureAsBase64,
publicKey: pemExported
};
toSend.setRequestHeader('Content-Type', 'application/json');
toSend.send(JSON.stringify(data));
//to let you see the values, I'll print them to console in result:
console.log("signature is:\n", signatureAsBase64);
console.log("publicKey is:\n", pemExported);
}).catch((error)=>{
console.log("error",error.message);
})
Server Code(I use express for this purpose):
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
router.post('/authentication', async (req, res)=>{
try{
const publicKey = crypto.createPublicKey({
key: req.body.publicKey,
format: 'pem',
type: 'spki'
});
console.log(publicKey.asymmetricKeyType, publicKey.asymmetricKeySize, publicKey.type);
let signature = Buffer.from(req.body.signature, 'base64').toString();
signature = str2ab(signature);
const result = crypto.verify('rsa-sha256', new TextEncoder().encode('test'),
publicKey, new Uint8Array(signature));
console.log(result);
}catch(error){
console.log('Error when autheticating user: ', error.message);
}
})
Server Console Log:
rsa undefined public
false
NOTE:
I think the public key is imported correctly in server because when I export the
public key again in server, the pem formats of both sides(client & server) are completely
equal. so I think the problem is associated with 'verification' or 'converting signature' in server.
I prefer to use the built-in crypto module if it's possible, so other libraries such as subtle-crypto are my second options and I'm here to see if this can be done with crypto or not.
I want to learn how to verify a signature that is signed by JavaScript SubtleCrypto, due to this, Please don't ask some questions such as:
Why do you want to verify the public key in server?
Why don't you use 'X' library in client?
Feel free to change Exported format(pem), Public key format('spki'), Algorithm format(RSA-PSS) and so on.
The failed verification has two reasons:
The PSS padding must be specified explicitly, since PKCS#1 v1.5 padding is the default, s. here.
The conversion of the signature corrupts the data: The line:
let signature = Buffer.from(req.body.signature, 'base64').toString();
performs a UTF8 decoding, s. here, which irreversibly changes the data, s. here. The signature consists of binary data that is generally UTF8 incompatible. A conversion to a string is only possible with suitable binary-to-text encodings (like Base64, hex etc.), s. here.But apart from that a conversion is actually not necessary at all, because the signature can be passed directly as a buffer, s. here.
The following NodeJS code performs a successful verification (for a signature and public key produced with the client code):
const publicKey = crypto.createPublicKey(
{
key: req.body.publicKey,
format: 'pem',
type: 'spki'
});
const result = crypto.verify(
'rsa-sha256',
new TextEncoder().encode('test'),
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
},
Buffer.from(req.body.signature, 'base64'));
console.log(result); // true
const crypto = require('crypto');
const util = require('util');
class AES {
constructor(key, iv) {
if (!key) throw new Error('A 32 byte / 256 bit key is required.');
if (!iv) throw new Error('Initialization vector is required.');
this.key = key;
this.iv = iv;
}
encrypt(data) {
let cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv);
let encrypted = cipher.update(data, 'utf-8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
decrypt(data) {
let decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv);
let decrypted = decipher.update(data, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}
static randomBytes = async bytes => {
let result;
result = await util.promisify(crypto.randomBytes)(bytes);
return result;
}
sha256(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}
}
The above is my code and I'm wondering if the encrypt and decrypt methods should be made async? It takes less than 1 ms to execute using my test data, but w/thousands of concurrent users, would it be better to build these methods using async? I wasn't able to promisify them in earlier tests so I thought maybe the module can't be async'd?
Making the functions async wouldn't improve performance for multiple users, especially if this is a REST service. You should look at utilizing your hardware by threads.