0

I generate a pair public/private key on Client and send the publicKey to the Server and the backend will generate a sharedKey on its side and respond me a publicKey which help me to generate a sharedKey on the client too for encryption/decryption. So I encrypt a message by AES-256-GCM on Nodejs and decrypted the message on the Client.

Backend-Side:

export function encrypt(sharedKey: string, message: string) {
  const firstIv = getRandomIV();
  const cipher = crypto.createCipheriv(
    'aes-256-gcm',
    Buffer.from(sharedKey, 'base64'),
    firstIv
  );

  const encrypted = cipher.update(message, 'utf8');

  return Buffer.from(encrypted + cipher.final()).toString('base64');
}
function getRandomIV() {
  return crypto.randomBytes(12);
}

Client-Side:

async function decrypt(encryptedData: Uint8Array) {
    const aesKey = await generateAesKey();
    const nonce = encryptedData.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
    const data = encryptedData.subarray(SERVER_ENCRYPTION_IV_LENGTH);

    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'AES-GCM',
        iv: nonce,
      },
      aesKey,
      data
    );
    return {
      decrypted: new Uint8Array(decrypted),
      decryptedString: new TextDecoder().decode(decrypted),
    };
  }

async function generateAesKey() {
    const publicKey = await getServerPublicKey();
    const privateKey = await getPrivateKey();
    const sharedSecret = await crypto.subtle.deriveBits(
      {
        name: 'ECDH',
        public: publicKey!,
      },
      privateKey,
      256
    );

    const aesSecret = await crypto.subtle.digest('SHA-256', sharedSecret);
    return crypto.subtle.importKey('raw', aesSecret, 'AES-GCM', true, [
      'encrypt',
      'decrypt',
    ]);
  }

Now, I can't decrypt the server encrypted response in the client and I encounter to DOMException error and I don't know why?

Ali Torki
  • 1,929
  • 16
  • 26
  • Please create an [mcve]. You haven't even shown that you tried if the AES keys match (e.g. by comparing hexadecimal strings of the AES keys). – Maarten Bodewes Sep 11 '21 at 12:06
  • You're right, for your information, it works when I encrypt a message in the client and decrypt it on the backend, and `sharedKey` on the backend/client is true and there is no issue. the main issue is the encryption phase on the backend and decryption in the client as I wrote in my question. – Ali Torki Sep 11 '21 at 12:23
  • 1
    You are not using GCM in the NodeJS code, but CTR. You should change that accordingly. Also you should post all functions that are necessary for a repro, e.g. `getRandomValue()`, `concatUint8Array()`, `generateAesKey()` etc. – Topaco Sep 11 '21 at 12:23
  • Sorry, my bad, I updated my question – Ali Torki Sep 11 '21 at 12:40
  • `generateAesKey` function generates the same `sharedKey` that the backend has generated and there is no issue with that. – Ali Torki Sep 11 '21 at 12:44

1 Answers1

3

GCM uses an authentication tag that is handled separately by NodeJS/Crypto, while WebCrypto automatically concatenates it with the ciphertext.
Therefore, in the NodeJS code, the tag must be explicitly determined and appended to the ciphertext. This is missing in the current NodeJS code and can be taken into account as follows. Note the determination of the tag with cipher.getAuthTag() and its concatenation:

var crypto = require('crypto');

function encrypt(key, plaintext) {
  
    var nonce = getRandomIV();
    var cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
    var nonceCiphertextTag = Buffer.concat([
        nonce, 
        cipher.update(plaintext), 
        cipher.final(), 
        cipher.getAuthTag() // Fix: Get tag with cipher.getAuthTag() and concatenate: nonce|ciphertext|tag
    ]); 
    return nonceCiphertextTag.toString('base64');
}

function getRandomIV() {
    return crypto.randomBytes(12);
}

var message = Buffer.from('The quick brown fox jumps over the lazy dog', 'utf8');
var sharedKey = Buffer.from('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=', 'base64');
var ciphertext = encrypt(sharedKey, message);
console.log(ciphertext); // wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=

A possible output is

wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=

The following code for decryption on the WebCrypto side is essentially based on your code (without the derivation of the key from a shared secret, which is irrelevant to the current problem):

(async () => {

    var nonceCiphertextTag = base64ToArrayBuffer('wRE5KM6FG81QSMNvG0xR+iaIeF77cyyeBceGS5NkcYaD17K9nL0/helnqRBOkD9pLVoWM/nRAcaKg/YdvfNJcO1Zn/7ZM0k=');
    var nonceCiphertextTag = new Uint8Array(nonceCiphertextTag);
    var decrypted = await decrypt(nonceCiphertextTag);
    console.log(decrypted); // The quick brown fox jumps over the lazy dog
})();

async function decrypt(nonceCiphertextTag) {
    
    const SERVER_ENCRYPTION_IV_LENGTH = 12; // For GCM a nonce length of 12 bytes is recommended!
    var nonce = nonceCiphertextTag.subarray(0, SERVER_ENCRYPTION_IV_LENGTH);
    var ciphertextTag = nonceCiphertextTag.subarray(SERVER_ENCRYPTION_IV_LENGTH);

    var aesKey = base64ToArrayBuffer('MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=');
    aesKey = await window.crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
    var decrypted = await crypto.subtle.decrypt({name: 'AES-GCM', iv: nonce}, aesKey, ciphertextTag);
    return new TextDecoder().decode(decrypted);
}

// Helper

// https://stackoverflow.com/a/21797381/9014097
function base64ToArrayBuffer(base64) {
    var binary_string = window.atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}

which successfully decrypts the ciphertext of the NodeJS side:

The quick brown fox jumps over the lazy dog
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Hi there, I started a chat room with your here. did you see it? may you help me? – Ali Torki Sep 17 '21 at 11:23
  • 2
    @AliTorki - I don't have enough time for this task and honestly don't want to commit. Sorry. I recommend that you post your questions on SO. This way you will reach a large community and the chance of a successful answer will grow. But who knows, maybe I'll answer one or two of your questions on SO. Good luck. – Topaco Sep 17 '21 at 15:38