From c4a0a43ce832c85de6bb97a4afc9ecf915e63e5a Mon Sep 17 00:00:00 2001 From: Sean Michael Wykes <8363933+SeanWykes@users.noreply.github.com> Date: Tue, 4 Jan 2022 21:00:37 -0300 Subject: [PATCH] fix(ext/crypto) - exportKey JWK for AES/HMAC must use base64url (#13264) Co-authored-by: Divy Srivastava --- cli/tests/unit/webcrypto_test.ts | 121 +++++++++++++++++++++++++++---- ext/crypto/00_crypto.js | 43 ++++++----- ext/crypto/export_key.rs | 32 ++++++++ 3 files changed, 160 insertions(+), 36 deletions(-) diff --git a/cli/tests/unit/webcrypto_test.ts b/cli/tests/unit/webcrypto_test.ts index 2d101e9750..8b5ce55e86 100644 --- a/cli/tests/unit/webcrypto_test.ts +++ b/cli/tests/unit/webcrypto_test.ts @@ -1388,8 +1388,6 @@ Deno.test(async function testImportEcSpkiPkcs8() { for ( const hash of [/*"SHA-1", */ "SHA-256" /*"SHA-384", "SHA-512"*/] ) { - console.log(hash); - const signatureECDSA = await subtle.sign( { name: "ECDSA", hash }, privateKeyECDSA, @@ -1420,27 +1418,118 @@ Deno.test(async function testImportEcSpkiPkcs8() { } }); -Deno.test(async function testBase64Forgiving() { - const keyData = `{ - "kty": "oct", - "k": "xxx", - "alg": "HS512", - "key_ops": ["sign", "verify"], - "ext": true - }`; - +async function roundTripSecretJwk( + jwk: JsonWebKey, + algId: AlgorithmIdentifier | HmacImportParams, + ops: KeyUsage[], + validateKeys: ( + key: CryptoKey, + originalJwk: JsonWebKey, + exportedJwk: JsonWebKey, + ) => void, +) { const key = await crypto.subtle.importKey( "jwk", - JSON.parse(keyData), - { name: "HMAC", hash: "SHA-512" }, + jwk, + algId, true, - ["sign", "verify"], + ops, ); assert(key instanceof CryptoKey); assertEquals(key.type, "secret"); - assertEquals((key.algorithm as HmacKeyAlgorithm).length, 16); const exportedKey = await crypto.subtle.exportKey("jwk", key); - assertEquals(exportedKey.k, "xxw"); + + validateKeys(key, jwk, exportedKey); +} + +Deno.test(async function testSecretJwkBase64Url() { + // Test 16bits with "overflow" in 3rd pos of 'quartet', no padding + const keyData = `{ + "kty": "oct", + "k": "xxx", + "alg": "HS512", + "key_ops": ["sign", "verify"], + "ext": true + }`; + + await roundTripSecretJwk( + JSON.parse(keyData), + { name: "HMAC", hash: "SHA-512" }, + ["sign", "verify"], + (key, _orig, exp) => { + assertEquals((key.algorithm as HmacKeyAlgorithm).length, 16); + + assertEquals(exp.k, "xxw"); + }, + ); + + // HMAC 128bits with base64url characters (-_) + await roundTripSecretJwk( + { + kty: "oct", + k: "HnZXRyDKn-_G5Fx4JWR1YA", + alg: "HS256", + "key_ops": ["sign", "verify"], + ext: true, + }, + { name: "HMAC", hash: "SHA-256" }, + ["sign", "verify"], + (key, orig, exp) => { + assertEquals((key.algorithm as HmacKeyAlgorithm).length, 128); + + assertEquals(orig.k, exp.k); + }, + ); + + // HMAC 104bits/(12+1) bytes with base64url characters (-_), padding and overflow in 2rd pos of "quartet" + await roundTripSecretJwk( + { + kty: "oct", + k: "a-_AlFa-2-OmEGa_-z==", + alg: "HS384", + "key_ops": ["sign", "verify"], + ext: true, + }, + { name: "HMAC", hash: "SHA-384" }, + ["sign", "verify"], + (key, _orig, exp) => { + assertEquals((key.algorithm as HmacKeyAlgorithm).length, 104); + + assertEquals("a-_AlFa-2-OmEGa_-w", exp.k); + }, + ); + + // AES-CBC 128bits with base64url characters (-_) no padding + await roundTripSecretJwk( + { + kty: "oct", + k: "_u3K_gEjRWf-7cr-ASNFZw", + alg: "A128CBC", + "key_ops": ["encrypt", "decrypt"], + ext: true, + }, + { name: "AES-CBC" }, + ["encrypt", "decrypt"], + (_key, orig, exp) => { + assertEquals(orig.k, exp.k); + }, + ); + + // AES-CBC 128bits of '1' with padding chars + await roundTripSecretJwk( + { + kty: "oct", + k: "_____________________w==", + alg: "A128CBC", + "key_ops": ["encrypt", "decrypt"], + ext: true, + }, + { name: "AES-CBC" }, + ["encrypt", "decrypt"], + (_key, _orig, exp) => { + assertEquals(exp.k, "_____________________w"); + }, + ); }); diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index 5d216dbf45..95eb18daa0 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -12,7 +12,6 @@ const core = window.Deno.core; const webidl = window.__bootstrap.webidl; const { DOMException } = window.__bootstrap.domException; - const { btoa } = window.__bootstrap.base64; const { ArrayBuffer, @@ -25,8 +24,6 @@ Int32Array, Int8Array, ObjectAssign, - StringFromCharCode, - StringPrototypeReplace, StringPrototypeToLowerCase, StringPrototypeToUpperCase, Symbol, @@ -175,15 +172,6 @@ }, }; - function unpaddedBase64(bytes) { - let binaryString = ""; - for (let i = 0; i < bytes.length; i++) { - binaryString += StringFromCharCode(bytes[i]); - } - const base64String = btoa(binaryString); - return StringPrototypeReplace(base64String, /=/g, ""); - } - // See https://www.w3.org/TR/WebCryptoAPI/#dfn-normalize-an-algorithm // 18.4.4 function normalizeAlgorithm(algorithm, op) { @@ -1836,16 +1824,18 @@ return data.buffer; } case "jwk": { - // 1-3. + // 1-2. const jwk = { kty: "oct", - // 5. - ext: key[_extractable], - // 6. - "key_ops": key.usages, - k: unpaddedBase64(innerKey.data), }; + // 3. + const data = core.opSync("op_crypto_export_key", { + format: "jwksecret", + algorithm: "AES", + }, innerKey); + ObjectAssign(jwk, data); + // 4. const algorithm = key[_algorithm]; switch (algorithm.length) { @@ -1865,6 +1855,12 @@ ); } + // 5. + jwk.key_ops = key.usages; + + // 6. + jwk.ext = key[_extractable]; + // 7. return jwk; } @@ -3092,11 +3088,18 @@ return bits.buffer; } case "jwk": { - // 1-3. + // 1-2. const jwk = { kty: "oct", - k: unpaddedBase64(innerKey.data), }; + + // 3. + const data = core.opSync("op_crypto_export_key", { + format: "jwksecret", + algorithm: key[_algorithm].name, + }, innerKey); + jwk.k = data.k; + // 4. const algorithm = key[_algorithm]; // 5. diff --git a/ext/crypto/export_key.rs b/ext/crypto/export_key.rs index 2e74d61f22..25faf1791f 100644 --- a/ext/crypto/export_key.rs +++ b/ext/crypto/export_key.rs @@ -26,6 +26,7 @@ pub enum ExportKeyFormat { Spki, JwkPublic, JwkPrivate, + JwkSecret, } #[derive(Deserialize)] @@ -37,6 +38,10 @@ pub enum ExportKeyAlgorithm { RsaPss {}, #[serde(rename = "RSA-OAEP")] RsaOaep {}, + #[serde(rename = "AES")] + Aes {}, + #[serde(rename = "HMAC")] + Hmac {}, } #[derive(Serialize)] @@ -44,6 +49,9 @@ pub enum ExportKeyAlgorithm { pub enum ExportKeyResult { Pkcs8(ZeroCopyBuf), Spki(ZeroCopyBuf), + JwkSecret { + k: String, + }, JwkPublicRsa { n: String, e: String, @@ -69,6 +77,9 @@ pub fn op_crypto_export_key( ExportKeyAlgorithm::RsassaPkcs1v15 {} | ExportKeyAlgorithm::RsaPss {} | ExportKeyAlgorithm::RsaOaep {} => export_key_rsa(opts.format, key_data), + ExportKeyAlgorithm::Aes {} | ExportKeyAlgorithm::Hmac {} => { + export_key_symmetric(opts.format, key_data) + } } } @@ -76,6 +87,10 @@ fn uint_to_b64(bytes: UIntBytes) -> String { base64::encode_config(bytes.as_bytes(), base64::URL_SAFE_NO_PAD) } +fn bytes_to_b64(bytes: &[u8]) -> String { + base64::encode_config(bytes, base64::URL_SAFE_NO_PAD) +} + fn export_key_rsa( format: ExportKeyFormat, key_data: RawKeyData, @@ -166,5 +181,22 @@ fn export_key_rsa( qi: uint_to_b64(private_key.coefficient), }) } + _ => Err(unsupported_format()), + } +} + +fn export_key_symmetric( + format: ExportKeyFormat, + key_data: RawKeyData, +) -> Result { + match format { + ExportKeyFormat::JwkSecret => { + let bytes = key_data.as_secret_key()?; + + Ok(ExportKeyResult::JwkSecret { + k: bytes_to_b64(bytes), + }) + } + _ => Err(unsupported_format()), } }