1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-12 00:54:02 -05:00

feat(ext/crypto): support JWK import for HMAC (#11716)

This commit is contained in:
Divy Srivastava 2021-08-27 16:49:41 +05:30 committed by GitHub
parent ad037b3b1a
commit 1f57cd2c0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 386 additions and 94 deletions

View file

@ -243,22 +243,43 @@ unitTest(async function testSignRSASSAKey() {
assert(signature);
});
// deno-fmt-ignore
const rawKey = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16
]);
const jwk: JsonWebKey = {
kty: "oct",
// unpadded base64 for rawKey.
k: "AQIDBAUGBwgJCgsMDQ4PEA",
alg: "HS256",
};
unitTest(async function subtleCryptoHmacImportExport() {
// deno-fmt-ignore
const rawKey = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16
]);
const key = await crypto.subtle.importKey(
const key1 = await crypto.subtle.importKey(
"raw",
rawKey,
{ name: "HMAC", hash: "SHA-256" },
true,
["sign"],
);
const actual = await crypto.subtle.sign(
const key2 = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "HMAC", hash: "SHA-256" },
true,
["sign"],
);
const actual1 = await crypto.subtle.sign(
{ name: "HMAC" },
key,
key1,
new Uint8Array([1, 2, 3, 4]),
);
const actual2 = await crypto.subtle.sign(
{ name: "HMAC" },
key2,
new Uint8Array([1, 2, 3, 4]),
);
// deno-fmt-ignore
@ -269,10 +290,14 @@ unitTest(async function subtleCryptoHmacImportExport() {
23, 122, 222, 1, 146, 46, 182, 87,
]);
assertEquals(
new Uint8Array(actual),
new Uint8Array(actual1),
expected,
);
const exportedKey = await crypto.subtle.exportKey("raw", key);
assertEquals(
new Uint8Array(actual2),
expected,
);
// TODO(@littledivy): Add a test for exporting JWK key when supported.
const exportedKey = await crypto.subtle.exportKey("raw", key1);
assertEquals(new Uint8Array(exportedKey), rawKey);
});

View file

