Search code examples
phphashcryptographycryptojs

Derive key and IV from string for AES encryption in CryptoJS and PHP


How can I generate key and IV each from a common string, that they may serve in the encryption algorithm using the AES in PHP?

Example:

$key_string = derivate_in_valid_key("i love stackoverflow");
$iv_string = derivate__in_valid_iv("i love questions");

and in a way that I can repeat the derivation process in JavaScript also.


Solution

  • You can use PBKDF2 to derive a key and IV from a password. CryptoJS and PHP both provide implementations thereof.

    AES supports key sizes of 128, 192 and 256 bits, and a block size of 128-bit. The IV must be the same size as the block size for modes such as CBC.

    Single invocation of PBKDF2 to generate only the key

    The following code works also to generate the IV. Doing so, requires a second random salt that must be sent alongside the ciphertext.

    JavaScript (DEMO):

    var password = "test";
    
    var iterations = 500;
    var keySize = 256;
    var salt = CryptoJS.lib.WordArray.random(128/8);
    
    console.log(salt.toString(CryptoJS.enc.Base64));
    
    var output = CryptoJS.PBKDF2(password, salt, {
        keySize: keySize/32,
        iterations: iterations
    });
    
    console.log(output.toString(CryptoJS.enc.Base64));
    

    example output:

    CgxEDCi5z4ju1ycmKRh6aw==  
    7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=
    

    PHP:

    $password = "test";
    $expected = "7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=";
    
    $salt = 'CgxEDCi5z4ju1ycmKRh6aw==';
    $hasher = "sha1"; // CryptoJS uses SHA1 by default
    $iterations = 500;
    $outsize = 256;
    
    $out = hash_pbkdf2($hasher, $password, base64_decode($salt), $iterations, $outsize/8, true);
    
    echo "expected: ".$expected."\ngot:      ".base64_encode($out);
    

    output:

    expected: 7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=
    got:      7G3+NUWtbOooVeTDyLqMaDgnqCkiQCjZi3wnspRPabU=
    

    Single invocation of PBKDF2 to generate key and IV

    The previous section is a bit clunky, because one needs to generate two salts and do two invocations of PBKDF2. PBKDF2 supports a variable output, so it is possible to simply use one salt, request an output of the key size plus iv size and slice them off. The following code does that, so only one salt must be sent alongside of the ciphertext.

    JavaScript (DEMO):

    var password = "test";
    
    var iterations = 1000;
    // sizes must be a multiple of 32
    var keySize = 256;
    var ivSize = 128;
    var salt = CryptoJS.lib.WordArray.random(128/8);
    
    console.log(salt.toString(CryptoJS.enc.Base64));
    
    var output = CryptoJS.PBKDF2(password, salt, {
        keySize: (keySize+ivSize)/32,
        iterations: iterations
    });
    
    // the underlying words arrays might have more content than was asked: remove insignificant words
    output.clamp();
    
    // split key and IV
    var key = CryptoJS.lib.WordArray.create(output.words.slice(0, keySize/32));
    var iv = CryptoJS.lib.WordArray.create(output.words.slice(keySize/32));
    
    console.log(key.toString(CryptoJS.enc.Base64));
    console.log(iv.toString(CryptoJS.enc.Base64));
    

    example output:

    0Iulef2TncciKGmdwvQX3Q==
    QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=
    L1YNlFe54+Cvepp/pXsHtg==
    

    PHP:

    $password = "test";
    $expectedKey = "QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=";
    $expectedIV = "L1YNlFe54+Cvepp/pXsHtg==";
    
    $salt = '0Iulef2TncciKGmdwvQX3Q==';
    $hasher = "sha1";
    $iterations = 1000;
    $keysize = 256;
    $ivsize = 128;
    
    $out = hash_pbkdf2($hasher, $password, base64_decode($salt), $iterations, ($keysize+$ivsize)/8, true);
    
    // split key and IV
    $key = substr($out, 0, $keysize/8);
    $iv = substr($out, $keysize/8, $ivsize/8);
    
    // print for demonstration purposes
    echo "expected key: ".$expectedKey."\ngot:          ".base64_encode($key);
    echo "\nexpected iv: ".$expectedIV."\ngot:         ".base64_encode($iv);
    

    output:

    expected key: QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=
    got:          QeTc3zHuG3JcdtOCkzU2uJWTnrMEggvF1dNUbgNMyzg=
    expected iv: L1YNlFe54+Cvepp/pXsHtg==
    got:         L1YNlFe54+Cvepp/pXsHtg==
    

    CryptoJS uses SHA1 by default, so you could use a different hash function by passing the hash function as the hasher object property. You should also probably use a higher iteration count.

    I don't have a PHP 5.5+ version available so I used the PBKDF2 implementation from here.