Jonnatan
Jonnatan

Reputation: 21

AES/CBC/PKCS5Padding nodejs encryption

I am trying to convert my java code to NodeJs code. It's a little more complicate because the customised format included the password and salt.

In main method there is one example.

Here is my java code:

public class App {
    private static final int DYN_SALT_LENGTH = 10;
    private static final int ITERATION_COUNT = 65556;
    private static final int KEY_LENGTH = 256;
    private static final String SECRET_KEY_ALGORITHM = "AES";
    private static final String CIPHER_TRANSFORMER = "AES/CBC/PKCS5Padding";
    private static Base64 base64Instance = new Base64();

    public static String decrypt(String data, String password, String salt) {
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8),
                    ITERATION_COUNT,
                    KEY_LENGTH);

            SecretKey secretKey = factory.generateSecret(spec);

            ByteBuffer buffer = ByteBuffer.wrap(base64Instance.decode(data));
            buffer.position(DYN_SALT_LENGTH);
            Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMER);

            // Read the IV
            byte[] ivBytes = new byte[cipher.getBlockSize()];
            buffer.get(ivBytes, 0, ivBytes.length);

            // Read encrypted text.
            byte[] encryptedTextBytes = new byte[buffer.capacity() - DYN_SALT_LENGTH - ivBytes.length];
            buffer.get(encryptedTextBytes);

            // Initialize Cipher.
            SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), SECRET_KEY_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(ivBytes));

            String result = new String(cipher.doFinal(encryptedTextBytes), StandardCharsets.UTF_8);
            return result;

        } catch (Exception e) {
            throw new RuntimeException("Failed to decrypt data", e);
        }
    }

    public static String encrypt(String data, String password, String salt) {
        // Create new salt for every new encryption request.
        byte[] saltBytes = new byte[DYN_SALT_LENGTH];
        new SecureRandom().nextBytes(saltBytes);

        try {
            // Create secret key spec.
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8),
                    ITERATION_COUNT,
                    KEY_LENGTH);
            SecretKey secretKey = factory.generateSecret(spec);
            SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), SECRET_KEY_ALGORITHM);
            byte[] ivBytes;
            byte[] encryptedTextBytes;

            // Initialize cipher
            Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMER);
            cipher.init(Cipher.ENCRYPT_MODE, secret);

            AlgorithmParameters params = cipher.getParameters();

            // Create initialization vector IV
            ivBytes = params.getParameterSpec(IvParameterSpec.class).getIV();

            // Encrypt the text.
            encryptedTextBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

            // Response will be in the form of <salt><IV><encryptedText>
            ByteBuffer byteBuffer = ByteBuffer.allocate(saltBytes.length + ivBytes.length + encryptedTextBytes.length);
            byteBuffer.put(saltBytes);
            byteBuffer.put(ivBytes);
            byteBuffer.put(encryptedTextBytes);

            return base64Instance.encodeToString(byteBuffer.array());
        } catch (Exception e) {
            throw new RuntimeException("Failed to encrypt data", e);
        }

    }

    public static void main(String[] args) {
        String password = "password";
        String salt = "salt";
        String data = "hello world";

        String resultEncrypted = encrypt(data, password, salt);
        System.out.println(resultEncrypted);
        String resultDecrypted = decrypt(resultEncrypted, password, salt);
        System.out.println(resultDecrypted);
    }
}

I'm trying with JS code like below but without clue about what i'm doing wrong:

function getAlgorithm(keyBase64) {
  var key = Buffer.from(keyBase64, "base64");
  switch (key.length) {
    case 16:
      return "aes-128-cbc";
    case 32:
      return "aes-256-cbc";
  }

  throw new Error("Invalid key length: " + key.length);
}

function decrypt(messagebase64, keyBase64, ivBase64) {
  const key = Buffer.from(keyBase64, "base64");
  const iv = Buffer.from(ivBase64, "base64");

  const decipher = crypto.createDecipheriv(
    getAlgorithm(keyBase64),
    key,
    iv.slice(0, 16)
  );
  let decrypted = decipher.update(messagebase64, "base64", "utf8");
  decrypted += decipher.final("utf8");
  return decrypted;
}

const base64Encrypted =
  "2vSIh0J64zhrQuayUV+UIyPTpmSaN4gAv7B3CVC/a68eBfeU0bMwRm2I";

const key = crypto.scryptSync("password", "salt", 16);

const encrypted = Buffer.from(base64Encrypted, "base64");
const encryptedWOSalt = Buffer.from(base64Encrypted, "base64").slice(10);
const iv = encrypted.slice(10, 10 + 17);

const result = decrypt(
  encryptedWOSalt.toString("base64"),
  key,
  iv.toString("base64")
);
console.log(result);

It's throwing an error: Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt

Thanks

Upvotes: 2

Views: 846

Answers (1)

Topaco
Topaco

Reputation: 49341

Since you only posted a NodeJS decryption code, I focus on decryption. The encryption is to be implemented analogously. If you have problems with this, please post a new question with the corresponding encryption code.

There are several bugs in the NodeJS code:

  • Wrong key derivation (in the NodeJS code scrypt is used, while in the Java code PBKDF2/HMAC-SHA1 is applied).
  • Incorrect/missing separation of salt, IV and ciphertext
  • Encoding bugs and unnecessary encoding/decoding cycles

The following NodeJS code works:

var crypto = require('crypto')

function getAlgorithm(key) {
   switch (key.length) {
    case 16:
      return "aes-128-cbc";
    case 32:
      return "aes-256-cbc";
  }
  throw new Error("Invalid key length: " + key.length);
}

function decrypt(message, key, iv) {
  const decipher = crypto.createDecipheriv(
    getAlgorithm(key),
    key,
    iv
  );
  let decrypted = Buffer.concat([decipher.update(message), decipher.final()]);
  return decrypted.toString("utf8");
}

const DYN_SALT_LENGTH = 10;
const IV_LENGTH = 16;
const ITERATION_COUNT = 65556;
const KEY_LENGTH = 256;

const base64Encrypted = "ossqoyCaaQINWUkTsHNGRe5Isd5s7c7U8KcLua78Ehm9jAxQNOd2tyjj";

// Separate salt, IV and ciphertext
const encrypted = Buffer.from(base64Encrypted, "base64");
const salt = encrypted.slice(0, DYN_SALT_LENGTH);
const iv = encrypted.slice(DYN_SALT_LENGTH, DYN_SALT_LENGTH + IV_LENGTH);
const ciphertext = encrypted.slice(DYN_SALT_LENGTH + IV_LENGTH);

// Derive key voa PBKDF2/HMAC-SHA1
const key = crypto.pbkdf2Sync("password", "salt", ITERATION_COUNT, KEY_LENGTH/8, "sha1");

// Decrypt
const result = decrypt(
  ciphertext,
  key,
  iv
);
console.log(result); // hello world

Note that the current Java code (and therefore also the NodeJS code) does not use the random salt for key derivation, but a static salt, possibly for testing purposes. In the final solution, the random salt is to be used for security reasons.

Upvotes: 3

Related Questions