Search code examples
javascriptencryptionerror-handlingpbkdf2

how to show error when wrong password is given NodeJS crypto - createDecipheriv and pbkdf2


Created a simple app that encrypts text, but how do i show error when wrong password or salt is given. Hosted it on replit. But when i give wrong password or salt it just decrypts it. There isn't a callback or function in crypto for crypto.createDecipheriv()

const app = {
  encrypt(text, password, salt) {
    password = password.repeat(32).substr(0, 32);
    salt = salt.repeat(16).substr(0, 16);
    crypto.pbkdf2(password, salt, 10, 16, 'sha512', (err, key) => {
      if (err) {
        console.log(err); 
      } else {
        key = key.toString('hex');
        const cipher = crypto.createCipheriv('aes-256-gcm', key, salt);
        let encrypted = cipher.update(text, 'utf8', 'hex');
        console.log(encrypted);
      }
    });
  },
  decrypt(text, password, salt) {
    password = password.repeat(32).substr(0, 32);
    salt = salt.repeat(16).substr(0, 16);
    crypto.pbkdf2(password, salt, 10, 16, 'sha512', (err, key) => {
      if (err) {
        console.log(err); 
      } else {
        key = key.toString('hex');
        const cipher = crypto.createDecipheriv('aes-256-gcm', key, salt);
        let decrypted = cipher.update(text, 'hex', 'utf8');
        console.log(decrypted);
      }
    });
  }
}

const message = 'Hello World';
app.encrypt(message, 'password', 'salt');

const cipherText = 'a0a4e0ad97133494856502';
app.decrypt(cipherText, 'password', 'salt');

Solution

  • GCM generates an authentication tag (16 bytes by default) during encryption, which is used for authentication during decryption.
    Some libraries (e.g. Java) implicitly concatenate ciphertext and tag (ciphertext|tag) during encryption and implicitly separate both during decryption (this is not critical since the tag is not secret).
    The crypto module of NodeJS, on the other hand, handles ciphertext and tag independently, so the tag must be considered explicitly. It can be determined during encryption with getAuthTag() and must be set during decryption with setAuthTag(). Both is missing in the posted code.
    Also missing are the final() calls that generate the tag on encryption and perform authentication on decryption.
    To pass the tag to the decryption in this example, the following fix concatenates the tag with the ciphertext during encryption and separates it during decryption (following the Java pattern).

    If these problems are fixed, the decryption works for correct data and displays a corresponding message for incorrect data: Error: Unsupported state or unable to authenticate data.

    Fixed code, see the comments for details:

    const crypto = require('crypto');
    
    const app = {
      encrypt(text, password, salt) {
        password = password.repeat(32).substr(0, 32);
        salt = salt.repeat(16).substr(0, 16);
        crypto.pbkdf2(password, salt, 10, 16, 'sha512', (err, key) => {
          if (err) {
            console.log(err); 
          } else {
            key = key.toString('hex');
            const cipher = crypto.createCipheriv('aes-256-gcm', key, salt);
            let encrypted = cipher.update(text, 'utf8', 'hex') + cipher.final('hex'); // Fix 1a: call final(): create tag
            let tag = cipher.getAuthTag(); // Fix 2a: get tag 
            console.log(encrypted + tag.toString('hex')); // Fix 3a: concat ciphertext and tag 
          }
        });
      },
      decrypt(text, password, salt) {
        var tag = Buffer.from(text.substr(-32, 32), 'hex'); // Fix 3b: Separate ciphertext and tag
        var ciphertext = text.substr(0, text.length - 32);
        password = password.repeat(32).substr(0, 32);
        salt = salt.repeat(16).substr(0, 16);
        crypto.pbkdf2(password, salt, 10, 16, 'sha512', (err, key) => {
          if (err) {
            console.log(err); 
          } else {
            key = key.toString('hex');
            const cipher = crypto.createDecipheriv('aes-256-gcm', key, salt);
            cipher.setAuthTag(tag); // Fix 2b: set  tag
            try {
              let decrypted = cipher.update(ciphertext, 'hex', 'utf8') + cipher.final('utf8'); // Fix 1b: call final(): authenticate
              console.log(decrypted);
            } catch (e) {
              console.log("Authentication failed!");
            }
          }
        });
      }
    }
    
    const message = 'Hello World';
    app.encrypt(message, 'password', 'salt');
    
    const cipherText = 'a0a4e0ad971334948565023568ae285d45b9cefc80abe3afcf9155';
    app.decrypt(cipherText, 'password', 'salt');
    app.decrypt(cipherText, 'password123', 'salt');