Search code examples
node.jsencryptionaescryptojs

nodejs crypto Is there any encrypt and decrypt text, should give different encryption at every time


I know AES 256 CBC with buffer to give different encrypt but its length 66. here is my code

  const crypto = require('crypto');
  const ENCRYPTION_KEY = 'Must256bytes(32characters)secret';
  const IV_LENGTH = 16;
  function encrypt(text) {
    let iv = crypto.randomBytes(IV_LENGTH);
    let cipher = crypto.createCipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
    let encrypted = cipher.update(text.toString());
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return iv.toString('hex') + 'XX' + encrypted.toString('hex');
  }
  function decrypt(text) {
    let textParts = text.split('XX');
    let iv = new Buffer(textParts.shift(), 'hex');
    let encryptedText = new Buffer(textParts.join('XX'), 'hex');
    let decipher = crypto.createDecipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
    let decrypted = decipher.update(encryptedText);
    try{
      decrypted = Buffer.concat([decrypted, decipher.final()]);
      return decrypted.toString();
    }catch(Err){
      return 'NULL';
    }
  }

Problem is encryption data length is 66 even for text is 1
So is there any encryption and decryption method should give different encryption data at every time with less than 10 characters for text is 1(according to my example)

Thank you


Solution

  • Maarten covers most of the major points that I wanted to make, so this is just some elaboration and an example of it in Node.

    The changes from your code are:

    • Encode in Base64 rather than Hex. This is much more space-efficient. It would be even better to just use Buffers and not create a string at all; then we could have a 9-byte nonce rather than a 5-byte nonce.
    • Get rid of separator character between the IV/nonce and the ciphertext. We know how long the IV/nonce is; we don't need a separator.
    • Use CTR mode rather than CBC mode. This makes output length equal to input length.
    • Use a nonce rather than an IV. Randomly choose the nonce from a 2^40 space. (Randomly choosing a CTR nonce is very dangerous in general. See below for why it might be acceptable in your use case; it is still never recommended.)
    • Fix password generation by adding PBKDF2 (you could also just use randomBytes). Your password is highly insecure. It's an ASCII string, which means it represents a tiny fraction of the AES-256 keyspace. On the order of 0.000000000002% of the keyspace. That's how much less secure this is than AES-256.

    As Maarten notes, it is quite dangerous for CTR mode to duplicate a Key+Nonce pair. If someone does that, they can learn the XOR of the two original messages. With that, they have a good chance of decrypting both messages. For example, if you duplicated your key+nonce on two messages and the attacker used that to discover that their XOR was 3 and knew that the encrypted text was a capital letter, they would know that the two messages had to be one of these:

    [('A', 'B'), ('D', 'G'), ('E', 'F'), ('H', 'K'), ('I', 'J'), ('L', 'O'),
     ('M', 'N'), ('P', 'S'), ('Q', 'R'), ('T', 'W'), ('U', 'V')]
    

    This kind of information is devastating for structured data like human language or computer protocols. It can very quickly be used to decrypt the whole message. Key+nonce reuse is how WEP was broken. (When you do this by hand, it's basically identical to solving a cryptogram puzzle you'd find in the newspaper.) It is less powerful the more random the encrypted data is, and the less context it provides.

    With a random 5-byte nonce, there is a 50% likelihood of a collision after about 1.3M encryptions. With a random 8-byte nonce, there is a 50% likelihood of a collision after about 5.3B encryptions. sqrt(pi/2 * 2^bits)

    In cryptographic terms, this is a completely broken. It may or may not be sufficient for your purposes. To do it correctly (which I do not do below), as Maarten notes, you should keep track of your counter and increment it for every encryption rather than using a random one. After 2^40 encryptions (~1T), you change your key.

    Assuming that leaking information about two messages per million is acceptable, this is how you would implement that.

    const crypto = require('crypto');
    
    const ENCRYPTION_KEY = 'Must256bytes(32characters)secret';
    const SALT = 'somethingrandom';
    const IV_LENGTH = 16;
    
    const NONCE_LENGTH = 5; // Gives us 8-character Base64 output. The higher this number, the better
    
    function encrypt(key, text) {
      let nonce = crypto.randomBytes(NONCE_LENGTH);
      let iv = Buffer.alloc(IV_LENGTH)
      nonce.copy(iv)
    
      let cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
      let encrypted = cipher.update(text.toString());
      message = Buffer.concat([nonce, encrypted, cipher.final()]);
      return message.toString('base64')
    }
    
    function decrypt(key, text) {
      let message = Buffer.from(text, 'base64')
      let iv = Buffer.alloc(IV_LENGTH)
      message.copy(iv, 0, 0, NONCE_LENGTH)
      let encryptedText = message.slice(NONCE_LENGTH)
      let decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
      let decrypted = decipher.update(encryptedText);
      try{
        decrypted = Buffer.concat([decrypted, decipher.final()]);
        return decrypted.toString();
      }catch(Err){
        return 'NULL';
      }
    }
    
    // You could do this one time and record the result. Or you could just
    // generate a random 32-byte key and record that. But you should never
    // pass an ASCII string to the encryption function.
    let key = crypto.pbkdf2Sync(ENCRYPTION_KEY, SALT, 10000, 32, 'sha512')
    
    let encrypted = encrypt(key, "X")
    console.log(encrypted + " : " + encrypted.length)
    
    let decrypted = decrypt(key, encrypted)
    console.log(decrypted)