Webcrypto keys derived from PBKDF2

Asked

Viewed 191 times

4

I’m using PBKDF2 in Webcryptoapi to generate a "derivable" key based on a user input (a password) and derive a key from it AES-GCM.

I’m doing a round of tests where:

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

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

In my test I am using a string with 4 zeros ("0000") as a 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 be two separate keys?

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>

1 answer

2


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

Not, all algorithms involved in cryptographic key generation(PBKDF2, SHA-2) are deterministic algorithms. If that’s not the answer you were hoping for, keep reading.

I’ve analyzed your code, and to summarize, the general picture is:

  1. Create symmetrical cryptographic key
  2. Encrypt content using AES algorithm with derived key
  3. Export and Import key in JSON Web Key format (JWK)
  4. Decrypt content

It seems the biggest problem is understanding how to use PBKDF2. The creation of the key occurred this way:

  1. To password is the naked key (raw key):
const WebCryptoGenerateKey = (password) => {
    return crypto.subtle.importKey(
        'raw',
        UTILS.convertStringToArrayBuffer(password),
        {
            name: 'PBKDF2'
        },
        false, // PBKDF2 don't exportable
        [ 'deriveKey', 'deriveBits' ]
    )
}
  1. From this key a derived key is created using the algorithm PBKDF2, using the cryptographic hash function SHA-256(SHA-2 256bits), with one hundred thousand iterations(iterations), using as salt(salt) to 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' ]
    )
}
  1. In this same step, it is specified that the derived key must be formatted in 256bit size, for use in AES in operation mode GCM:
        {
            name: 'AES-GCM',
            length: 256
        },

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

Going back 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 account, that once changed the value of the salt, or of iterations, the resulting key will be totally different. So in this case, the answer is yes.

  • Thanks for your reply, I got an idea of how it works better than the Mozilla documentation explains. Could you tell me what shape this is WKS? I saw in the documentation only: raw, jwk, spki and pkcs8. And without wanting to seem abused, what length would be suitable for salt ... vi in several places sizes with 8, 12 or 16 more, found no reference on this length in relation to the algorithm and its "length" (there is this relation)?

  • 1

    The document RFC 8018 recommends 64bits, but the document NIST SP 800-132 recommends 128bits. In this case, I get the largest, a 128bit salt. For AES-GCM IV the indicated is 96bits.

  • WKS was a typo, the correct is JWK.

  • To iv I had already seen the NIST 800-38d in section 5.2.1.1 already recommended 96bits for iv ... I thank you once again for the reply and the guidelines.

  • That’s the document. My first guess would be to say that the ideal size of IV for a block Cipher is always the size of the block. But how is the mode of operation that decides what to do with the IV, he’s the one who makes the rules.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.