@ -12,13 +12,18 @@
const core = window.Deno.core;
const webidl = window.__bootstrap.webidl;
const { DOMException } = window.__bootstrap.domException;
const { atob } = window.__bootstrap.base64;
const {
ArrayPrototypeFind,
ArrayBufferIsView,
ArrayPrototypeEvery,
ArrayPrototypeIncludes,
ArrayBuffer,
ArrayBufferIsView,
BigInt64Array,
StringPrototypeToUpperCase,
StringPrototypeReplace,
StringPrototypeCharCodeAt,
Symbol,
SymbolFor,
SymbolToStringTag,
@ -100,6 +105,23 @@
},
};
// Decodes the unpadded base64 to the octet sequence containing key value `k` defined in RFC7518 Section 6.4
function decodeSymmetricKey(key) {
// Decode from base64url without `=` padding.
const base64 = StringPrototypeReplace(
StringPrototypeReplace(key, /\-/g, "+"),
/\_/g,
"/",
);
const decodedKey = atob(base64);
const keyLength = decodedKey.length;
const keyBytes = new Uint8Array(keyLength);
for (let i = 0; i < keyLength; i++) {
keyBytes[i] = StringPrototypeCharCodeAt(decodedKey, i);
}
return keyBytes;
}
// See https://www.w3.org/TR/WebCryptoAPI/#dfn-normalize-an-algorithm
// 18.4.4
function normalizeAlgorithm(algorithm, op) {
@ -631,7 +653,7 @@
prefix,
context: "Argument 1",
});
keyData = webidl.converters.BufferSource(keyData, {
keyData = webidl.converters["BufferSource or JsonWebKey"](keyData, {
prefix,
context: "Argument 2",
});
@ -649,22 +671,32 @@
});
// 2.
if (ArrayBufferIsView(keyData)) {
keyData = new Uint8Array(
keyData.buffer,
keyData.byteOffset,
keyData.byteLength,
);
if (format !== "jwk") {
if (ArrayBufferIsView(keyData) || keyData instanceof ArrayBuffer) {
if (ArrayBufferIsView(keyData)) {
keyData = new Uint8Array(
keyData.buffer,
keyData.byteOffset,
keyData.byteLength,
);
} else {
keyData = new Uint8Array(keyData);
}
keyData = TypedArrayPrototypeSlice(keyData);
} else {
throw new TypeError("keyData is a JsonWebKey");
}
} else {
keyData = new Uint8Array(keyData);
if (ArrayBufferIsView(keyData) || keyData instanceof ArrayBuffer) {
throw new TypeError("keyData is not a JsonWebKey");
}
}
keyData = TypedArrayPrototypeSlice(keyData);
const normalizedAlgorithm = normalizeAlgorithm(algorithm, "importKey");
switch (normalizedAlgorithm.name) {
// https://w3c.github.io/webcrypto/#hmac-operations
case "HMAC": {
// 2.
if (
ArrayPrototypeFind(
keyUsages,
@ -674,59 +706,177 @@
throw new DOMException("Invalid key usages", "SyntaxError");
}
// 3.
let hash;
let data;
// 4. https://w3c.github.io/webcrypto/#hmac-operations
switch (format) {
case "raw": {
const hash = normalizedAlgorithm.hash;
// 5.
let length = keyData.byteLength * 8;
// 6.
if (length === 0) {
throw new DOMException("Key length is zero", "DataError");
data = keyData;
hash = normalizedAlgorithm.hash;
break;
}
case "jwk": {
// TODO(@littledivy): Why does the spec validate JWK twice?
const jwk = keyData;
// 2.
if (jwk.kty !== "oct") {
throw new DOMException(
"`kty` member of JsonWebKey must be `oct`",
"DataError",
);
}
if (normalizeAlgorithm.length) {
// 7.
// Section 6.4.1 of RFC7518
if (!jwk.k) {
throw new DOMException(
"`k` member of JsonWebKey must be present",
"DataError",
);
}
// 4.
data = decodeSymmetricKey(jwk.k);
// 5.
hash = normalizedAlgorithm.hash;
// 6.
switch (hash.name) {
case "SHA-1": {
if (jwk.alg !== undefined && jwk.alg !== "HS1") {
throw new DOMException(
"`alg` member of JsonWebKey must be `HS1`",
"DataError",
);
}
break;
}
case "SHA-256": {
if (jwk.alg !== undefined && jwk.alg !== "HS256") {
throw new DOMException(
"`alg` member of JsonWebKey must be `HS256`",
"DataError",
);
}
break;
}
case "SHA-384": {
if (jwk.alg !== undefined && jwk.alg !== "HS384") {
throw new DOMException(
"`alg` member of JsonWebKey must be `HS384`",
"DataError",
);
}
break;
}
case "SHA-512": {
if (jwk.alg !== undefined && jwk.alg !== "HS512") {
throw new DOMException(
"`alg` member of JsonWebKey must be `HS512`",
"DataError",
);
}
break;
}
default:
throw new TypeError("unreachable");
}
// 7.
if (keyUsages.length > 0 && jwk.use && jwk.use !== "sign") {
throw new DOMException(
"`use` member of JsonWebKey must be `sign`",
"DataError",
);
}
// 8.
// Section 4.3 of RFC7517
if (jwk.key_ops) {
if (
normalizedAlgorithm.length > length ||
normalizedAlgorithm.length <= (length - 8)
ArrayPrototypeFind(
jwk.key_ops,
(u) => !ArrayPrototypeIncludes(recognisedUsages, u),
) !== undefined
) {
throw new DOMException(
"Key length is invalid",
"`key_ops` member of JsonWebKey is invalid",
"DataError",
);
}
if (
!ArrayPrototypeEvery(
jwk.key_ops,
(u) => ArrayPrototypeIncludes(keyUsages, u),
)
) {
throw new DOMException(
"`key_ops` member of JsonWebKey is invalid",
"DataError",
);
}
length = normalizeAlgorithm.length;
}
if (keyUsages.length == 0) {
throw new DOMException("Key usage is empty", "SyntaxError");
// 9.
if (jwk.ext === false && extractable == true) {
throw new DOMException(
"`ext` member of JsonWebKey is invalid",
"DataError",
);
}
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
type: "raw",
data: keyData,
});
const algorithm = {
name: "HMAC",
length,
hash,
};
const key = constructKey(
"secret",
extractable,
usageIntersection(keyUsages, recognisedUsages),
algorithm,
handle,
);
return key;
break;
}
// TODO(@littledivy): jwk
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
// 5.
let length = data.byteLength * 8;
// 6.
if (length === 0) {
throw new DOMException("Key length is zero", "DataError");
}
// 7.
if (normalizedAlgorithm.length !== undefined) {
if (
normalizedAlgorithm.length > length ||
normalizedAlgorithm.length <= (length - 8)
) {
throw new DOMException(
"Key length is invalid",
"DataError",
);
}
length = normalizedAlgorithm.length;
}
if (keyUsages.length == 0) {
throw new DOMException("Key usage is empty", "SyntaxError");
}
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
type: "raw",
data,
});
const algorithm = {
name: "HMAC",
length,
hash,
};
const key = constructKey(
"secret",
extractable,
usageIntersection(keyUsages, recognisedUsages),
algorithm,
handle,
);
return key;
}
// TODO(@littledivy): RSASSA-PKCS1-v1_5
// TODO(@littledivy): RSA-PSS

View file

@ -9,6 +9,7 @@
((window) => {
const webidl = window.__bootstrap.webidl;
const { CryptoKey } = window.__bootstrap.crypto;
const { ArrayBufferIsView, ArrayBuffer } = window.__bootstrap.primordials;
webidl.converters.AlgorithmIdentifier = (V, opts) => {
// Union for (object or DOMString)
@ -18,6 +19,14 @@
return webidl.converters.DOMString(V, opts);
};
webidl.converters["BufferSource or JsonWebKey"] = (V, opts) => {
// Union for (BufferSource or JsonWebKey)
if (ArrayBufferIsView(V) || V instanceof ArrayBuffer) {
return webidl.converters.BufferSource(V, opts);
}
return webidl.converters.JsonWebKey(V, opts);
};
webidl.converters.KeyType = webidl.createEnumConverter("KeyType", [
"public",
"private",
@ -178,6 +187,115 @@
webidl.converters.HmacImportParams = webidl
.createDictionaryConverter("HmacImportParams", dictHmacImportParams);
const dictRsaOtherPrimesInfo = [
{
key: "r",
converter: webidl.converters["DOMString"],
},
{
key: "d",
converter: webidl.converters["DOMString"],
},
{
key: "t",
converter: webidl.converters["DOMString"],
},
];
webidl.converters.RsaOtherPrimesInfo = webidl.createDictionaryConverter(
"RsaOtherPrimesInfo",
dictRsaOtherPrimesInfo,
);
webidl.converters["sequence<RsaOtherPrimesInfo>"] = webidl
.createSequenceConverter(
webidl.converters.RsaOtherPrimesInfo,
);
const dictJsonWebKey = [
// Sections 4.2 and 4.3 of RFC7517.
// https://datatracker.ietf.org/doc/html/rfc7517#section-4
{
key: "kty",
converter: webidl.converters["DOMString"],
},
{
key: "use",
converter: webidl.converters["DOMString"],
},
{
key: "key_ops",
converter: webidl.converters["sequence<DOMString>"],
},
{
key: "alg",
converter: webidl.converters["DOMString"],
},
// JSON Web Key Parameters Registration
{
key: "ext",
converter: webidl.converters["boolean"],
},
// Section 6 of RFC7518 JSON Web Algorithms
// https://datatracker.ietf.org/doc/html/rfc7518#section-6
{
key: "crv",
converter: webidl.converters["DOMString"],
},
{
key: "x",
converter: webidl.converters["DOMString"],
},
{
key: "y",
converter: webidl.converters["DOMString"],
},
{
key: "d",
converter: webidl.converters["DOMString"],
},
{
key: "n",
converter: webidl.converters["DOMString"],
},
{
key: "e",
converter: webidl.converters["DOMString"],
},
{
key: "p",
converter: webidl.converters["DOMString"],
},
{
key: "q",
converter: webidl.converters["DOMString"],
},
{
key: "dp",
converter: webidl.converters["DOMString"],
},
{
key: "dq",
converter: webidl.converters["DOMString"],
},
{
key: "qi",
converter: webidl.converters["DOMString"],
},
{
key: "oth",
converter: webidl.converters["sequence<RsaOtherPrimesInfo>"],
},
{
key: "k",
converter: webidl.converters["DOMString"],
},
];
webidl.converters.JsonWebKey = webidl.createDictionaryConverter(
"JsonWebKey",
dictJsonWebKey,
);
const dictPbkdf2Params = [
...dictAlgorithm,
{

View file

@ -28,6 +28,34 @@ type KeyUsage =
type NamedCurve = string;
interface RsaOtherPrimesInfo {
d?: string;
r?: string;
t?: string;
}
interface JsonWebKey {
alg?: string;
crv?: string;
d?: string;
dp?: string;
dq?: string;
e?: string;
ext?: boolean;
k?: string;
// deno-lint-ignore camelcase
key_ops?: string[];
kty?: string;
n?: string;
oth?: RsaOtherPrimesInfo[];
p?: string;
q?: string;
qi?: string;
use?: string;
x?: string;
y?: string;
}
interface HmacKeyGenParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
@ -122,6 +150,13 @@ interface SubtleCrypto {
extractable: boolean,
keyUsages: KeyUsage[],
): Promise<CryptoKeyPair | CryptoKey>;
importKey(
format: "jwk",
keyData: JsonWebKey,
algorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: KeyUsage[],
): Promise<CryptoKey>;
importKey(
format: "raw",
keyData: BufferSource,

View file

@ -14041,42 +14041,6 @@
"Good parameters: 256 bits (jwk, {alg: A256KW, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {name: AES-KW}, true, [unwrapKey])",
"Good parameters: 256 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 16: 17, 17: 18, 18: 19, 19: 20, 2: 3, 20: 21, 21: 22, 22: 23, 23: 24, 24: 25, 25: 26, 26: 27, 27: 28, 28: 29, 29: 30, 3: 4, 30: 31, 31: 32, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: AES-KW}, false, [unwrapKey])",
"Good parameters: 256 bits (jwk, {alg: A256KW, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {name: AES-KW}, false, [unwrapKey])",
"Good parameters: 128 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [sign])",
"Good parameters: 128 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify, sign])",
"Good parameters: 128 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify])",
"Good parameters: 192 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-1, name: HMAC}, false, [sign])",
"Good parameters: 192 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify, sign])",
"Good parameters: 192 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify])",
"Good parameters: 256 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [sign])",
"Good parameters: 256 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify, sign])",
"Good parameters: 256 bits (jwk, {alg: HS1, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-1, name: HMAC}, false, [verify])",
"Good parameters: 128 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [sign])",
"Good parameters: 128 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify, sign])",
"Good parameters: 128 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify])",
"Good parameters: 192 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-256, name: HMAC}, false, [sign])",
"Good parameters: 192 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify, sign])",
"Good parameters: 192 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify])",
"Good parameters: 256 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [sign])",
"Good parameters: 256 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify, sign])",
"Good parameters: 256 bits (jwk, {alg: HS256, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-256, name: HMAC}, false, [verify])",
"Good parameters: 128 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [sign])",
"Good parameters: 128 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify, sign])",
"Good parameters: 128 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify])",
"Good parameters: 192 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-384, name: HMAC}, false, [sign])",
"Good parameters: 192 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify, sign])",
"Good parameters: 192 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify])",
"Good parameters: 256 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [sign])",
"Good parameters: 256 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify, sign])",
"Good parameters: 256 bits (jwk, {alg: HS384, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-384, name: HMAC}, false, [verify])",
"Good parameters: 128 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [sign])",
"Good parameters: 128 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify, sign])",
"Good parameters: 128 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify])",
"Good parameters: 192 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-512, name: HMAC}, false, [sign])",
"Good parameters: 192 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify, sign])",
"Good parameters: 192 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify])",
"Good parameters: 256 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [sign])",
"Good parameters: 256 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify, sign])",
"Good parameters: 256 bits (jwk, {alg: HS512, k: AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA, kty: oct}, {hash: SHA-512, name: HMAC}, false, [verify])",
"Good parameters: 128 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: HKDF}, false, [deriveBits])",
"Good parameters: 128 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: HKDF}, false, [deriveKey, deriveBits])",
"Good parameters: 128 bits (raw, {0: 1, 1: 2, 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}, {name: HKDF}, false, [deriveKey])",