Gamer2015
Gamer2015

Reputation: 289

WebCryptoApi: Cannot wrap&unwrap aes-gcm key into&from "jwk" format with "encrypt" and "decrypt" active

I'm generating a key for encrypting data, then wrap it using a master key and store it alongside the encrypted data. All is well when wrapping into raw format, but when wrapping as jwk I get the error DOMException: Data provided to an operation does not meet requirements.

It works when specifying they key for being used either for encryption or for decryption, but not when both are specified as key usages.

let wrapAlgo = {
    name: "AES-KW",
    length: 256
};
let encAlgo = {
    name:"AES-GCM",
    length:256
}
let format = "jwk";
let extractable=true;
let keyUsages = ["encrypt", "decrypt"];

let kek = await crypto.subtle.generateKey(
    wrapAlgo, 
    false, 
    ["wrapKey", "unwrapKey"]
);

let key = await window.crypto.subtle.generateKey(
    encAlgo,
    extractable, // the key is extractable (i.e. can be used in exportKey)
    keyUsages
);
console.log("key", key);

let wrappedKey = await crypto.subtle.wrapKey(
    format,
    key,
    kek,
    wrapAlgo
);
console.log("wrappedKey", wrappedKey);

let unwrappedKey = await crypto.subtle.unwrapKey(
    format,
    wrappedKey,
    kek,
    wrapAlgo,
    encAlgo,
    extractable,
    keyUsages
);
console.log("key", await crypto.subtle.exportKey("jwk", unwrappedKey));

Upvotes: 3

Views: 797

Answers (1)

Topaco
Topaco

Reputation: 49470

AES-KW is a key wrap algorithm described in RFC3394. The algorithm is used to wrap i. e. encrypt a key. The input, i.e. the key to be encrypted, must be an integer multiple of 8 bytes, s. also here.

The key to be encrypted is passed in SubtleCrypto.wrapKey() in the 2nd parameter key as CryptoKey and must therefore be exported before the actual encryption. For this purpose the format in which the key is exported is specified in the 1st parameter format:

const result = crypto.subtle.wrapKey(format, key, wrappingKey, wrapAlgo);

In the posted example, the key to be wrapped is a 32 bytes key for AES-256. In raw format, the key thus satisfies the AES-KW length criterion. In jwk format, however, the length criterion is generally not met:
If the key exported in jwk format is serialized, it has a length for the key usage ["encrypt"] or ["decrypt"] that happens to be an integer multiple of 8 bytes (112 bytes), while this is not the case for the key usages ["encrypt", "decrypt"] (122 bytes):

(async () => {
    async function getLength(keyUsages) {  
        var key = await window.crypto.subtle.generateKey(
            {name:"AES-GCM", length: 256},
            true, 
            keyUsages
        );
        var expkey = await crypto.subtle.exportKey("jwk", key)
        var expkeySerLen = JSON.stringify(expkey).length;
        return {KeyUsages: keyUsages, length: expkeySerLen, lenMod8: expkeySerLen % 8}; 
    }
    console.log(await getLength(["encrypt"]));            // works
    console.log(await getLength(["decrypt"]));            // works
    console.log(await getLength(["encrypt", "decrypt"])); // doesn't work
})();

This is most likely the reason why the code with the key usage ["encrypt"] or ["decrypt"] is executed, but not the code for the key usages ["encrypt", "decrypt"].

The bottom line is that AES-KW works reliably for the raw format, but not for the jwk format.

However, the jwk format can be used in SubtleCrypto.wrapKey() for other wrapping algorithms, such as AES-GCM:

(async () => {

    let encAlgo = {
        name:"AES-GCM",
        length:256
    };
    let wrapAlgo = {
        name:"AES-GCM",
        length:256
    };
    let aesGcmParams = {
        name:"AES-GCM",
        iv: window.crypto.getRandomValues(new Uint8Array(12))
    };
    let format = "jwk";
    let extractable=true;
    let keyUsages = ["encrypt", "decrypt"];

    let kek = await crypto.subtle.generateKey(
        wrapAlgo, 
        false, 
        ["wrapKey", "unwrapKey"]
    );

    let key = await window.crypto.subtle.generateKey(
        encAlgo,
        extractable, // the key is extractable (i.e. can be used in exportKey)
        keyUsages
    );
    console.log("key (CryptoKey)", key);
    console.log("key (jwk)", await crypto.subtle.exportKey("jwk", key));

    let wrappedKey = await crypto.subtle.wrapKey(
        format,
        key,
        kek,
        aesGcmParams
    );
    console.log("wrappedKey (ArrayBuffer)", wrappedKey);

    let unwrappedKey = await crypto.subtle.unwrapKey(
        format,
        wrappedKey,
        kek,
        aesGcmParams,
        encAlgo,
        extractable,
        keyUsages
    );
    console.log("unwrappedKey (jwk) ", await crypto.subtle.exportKey("jwk", unwrappedKey));

})();

Upvotes: 2

Related Questions