Search code examples
node.jscryptographycryptojs

PBEWithHmacSHA256AndAES_128 encryption in nodejs


I'm trying to encrypt a string using PBEWithHmacSHA256AndAES_128 in nodejs, however I'm having a bit of trouble determining the correct way to do it.

Lots of documentation state I can use the crypto library, which when I try crypto.getCiphers() I see 'aes-128-cbc-hmac-sha256' is supported.

I've tried various tutorials, https://www.geeksforgeeks.org/node-js-crypto-createcipheriv-method/ and such but I'm mainly hitting "Invalid key length" or "Invalid initialization vector" when I try to change the cipher type.

Could anyone point me to some documentation or code samples that may assist in achieving this?


Solution

  • PBEWithHmacSHA256AndAES_128 and aes-128-cbc-hmac-sha256 refer to different things.

    Both encrypt with AES-128 in CBC mode, but the former uses a key derivation, the latter a MAC (more precisely an HMAC) for authentication.

    Regarding NodeJS, the latter has apparently never worked reliably. In some versions exceptions are generated, in others no authentication is performed (i.e. the processing is functionally identical to AES-128-CBC), see here. This is not surprising since OpenSSL only intends this to be used in the context of TLS, see here, which of course also applies to NodeJS as this is just an OpenSSL wrapper.

    But since you are concerned with PBEWithHmacSHA256AndAES_128, the aes-128-cbc-hmac-sha256 issues are in the end not relevant.
    PBEWithHmacSHA256AndAES_128 uses PBKDF2 (HMAC/SHA256) as key derivation, which is supported by NodeJS. A possible implementation that is functionally identical to PBEWithHmacSHA256AndAES_128 is:

    var crypto = require("crypto");
    
    // Key derivation
    var password = 'my passphrase';
    var salt = crypto.randomBytes(16); // some random salt
    var digest = 'sha256';
    var length = 16;
    var iterations = 10000; 
    var key = crypto.pbkdf2Sync(password, salt, iterations, length, digest);
    
    // Encryption
    var iv = crypto.randomBytes(16); // some random iv
    var cipher = crypto.createCipheriv('AES-128-CBC', key, iv); 
    var encrypted = Buffer.concat([cipher.update('The quick brown fox jumps over the lazy dog', 'utf8'), cipher.final()]);
    
    // Output
    console.log(salt.toString('base64'));       // d/Gg4rn0Gp3vG6kOhzbAgw==
    console.log(iv.toString('base64'));         // x7wfJAveb6hLdO4xqgWGKw==
    console.log(encrypted.toString('base64'));  // RbN0MsUxCOWgBYatSbh+OIWJi8Q4BuvaYi6zMxqERvTzGtkmD2O4cmc0uMsuq9Tf
    

    The encryption with PBEWithHmacSHA256AndAES_128 gives the same ciphertext when applying the same parameters. This can be checked e.g. with Java and the SunJCE provider which supports PBEWithHmacSHA256AndAES_128 (here).


    Edit:

    From the linked Java code for decryption all important parameters can be extracted directly:

    var crypto = require("crypto");
    
    // Input parameter (from the Java code for decryption)
    var password = 'azerty34';
    var salt = '12345678'; 
    var digest = 'sha256';
    var length = 16;
    var iterations = 20; 
    var iv = password.padEnd(16, '\0').substr(0, 16);
    var plaintext = '"My53cr3t"';
    
    // Key derivation
    var key = crypto.pbkdf2Sync(password, salt, iterations, length, digest);
    
    // Encryption
    var cipher = crypto.createCipheriv('AES-128-CBC', key, iv); 
    var encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
    
    // Output
    console.log(encrypted.toString('base64'));  // bEimOZ7qSoAd1NvoTNypIA==
    

    Note that the IV is equal to the password, but truncated if it is larger than 16 bytes or padded with 0x00 values at the end if it is shorter than 16 bytes (as is the case here).

    The NodeJS code now returns the required ciphertext for the given input parameters.

    Keep in mind that the static salt is a serious security risk.