mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 00:29:09 -05:00
fix(ext/crypto) - exportKey JWK for AES/HMAC must use base64url (#13264)
Co-authored-by: Divy Srivastava <dj.srivastava23@gmail.com>
This commit is contained in:
parent
80bf2828c6
commit
c4a0a43ce8
3 changed files with 160 additions and 36 deletions
|
@ -1388,8 +1388,6 @@ Deno.test(async function testImportEcSpkiPkcs8() {
|
||||||
for (
|
for (
|
||||||
const hash of [/*"SHA-1", */ "SHA-256" /*"SHA-384", "SHA-512"*/]
|
const hash of [/*"SHA-1", */ "SHA-256" /*"SHA-384", "SHA-512"*/]
|
||||||
) {
|
) {
|
||||||
console.log(hash);
|
|
||||||
|
|
||||||
const signatureECDSA = await subtle.sign(
|
const signatureECDSA = await subtle.sign(
|
||||||
{ name: "ECDSA", hash },
|
{ name: "ECDSA", hash },
|
||||||
privateKeyECDSA,
|
privateKeyECDSA,
|
||||||
|
@ -1420,7 +1418,34 @@ Deno.test(async function testImportEcSpkiPkcs8() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(async function testBase64Forgiving() {
|
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",
|
||||||
|
jwk,
|
||||||
|
algId,
|
||||||
|
true,
|
||||||
|
ops,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(key instanceof CryptoKey);
|
||||||
|
assertEquals(key.type, "secret");
|
||||||
|
|
||||||
|
const exportedKey = await crypto.subtle.exportKey("jwk", key);
|
||||||
|
|
||||||
|
validateKeys(key, jwk, exportedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test(async function testSecretJwkBase64Url() {
|
||||||
|
// Test 16bits with "overflow" in 3rd pos of 'quartet', no padding
|
||||||
const keyData = `{
|
const keyData = `{
|
||||||
"kty": "oct",
|
"kty": "oct",
|
||||||
"k": "xxx",
|
"k": "xxx",
|
||||||
|
@ -1429,18 +1454,82 @@ Deno.test(async function testBase64Forgiving() {
|
||||||
"ext": true
|
"ext": true
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const key = await crypto.subtle.importKey(
|
await roundTripSecretJwk(
|
||||||
"jwk",
|
|
||||||
JSON.parse(keyData),
|
JSON.parse(keyData),
|
||||||
{ name: "HMAC", hash: "SHA-512" },
|
{ name: "HMAC", hash: "SHA-512" },
|
||||||
true,
|
|
||||||
["sign", "verify"],
|
["sign", "verify"],
|
||||||
);
|
(key, _orig, exp) => {
|
||||||
|
|
||||||
assert(key instanceof CryptoKey);
|
|
||||||
assertEquals(key.type, "secret");
|
|
||||||
assertEquals((key.algorithm as HmacKeyAlgorithm).length, 16);
|
assertEquals((key.algorithm as HmacKeyAlgorithm).length, 16);
|
||||||
|
|
||||||
const exportedKey = await crypto.subtle.exportKey("jwk", key);
|
assertEquals(exp.k, "xxw");
|
||||||
assertEquals(exportedKey.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");
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
const core = window.Deno.core;
|
const core = window.Deno.core;
|
||||||
const webidl = window.__bootstrap.webidl;
|
const webidl = window.__bootstrap.webidl;
|
||||||
const { DOMException } = window.__bootstrap.domException;
|
const { DOMException } = window.__bootstrap.domException;
|
||||||
const { btoa } = window.__bootstrap.base64;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ArrayBuffer,
|
ArrayBuffer,
|
||||||
|
@ -25,8 +24,6 @@
|
||||||
Int32Array,
|
Int32Array,
|
||||||
Int8Array,
|
Int8Array,
|
||||||
ObjectAssign,
|
ObjectAssign,
|
||||||
StringFromCharCode,
|
|
||||||
StringPrototypeReplace,
|
|
||||||
StringPrototypeToLowerCase,
|
StringPrototypeToLowerCase,
|
||||||
StringPrototypeToUpperCase,
|
StringPrototypeToUpperCase,
|
||||||
Symbol,
|
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
|
// See https://www.w3.org/TR/WebCryptoAPI/#dfn-normalize-an-algorithm
|
||||||
// 18.4.4
|
// 18.4.4
|
||||||
function normalizeAlgorithm(algorithm, op) {
|
function normalizeAlgorithm(algorithm, op) {
|
||||||
|
@ -1836,16 +1824,18 @@
|
||||||
return data.buffer;
|
return data.buffer;
|
||||||
}
|
}
|
||||||
case "jwk": {
|
case "jwk": {
|
||||||
// 1-3.
|
// 1-2.
|
||||||
const jwk = {
|
const jwk = {
|
||||||
kty: "oct",
|
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.
|
// 4.
|
||||||
const algorithm = key[_algorithm];
|
const algorithm = key[_algorithm];
|
||||||
switch (algorithm.length) {
|
switch (algorithm.length) {
|
||||||
|
@ -1865,6 +1855,12 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5.
|
||||||
|
jwk.key_ops = key.usages;
|
||||||
|
|
||||||
|
// 6.
|
||||||
|
jwk.ext = key[_extractable];
|
||||||
|
|
||||||
// 7.
|
// 7.
|
||||||
return jwk;
|
return jwk;
|
||||||
}
|
}
|
||||||
|
@ -3092,11 +3088,18 @@
|
||||||
return bits.buffer;
|
return bits.buffer;
|
||||||
}
|
}
|
||||||
case "jwk": {
|
case "jwk": {
|
||||||
// 1-3.
|
// 1-2.
|
||||||
const jwk = {
|
const jwk = {
|
||||||
kty: "oct",
|
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.
|
// 4.
|
||||||
const algorithm = key[_algorithm];
|
const algorithm = key[_algorithm];
|
||||||
// 5.
|
// 5.
|
||||||
|
|
|
@ -26,6 +26,7 @@ pub enum ExportKeyFormat {
|
||||||
Spki,
|
Spki,
|
||||||
JwkPublic,
|
JwkPublic,
|
||||||
JwkPrivate,
|
JwkPrivate,
|
||||||
|
JwkSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -37,6 +38,10 @@ pub enum ExportKeyAlgorithm {
|
||||||
RsaPss {},
|
RsaPss {},
|
||||||
#[serde(rename = "RSA-OAEP")]
|
#[serde(rename = "RSA-OAEP")]
|
||||||
RsaOaep {},
|
RsaOaep {},
|
||||||
|
#[serde(rename = "AES")]
|
||||||
|
Aes {},
|
||||||
|
#[serde(rename = "HMAC")]
|
||||||
|
Hmac {},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -44,6 +49,9 @@ pub enum ExportKeyAlgorithm {
|
||||||
pub enum ExportKeyResult {
|
pub enum ExportKeyResult {
|
||||||
Pkcs8(ZeroCopyBuf),
|
Pkcs8(ZeroCopyBuf),
|
||||||
Spki(ZeroCopyBuf),
|
Spki(ZeroCopyBuf),
|
||||||
|
JwkSecret {
|
||||||
|
k: String,
|
||||||
|
},
|
||||||
JwkPublicRsa {
|
JwkPublicRsa {
|
||||||
n: String,
|
n: String,
|
||||||
e: String,
|
e: String,
|
||||||
|
@ -69,6 +77,9 @@ pub fn op_crypto_export_key(
|
||||||
ExportKeyAlgorithm::RsassaPkcs1v15 {}
|
ExportKeyAlgorithm::RsassaPkcs1v15 {}
|
||||||
| ExportKeyAlgorithm::RsaPss {}
|
| ExportKeyAlgorithm::RsaPss {}
|
||||||
| ExportKeyAlgorithm::RsaOaep {} => export_key_rsa(opts.format, key_data),
|
| 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)
|
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(
|
fn export_key_rsa(
|
||||||
format: ExportKeyFormat,
|
format: ExportKeyFormat,
|
||||||
key_data: RawKeyData,
|
key_data: RawKeyData,
|
||||||
|
@ -166,5 +181,22 @@ fn export_key_rsa(
|
||||||
qi: uint_to_b64(private_key.coefficient),
|
qi: uint_to_b64(private_key.coefficient),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
_ => Err(unsupported_format()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn export_key_symmetric(
|
||||||
|
format: ExportKeyFormat,
|
||||||
|
key_data: RawKeyData,
|
||||||
|
) -> Result<ExportKeyResult, deno_core::anyhow::Error> {
|
||||||
|
match format {
|
||||||
|
ExportKeyFormat::JwkSecret => {
|
||||||
|
let bytes = key_data.as_secret_key()?;
|
||||||
|
|
||||||
|
Ok(ExportKeyResult::JwkSecret {
|
||||||
|
k: bytes_to_b64(bytes),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(unsupported_format()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue