7

I am trying to encrypt a piece of string in nodejs and need to decrypt that in front end javascript. In nodejs I was using the crypto library and in front end using web crypto. Facing some error while decrypting in the front end.

NodeJS

const crypto = require('crypto'); const iv = crypto.randomBytes(12); const algorithm = 'aes-256-gcm'; let password = 'passwordpasswordpasswordpassword'; let text = 'Hello World!'; let cipher = crypto.createCipheriv(algorithm, password, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); var tag = cipher.getAuthTag(); let cipherObj = { content: encrypted, tag: tag, iv: iv } 

Front End

let cipherObj; //GET FROM BE let aesKey = await crypto.subtle.importKey( "raw", Buffer.from('passwordpasswordpasswordpassword'), //password "AES-GCM", true, ["decrypt"] ); let decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: Buffer.from(cipherObj.iv), tagLength: 128 }, aesKey, Buffer.concat([Buffer.from(cipherObj.content), Buffer.from(cipherObj.tag)]) ); 

Decrypt function in the front-end is throwing an error.

ERROR Error: Uncaught (in promise): OperationError at resolvePromise (zone.js:814) at zone.js:724 at rejected (main.js:231) at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:388) at Object.onInvoke (core.js:3820) at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:387) at Zone.push../node_modules/zone.js/dist/zone.js.Zone.run (zone.js:138) at zone.js:872 at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421) at Object.onInvokeTask (core.js:3811) at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:420) at Zone.push../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188) at drainMicroTaskQueue (zone.js:595) 

PS: I am using Angular 7 in front end

1
  • did you resolve it? Commented Aug 3, 2020 at 22:28

1 Answer 1

3

I was able to get this working with some changes:

  1. I use SHA-256 to hash the password, so it can be any length. (The OP requires a 32-byte string.)
  2. I added additional helper functions from another answer for converting buffers to/from hex.
  3. I print the output cipherObj in JSON. This is your encrypted message payload.

Helpers - NodeJS and Browser

// look up tables var to_hex_array = []; var to_byte_map = {}; for (var ord=0; ord<=0xff; ord++) { var s = ord.toString(16); if (s.length < 2) { s = "0" + s; } to_hex_array.push(s); to_byte_map[s] = ord; } // converter using lookups function bufferToHex2(buffer) { var hex_array = []; //(new Uint8Array(buffer)).forEach((v) => { hex_array.push(to_hex_array[v]) }); for (var i=0; i<buffer.length; i++) { hex_array.push(to_hex_array[buffer[i]]); } return hex_array.join('') } // reverse conversion using lookups function hexToBuffer(s) { var length2 = s.length; if ((length2 % 2) != 0) { throw "hex string must have length a multiple of 2"; } var length = length2 / 2; var result = new Uint8Array(length); for (var i=0; i<length; i++) { var i2 = i * 2; var b = s.substring(i2, i2 + 2); result[i] = to_byte_map[b]; } return result; } 

The backend uses hex2buffer and the frontend uses buffer2hex, but you can just include that code in both.

So the backend code is the above helpers plus:

NodeJS

const crypto = require('crypto'); const iv = crypto.randomBytes(12); const algorithm = 'aes-256-gcm'; let password = 'This is my password'; let key = crypto.createHash("sha256").update(password).digest(); let text = 'This is my test string, with 🎉 emoji in it!'; let cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); var tag = cipher.getAuthTag(); let cipherObj = { content: encrypted, tag: bufferToHex2(tag), iv: bufferToHex2(iv) } console.log(JSON.stringify(cipherObj)); 

The output changes every run due to random IV, but for example:

{"content":"22da4796365ac1466f40022dd4510266fa3e24900b816f365e308cf06c95237783d1043c7deeb45d00381f8ff9ed","tag":"b7007905163b2d4890c9452c8edc1821","iv":"eb4758787164f95ac22ee50d"} 

So then the example frontend code is the above helper functions, plus:

Browser

let cipherObj; //GET FROM BACKEND // For example: cipherObj = {"content":"22da4796365ac1466f40022dd4510266fa3e24900b816f365e308cf06c95237783d1043c7deeb45d00381f8ff9ed","tag":"b7007905163b2d4890c9452c8edc1821","iv":"eb4758787164f95ac22ee50d"} let password = 'This is my password'; let enc = new TextEncoder(); let key = await window.crypto.subtle.digest({ name:"SHA-256" }, enc.encode(password)); let aesKey = await crypto.subtle.importKey( "raw", key, "AES-GCM", true, ["decrypt"] ); let decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: hexToBuffer(cipherObj.iv), tagLength: 128 }, aesKey, hexToBuffer(cipherObj.content + cipherObj.tag) ); let dec = new TextDecoder(); console.log(dec.decode(decrypted)); // This is my test string, with 🎉 emoji in it! 

Some crypto reference examples.

Sign up to request clarification or add additional context in comments.

2 Comments

Seriously, who thought that NodeJS and browsers should have different APIs every single time. 🙄
Thank you SO much! Lol did you really post this 2 hours ago? I'm up at 2:30AM, struggling with this exact problem, and I was about to give up! What finally helped me is seeing that I need to append the auth tag to the message before decrypting! Also, the realization that the 16 BYTE tag node.js crypto produces matches the 128 BIT tag crypto.subtle expects! 2 minutes after finding your post, I had it working! Wish I could buy you a beer :)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.