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