node.jsencryptionsalt-cryptography

hash password using node crypto scryptSync


I need to secure the user input password of an electron app to create a key for crypto AES ancryption. As suggested here on SO I've replaced the createHash function with scryptSync function. I'm not sure about the salt.

// this is the old method I've used to hash the password to obtain the correct lenght for aes-256-gcm
  // let key = crypto.createHash('sha256')
  //       .update(data.password)
  //       .digest();
// salt creation 
  let salt = crypto.randomBytes(64);
  let key = crypto.scryptSync(data.password, salt, 32);

Is there a predefined size that I need to pass to randomBytes function for aes-256-gcm encryption algo?

Also when I attach the salt to the encrypted output I'm using this code

   let encryptedData = Buffer.concat([cipher.update(base64, 'utf8'), cipher.final()]);
    let payload = `${salt.toString('base64')}:${iv.toString('base64')}:${encryptedData.toString('base64')}`;

Since I'm using the join() function to merge multiple encoded strings, I need a way to split them when the encrypted output file needs to be decoded. At the moment I'm using this way, but I've figured out that I need to extract the salt to check the password. Is there a way to do it better?

const decryptData = (data) => {
  let output = [];
  // I need the salt here but the file needs to be splitted before
  let key = crypto.scryptSync(data.password, 'salt', 32);

  return new Promise( (resolve, reject) => {
    fs.readFile(data.path, 'utf8', (err, content) => {
      if(err) return reject(err);
      let fileContents = content.split(' ');
      output = fileContents.map( (file) => {
        let [salt, iv, content] = file.split(':');
        let cipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
        let decrypted = cipher.update(content, 'base64', 'utf8');
        return decryptedData;
      });
      resolve(output);
    });
  });
}

Solution

  • As the Node.js indicates, the hash needs to be minimally 128 bits or 16 bytes. 64 bytes is really too much, I'd max out on 256 bits but 128 is really enough when it comes to a password. As you seem to be aiming at max security, try 32 bytes. Don't forget that requiring a strong password is at least as important, if not more important.

    Currently you are using the default cost parameter for scryptSync. That's probably not a good idea as it is set to 16384 by default. The higher the better, so try and see what's best for your particular target platforms.

    Note that you both know the salt and IV size in bytes. This means that it would be just as easy to get them from a binary string before it is encoded to base 64. That also has some advantages: if you load the binary data into a Buffer then you can simply use methods like from to get a different view, which can then represent an IV or salt without copying the data.

    That's most important for the plaintext of course. You want to copy that as few times as possible.