Decrypting SubtleCrypto AES-GCM encoded string with Go - javascript

I'm trying to decrypt a string that was encrypted with SubtleCrypto AES-GCM in the browser (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt). Using the following code in Javascript:
async function aesGcmEncrypt(plaintext, password) {
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv
const ivStr = Array.from(iv).map(b => String.fromCharCode(b)).join(''); // iv as utf-8 string
const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw
const ptUint8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8
const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8); // encrypt plaintext using key
const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array
const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string
return btoa(ivStr) + "." + btoa(ctStr); // iv+ciphertext base64-encoded
};
And im trying to decrypt the string in Go using:
func Decrypt(encryptedString string, keyString string) string {
split := strings.Split(encryptedString, ".")
key := []byte(keyString)
enc, err := base64.StdEncoding.DecodeString(split[1])
if err != nil {
panic(err.Error())
}
nonce, err := base64.StdEncoding.DecodeString(split[0])
if err != nil {
panic(err.Error())
}
//Create a new Cipher Block from the key
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
//Create a new GCM
aesGCM, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
//Decrypt the data
plaintext, err := aesGCM.Open(nil, nonce, enc, nil)
if err != nil {
panic(err.Error())
}
return string(plaintext)
}
However, every time I run into: "cipher: message authentication failed".
I guess there's something different with the implementations but I can't seem figure out what.
I tried using HEX instead of base64 (encoding and decoding) and tried using "cipher.NewGCMWithNonceSize()" instead of "cipher.NewGCM(block)".

Related

GO decryption nodejs has been deprecated data encryption method?

This is a 4 year old nodejs project I took over, and I was asked to refactor it using golang, but in the refactoring I found that the nodejs encryption was deprecated. And, I don't know which mode of AES is used to encrypt this code
Can any expert help me to see how to decrypt this nodejs encryption with golang? Thank you very much!
Encryption code for nodejs :
exports.createToken = function (src: string, timestamp: string, key: any) {
var msg = src + '|' + timestamp;
var cipher: any = CryptoJS.createCipher('aes256', key);
var enc: any = cipher.update(msg, 'utf8', 'hex');
enc += cipher.final('hex');
return enc;
};
Decryption code for nodejs :
exports.parseToken = function (token: string, key: string): any {
let decipher = CryptoJS.createDecipher('aes256', key);
let dec: string;
try {
dec = decipher.update(token, 'hex', 'utf8');
dec += decipher.final('utf8');
} catch (err) {
console.error('[token] fail to decrypt token. %j', token);
return null;
}
var ts = dec.split('|');
if (ts.length !== 2) {
// illegal token
return null;
}
return { src: ts[0], timestamp: Number(ts[1]) };
};
The deprecated methods crypto.createCipher() and crypto.createDecipher() apply the proprietary OpenSSL function EVP_BytesToKey() to derive a 32 bytes key and a 16 bytes IV from a password. No salt is used, the digest MD5 and an iteration count of 1. This algorithm is very insecure, which is why both methods are deprecated.
The posted code applies aes-256-cbc (i.e. AES-256 in CBC mode) and just this key derivation to derive a key/IV pair. Since the key derivation does not use a salt, always the same ciphertext results for the same plaintext and password. E.g. for:
var src = 'The quick brown fox jumps over the lazy dog';
var timestamp = '1616409134831';
var passphrase = 'my secret passphrase';
the ciphertext is:
60673700fb64da36b65829ee3c578d1ec675638a95c8dee4e7c026ee72a837c2170c13b7b24125c02871663a64fd646dd9994793943eeb70b3e959cbc4cd423a
So for decryption in Go you need an implementation of EVP_BytesToKey(), e.g. here:
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"github.com/walkert/go-evp"
"encoding/hex"
)
func main() {
key, iv := evp.BytesToKeyAES256CBCMD5([]byte(""), []byte("my secret passphrase")) // MD5, no salt, passprase: my secret passphrase
ciphertext, _ := hex.DecodeString("60673700fb64da36b65829ee3c578d1ec675638a95c8dee4e7c026ee72a837c2170c13b7b24125c02871663a64fd646dd9994793943eeb70b3e959cbc4cd423a")
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
cbc := cipher.NewCBCDecrypter(block, iv)
plaintextPadded := make([]byte, len(ciphertext))
cbc.CryptBlocks(plaintextPadded , ciphertext)
plaintext := string(PKCS7Unpad(plaintextPadded ))
fmt.Println("Decrypted data: ", string(plaintext))
}
func PKCS7Unpad(src []byte) []byte { // PKCS7 unpadding from https://stackoverflow.com/a/41595640/9014097
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
Running this code results in the above plaintext.

WebCrypto API: DOMException: The provided data is too small

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));
}

Need help in interpreting the aes-256-cbc encyption with oaepHash

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.

