What are the AES parameters used and steps performed internally by crypto-js while encrypting a message with a password?

后端 未结 3 1812
感情败类
感情败类 2020-11-28 12:20

Background: The application that I am working on is supposed to work offline. I should encrypt some text data using a password as a key at the java server

相关标签:
3条回答
  • 2020-11-28 12:50

    CryptoJS uses the non-standardized OpenSSL KDF for key derivation (EvpKDF) with MD5 as the hashing algorithm and 1 iteration. The IV is also derived from the password which means that only the actual ciphertext, the password and the salt are needed to decrypt this on Java side.

    In other words, PBKDF2 is not used for key derivation in password mode of CryptoJS. By default AES-256 is used in CBC mode with PKCS5 padding (which is the same as PKCS7 padding). Keep in mind that you might need the JCE Unlimited Strength Jurisdiction Policy Files. See also Why there are limitations on using encryption with keys beyond certain length?

    The following code recreates the KDF in Java (keySize and ivSize are 8 respectively 4 for AES-256 and come from ).

    public static byte[] evpKDF(byte[] password, int keySize, int ivSize, byte[] salt, int iterations, String hashAlgorithm, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        int targetKeySize = keySize + ivSize;
        byte[] derivedBytes = new byte[targetKeySize * 4];
        int numberOfDerivedWords = 0;
        byte[] block = null;
        MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
        while (numberOfDerivedWords < targetKeySize) {
            if (block != null) {
                hasher.update(block);
            }
            hasher.update(password);
            block = hasher.digest(salt);
            hasher.reset();
    
            // Iterations
            for (int i = 1; i < iterations; i++) {
                block = hasher.digest(block);
                hasher.reset();
            }
    
            System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4,
                    Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));
    
            numberOfDerivedWords += block.length/4;
        }
    
        System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
        System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);
    
        return derivedBytes; // key + iv
    }
    

    Here is the complete class for reference:

    public class RecreateEVPkdfFromCryptoJS {
        public static void main(String[] args) throws UnsupportedEncodingException, GeneralSecurityException {
            String msg = "hello";
            String password = "mypassword";
            String ivHex = "aab7d6aca0cc6ffc18f9f5909753aa5f";
            int keySize = 8; // 8 words = 256-bit
            int ivSize = 4; // 4 words = 128-bit
            String keyHex = "844a86d27d96acf3147aa460f535e20e989d1f8b5d79c0403b4a0f34cebb093b";
            String saltHex = "ca35168ed6b82778";
            String openSslFormattedCipherTextString = "U2FsdGVkX1/KNRaO1rgneK9S3zuYaYZcdXmVKJGqVqk=";
            String cipherTextHex = "af52df3b9869865c7579952891aa56a9";
            String padding = "PKCS5Padding";
    
            byte[] key = hexStringToByteArray(keyHex);
            byte[] iv = hexStringToByteArray(ivHex);
            byte[] salt = hexStringToByteArray(saltHex);
            byte[] cipherText = hexStringToByteArray(cipherTextHex);
    
            byte[] javaKey = new byte[keySize * 4];
            byte[] javaIv = new byte[ivSize * 4];
            evpKDF(password.getBytes("UTF-8"), keySize, ivSize, salt, javaKey, javaIv);
            System.out.println(Arrays.equals(key, javaKey) + " " + Arrays.equals(iv, javaIv));
    
            Cipher aesCipherForEncryption = Cipher.getInstance("AES/CBC/PKCS5Padding"); // Must specify the mode explicitly as most JCE providers default to ECB mode!!
    
            IvParameterSpec ivSpec = new IvParameterSpec(javaIv);
            aesCipherForEncryption.init(Cipher.DECRYPT_MODE, new SecretKeySpec(javaKey, "AES"), ivSpec);
    
            byte[] byteMsg = aesCipherForEncryption.doFinal(cipherText);
            System.out.println(Arrays.equals(byteMsg, msg.getBytes("UTF-8")));
        }
    
        public static byte[] evpKDF(byte[] password, int keySize, int ivSize, byte[] salt, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
            return evpKDF(password, keySize, ivSize, salt, 1, "MD5", resultKey, resultIv);
        }
    
        public static byte[] evpKDF(byte[] password, int keySize, int ivSize, byte[] salt, int iterations, String hashAlgorithm, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
            int targetKeySize = keySize + ivSize;
            byte[] derivedBytes = new byte[targetKeySize * 4];
            int numberOfDerivedWords = 0;
            byte[] block = null;
            MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
            while (numberOfDerivedWords < targetKeySize) {
                if (block != null) {
                    hasher.update(block);
                }
                hasher.update(password);
                block = hasher.digest(salt);
                hasher.reset();
    
                // Iterations
                for (int i = 1; i < iterations; i++) {
                    block = hasher.digest(block);
                    hasher.reset();
                }
    
                System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4,
                        Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));
    
                numberOfDerivedWords += block.length/4;
            }
    
            System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
            System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);
    
            return derivedBytes; // key + iv
        }
    
        /**
         * Copied from http://stackoverflow.com/a/140861
         * */
        public static byte[] hexStringToByteArray(String s) {
            int len = s.length();
            byte[] data = new byte[len / 2];
            for (int i = 0; i < len; i += 2) {
                data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                        + Character.digit(s.charAt(i+1), 16));
            }
            return data;
        }
    }
    

    and the JavaScript code which was used for the generation of the values in the Java code:

    var msg = "hello";
    var password = "mypassword"; // must be present on the server
    var encrypted = CryptoJS.AES.encrypt( msg, password );
    var ivHex = encrypted.iv.toString();
    var ivSize = encrypted.algorithm.ivSize; // same as the blockSize
    var keySize = encrypted.algorithm.keySize;
    var keyHex = encrypted.key.toString();
    var saltHex = encrypted.salt.toString(); // must be sent as well
    var openSslFormattedCipherTextString = encrypted.toString(); // not used
    var cipherTextHex = encrypted.ciphertext.toString(); // must be sent
    
    0 讨论(0)
  • 2020-11-28 13:12

    Following @Artjom B's great answer both on this question and here for python users, I am joining the full java code that helped me decrypt a string that was encrypted this way

    var encrypted = CryptoJS.AES.encrypt(message, password).toString();
    

    This piece of Java code is useful when you only know the password (i.e. salt was not sent with the encrypted string):

    public String decrypt(String encrypted, String password) throws Exception {
        int keySize = 8;
        int ivSize = 4;
        // Start by decoding the encrypted string (Base64)
        // Here I used the Android implementation (other Java implementations might exist)
        byte[] cipherText = Base64.decode(encrypted, Base64.DEFAULT);
        // prefix (first 8 bytes) is not actually useful for decryption, but you should probably check that it is equal to the string "Salted__"
        byte[] prefix = new byte[8];
        System.arraycopy(cipherText, 0, prefix, 0, 8);
        // Check here that prefix is equal to "Salted__"
        // Extract salt (next 8 bytes)
        byte[] salt = new byte[8];
        System.arraycopy(cipherText, 8, salt, 0, 8);
        // Extract the actual cipher text (the rest of the bytes)
        byte[] trueCipherText = new byte[cipherText.length - 16];
        System.arraycopy(cipherText, 16, trueCipherText, 0, cipherText.length - 16);
        byte[] javaKey = new byte[keySize * 4];
        byte[] javaIv = new byte[ivSize * 4];
        evpKDF(password.getBytes("UTF-8"), keySize, ivSize, salt, javaKey, javaIv);
        Cipher aesCipherForEncryption = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec ivSpec = new IvParameterSpec(javaIv);
        aesCipherForEncryption.init(Cipher.DECRYPT_MODE, new SecretKeySpec(javaKey, "AES"), ivSpec);
    
        byte[] byteMsg = aesCipherForEncryption.doFinal(trueCipherText);
        return new String(byteMsg, "UTF-8");
    }
    
    public  byte[] evpKDF(byte[] password, int keySize, int ivSize, byte[] salt, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        return evpKDF(password, keySize, ivSize, salt, 1, "MD5", resultKey, resultIv);
    }
    
    public  byte[] evpKDF(byte[] password, int keySize, int ivSize, byte[] salt, int iterations, String hashAlgorithm, byte[] resultKey, byte[] resultIv) throws NoSuchAlgorithmException {
        int targetKeySize = keySize + ivSize;
        byte[] derivedBytes = new byte[targetKeySize * 4];
        int numberOfDerivedWords = 0;
        byte[] block = null;
        MessageDigest hasher = MessageDigest.getInstance(hashAlgorithm);
        while (numberOfDerivedWords < targetKeySize) {
            if (block != null) {
                hasher.update(block);
            }
            hasher.update(password);
            block = hasher.digest(salt);
            hasher.reset();
    
            // Iterations
            for (int i = 1; i < iterations; i++) {
                block = hasher.digest(block);
                hasher.reset();
            }
    
            System.arraycopy(block, 0, derivedBytes, numberOfDerivedWords * 4,
                    Math.min(block.length, (targetKeySize - numberOfDerivedWords) * 4));
    
            numberOfDerivedWords += block.length/4;
        }
    
        System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4);
        System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4);
    
        return derivedBytes; // key + iv
    }
    
    0 讨论(0)
  • 2020-11-28 13:13

    I'm looking at the documentation here:

    • key size: "If you use a passphrase, then it will generate a 256-bit key."
    • padding: Pkcs7 (the default)
    • mode: CBC (the default)
    • iv: generated and stored in the cipher text object: use with "encrypted.iv"

    Stuff for generating the key:

    • salt: generated and stored in the cipher text object: use with "encrypted.salt" (although it doesn't really say that, so I'm guessing here)
    • pbe algorithm: Unclear. It's not documented.
    • iteration count: I can't find this documented anywhere. The examples in the code seem to use 1000.

    You can set the parameters by hand, which is maybe safer than relying on the defaults, e.g. some pseudo-code based on the examples in the documentation:

    var salt = CryptoJS.lib.WordArray.random(128/8);
    var iv = CryptoJS.lib.WordArray.random(128);
    var key256Bits10000Iterations = CryptoJS.PBKDF2("Secret Passphrase", salt, { keySize: 256/32, iterations: 10000 }); //I don't know this is dividing by 32
    var encrypted = CryptoJS.AES.encrypt("Message", key, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv:iv });
    

    You will probably have to experiment. I'd take it one step at a time. Get the password-based keys to match by fiddling those parameters, then get the ciphertext to match, then figure out decryption. Avoid the urge to simplify things like skipping the IV or using ECB.

    0 讨论(0)
提交回复
热议问题