WebCrypto keys derived from PBKDF2

4

I'm using PBKDF2 in WebCryptoAPI to generate a "derivable" key based on a (a password) and deriving from it a AES-GCM key.

I'm doing a round of testing where:

  • In the first round I generate the keys (PBKDF2 and AES-GCM), except this CryptoKey object (AES-GCM) in a variable in the upper scope, it generates an , string "Hello World!", I export the AES-GCM key to a jwk
  • in the second round decrypt using iv and the AES-GCM key
  • In the third I import the AES-GCM key of the format jwk use to iv and described
  • In the fourth round I generate a new key (PBKDF2 and AES-GCM), I use this new AES-GCM key with iv to describe

Nothing unusual between the first and third bad round (the devil resides in the "bad"), the fourth round where a new key is generated, is effectively decrypting the string ...

In my test I am using a string with 4 zeros ("0000") as the password for these keys (for PBKDF2 and AES-GCM) but I wonder if:

  • Even if a key is generated using the same password used in another key, should not two keys be different?

The snippet below expresses this question:

let cry = document.getElementById('cry')
let dec1 = document.getElementById('dec-1')
let dec2 = document.getElementById('dec-2')
let dec3 = document.getElementById('dec-3')
let logger = document.getElementById('logger')

const UTILS = {
    convertStringToArrayBuffer(str) {
        let encoder = new TextEncoder('utf-8')
        return encoder.encode(str)
    },
    convertArrayBuffertoString(buffer) {
        let decoder = new TextDecoder('utf-8')
        return decoder.decode(buffer)
    },
    bufferToHex(arr) {
        let i,
            len,
            hex = '',
            c
        for (i = 0, len = arr.length; i < len; i += 1) {
             c = arr[i].toString(16)
             if ( c.length < 2 ) {
                 c = '0' + c
             }
             hex += c
        }
        return hex
    },
    hexToBuffer(hex) {
      let i,
          byteLen = hex.length / 2,
          arr,
          j = 0
      if ( byteLen !== parseInt(byteLen, 10) ) {
          throw new Error("Invalid hex length '" + hex.length + "'")
      }
      arr = new Uint8Array(byteLen)
      for (i = 0; i < byteLen; i += 1) {
           arr[i] = parseInt(hex[j] + hex[j + 1], 16)
           j += 2
      }
      return arr
    }
}

const WebCryptoGenerateKey = (password) => {
    return crypto.subtle.importKey(
        'raw',
        UTILS.convertStringToArrayBuffer(password),
        {
            name: 'PBKDF2'
        },
        false, // PBKDF2 don't exportable
        [ 'deriveKey', 'deriveBits' ]
    )
}

const AES_GCM = (CryptoKey, opts) => {
    return crypto.subtle.deriveKey(
        {
            name: 'PBKDF2',
            salt: new Uint8Array(UTILS.convertStringToArrayBuffer(opts.password)),
            iterations: 100000, // mobile = 100 000, desktop.32bit = 1 000 000, desktop.64bit = 10 000 000
            hash: 'SHA-256'
        },
        CryptoKey,
        {
            name: 'AES-GCM',
            length: 256
        },
        opts.export, // Extractable is set to false so that underlying key details cannot be accessed.
        [ 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey' ]
    )
}

const AES_GCM_ENCRYPT = (data, key, iv) => {
    return crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            // Don't re-use initialization vectors!
            // Always generate a new iv every time your encrypt!
            // Recommended to use 12 bytes length
            iv: iv,
            // Additional authentication data (optional)
            //additionalData: ArrayBuffer,
            // Tag length (optional)
            length: 256, //can be 32, 64, 96, 104, 112, 120 or 128 (default)
        },
        key, // from generateKey or importKey above
        data // ArrayBuffer of data you want to encrypt
    )
}

const AES_GCM_DECRYPT = (enc, key, iv) => {
    return crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: iv, // The initialization vector you used to encrypt
            //additionalData: ArrayBuffer, //The addtionalData you used to encrypt (if any)
            length: 256, //The tagLength you used to encrypt (if any)
        },
        key, //from generateKey or importKey above
        enc //ArrayBuffer of the data
    )
}

const AES_GCM_IMPORT = (jwk) => {
    return crypto.subtle.importKey(
        "jwk", //can be "jwk" or "raw"
        jwk,
        {   //this is the algorithm options
            name: "AES-GCM",
        },
        true, //whether the key is extractable (i.e. can be used in exportKey)
        ["encrypt", "decrypt"] //can "encrypt", "decrypt", "wrapKey", or "unwrapKey"
    )
}

const AES_GCM_EXPORT = (key) => {
    return crypto.subtle.exportKey(
        "jwk", //can be "jwk" or "raw"
        key //extractable must be true
    )
}

let AES_GCM_KEY,
    EXPORTED_AES_GCM_JWK,
    IMPORTED_AES_GCM_JWK,
    ENCRYPTED


let vector = crypto.getRandomValues(new Uint8Array(16))