Mismatch AES 128 cypher text between iOS app and Node.js server

On iOS app I successfully encrypt/decrypt a string using AES128 algorithm and PKCS7 padding. I have used several AES online tools to encrypt/decrypt but the results are not the same. For example: http://www.txtwizard.net/crypto
For testing purposes I am using:
string to encrypt: You will find the ruby at position (x,7)
key: sec_key_16_bytes
iv: ivr_key_16_bytes
On iOS I obtain the string encrypted (base64): AAAAABAAAABwhAlwAQAAQGSHCxTygRNNvTrRNPtV6SV4eRSAkMyyToXq9XN6cEpip8QDuoV9Bkv0phJS4pocLQ==
I would like to obtain same result on a Node.js instance or simple JavaScript on client-side.
I've looked into crypto official documentation, but it has something like
const crypto = require('crypto');
const algorithm = 'aes-192-cbc';
const password = 'Password used to generate key';
// Key length is dependent on the algorithm. In this case for aes192
// it is 24 bytes (192 bits).
// Use the async `crypto.scrypt()` instead.
const key = crypto.scryptSync(password, 'salt', 24);
// The IV is usually passed along with the ciphertext.
const iv = Buffer.alloc(16, 0); // Initialization vector.
Here I have also salt and password, while on iOS I have only the algorithm and key.
This is the Swift code:
import Foundation
import CommonCrypto
// Advanced Encryption Standard (symmetric key algorithm)
// More at this great tutorial:
http://www.splinter.com.au/2019/06/09/pure-swift-common-crypto-aes-
encryption/
protocol Cryptable {
func encrypt(_ string: String) throws -> Data
func decrypt(_ data: Data) throws -> String
}
struct AES {
private let key: Data
private let ivSize: Int = kCCBlockSizeAES128
private let options: CCOptions = CCOptions(kCCOptionPKCS7Padding)
init(keyString: String) throws {
guard keyString.count == kCCKeySizeAES128 else {
throw Error.invalidKeySize
}
self.key = Data(keyString.utf8)
}
static func test() {
do {
let aes = try AES(keyString: "sec_key_16_bytes")
let stringToEncrypt: String = "You will find the ruby at position (x,7)"
print("String to encrypt:\t\t\t\(stringToEncrypt)")
let encryptedData: Data = try aes.encrypt(stringToEncrypt)
print("String encrypted (base64):\t\(encryptedData.base64EncodedString())")
let decryptedData: String = try aes.decrypt(encryptedData)
print("String decrypted:\t\t\t\(decryptedData)")
} catch {
print("Something went wrong: \(error)")
}
}
}
extension AES {
enum Error: Swift.Error {
case invalidKeySize
case generateRandomIVFailed
case encryptionFailed
case decryptionFailed
case dataToStringFailed
}
}
private extension AES {
func generateRandomIV(for data: inout Data) throws {
let string = "ivr_key_16_bytes"
let value = string.data(using: .utf8) ?? Data()
try data.withUnsafeMutableBytes { dataBytes in
guard let dataBytesBaseAddress = dataBytes.baseAddress else {
throw Error.generateRandomIVFailed
}
// let status: Int32 = SecRandomCopyBytes(
// kSecRandomDefault,
// kCCBlockSizeAES128,
// dataBytesBaseAddress
// )
dataBytesBaseAddress.storeBytes(of: value, as: Data.self)
// guard status == 0 else {
// throw Error.generateRandomIVFailed
// }
}
}
}
extension AES: Cryptable {
func encrypt(_ string: String) throws -> Data {
let dataToEncrypt = Data(string.utf8)
let bufferSize: Int = ivSize + dataToEncrypt.count + kCCBlockSizeAES128
var buffer = Data(count: bufferSize)
try generateRandomIV(for: &buffer)
var numberBytesEncrypted: Int = 0
do {
try key.withUnsafeBytes { keyBytes in
try dataToEncrypt.withUnsafeBytes { dataToEncryptBytes in
try buffer.withUnsafeMutableBytes { bufferBytes in
guard let keyBytesBaseAddress = keyBytes.baseAddress,
let dataToEncryptBytesBaseAddress = dataToEncryptBytes.baseAddress,
let bufferBytesBaseAddress = bufferBytes.baseAddress else {
throw Error.encryptionFailed
}
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
CCOperation(kCCEncrypt), // op: CCOperation
CCAlgorithm(kCCAlgorithmAES), // alg: CCAlgorithm
options, // options: CCOptions
keyBytesBaseAddress, // key: the "password"
key.count, // keyLength: the "password" size
bufferBytesBaseAddress, // iv: Initialization Vector
dataToEncryptBytesBaseAddress, // dataIn: Data to encrypt bytes
dataToEncryptBytes.count, // dataInLength: Data to encrypt size
bufferBytesBaseAddress + ivSize, // dataOut: encrypted Data buffer
bufferSize, // dataOutAvailable: encrypted Data buffer size
&numberBytesEncrypted // dataOutMoved: the number of bytes written
)
guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
throw Error.encryptionFailed
}
}
}
}
} catch {
throw Error.encryptionFailed
}
let encryptedData: Data = buffer[..<(numberBytesEncrypted + ivSize)]
return encryptedData
}
func decrypt(_ data: Data) throws -> String {
let bufferSize: Int = data.count - ivSize
var buffer = Data(count: bufferSize)
var numberBytesDecrypted: Int = 0
do {
try key.withUnsafeBytes { keyBytes in
try data.withUnsafeBytes { dataToDecryptBytes in
try buffer.withUnsafeMutableBytes { bufferBytes in
guard let keyBytesBaseAddress = keyBytes.baseAddress,
let dataToDecryptBytesBaseAddress = dataToDecryptBytes.baseAddress,
let bufferBytesBaseAddress = bufferBytes.baseAddress else {
throw Error.encryptionFailed
}
let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
CCOperation(kCCDecrypt), // op: CCOperation
CCAlgorithm(kCCAlgorithmAES128), // alg: CCAlgorithm
options, // options: CCOptions
keyBytesBaseAddress, // key: the "password"
key.count, // keyLength: the "password" size
dataToDecryptBytesBaseAddress, // iv: Initialization Vector
dataToDecryptBytesBaseAddress + ivSize, // dataIn: Data to decrypt bytes
bufferSize, // dataInLength: Data to decrypt size
bufferBytesBaseAddress, // dataOut: decrypted Data buffer
bufferSize, // dataOutAvailable: decrypted Data buffer size
&numberBytesDecrypted // dataOutMoved: the number of bytes written
)
guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
throw Error.decryptionFailed
}
}
}
}
} catch {
throw Error.encryptionFailed
}
let decryptedData: Data = buffer[..<numberBytesDecrypted]
guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
throw Error.dataToStringFailed
}
return decryptedString
}
}
The result in the iOS console is:
String to encrypt: You will find the ruby at position (x,7)
String encrypted (base64): AAAAABAAAABwhAlwAQAAQGSHCxTygRNNvTrRNPtV6SV4eRSAkMyyToXq9XN6cEpip8QDuoV9Bkv0phJS4pocLQ==
String decrypted: You will find the ruby at position (x,7)
How can I achieve same result with JavaScript or server-side? I am setting incorrect options in Swift CCCrypt? Are there any cross-platform solutions?
Thank you!

