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