cry.addEventListener('click', function() {
    WebCryptoGenerateKey('0000').then(CryptoKey => {
        AES_GCM(CryptoKey, {
            password: '0000',
            export: true
        }).then(aes_gcm => {
            AES_GCM_KEY = aes_gcm
            AES_GCM_ENCRYPT(UTILS.convertStringToArrayBuffer('Hello World!'), aes_gcm, vector).then(encData => {
                ENCRYPTED = UTILS.bufferToHex(new Uint8Array(encData))
                logger.innerHTML += '<br>' + ENCRYPTED
            })
            AES_GCM_EXPORT(aes_gcm).then(jwk => {
                EXPORTED_AES_GCM_JWK = jwk
            })
        })
    })
}, false)

dec1.addEventListener('click', function() {
    AES_GCM_DECRYPT(UTILS.hexToBuffer(ENCRYPTED), AES_GCM_KEY, vector).then(result => {
        logger.innerHTML += '<br>' + UTILS.convertArrayBuffertoString(result)
    })
}, false)

dec2.addEventListener('click', function() {
    AES_GCM_IMPORT(EXPORTED_AES_GCM_JWK).then(aes_gcm => {
        AES_GCM_DECRYPT(UTILS.hexToBuffer(ENCRYPTED), aes_gcm, vector).then(result => {
            logger.innerHTML += '<br>' + UTILS.convertArrayBuffertoString(result)
        })
    })
}, false)


dec3.addEventListener('click', function() {
    WebCryptoGenerateKey('0000').then(CryptoKey => {
        AES_GCM(CryptoKey, {
            password: '0000',
            export: true
        }).then(aes_gcm2 => {
            AES_GCM_DECRYPT(UTILS.hexToBuffer(ENCRYPTED), aes_gcm2, vector).then(result => {
                logger.innerHTML += '<br>' + UTILS.convertArrayBuffertoString(result)
            })
        })
    })
}, false)
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"/>

<button id="cry" type="button" class="btn btn-secondary">crypt</button>
               <button id="dec-1" type="button" class="btn btn-secondary">decrypt 1</button>
               <button id="dec-2" type="button" class="btn btn-secondary">decrypt 2</button>
               <button id="dec-3" type="button" class="btn btn-secondary">decrypt 3</button>
               
<div id="logger" class="col-12 mx-auto mt-3"></div>

<script
  src="https://code.jquery.com/jquery-3.3.1.min.js"integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
  crossorigin="anonymous"></script>
  
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.bundle.min.js"></script>
    
asked by anonymous 17.08.2018 / 07:41

1 answer

2
  

even if a key is generated using the same password used in   another key should not be two different keys?

No , all algorithms involved in cryptographic key generation (PBKDF2, SHA-2) are deterministic algorithms . If that was not the answer you expected, read on.

I have analyzed your code, and in summary, the general framework is:

  • Create symmetric cryptographic key
  • Encrypt content using the AES algorithm with the derived key
  • Export and Import key in JSON Web Key (JWK) format
  • Decrypt content
  • It seems the biggest problem is understanding how to use PBKDF2 . The key creation occurred this way:

    The password is the raw key ):
    const WebCryptoGenerateKey = (password) => {
        return crypto.subtle.importKey(
            'raw',
            UTILS.convertStringToArrayBuffer(password),
            {
                name: 'PBKDF2'
            },
            false, // PBKDF2 don't exportable
            [ 'deriveKey', 'deriveBits' ]
        )
    }
    
  • This key creates a derived key using the algorithm PBKDF2 , using the cryptographic hash function SHA-256 (SHA-2 256bits), with one hundred thousand iterations , using as password :
  • const AES_GCM = (CryptoKey, opts) => {
        return crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                salt: new Uint8Array(UTILS.convertStringToArrayBuffer(opts.password)),
                iterations: 100000, // mobile = 100 000, desktop.32bit = 1 000 000, desktop.64bit = 10 000 000
                hash: 'SHA-256'
            },
            CryptoKey,
            {
                name: 'AES-GCM',
                length: 256
            },
            opts.export, // Extractable is set to false so that underlying key details cannot be accessed.
            [ 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey' ]
        )
    }
    
  • In this same step, it is specified that the derived key must be formatted in the size of 256bits, for use in AES in mode of operation GCM :
  •         {
                name: 'AES-GCM',
                length: 256
            },
    

    The password entered by the user is a bare key, once it falls into the wrong hands, the secret of the safe is discovered. When this key is derived, an armor is added to it, making it difficult to use it in the safe. PBKDF2 places several layers of steel in this armature, iterations is the property that defines the number of layers. The salt in PBKDF2 represents the weak point of your defense, which can rust your armor. If every key has the same weak point, once the enemy discovers the fault, it will uncover all the keys. So it's important to have a different salt for each key.

    Returning to your code, the ideal would be to change the salt to something like:

    salt: crypto.getRandomValues(new Uint8Array(8)),
    

    As for your doubt, from the same password generate the same key. Take into consideration that once the salt value, or iterations value is changed, the resulting key will be totally different . So in this case, the answer is yes .

        
    25.08.2018 / 05:37