node.jsnginxcryptographywebcryptonjs

What is WebCrypto.subtle.decrypt analog for node.js crypto.createDecipheriv (AES-CTR algorithm)?


I am trying to rewrite old NodeJs encryption algorithm from

crypto.createDecipheriv(algorithm, key, iv[, options])

into webcrypto

subtle.decrypt(algorithm, key, data)

This code work good enough with AES-128-CTR algorithm

const algorithm = 'aes-128-ctr';

const iv = '0123456789ABCDEF0123456789ABCDEF';
const privateKey = '16Random_Letters';
const hexBufferFromIv = Buffer.from(iv, 'hex');
const utfBufferFromPrivateKey = Buffer.from(privateKey, 'utf8');

function oldEncryptData(data: string): string {
  const cipher = createCipheriv(
    algorithm,
    utfBufferFromPrivateKey,
    hexBufferFromIv,
  );
  let crypted = cipher.update(data, 'utf8', 'base64');
  crypted += cipher.final('base64');
  return crypted;
}

function oldDecryptData(data: string): string {
  const decipher = createDecipheriv(
    algorithm,
    utfBufferFromPrivateKey,
    hexBufferFromIv,
  );
  let dec = decipher.update(data, 'base64', 'utf8');
  dec += decipher.final('utf8');
  return dec;
}

async function testDecrypt() {
  const sourceText = `any text to encrypt!`;

  const encryptedText = oldEncryptData(sourceText);

  const decryptedText = oldDecryptData(encryptedText);

  return sourceText === decryptedText;
}

testDecrypt().then(console.log);

Right now I test this code and WebCrypto examples in nodejs, but as a final result I wont to move webCrypto.subtle.decrypt functionality into NGINX njs and as I know, njs doesn't support other options for decryption except for WebCrypto.

Interface for WebCrypto decrypt for AES-CTR in general looks like

const data = await crypto.subtle.decrypt(
  {
    name: "AES-CTR",
    counter,     // BufferSource
    length: 128, // 1-128
  },
  key,  // AES key
  encData, // BufferSource
);

And I don't undersatnd.

  1. counter is the same thing as the Initialization vector in createDecipheriv method?
  2. How I should generate key for subtle.decrypt method from the same passphrase?
  3. Do I need to do any additional transformation from or to base64 or utf8 encoding to reproduce input and output encoding in cipher.update(data, 'utf8', 'base64'); and in decipher.update(data, 'base64', 'utf8'); methods?

Solution

  • Thanks Topaco for hints. I'll write a more complete answer. Maybe it will be useful for someone.

    1. Yes, Initialization vector and counter can be treated as the same thing.
    2. For generating a key from the same passphrase you should use importKey method. And you should sent the same ArrayBuffer from the passphrase as in createCipheriv method.
    3. Yes, if your old method used some specific encoding and decoding, you should repeat the same encoding/decoding logic after Webcrypto.SubtleCrypto.encrypt() and decrypt() methods.

    Full workable example may looks something like

    import { webcrypto } from 'crypto';
    
    const iv = '0123456789ABCDEF0123456789ABCDEF';
    const privateKey = '16Random_Letters';
    const hexBufferFromIv = Buffer.from(iv, 'hex');
    const utfBufferFromPrivateKey = Buffer.from(privateKey, 'utf8');
    
    async function generateKeyFromPassPhrase(): Promise<CryptoKey> {
      return webcrypto.subtle.importKey(
        'raw',
        utfBufferFromPrivateKey,
        {
          name: 'AES-CTR',
        },
        true,
        ['decrypt', 'encrypt'],
      );
    }
    
    async function newEncryptData(data: string): Promise<string> {
      const key = await generateKeyFromPassPhrase();
    
      const encryptResult = await webcrypto.subtle.encrypt(
        {
          name: 'AES-CTR',
          length: 128,
          counter: hexBufferFromIv,
        },
        key,
        Buffer.from(data),
      );
    
      return Buffer.from(encryptResult).toString('base64');
    }
    
    async function newDecryptData(data: string): Promise<string> {
      const key = await generateKeyFromPassPhrase();
    
      const decryptResult = await webcrypto.subtle.decrypt(
        {
          name: 'AES-CTR',
          length: 128,
          counter: hexBufferFromIv,
        },
        key,
        Buffer.from(data, 'base64'),
      );
    
      return Buffer.from(decryptResult).toString();
    }
    
    async function testDecrypt() {
      const sourceText = `any text to encrypt!`;
      const encrypted2 = await newEncryptData(sourceText);
    
      const decrypted2 = await newDecryptData(encrypted2);
    
      return sourceText === decrypted2;
    }
    
    testDecrypt().then(console.log);