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 ajwk
- 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>