JOSE_JS JWE The data that encrypted by javascript won't decrypt by go

Server side code for generating public key is:
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil
}
publicKey := &privateKey.PublicKey
publicKeyBytes, err := json.Marshal(publicKey)
privateKeyBytes,err:=json.Marshal(privateKey)
The private key is a singleton stored in memory at server side and the public key is another singleton which is being returned to the clients requesting it.
Then the client which is a web browser here encrypts the data by server's public key:
cookieParts=document.cookie.split('pk=')
if(cookieParts.length==1)
{
serverPublicKey= unescape(cookieParts[0].split(';')[0].toString())
}
else
{
serverPublicKey= unescape(cookieParts[1].split(';')[0].toString())
}
serverPublicKey =serverPublicKey.replace(/([\[😏)?(\d+)([,\}\]])/g, "$1\"$2\"$3");
serverPublicKey = JSON.parse(serverPublicKey)
var rsa_key = {
"n":btoa(serverPublicKey.N).replace(/=/g, ''),
//Maybe the above line causes the problem.But I couldn't find any other way.
"e": 65537,
};
var cryptographer = new Jose.WebCryptographer();
cryptographer.setKeyEncryptionAlgorithm("RSA-OAEP");
cryptographer.setContentEncryptionAlgorithm("A128GCM");
cryptographer.setContentEncryptionAlgorithm("A128CBC-HS256");
var public_rsa_key = Jose.Utils.importRsaPublicKey(rsa_key, "RSA-OAEP");
var encrypter = new JoseJWE.Encrypter(cryptographer, public_rsa_key);
str="test"
encrypter.encrypt("sara").then(function(data) {
$scope.params.Param1=data
TestService.SendParamToServer($scope.params).then(function(result){
console.log("success")
}).catch(function(error){
console.log("error")
})
And then the server tires to decrypt the data just encrypted by the code above:
jweString = string(p.Param1)
jwe, err = jose.ParseEncrypted(jweString)
if err != nil {
panic(err.Error())
}
data, err := jwe.Decrypt(services.NewSecurityService().GetPrivateKey())
if err != nil {
// The error is not nil:
// square/go-jose: error in cryptographic primitive
panic(err.Error())
}
But unfortunately we get the following error:
square/go-jose: error in cryptographic primitive

Categories