• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// META: title=WebCryptoAPI: wrapKey() and unwrapKey()
2// META: timeout=long
3// META: script=../util/helpers.js
4
5// Tests for wrapKey and unwrapKey round tripping
6
7    var subtle = self.crypto.subtle;
8
9    var wrappers = [];  // Things we wrap (and upwrap) keys with
10    var keys = [];      // Things to wrap and unwrap
11
12    // Generate all the keys needed, then iterate over all combinations
13    // to test wrapping and unwrapping.
14    promise_test(function() {
15    return Promise.all([generateWrappingKeys(), generateKeysToWrap()])
16    .then(function(results) {
17        var promises = [];
18        wrappers.forEach(function(wrapper) {
19            keys.forEach(function(key) {
20                promises.push(testWrapping(wrapper, key));
21            })
22        });
23        return Promise.allSettled(promises);
24    });
25    }, "setup");
26
27    function generateWrappingKeys() {
28        // There are five algorithms that can be used for wrapKey/unwrapKey.
29        // Generate one key with typical parameters for each kind.
30        //
31        // Note: we don't need cryptographically strong parameters for things
32        // like IV - just any legal value will do.
33        var parameters = [
34            {
35                name: "RSA-OAEP",
36                generateParameters: {name: "RSA-OAEP", modulusLength: 4096, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"},
37                wrapParameters: {name: "RSA-OAEP", label: new Uint8Array(8)}
38            },
39            {
40                name: "AES-CTR",
41                generateParameters: {name: "AES-CTR", length: 128},
42                wrapParameters: {name: "AES-CTR", counter: new Uint8Array(16), length: 64}
43            },
44            {
45                name: "AES-CBC",
46                generateParameters: {name: "AES-CBC", length: 128},
47                wrapParameters: {name: "AES-CBC", iv: new Uint8Array(16)}
48            },
49            {
50                name: "AES-GCM",
51                generateParameters: {name: "AES-GCM", length: 128},
52                wrapParameters: {name: "AES-GCM", iv: new Uint8Array(16), additionalData: new Uint8Array(16), tagLength: 128}
53            },
54            {
55                name: "AES-KW",
56                generateParameters: {name: "AES-KW", length: 128},
57                wrapParameters: {name: "AES-KW"}
58            }
59        ];
60
61        // Using allSettled to skip unsupported test cases.
62        return Promise.allSettled(parameters.map(function(params) {
63            return subtle.generateKey(params.generateParameters, true, ["wrapKey", "unwrapKey"])
64            .then(function(key) {
65                var wrapper;
66                if (params.name === "RSA-OAEP") { // we have a key pair, not just a key
67                    wrapper = {wrappingKey: key.publicKey, unwrappingKey: key.privateKey, parameters: params};
68                } else {
69                    wrapper = {wrappingKey: key, unwrappingKey: key, parameters: params};
70                }
71                wrappers.push(wrapper);
72                return true;
73            })
74        }));
75    }
76
77
78    function generateKeysToWrap() {
79        var parameters = [
80            {algorithm: {name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"}, privateUsages: ["sign"], publicUsages: ["verify"]},
81            {algorithm: {name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"}, privateUsages: ["sign"], publicUsages: ["verify"]},
82            {algorithm: {name: "RSA-OAEP", modulusLength: 1024, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"}, privateUsages: ["decrypt"], publicUsages: ["encrypt"]},
83            {algorithm: {name: "ECDSA", namedCurve: "P-256"}, privateUsages: ["sign"], publicUsages: ["verify"]},
84            {algorithm: {name: "ECDH", namedCurve: "P-256"}, privateUsages: ["deriveBits"], publicUsages: []},
85            {algorithm: {name: "Ed25519" }, privateUsages: ["sign"], publicUsages: ["verify"]},
86            {algorithm: {name: "Ed448" }, privateUsages: ["sign"], publicUsages: ["verify"]},
87            {algorithm: {name: "X25519" }, privateUsages: ["deriveBits"], publicUsages: []},
88            {algorithm: {name: "X448" }, privateUsages: ["deriveBits"], publicUsages: []},
89            {algorithm: {name: "AES-CTR", length: 128}, usages: ["encrypt", "decrypt"]},
90            {algorithm: {name: "AES-CBC", length: 128}, usages: ["encrypt", "decrypt"]},
91            {algorithm: {name: "AES-GCM", length: 128}, usages: ["encrypt", "decrypt"]},
92            {algorithm: {name: "AES-KW", length: 128}, usages: ["wrapKey", "unwrapKey"]},
93            {algorithm: {name: "HMAC", length: 128, hash: "SHA-256"}, usages: ["sign", "verify"]}
94        ];
95
96        // Using allSettled to skip unsupported test cases.
97        return Promise.allSettled(parameters.map(function(params) {
98            var usages;
99            if ("usages" in params) {
100                usages = params.usages;
101            } else {
102                usages = params.publicUsages.concat(params.privateUsages);
103            }
104
105            return subtle.generateKey(params.algorithm, true, usages)
106            .then(function(result) {
107                if (result.constructor === CryptoKey) {
108                    keys.push({name: params.algorithm.name, algorithm: params.algorithm, usages: params.usages, key: result});
109                } else {
110                    keys.push({name: params.algorithm.name + " public key", algorithm: params.algorithm, usages: params.publicUsages, key: result.publicKey});
111                    keys.push({name: params.algorithm.name + " private key", algorithm: params.algorithm, usages: params.privateUsages, key: result.privateKey});
112                }
113                return true;
114            });
115        }));
116    }
117
118    // Can we successfully "round-trip" (wrap, then unwrap, a key)?
119    function testWrapping(wrapper, toWrap) {
120        var formats;
121
122        if (toWrap.name.includes("private")) {
123            formats = ["pkcs8", "jwk"];
124        } else if (toWrap.name.includes("public")) {
125            formats = ["spki", "jwk"]
126        } else {
127            formats = ["raw", "jwk"]
128        }
129
130        return Promise.all(formats.map(function(fmt) {
131            var originalExport;
132            return subtle.exportKey(fmt, toWrap.key).then(function(exportedKey) {
133                originalExport = exportedKey;
134                const isPossible = wrappingIsPossible(originalExport, wrapper.parameters.name);
135                promise_test(function(test) {
136                    if (!isPossible) {
137                        return Promise.resolve().then(() => {
138                            assert_false(false, "Wrapping is not possible");
139                        })
140                    }
141                    return subtle.wrapKey(fmt, toWrap.key, wrapper.wrappingKey, wrapper.parameters.wrapParameters)
142                    .then(function(wrappedResult) {
143                        return subtle.unwrapKey(fmt, wrappedResult, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, true, toWrap.usages);
144                    }).then(function(unwrappedResult) {
145                        assert_goodCryptoKey(unwrappedResult, toWrap.algorithm, true, toWrap.usages, toWrap.key.type);
146                        return subtle.exportKey(fmt, unwrappedResult)
147                    }).then(function(roundTripExport) {
148                        assert_true(equalExport(originalExport, roundTripExport), "Post-wrap export matches original export");
149                    }, function(err) {
150                        assert_unreached("Round trip for extractable key threw an error - " + err.name + ': "' + err.message + '"');
151                    });
152                }, "Can wrap and unwrap " + toWrap.name + " keys using " + fmt + " and " + wrapper.parameters.name);
153
154                if (canCompareNonExtractableKeys(toWrap.key)) {
155                    promise_test(function(test){
156                        if (!isPossible) {
157                            return Promise.resolve().then(() => {
158                                assert_false(false, "Wrapping is not possible");
159                            })
160                        }
161                        return subtle.wrapKey(fmt, toWrap.key, wrapper.wrappingKey, wrapper.parameters.wrapParameters)
162                        .then(function(wrappedResult) {
163                            return subtle.unwrapKey(fmt, wrappedResult, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, false, toWrap.usages);
164                        }).then(function(unwrappedResult){
165                            assert_goodCryptoKey(unwrappedResult, toWrap.algorithm, false, toWrap.usages, toWrap.key.type);
166                            return equalKeys(toWrap.key, unwrappedResult);
167                        }).then(function(result){
168                            assert_true(result, "Unwrapped key matches original");
169                        }).catch(function(err){
170                            assert_unreached("Round trip for key unwrapped non-extractable threw an error - " + err.name + ': "' + err.message + '"');
171                        });
172                    }, "Can wrap and unwrap " + toWrap.name + " keys as non-extractable using " + fmt + " and " + wrapper.parameters.name);
173
174                    if (fmt === "jwk") {
175                        promise_test(function(test){
176                            if (!isPossible) {
177                                return Promise.resolve().then(() => {
178                                    assert_false(false, "Wrapping is not possible");
179                                })
180                            }
181                            var wrappedKey;
182                            return wrapAsNonExtractableJwk(toWrap.key,wrapper).then(function(wrappedResult){
183                                wrappedKey = wrappedResult;
184                                return subtle.unwrapKey("jwk", wrappedKey, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, false, toWrap.usages);
185                            }).then(function(unwrappedResult){
186                                assert_false(unwrappedResult.extractable, "Unwrapped key is non-extractable");
187                                return equalKeys(toWrap.key,unwrappedResult);
188                            }).then(function(result){
189                                assert_true(result, "Unwrapped key matches original");
190                            }).catch(function(err){
191                                assert_unreached("Round trip for non-extractable key threw an error - " + err.name + ': "' + err.message + '"');
192                            }).then(function(){
193                                return subtle.unwrapKey("jwk", wrappedKey, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, true, toWrap.usages);
194                            }).then(function(unwrappedResult){
195                                assert_unreached("Unwrapping a non-extractable JWK as extractable should fail");
196                            }).catch(function(err){
197                                assert_equals(err.name, "DataError", "Unwrapping a non-extractable JWK as extractable fails with DataError");
198                            });
199                        }, "Can unwrap " + toWrap.name + " non-extractable keys using jwk and " + wrapper.parameters.name);
200                    }
201                }
202            });
203        }));
204    }
205
206    // Implement key wrapping by hand to wrap a key as non-extractable JWK
207    function wrapAsNonExtractableJwk(key, wrapper){
208        var wrappingKey = wrapper.wrappingKey,
209            encryptKey;
210
211        return subtle.exportKey("jwk",wrappingKey)
212        .then(function(jwkWrappingKey){
213            // Update the key generation parameters to work as key import parameters
214            var params = Object.create(wrapper.parameters.generateParameters);
215            if(params.name === "AES-KW") {
216                params.name = "AES-CBC";
217                jwkWrappingKey.alg = "A"+params.length+"CBC";
218            } else if (params.name === "RSA-OAEP") {
219                params.modulusLength = undefined;
220                params.publicExponent = undefined;
221            }
222            jwkWrappingKey.key_ops = ["encrypt"];
223            return subtle.importKey("jwk", jwkWrappingKey, params, true, ["encrypt"]);
224        }).then(function(importedWrappingKey){
225            encryptKey = importedWrappingKey;
226            return subtle.exportKey("jwk",key);
227        }).then(function(exportedKey){
228            exportedKey.ext = false;
229            var jwk = JSON.stringify(exportedKey)
230            if (wrappingKey.algorithm.name === "AES-KW") {
231                return aeskw(encryptKey, str2ab(jwk.slice(0,-1) + " ".repeat(jwk.length%8 ? 8-jwk.length%8 : 0) + "}"));
232            } else {
233                return subtle.encrypt(wrapper.parameters.wrapParameters,encryptKey,str2ab(jwk));
234            }
235        });
236    }
237
238
239    // RSA-OAEP can only wrap relatively small payloads. AES-KW can only
240    // wrap payloads a multiple of 8 bytes long.
241    function wrappingIsPossible(exportedKey, algorithmName) {
242        if ("byteLength" in exportedKey && algorithmName === "AES-KW") {
243            return exportedKey.byteLength % 8 === 0;
244        }
245
246        if ("byteLength" in exportedKey && algorithmName === "RSA-OAEP") {
247            // RSA-OAEP can only encrypt payloads with lengths shorter
248            // than modulusLength - 2*hashLength - 1 bytes long. For
249            // a 4096 bit modulus and SHA-256, that comes to
250            // 4096/8 - 2*(256/8) - 1 = 512 - 2*32 - 1 = 447 bytes.
251            return exportedKey.byteLength <= 446;
252        }
253
254        if ("kty" in exportedKey && algorithmName === "AES-KW") {
255            return JSON.stringify(exportedKey).length % 8 == 0;
256        }
257
258        if ("kty" in exportedKey && algorithmName === "RSA-OAEP") {
259            return JSON.stringify(exportedKey).length <= 478;
260        }
261
262        return true;
263    }
264
265
266    // Helper methods follow:
267
268    // Are two exported keys equal
269    function equalExport(originalExport, roundTripExport) {
270        if ("byteLength" in originalExport) {
271            return equalBuffers(originalExport, roundTripExport);
272        } else {
273            return equalJwk(originalExport, roundTripExport);
274        }
275    }
276
277    // Are two array buffers the same?
278    function equalBuffers(a, b) {
279        if (a.byteLength !== b.byteLength) {
280            return false;
281        }
282
283        var aBytes = new Uint8Array(a);
284        var bBytes = new Uint8Array(b);
285
286        for (var i=0; i<a.byteLength; i++) {
287            if (aBytes[i] !== bBytes[i]) {
288                return false;
289            }
290        }
291
292        return true;
293    }
294
295    // Are two Jwk objects "the same"? That is, does the object returned include
296    // matching values for each property that was expected? It's okay if the
297    // returned object has extra methods; they aren't checked.
298    function equalJwk(expected, got) {
299        var fields = Object.keys(expected);
300        var fieldName;
301
302        for(var i=0; i<fields.length; i++) {
303            fieldName = fields[i];
304            if (!(fieldName in got)) {
305                return false;
306            }
307            if (objectToString(expected[fieldName]) !== objectToString(got[fieldName])) {
308                return false;
309            }
310        }
311
312        return true;
313    }
314
315    // Character representation of any object we may use as a parameter.
316    function objectToString(obj) {
317        var keyValuePairs = [];
318
319        if (Array.isArray(obj)) {
320            return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
321        } else if (typeof obj === "object") {
322            Object.keys(obj).sort().forEach(function(keyName) {
323                keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
324            });
325            return "{" + keyValuePairs.join(", ") + "}";
326        } else if (typeof obj === "undefined") {
327            return "undefined";
328        } else {
329            return obj.toString();
330        }
331
332        var keyValuePairs = [];
333
334        Object.keys(obj).sort().forEach(function(keyName) {
335            var value = obj[keyName];
336            if (typeof value === "object") {
337                value = objectToString(value);
338            } else if (typeof value === "array") {
339                value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
340            } else {
341                value = value.toString();
342            }
343
344            keyValuePairs.push(keyName + ": " + value);
345        });
346
347        return "{" + keyValuePairs.join(", ") + "}";
348    }
349
350    // Can we compare key values by using them
351    function canCompareNonExtractableKeys(key){
352        if (key.usages.indexOf("decrypt") !== -1) {
353            return true;
354        }
355        if (key.usages.indexOf("sign") !== -1) {
356            return true;
357        }
358        if (key.usages.indexOf("wrapKey") !== -1) {
359            return true;
360        }
361        if (key.usages.indexOf("deriveBits") !== -1) {
362            return true;
363        }
364        return false;
365    }
366
367    // Compare two keys by using them (works for non-extractable keys)
368    function equalKeys(expected, got){
369        if ( expected.algorithm.name !== got.algorithm.name ) {
370            return Promise.resolve(false);
371        }
372
373        var cryptParams, signParams, wrapParams, deriveParams;
374        switch(expected.algorithm.name){
375            case "AES-CTR" :
376                cryptParams = {name: "AES-CTR", counter: new Uint8Array(16), length: 64};
377                break;
378            case "AES-CBC" :
379                cryptParams = {name: "AES-CBC", iv: new Uint8Array(16) };
380                break;
381            case "AES-GCM" :
382                cryptParams = {name: "AES-GCM", iv: new Uint8Array(16) };
383                break;
384            case "RSA-OAEP" :
385                cryptParams = {name: "RSA-OAEP", label: new Uint8Array(8) };
386                break;
387            case "RSASSA-PKCS1-v1_5" :
388                signParams = {name: "RSASSA-PKCS1-v1_5"};
389                break;
390            case "RSA-PSS" :
391                signParams = {name: "RSA-PSS", saltLength: 32 };
392                break;
393            case "ECDSA" :
394                signParams = {name: "ECDSA", hash: "SHA-256"};
395                break;
396            case "Ed25519" :
397                signParams = {name: "Ed25519"};
398                break;
399            case "Ed448" :
400                signParams = {name: "Ed448"};
401                break;
402            case "X25519" :
403                deriveParams = {name: "X25519"};
404                break;
405            case "X448" :
406                deriveParams = {name: "X448"};
407                break;
408            case "HMAC" :
409                signParams = {name: "HMAC"};
410                break;
411            case "AES-KW" :
412                wrapParams = {name: "AES-KW"};
413                break;
414            case "ECDH" :
415                deriveParams = {name: "ECDH"};
416                break;
417            default:
418                throw new Error("Unsupported algorithm for key comparison");
419        }
420
421        if (cryptParams) {
422            return subtle.exportKey("jwk",expected)
423            .then(function(jwkExpectedKey){
424                if (expected.algorithm.name === "RSA-OAEP") {
425                    ["d","p","q","dp","dq","qi","oth"].forEach(function(field){ delete jwkExpectedKey[field]; });
426                }
427                jwkExpectedKey.key_ops = ["encrypt"];
428                return subtle.importKey("jwk", jwkExpectedKey, expected.algorithm, true, ["encrypt"]);
429            }).then(function(expectedEncryptKey){
430                return subtle.encrypt(cryptParams, expectedEncryptKey, new Uint8Array(32));
431            }).then(function(encryptedData){
432                return subtle.decrypt(cryptParams, got, encryptedData);
433            }).then(function(decryptedData){
434                var result = new Uint8Array(decryptedData);
435                return !result.some(x => x);
436            });
437        } else if (signParams) {
438            var verifyKey;
439            return subtle.exportKey("jwk",expected)
440            .then(function(jwkExpectedKey){
441                if (expected.algorithm.name === "RSA-PSS" || expected.algorithm.name === "RSASSA-PKCS1-v1_5") {
442                    ["d","p","q","dp","dq","qi","oth"].forEach(function(field){ delete jwkExpectedKey[field]; });
443                }
444                if (expected.algorithm.name === "ECDSA" || expected.algorithm.name.startsWith("Ed")) {
445                    delete jwkExpectedKey["d"];
446                }
447                jwkExpectedKey.key_ops = ["verify"];
448                return subtle.importKey("jwk", jwkExpectedKey, expected.algorithm, true, ["verify"]);
449            }).then(function(expectedVerifyKey){
450                verifyKey = expectedVerifyKey;
451                return subtle.sign(signParams, got, new Uint8Array(32));
452            }).then(function(signature){
453                return subtle.verify(signParams, verifyKey, signature, new Uint8Array(32));
454            });
455        } else if (wrapParams) {
456            var aKeyToWrap, wrappedWithExpected;
457            return subtle.importKey("raw", new Uint8Array(16), "AES-CBC", true, ["encrypt"])
458            .then(function(key){
459                aKeyToWrap = key;
460                return subtle.wrapKey("raw", aKeyToWrap, expected, wrapParams);
461            }).then(function(wrapResult){
462                wrappedWithExpected = Array.from((new Uint8Array(wrapResult)).values());
463                return subtle.wrapKey("raw", aKeyToWrap, got, wrapParams);
464            }).then(function(wrapResult){
465                var wrappedWithGot = Array.from((new Uint8Array(wrapResult)).values());
466                return wrappedWithGot.every((x,i) => x === wrappedWithExpected[i]);
467            });
468        } else if (deriveParams) {
469            var expectedDerivedBits;
470            return subtle.generateKey(expected.algorithm, true, ['deriveBits']).then(({ publicKey }) => {
471                deriveParams.public = publicKey;
472                return subtle.deriveBits(deriveParams, expected, 128)
473            })
474            .then(function(result){
475                expectedDerivedBits = Array.from((new Uint8Array(result)).values());
476                return subtle.deriveBits(deriveParams, got, 128);
477            }).then(function(result){
478                var gotDerivedBits = Array.from((new Uint8Array(result)).values());
479                return gotDerivedBits.every((x,i) => x === expectedDerivedBits[i]);
480            });
481        }
482    }
483
484    // Raw AES encryption
485    function aes( k, p ) {
486        return subtle.encrypt({name: "AES-CBC", iv: new Uint8Array(16) }, k, p).then(function(ciphertext){return ciphertext.slice(0,16);});
487    }
488
489    // AES Key Wrap
490    function aeskw(key, data) {
491        if (data.byteLength % 8 !== 0) {
492            throw new Error("AES Key Wrap data must be a multiple of 8 bytes in length");
493        }
494
495        var A = Uint8Array.from([0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0, 0, 0, 0, 0, 0, 0, 0]),
496            Av = new DataView(A.buffer),
497            R = [],
498            n = data.byteLength / 8;
499
500        for(var i = 0; i<data.byteLength; i+=8) {
501            R.push(new Uint8Array(data.slice(i,i+8)));
502        }
503
504        function aeskw_step(j, i, final, B) {
505            A.set(new Uint8Array(B.slice(0,8)));
506            Av.setUint32(4,Av.getUint32(4) ^ (n*j+i+1));
507            R[i] = new Uint8Array(B.slice(8,16));
508            if (final) {
509                R.unshift(A.slice(0,8));
510                var result = new Uint8Array(R.length * 8);
511                R.forEach(function(Ri,i){ result.set(Ri, i*8); });
512                return result;
513            } else {
514                A.set(R[(i+1)%n],8);
515                return aes(key,A);
516            }
517        }
518
519        var p = new Promise(function(resolve){
520            A.set(R[0],8);
521            resolve(aes(key,A));
522        });
523
524        for(var j=0;j<6;++j) {
525            for(var i=0;i<n;++i) {
526                p = p.then(aeskw_step.bind(undefined, j, i,j===5 && i===(n-1)));
527            }
528        }
529
530        return p;
531    }
532
533    function str2ab(str)        { return Uint8Array.from( str.split(''), function(s){return s.charCodeAt(0)} ); }
534    function ab2str(ab)         { return String.fromCharCode.apply(null, new Uint8Array(ab)); }
535
536