diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2021-08-11 12:27:05 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-11 12:27:05 +0200 |
commit | a0285e2eb88f6254f6494b0ecd1878db3a3b2a58 (patch) | |
tree | 90671b004537e20f9493fd3277ffd21d30b39a0e /ext/crypto | |
parent | 3a6994115176781b3a93d70794b1b81bc95e42b4 (diff) |
Rename extensions/ directory to ext/ (#11643)
Diffstat (limited to 'ext/crypto')
-rw-r--r-- | ext/crypto/00_crypto.js | 1013 | ||||
-rw-r--r-- | ext/crypto/01_webidl.js | 188 | ||||
-rw-r--r-- | ext/crypto/Cargo.toml | 28 | ||||
-rw-r--r-- | ext/crypto/README.md | 5 | ||||
-rw-r--r-- | ext/crypto/key.rs | 117 | ||||
-rw-r--r-- | ext/crypto/lib.deno_crypto.d.ts | 155 | ||||
-rw-r--r-- | ext/crypto/lib.rs | 558 |
7 files changed, 2064 insertions, 0 deletions
diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js new file mode 100644 index 000000000..449946295 --- /dev/null +++ b/ext/crypto/00_crypto.js @@ -0,0 +1,1013 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// <reference path="../../core/internal.d.ts" /> +/// <reference path="../../core/lib.deno_core.d.ts" /> +/// <reference path="../webidl/internal.d.ts" /> +/// <reference path="../web/lib.deno_web.d.ts" /> + +"use strict"; + +((window) => { + const core = window.Deno.core; + const webidl = window.__bootstrap.webidl; + const { DOMException } = window.__bootstrap.domException; + + const { + ArrayPrototypeFind, + ArrayBufferIsView, + ArrayPrototypeIncludes, + BigInt64Array, + StringPrototypeToUpperCase, + Symbol, + SymbolFor, + SymbolToStringTag, + WeakMap, + WeakMapPrototypeGet, + WeakMapPrototypeSet, + Int8Array, + Uint8Array, + TypedArrayPrototypeSlice, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Uint8ClampedArray, + TypeError, + } = window.__bootstrap.primordials; + + // P-521 is not yet supported. + const supportedNamedCurves = ["P-256", "P-384"]; + const recognisedUsages = [ + "encrypt", + "decrypt", + "sign", + "verify", + "deriveKey", + "deriveBits", + "wrapKey", + "unwrapKey", + ]; + + const simpleAlgorithmDictionaries = { + RsaHashedKeyGenParams: { hash: "HashAlgorithmIdentifier" }, + EcKeyGenParams: {}, + HmacKeyGenParams: { hash: "HashAlgorithmIdentifier" }, + RsaPssParams: {}, + EcdsaParams: { hash: "HashAlgorithmIdentifier" }, + HmacImportParams: { hash: "HashAlgorithmIdentifier" }, + }; + + const supportedAlgorithms = { + "digest": { + "SHA-1": null, + "SHA-256": null, + "SHA-384": null, + "SHA-512": null, + }, + "generateKey": { + "RSASSA-PKCS1-v1_5": "RsaHashedKeyGenParams", + "RSA-PSS": "RsaHashedKeyGenParams", + "ECDSA": "EcKeyGenParams", + "HMAC": "HmacKeyGenParams", + }, + "sign": { + "RSASSA-PKCS1-v1_5": null, + "RSA-PSS": "RsaPssParams", + "ECDSA": "EcdsaParams", + "HMAC": null, + }, + "verify": { + "RSASSA-PKCS1-v1_5": null, + "RSA-PSS": "RsaPssParams", + "HMAC": null, + }, + "importKey": { + "HMAC": "HmacImportParams", + }, + }; + + // See https://www.w3.org/TR/WebCryptoAPI/#dfn-normalize-an-algorithm + function normalizeAlgorithm(algorithm, op) { + if (typeof algorithm == "string") { + return normalizeAlgorithm({ name: algorithm }, op); + } + + // 1. + const registeredAlgorithms = supportedAlgorithms[op]; + // 2. 3. + const initialAlg = webidl.converters.Algorithm(algorithm, { + prefix: "Failed to normalize algorithm", + context: "passed algorithm", + }); + // 4. + let algName = initialAlg.name; + + // 5. + let desiredType = undefined; + for (const key in registeredAlgorithms) { + if ( + StringPrototypeToUpperCase(key) === StringPrototypeToUpperCase(algName) + ) { + algName = key; + desiredType = registeredAlgorithms[key]; + } + } + if (desiredType === undefined) { + throw new DOMException( + "Unrecognized algorithm name", + "NotSupportedError", + ); + } + + // Fast path everything below if the registered dictionary is "None". + if (desiredType === null) { + return { name: algName }; + } + + const normalizedAlgorithm = webidl.converters[desiredType](algorithm, { + prefix: "Failed to normalize algorithm", + context: "passed algorithm", + }); + normalizedAlgorithm.name = algName; + + const dict = simpleAlgorithmDictionaries[desiredType]; + for (const member in dict) { + const idlType = dict[member]; + const idlValue = normalizedAlgorithm[member]; + + if (idlType === "BufferSource") { + normalizedAlgorithm[member] = new Uint8Array( + TypedArrayPrototypeSlice( + (ArrayBufferIsView(idlValue) ? idlValue.buffer : idlValue), + idlValue.byteOffset ?? 0, + idlValue.byteLength, + ), + ); + } else if (idlType === "HashAlgorithmIdentifier") { + normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, "digest"); + } else if (idlType === "AlgorithmIdentifier") { + // TODO(lucacasonato): implement + throw new TypeError("unimplemented"); + } + } + + return normalizedAlgorithm; + } + + const _handle = Symbol("[[handle]]"); + const _algorithm = Symbol("[[algorithm]]"); + const _extractable = Symbol("[[extractable]]"); + const _usages = Symbol("[[usages]]"); + const _type = Symbol("[[type]]"); + + class CryptoKey { + /** @type {string} */ + [_type]; + /** @type {boolean} */ + [_extractable]; + /** @type {object} */ + [_algorithm]; + /** @type {string[]} */ + [_usages]; + /** @type {object} */ + [_handle]; + + constructor() { + webidl.illegalConstructor(); + } + + /** @returns {string} */ + get type() { + webidl.assertBranded(this, CryptoKey); + return this[_type]; + } + + /** @returns {boolean} */ + get extractable() { + webidl.assertBranded(this, CryptoKey); + return this[_extractable]; + } + + /** @returns {string[]} */ + get usages() { + webidl.assertBranded(this, CryptoKey); + // TODO(lucacasonato): return a SameObject copy + return this[_usages]; + } + + /** @returns {object} */ + get algorithm() { + webidl.assertBranded(this, CryptoKey); + // TODO(lucacasonato): return a SameObject copy + return this[_algorithm]; + } + + get [SymbolToStringTag]() { + return "CryptoKey"; + } + + [SymbolFor("Deno.customInspect")](inspect) { + return `${this.constructor.name} ${ + inspect({ + type: this.type, + extractable: this.extractable, + algorithm: this.algorithm, + usages: this.usages, + }) + }`; + } + } + + webidl.configurePrototype(CryptoKey); + + /** + * @param {string} type + * @param {boolean} extractable + * @param {string[]} usages + * @param {object} algorithm + * @param {object} handle + * @returns + */ + function constructKey(type, extractable, usages, algorithm, handle) { + const key = webidl.createBranded(CryptoKey); + key[_type] = type; + key[_extractable] = extractable; + key[_usages] = usages; + key[_algorithm] = algorithm; + key[_handle] = handle; + return key; + } + + // https://w3c.github.io/webcrypto/#concept-usage-intersection + /** + * @param {string[]} a + * @param {string[]} b + * @returns + */ + function usageIntersection(a, b) { + return a.filter((i) => b.includes(i)); + } + + // TODO(lucacasonato): this should be moved to rust + /** @type {WeakMap<object, object>} */ + const KEY_STORE = new WeakMap(); + + class SubtleCrypto { + constructor() { + webidl.illegalConstructor(); + } + + /** + * @param {string} algorithm + * @param {BufferSource} data + * @returns {Promise<Uint8Array>} + */ + async digest(algorithm, data) { + webidl.assertBranded(this, SubtleCrypto); + const prefix = "Failed to execute 'digest' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + prefix, + context: "Argument 1", + }); + data = webidl.converters.BufferSource(data, { + prefix, + context: "Argument 2", + }); + + if (ArrayBufferIsView(data)) { + data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + } else { + data = new Uint8Array(data); + } + + data = TypedArrayPrototypeSlice(data); + + algorithm = normalizeAlgorithm(algorithm, "digest"); + + const result = await core.opAsync( + "op_crypto_subtle_digest", + algorithm.name, + data, + ); + + return result.buffer; + } + + /** + * @param {string} algorithm + * @param {CryptoKey} key + * @param {BufferSource} data + * @returns {Promise<any>} + */ + async sign(algorithm, key, data) { + webidl.assertBranded(this, SubtleCrypto); + const prefix = "Failed to execute 'sign' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 3, { prefix }); + algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + prefix, + context: "Argument 1", + }); + key = webidl.converters.CryptoKey(key, { + prefix, + context: "Argument 2", + }); + data = webidl.converters.BufferSource(data, { + prefix, + context: "Argument 3", + }); + + // 1. + if (ArrayBufferIsView(data)) { + data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + } else { + data = new Uint8Array(data); + } + data = TypedArrayPrototypeSlice(data); + + // 2. + const normalizedAlgorithm = normalizeAlgorithm(algorithm, "sign"); + + const handle = key[_handle]; + const keyData = WeakMapPrototypeGet(KEY_STORE, handle); + + // 8. + if (normalizedAlgorithm.name !== key[_algorithm].name) { + throw new DOMException( + "Signing algorithm doesn't match key algorithm.", + "InvalidAccessError", + ); + } + + // 9. + if (!ArrayPrototypeIncludes(key[_usages], "sign")) { + throw new DOMException( + "Key does not support the 'sign' operation.", + "InvalidAccessError", + ); + } + + switch (normalizedAlgorithm.name) { + case "RSASSA-PKCS1-v1_5": { + // 1. + if (key[_type] !== "private") { + throw new DOMException( + "Key type not supported", + "InvalidAccessError", + ); + } + + // 2. + const hashAlgorithm = key[_algorithm].hash.name; + const signature = await core.opAsync("op_crypto_sign_key", { + key: keyData, + algorithm: "RSASSA-PKCS1-v1_5", + hash: hashAlgorithm, + }, data); + + return signature.buffer; + } + case "RSA-PSS": { + // 1. + if (key[_type] !== "private") { + throw new DOMException( + "Key type not supported", + "InvalidAccessError", + ); + } + + // 2. + const hashAlgorithm = key[_algorithm].hash.name; + const signature = await core.opAsync("op_crypto_sign_key", { + key: keyData, + algorithm: "RSA-PSS", + hash: hashAlgorithm, + saltLength: normalizedAlgorithm.saltLength, + }, data); + + return signature.buffer; + } + case "ECDSA": { + // 1. + if (key[_type] !== "private") { + throw new DOMException( + "Key type not supported", + "InvalidAccessError", + ); + } + + // 2. + const hashAlgorithm = normalizedAlgorithm.hash.name; + const namedCurve = key[_algorithm].namedCurve; + if (!ArrayPrototypeIncludes(supportedNamedCurves, namedCurve)) { + throw new DOMException("Curve not supported", "NotSupportedError"); + } + + const signature = await core.opAsync("op_crypto_sign_key", { + key: keyData, + algorithm: "ECDSA", + hash: hashAlgorithm, + namedCurve, + }, data); + + return signature.buffer; + } + case "HMAC": { + const hashAlgorithm = key[_algorithm].hash.name; + + const signature = await core.opAsync("op_crypto_sign_key", { + key: keyData, + algorithm: "HMAC", + hash: hashAlgorithm, + }, data); + + return signature.buffer; + } + } + + throw new TypeError("unreachable"); + } + + /** + * @param {string} format + * @param {BufferSource} keyData + * @param {string} algorithm + * @param {boolean} extractable + * @param {KeyUsages[]} keyUsages + * @returns {Promise<any>} + */ + // deno-lint-ignore require-await + async importKey(format, keyData, algorithm, extractable, keyUsages) { + webidl.assertBranded(this, SubtleCrypto); + const prefix = "Failed to execute 'importKey' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 4, { prefix }); + format = webidl.converters.KeyFormat(format, { + prefix, + context: "Argument 1", + }); + keyData = webidl.converters.BufferSource(keyData, { + prefix, + context: "Argument 2", + }); + algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + prefix, + context: "Argument 3", + }); + extractable = webidl.converters.boolean(extractable, { + prefix, + context: "Argument 4", + }); + keyUsages = webidl.converters["sequence<KeyUsage>"](keyUsages, { + prefix, + context: "Argument 5", + }); + + const normalizedAlgorithm = normalizeAlgorithm(algorithm, "importKey"); + + if ( + ArrayPrototypeFind( + keyUsages, + (u) => !ArrayPrototypeIncludes(["sign", "verify"], u), + ) !== undefined + ) { + throw new DOMException("Invalid key usages", "SyntaxError"); + } + + switch (normalizedAlgorithm.name) { + // https://w3c.github.io/webcrypto/#hmac-operations + case "HMAC": { + 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"); + } + if (normalizeAlgorithm.length) { + // 7. + if ( + normalizedAlgorithm.length > length || + normalizedAlgorithm.length <= (length - 8) + ) { + throw new DOMException( + "Key length is invalid", + "DataError", + ); + } + length = normalizeAlgorithm.length; + } + + if (keyUsages.length == 0) { + throw new DOMException("Key usage is empty", "SyntaxError"); + } + + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, { + type: "raw", + data: keyData, + }); + + const algorithm = { + name: "HMAC", + length, + hash, + }; + + const key = constructKey( + "secret", + true, + usageIntersection(keyUsages, recognisedUsages), + algorithm, + handle, + ); + + return key; + } + // TODO(@littledivy): jwk + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } + } + // TODO(@littledivy): RSASSA-PKCS1-v1_5 + // TODO(@littledivy): RSA-PSS + // TODO(@littledivy): ECDSA + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } + } + + /** + * @param {string} format + * @param {CryptoKey} key + * @returns {Promise<any>} + */ + // deno-lint-ignore require-await + async exportKey(format, key) { + webidl.assertBranded(this, SubtleCrypto); + const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + format = webidl.converters.KeyFormat(format, { + prefix, + context: "Argument 1", + }); + key = webidl.converters.CryptoKey(key, { + prefix, + context: "Argument 2", + }); + + const handle = key[_handle]; + // 2. + const bits = WeakMapPrototypeGet(KEY_STORE, handle); + + switch (key[_algorithm].name) { + case "HMAC": { + if (bits == null) { + throw new DOMException("Key is not available", "OperationError"); + } + switch (format) { + // 3. + case "raw": { + for (let _i = 7 & (8 - bits.length % 8); _i > 0; _i--) { + bits.push(0); + } + // 4-5. + return bits.buffer; + } + // TODO(@littledivy): jwk + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } + } + // TODO(@littledivy): RSASSA-PKCS1-v1_5 + // TODO(@littledivy): RSA-PSS + // TODO(@littledivy): ECDSA + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } + } + + /** + * @param {string} algorithm + * @param {CryptoKey} key + * @param {BufferSource} signature + * @param {BufferSource} data + * @returns {Promise<boolean>} + */ + async verify(algorithm, key, signature, data) { + webidl.assertBranded(this, SubtleCrypto); + const prefix = "Failed to execute 'verify' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 4, { prefix }); + algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + prefix, + context: "Argument 1", + }); + key = webidl.converters.CryptoKey(key, { + prefix, + context: "Argument 2", + }); + signature = webidl.converters.BufferSource(signature, { + prefix, + context: "Argument 3", + }); + data = webidl.converters.BufferSource(data, { + prefix, + context: "Argument 4", + }); + + // 2. + if (ArrayBufferIsView(signature)) { + signature = new Uint8Array( + signature.buffer, + signature.byteOffset, + signature.byteLength, + ); + } else { + signature = new Uint8Array(signature); + } + signature = TypedArrayPrototypeSlice(signature); + + // 3. + if (ArrayBufferIsView(data)) { + data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + } else { + data = new Uint8Array(data); + } + data = TypedArrayPrototypeSlice(data); + + const normalizedAlgorithm = normalizeAlgorithm(algorithm, "verify"); + + const handle = key[_handle]; + const keyData = WeakMapPrototypeGet(KEY_STORE, handle); + + if (normalizedAlgorithm.name !== key[_algorithm].name) { + throw new DOMException( + "Verifying algorithm doesn't match key algorithm.", + "InvalidAccessError", + ); + } + + if (!ArrayPrototypeIncludes(key[_usages], "verify")) { + throw new DOMException( + "Key does not support the 'verify' operation.", + "InvalidAccessError", + ); + } + + switch (normalizedAlgorithm.name) { + case "RSASSA-PKCS1-v1_5": { + if (key[_type] !== "public") { + throw new DOMException( + "Key type not supported", + "InvalidAccessError", + ); + } + + const hashAlgorithm = key[_algorithm].hash.name; + return await core.opAsync("op_crypto_verify_key", { + key: keyData, + algorithm: "RSASSA-PKCS1-v1_5", + hash: hashAlgorithm, + signature, + }, data); + } + case "RSA-PSS": { + if (key[_type] !== "public") { + throw new DOMException( + "Key type not supported", + "InvalidAccessError", + ); + } + + const hashAlgorithm = key[_algorithm].hash.name; + const saltLength = normalizedAlgorithm.saltLength; + return await core.opAsync("op_crypto_verify_key", { + key: keyData, + algorithm: "RSA-PSS", + hash: hashAlgorithm, + saltLength, + signature, + }, data); + } + case "HMAC": { + const hash = key[_algorithm].hash.name; + return await core.opAsync("op_crypto_verify_key", { + key: keyData, + algorithm: "HMAC", + hash, + signature, + }, data); + } + } + + throw new TypeError("unreachable"); + } + + /** + * @param {string} algorithm + * @param {boolean} extractable + * @param {KeyUsage[]} keyUsages + * @returns {Promise<any>} + */ + async generateKey(algorithm, extractable, keyUsages) { + webidl.assertBranded(this, SubtleCrypto); + const prefix = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 3, { prefix }); + algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { + prefix, + context: "Argument 1", + }); + extractable = webidl.converters["boolean"](extractable, { + prefix, + context: "Argument 2", + }); + keyUsages = webidl.converters["sequence<KeyUsage>"](keyUsages, { + prefix, + context: "Argument 3", + }); + + const usages = keyUsages; + + const normalizedAlgorithm = normalizeAlgorithm(algorithm, "generateKey"); + + // https://github.com/denoland/deno/pull/9614#issuecomment-866049433 + if (!extractable) { + throw new DOMException( + "Non-extractable keys are not supported", + "SecurityError", + ); + } + + const result = await generateKey( + normalizedAlgorithm, + extractable, + usages, + ); + + if (result instanceof CryptoKey) { + const type = result[_type]; + if ((type === "secret" || type === "private") && usages.length === 0) { + throw new DOMException("Invalid key usages", "SyntaxError"); + } + } else if (result.privateKey instanceof CryptoKey) { + if (result.privateKey[_usages].length === 0) { + throw new DOMException("Invalid key usages", "SyntaxError"); + } + } + + return result; + } + + get [SymbolToStringTag]() { + return "SubtleCrypto"; + } + } + + async function generateKey(normalizedAlgorithm, extractable, usages) { + switch (normalizedAlgorithm.name) { + case "RSASSA-PKCS1-v1_5": + case "RSA-PSS": { + // 1. + if ( + ArrayPrototypeFind( + usages, + (u) => !ArrayPrototypeIncludes(["sign", "verify"], u), + ) !== undefined + ) { + throw new DOMException("Invalid key usages", "SyntaxError"); + } + + // 2. + const keyData = await core.opAsync( + "op_crypto_generate_key", + { + name: normalizedAlgorithm.name, + modulusLength: normalizedAlgorithm.modulusLength, + publicExponent: normalizedAlgorithm.publicExponent, + }, + ); + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, { + type: "pkcs8", + data: keyData, + }); + + // 4-8. + const algorithm = { + name: normalizedAlgorithm.name, + modulusLength: normalizedAlgorithm.modulusLength, + publicExponent: normalizedAlgorithm.publicExponent, + hash: normalizedAlgorithm.hash, + }; + + // 9-13. + const publicKey = constructKey( + "public", + true, + usageIntersection(usages, ["verify"]), + algorithm, + handle, + ); + + // 14-18. + const privateKey = constructKey( + "private", + extractable, + usageIntersection(usages, ["sign"]), + algorithm, + handle, + ); + + // 19-22. + return { publicKey, privateKey }; + } + // TODO(lucacasonato): RSA-OAEP + case "ECDSA": { + // 1. + if ( + ArrayPrototypeFind( + usages, + (u) => !ArrayPrototypeIncludes(["sign", "verify"], u), + ) !== undefined + ) { + throw new DOMException("Invalid key usages", "SyntaxError"); + } + + // 2-3. + const handle = {}; + if ( + ArrayPrototypeIncludes( + supportedNamedCurves, + normalizedAlgorithm.namedCurve, + ) + ) { + const keyData = await core.opAsync("op_crypto_generate_key", { + name: "ECDSA", + namedCurve: normalizedAlgorithm.namedCurve, + }); + WeakMapPrototypeSet(KEY_STORE, handle, { + type: "pkcs8", + data: keyData, + }); + } else { + throw new DOMException("Curve not supported", "NotSupportedError"); + } + + // 4-6. + const algorithm = { + name: "ECDSA", + namedCurve: normalizedAlgorithm.namedCurve, + }; + + // 7-11. + const publicKey = constructKey( + "public", + true, + usageIntersection(usages, ["verify"]), + algorithm, + handle, + ); + + // 12-16. + const privateKey = constructKey( + "private", + extractable, + usageIntersection(usages, ["sign"]), + algorithm, + handle, + ); + + // 17-20. + return { publicKey, privateKey }; + } + // TODO(lucacasonato): ECDH + // TODO(lucacasonato): AES-CTR + // TODO(lucacasonato): AES-CBC + // TODO(lucacasonato): AES-GCM + // TODO(lucacasonato): AES-KW + case "HMAC": { + // 1. + if ( + ArrayPrototypeFind( + usages, + (u) => !ArrayPrototypeIncludes(["sign", "verify"], u), + ) !== undefined + ) { + throw new DOMException("Invalid key usages", "SyntaxError"); + } + + // 2. + let length; + if (normalizedAlgorithm.length === undefined) { + length = null; + } else if (normalizedAlgorithm.length !== 0) { + length = normalizedAlgorithm.length; + } else { + throw new DOMException("Invalid length", "OperationError"); + } + + // 3-4. + const keyData = await core.opAsync("op_crypto_generate_key", { + name: "HMAC", + hash: normalizedAlgorithm.hash.name, + length, + }); + const handle = {}; + WeakMapPrototypeSet(KEY_STORE, handle, { type: "raw", data: keyData }); + + // 6-10. + const algorithm = { + name: "HMAC", + hash: { + name: normalizedAlgorithm.hash.name, + }, + length: keyData.byteLength * 8, + }; + + // 5, 11-13. + const key = constructKey( + "secret", + extractable, + usages, + algorithm, + handle, + ); + + // 14. + return key; + } + } + } + + const subtle = webidl.createBranded(SubtleCrypto); + + class Crypto { + constructor() { + webidl.illegalConstructor(); + } + + getRandomValues(arrayBufferView) { + webidl.assertBranded(this, Crypto); + const prefix = "Failed to execute 'getRandomValues' on 'Crypto'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + arrayBufferView = webidl.converters.ArrayBufferView(arrayBufferView, { + prefix, + context: "Argument 1", + }); + if ( + !( + arrayBufferView instanceof Int8Array || + arrayBufferView instanceof Uint8Array || + arrayBufferView instanceof Uint8ClampedArray || + arrayBufferView instanceof Int16Array || + arrayBufferView instanceof Uint16Array || + arrayBufferView instanceof Int32Array || + arrayBufferView instanceof Uint32Array || + arrayBufferView instanceof BigInt64Array || + arrayBufferView instanceof BigUint64Array + ) + ) { + throw new DOMException( + "The provided ArrayBufferView is not an integer array type", + "TypeMismatchError", + ); + } + const ui8 = new Uint8Array( + arrayBufferView.buffer, + arrayBufferView.byteOffset, + arrayBufferView.byteLength, + ); + core.opSync("op_crypto_get_random_values", ui8); + return arrayBufferView; + } + + randomUUID() { + webidl.assertBranded(this, Crypto); + return core.opSync("op_crypto_random_uuid"); + } + + get subtle() { + webidl.assertBranded(this, Crypto); + return subtle; + } + + get [SymbolToStringTag]() { + return "Crypto"; + } + + [SymbolFor("Deno.customInspect")](inspect) { + return `${this.constructor.name} ${inspect({})}`; + } + } + + webidl.configurePrototype(Crypto); + + window.__bootstrap.crypto = { + SubtleCrypto, + crypto: webidl.createBranded(Crypto), + Crypto, + CryptoKey, + }; +})(this); diff --git a/ext/crypto/01_webidl.js b/ext/crypto/01_webidl.js new file mode 100644 index 000000000..7e78170b4 --- /dev/null +++ b/ext/crypto/01_webidl.js @@ -0,0 +1,188 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// <reference path="../../core/lib.deno_core.d.ts" /> +/// <reference path="../webidl/internal.d.ts" /> + +"use strict"; + +((window) => { + const webidl = window.__bootstrap.webidl; + const { CryptoKey } = window.__bootstrap.crypto; + + webidl.converters.AlgorithmIdentifier = (V, opts) => { + // Union for (object or DOMString) + if (webidl.type(V) == "Object") { + return webidl.converters.object(V, opts); + } + return webidl.converters.DOMString(V, opts); + }; + + webidl.converters.KeyType = webidl.createEnumConverter("KeyType", [ + "public", + "private", + "secret", + ]); + + webidl.converters.KeyFormat = webidl.createEnumConverter("KeyFormat", [ + "raw", + "pkcs8", + "spki", + "jwk", + ]); + + webidl.converters.KeyUsage = webidl.createEnumConverter("KeyUsage", [ + "encrypt", + "decrypt", + "sign", + "verify", + "deriveKey", + "deriveBits", + "wrapKey", + "unwrapKey", + ]); + + webidl.converters["sequence<KeyUsage>"] = webidl.createSequenceConverter( + webidl.converters.KeyUsage, + ); + + webidl.converters.HashAlgorithmIdentifier = + webidl.converters.AlgorithmIdentifier; + + /** @type {__bootstrap.webidl.Dictionary} */ + const dictAlgorithm = [{ + key: "name", + converter: webidl.converters.DOMString, + required: true, + }]; + + webidl.converters.Algorithm = webidl + .createDictionaryConverter("Algorithm", dictAlgorithm); + + webidl.converters.BigInteger = webidl.converters.Uint8Array; + + /** @type {__bootstrap.webidl.Dictionary} */ + const dictRsaKeyGenParams = [ + ...dictAlgorithm, + { + key: "modulusLength", + converter: (V, opts) => + webidl.converters["unsigned long"](V, { ...opts, enforceRange: true }), + required: true, + }, + { + key: "publicExponent", + converter: webidl.converters.BigInteger, + required: true, + }, + ]; + + webidl.converters.RsaKeyGenParams = webidl + .createDictionaryConverter("RsaKeyGenParams", dictRsaKeyGenParams); + + const dictRsaHashedKeyGenParams = [ + ...dictRsaKeyGenParams, + { + key: "hash", + converter: webidl.converters.HashAlgorithmIdentifier, + required: true, + }, + ]; + + webidl.converters.RsaHashedKeyGenParams = webidl.createDictionaryConverter( + "RsaHashedKeyGenParams", + dictRsaHashedKeyGenParams, + ); + + webidl.converters.NamedCurve = webidl.converters.DOMString; + + const dictEcKeyGenParams = [ + ...dictAlgorithm, + { + key: "namedCurve", + converter: webidl.converters.NamedCurve, + required: true, + }, + ]; + + webidl.converters.EcKeyGenParams = webidl + .createDictionaryConverter("EcKeyGenParams", dictEcKeyGenParams); + + const dictHmacKeyGenParams = [ + ...dictAlgorithm, + { + key: "hash", + converter: webidl.converters.HashAlgorithmIdentifier, + required: true, + }, + { + key: "length", + converter: (V, opts) => + webidl.converters["unsigned long"](V, { ...opts, enforceRange: true }), + }, + ]; + + webidl.converters.HmacKeyGenParams = webidl + .createDictionaryConverter("HmacKeyGenParams", dictHmacKeyGenParams); + + const dictRsaPssParams = [ + ...dictAlgorithm, + { + key: "saltLength", + converter: (V, opts) => + webidl.converters["unsigned long"](V, { ...opts, enforceRange: true }), + required: true, + }, + ]; + + webidl.converters.RsaPssParams = webidl + .createDictionaryConverter("RsaPssParams", dictRsaPssParams); + + const dictEcdsaParams = [ + ...dictAlgorithm, + { + key: "hash", + converter: webidl.converters.HashAlgorithmIdentifier, + required: true, + }, + ]; + + webidl.converters["EcdsaParams"] = webidl + .createDictionaryConverter("EcdsaParams", dictEcdsaParams); + + const dictHmacImportParams = [ + ...dictAlgorithm, + { + key: "hash", + converter: webidl.converters.HashAlgorithmIdentifier, + required: true, + }, + { + key: "length", + converter: (V, opts) => + webidl.converters["unsigned long"](V, { ...opts, enforceRange: true }), + }, + ]; + + webidl.converters.HmacImportParams = webidl + .createDictionaryConverter("HmacImportParams", dictHmacImportParams); + + webidl.converters.CryptoKey = webidl.createInterfaceConverter( + "CryptoKey", + CryptoKey, + ); + + const dictCryptoKeyPair = [ + { + key: "publicKey", + converter: webidl.converters.CryptoKey, + }, + { + key: "privateKey", + converter: webidl.converters.CryptoKey, + }, + ]; + + webidl.converters.CryptoKeyPair = webidl + .createDictionaryConverter("CryptoKeyPair", dictCryptoKeyPair); +})(this); diff --git a/ext/crypto/Cargo.toml b/ext/crypto/Cargo.toml new file mode 100644 index 000000000..8eb939e86 --- /dev/null +++ b/ext/crypto/Cargo.toml @@ -0,0 +1,28 @@ +# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_crypto" +version = "0.28.0" +authors = ["the Deno authors"] +edition = "2018" +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" +description = "Web Cryptography API implementation for Deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.96.0", path = "../../core" } +deno_web = { version = "0.45.0", path = "../web" } +lazy_static = "1.4.0" +num-traits = "0.2.14" +rand = "0.8.4" +ring = { version = "0.16.20", features = ["std"] } +rsa = { version = "0.5.0", default-features = false, features = ["std"] } +serde = { version = "1.0.126", features = ["derive"] } +sha-1 = "0.9.7" +sha2 = "0.9.5" +tokio = { version = "1.8.1", features = ["full"] } +uuid = { version = "0.8.2", features = ["v4"] } diff --git a/ext/crypto/README.md b/ext/crypto/README.md new file mode 100644 index 000000000..be0724458 --- /dev/null +++ b/ext/crypto/README.md @@ -0,0 +1,5 @@ +# deno_crypto + +This crate implements the Web Cryptography API. + +Spec: https://www.w3.org/TR/WebCryptoAPI/ diff --git a/ext/crypto/key.rs b/ext/crypto/key.rs new file mode 100644 index 000000000..cb44812fd --- /dev/null +++ b/ext/crypto/key.rs @@ -0,0 +1,117 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use ring::agreement::Algorithm as RingAlgorithm; +use ring::digest; +use ring::hmac::Algorithm as HmacAlgorithm; +use ring::signature::EcdsaSigningAlgorithm; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Serialize, Deserialize, Copy, Clone)] +#[serde(rename_all = "camelCase")] +pub enum KeyType { + Public, + Private, + Secret, +} + +#[derive(Serialize, Deserialize, Copy, Clone)] +pub enum CryptoHash { + #[serde(rename = "SHA-1")] + Sha1, + #[serde(rename = "SHA-256")] + Sha256, + #[serde(rename = "SHA-384")] + Sha384, + #[serde(rename = "SHA-512")] + Sha512, +} + +#[derive(Serialize, Deserialize, Copy, Clone)] +pub enum CryptoNamedCurve { + #[serde(rename = "P-256")] + P256, + #[serde(rename = "P-384")] + P384, +} + +impl From<CryptoNamedCurve> for &RingAlgorithm { + fn from(curve: CryptoNamedCurve) -> &'static RingAlgorithm { + match curve { + CryptoNamedCurve::P256 => &ring::agreement::ECDH_P256, + CryptoNamedCurve::P384 => &ring::agreement::ECDH_P384, + } + } +} + +impl From<CryptoNamedCurve> for &EcdsaSigningAlgorithm { + fn from(curve: CryptoNamedCurve) -> &'static EcdsaSigningAlgorithm { + match curve { + CryptoNamedCurve::P256 => { + &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING + } + CryptoNamedCurve::P384 => { + &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING + } + } + } +} + +impl From<CryptoHash> for HmacAlgorithm { + fn from(hash: CryptoHash) -> HmacAlgorithm { + match hash { + CryptoHash::Sha1 => ring::hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + CryptoHash::Sha256 => ring::hmac::HMAC_SHA256, + CryptoHash::Sha384 => ring::hmac::HMAC_SHA384, + CryptoHash::Sha512 => ring::hmac::HMAC_SHA512, + } + } +} + +impl From<CryptoHash> for &'static digest::Algorithm { + fn from(hash: CryptoHash) -> &'static digest::Algorithm { + match hash { + CryptoHash::Sha1 => &digest::SHA1_FOR_LEGACY_USE_ONLY, + CryptoHash::Sha256 => &digest::SHA256, + CryptoHash::Sha384 => &digest::SHA384, + CryptoHash::Sha512 => &digest::SHA512, + } + } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum KeyUsage { + Encrypt, + Decrypt, + Sign, + Verify, + DeriveKey, + DeriveBits, + WrapKey, + UnwrapKey, +} + +#[derive(Serialize, Deserialize, Clone, Copy)] +pub enum Algorithm { + #[serde(rename = "RSASSA-PKCS1-v1_5")] + RsassaPkcs1v15, + #[serde(rename = "RSA-PSS")] + RsaPss, + #[serde(rename = "RSA-OAEP")] + RsaOaep, + #[serde(rename = "ECDSA")] + Ecdsa, + #[serde(rename = "ECDH")] + Ecdh, + #[serde(rename = "AES-CTR")] + AesCtr, + #[serde(rename = "AES-CBC")] + AesCbc, + #[serde(rename = "AES-GCM")] + AesGcm, + #[serde(rename = "AES-KW")] + AesKw, + #[serde(rename = "HMAC")] + Hmac, +} diff --git a/ext/crypto/lib.deno_crypto.d.ts b/ext/crypto/lib.deno_crypto.d.ts new file mode 100644 index 000000000..b89b62f2e --- /dev/null +++ b/ext/crypto/lib.deno_crypto.d.ts @@ -0,0 +1,155 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +/// <reference no-default-lib="true" /> +/// <reference lib="esnext" /> + +declare var crypto: Crypto; + +interface Algorithm { + name: string; +} + +interface KeyAlgorithm { + name: string; +} + +type AlgorithmIdentifier = string | Algorithm; +type HashAlgorithmIdentifier = AlgorithmIdentifier; +type KeyType = "private" | "public" | "secret"; +type KeyUsage = + | "decrypt" + | "deriveBits" + | "deriveKey" + | "encrypt" + | "sign" + | "unwrapKey" + | "verify" + | "wrapKey"; + +type NamedCurve = string; + +interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} + +interface EcKeyGenParams extends Algorithm { + namedCurve: NamedCurve; +} + +interface EcdsaParams extends Algorithm { + hash: HashAlgorithmIdentifier; +} + +interface RsaHashedKeyGenParams extends RsaKeyGenParams { + hash: HashAlgorithmIdentifier; +} + +interface RsaKeyGenParams extends Algorithm { + modulusLength: number; + publicExponent: Uint8Array; +} + +interface RsaPssParams extends Algorithm { + saltLength: number; +} + +interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} + +/** The CryptoKey dictionary of the Web Crypto API represents a cryptographic key. */ +interface CryptoKey { + readonly algorithm: KeyAlgorithm; + readonly extractable: boolean; + readonly type: KeyType; + readonly usages: KeyUsage[]; +} + +declare var CryptoKey: { + prototype: CryptoKey; + new (): CryptoKey; +}; + +/** The CryptoKeyPair dictionary of the Web Crypto API represents a key pair for an asymmetric cryptography algorithm, also known as a public-key algorithm. */ +interface CryptoKeyPair { + privateKey: CryptoKey; + publicKey: CryptoKey; +} + +declare var CryptoKeyPair: { + prototype: CryptoKeyPair; + new (): CryptoKeyPair; +}; + +/** This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto). */ +interface SubtleCrypto { + generateKey( + algorithm: RsaHashedKeyGenParams | EcKeyGenParams, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise<CryptoKeyPair>; + generateKey( + algorithm: HmacKeyGenParams, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise<CryptoKey>; + generateKey( + algorithm: AlgorithmIdentifier, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise<CryptoKeyPair | CryptoKey>; + importKey( + format: "raw", + keyData: BufferSource, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: KeyUsage[], + ): Promise<CryptoKey>; + sign( + algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, + key: CryptoKey, + data: BufferSource, + ): Promise<ArrayBuffer>; + verify( + algorithm: AlgorithmIdentifier | RsaPssParams, + key: CryptoKey, + signature: BufferSource, + data: BufferSource, + ): Promise<boolean>; + digest( + algorithm: AlgorithmIdentifier, + data: BufferSource, + ): Promise<ArrayBuffer>; +} + +declare interface Crypto { + readonly subtle: SubtleCrypto; + getRandomValues< + T extends + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint16Array + | Uint32Array + | Uint8ClampedArray + | Float32Array + | Float64Array + | DataView + | null, + >( + array: T, + ): T; + randomUUID(): string; +} + +interface Algorithm { + name: string; +} + +declare var SubtleCrypto: { + prototype: SubtleCrypto; + new (): SubtleCrypto; +}; diff --git a/ext/crypto/lib.rs b/ext/crypto/lib.rs new file mode 100644 index 000000000..e77b34987 --- /dev/null +++ b/ext/crypto/lib.rs @@ -0,0 +1,558 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::custom_error; +use deno_core::error::not_supported; +use deno_core::error::null_opbuf; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::include_js_files; +use deno_core::op_async; +use deno_core::op_sync; +use deno_core::Extension; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use serde::Deserialize; + +use std::cell::RefCell; +use std::convert::TryInto; +use std::rc::Rc; + +use lazy_static::lazy_static; +use num_traits::cast::FromPrimitive; +use rand::rngs::OsRng; +use rand::rngs::StdRng; +use rand::thread_rng; +use rand::Rng; +use rand::SeedableRng; +use ring::digest; +use ring::hmac::Algorithm as HmacAlgorithm; +use ring::hmac::Key as HmacKey; +use ring::rand as RingRand; +use ring::rand::SecureRandom; +use ring::signature::EcdsaKeyPair; +use ring::signature::EcdsaSigningAlgorithm; +use rsa::padding::PaddingScheme; +use rsa::pkcs8::FromPrivateKey; +use rsa::pkcs8::ToPrivateKey; +use rsa::BigUint; +use rsa::PublicKey; +use rsa::RsaPrivateKey; +use rsa::RsaPublicKey; +use sha1::Sha1; +use sha2::Digest; +use sha2::Sha256; +use sha2::Sha384; +use sha2::Sha512; +use std::path::PathBuf; + +pub use rand; // Re-export rand + +mod key; + +use crate::key::Algorithm; +use crate::key::CryptoHash; +use crate::key::CryptoNamedCurve; + +// Allowlist for RSA public exponents. +lazy_static! { + static ref PUB_EXPONENT_1: BigUint = BigUint::from_u64(3).unwrap(); + static ref PUB_EXPONENT_2: BigUint = BigUint::from_u64(65537).unwrap(); +} + +pub fn init(maybe_seed: Option<u64>) -> Extension { + Extension::builder() + .js(include_js_files!( + prefix "deno:ext/crypto", + "00_crypto.js", + "01_webidl.js", + )) + .ops(vec![ + ( + "op_crypto_get_random_values", + op_sync(op_crypto_get_random_values), + ), + ("op_crypto_generate_key", op_async(op_crypto_generate_key)), + ("op_crypto_sign_key", op_async(op_crypto_sign_key)), + ("op_crypto_verify_key", op_async(op_crypto_verify_key)), + ("op_crypto_subtle_digest", op_async(op_crypto_subtle_digest)), + ("op_crypto_random_uuid", op_sync(op_crypto_random_uuid)), + ]) + .state(move |state| { + if let Some(seed) = maybe_seed { + state.put(StdRng::seed_from_u64(seed)); + } + Ok(()) + }) + .build() +} + +pub fn op_crypto_get_random_values( + state: &mut OpState, + mut zero_copy: ZeroCopyBuf, + _: (), +) -> Result<(), AnyError> { + if zero_copy.len() > 65536 { + return Err( + deno_web::DomExceptionQuotaExceededError::new(&format!("The ArrayBufferView's byte length ({}) exceeds the number of bytes of entropy available via this API (65536)", zero_copy.len())) + .into(), + ); + } + + let maybe_seeded_rng = state.try_borrow_mut::<StdRng>(); + if let Some(seeded_rng) = maybe_seeded_rng { + seeded_rng.fill(&mut *zero_copy); + } else { + let mut rng = thread_rng(); + rng.fill(&mut *zero_copy); + } + + Ok(()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AlgorithmArg { + name: Algorithm, + modulus_length: Option<u32>, + public_exponent: Option<ZeroCopyBuf>, + named_curve: Option<CryptoNamedCurve>, + hash: Option<CryptoHash>, + length: Option<usize>, +} + +pub async fn op_crypto_generate_key( + _state: Rc<RefCell<OpState>>, + args: AlgorithmArg, + _: (), +) -> Result<ZeroCopyBuf, AnyError> { + let algorithm = args.name; + + let key = match algorithm { + Algorithm::RsassaPkcs1v15 | Algorithm::RsaPss => { + let public_exponent = args.public_exponent.ok_or_else(not_supported)?; + let modulus_length = args.modulus_length.ok_or_else(not_supported)?; + + let exponent = BigUint::from_bytes_be(&public_exponent); + if exponent != *PUB_EXPONENT_1 && exponent != *PUB_EXPONENT_2 { + return Err(custom_error( + "DOMExceptionOperationError", + "Bad public exponent", + )); + } + + let mut rng = OsRng; + + let private_key: RsaPrivateKey = tokio::task::spawn_blocking( + move || -> Result<RsaPrivateKey, rsa::errors::Error> { + RsaPrivateKey::new_with_exp( + &mut rng, + modulus_length as usize, + &exponent, + ) + }, + ) + .await + .unwrap() + .map_err(|e| custom_error("DOMExceptionOperationError", e.to_string()))?; + + private_key.to_pkcs8_der()?.as_ref().to_vec() + } + Algorithm::Ecdsa => { + let curve: &EcdsaSigningAlgorithm = + args.named_curve.ok_or_else(not_supported)?.into(); + let rng = RingRand::SystemRandom::new(); + let private_key: Vec<u8> = tokio::task::spawn_blocking( + move || -> Result<Vec<u8>, ring::error::Unspecified> { + let pkcs8 = EcdsaKeyPair::generate_pkcs8(curve, &rng)?; + Ok(pkcs8.as_ref().to_vec()) + }, + ) + .await + .unwrap() + .map_err(|_| { + custom_error("DOMExceptionOperationError", "Key generation failed") + })?; + + private_key + } + Algorithm::Hmac => { + let hash: HmacAlgorithm = args.hash.ok_or_else(not_supported)?.into(); + + let length = if let Some(length) = args.length { + if (length % 8) != 0 { + return Err(custom_error( + "DOMExceptionOperationError", + "hmac block length must be byte aligned", + )); + } + let length = length / 8; + if length > ring::digest::MAX_BLOCK_LEN { + return Err(custom_error( + "DOMExceptionOperationError", + "hmac block length is too large", + )); + } + length + } else { + hash.digest_algorithm().block_len + }; + + let rng = RingRand::SystemRandom::new(); + let mut key_bytes = [0; ring::digest::MAX_BLOCK_LEN]; + let key_bytes = &mut key_bytes[..length]; + rng.fill(key_bytes).map_err(|_| { + custom_error("DOMExceptionOperationError", "Key generation failed") + })?; + + key_bytes.to_vec() + } + _ => return Err(not_supported()), + }; + + Ok(key.into()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum KeyFormat { + Raw, + Pkcs8, +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub struct KeyData { + // TODO(littledivy): Kept here to be used to importKey() in future. + #[allow(dead_code)] + r#type: KeyFormat, + data: ZeroCopyBuf, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignArg { + key: KeyData, + algorithm: Algorithm, + salt_length: Option<u32>, + hash: Option<CryptoHash>, + named_curve: Option<CryptoNamedCurve>, +} + +pub async fn op_crypto_sign_key( + _state: Rc<RefCell<OpState>>, + args: SignArg, + zero_copy: Option<ZeroCopyBuf>, +) -> Result<ZeroCopyBuf, AnyError> { + let zero_copy = zero_copy.ok_or_else(null_opbuf)?; + let data = &*zero_copy; + let algorithm = args.algorithm; + + let signature = match algorithm { + Algorithm::RsassaPkcs1v15 => { + let private_key = RsaPrivateKey::from_pkcs8_der(&*args.key.data)?; + let (padding, hashed) = match args + .hash + .ok_or_else(|| type_error("Missing argument hash".to_string()))? + { + CryptoHash::Sha1 => { + let mut hasher = Sha1::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA1), + }, + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA2_256), + }, + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha384 => { + let mut hasher = Sha384::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA2_384), + }, + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha512 => { + let mut hasher = Sha512::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA2_512), + }, + hasher.finalize()[..].to_vec(), + ) + } + }; + + private_key.sign(padding, &hashed)? + } + Algorithm::RsaPss => { + let private_key = RsaPrivateKey::from_pkcs8_der(&*args.key.data)?; + + let salt_len = args + .salt_length + .ok_or_else(|| type_error("Missing argument saltLength".to_string()))? + as usize; + + let rng = OsRng; + let (padding, digest_in) = match args + .hash + .ok_or_else(|| type_error("Missing argument hash".to_string()))? + { + CryptoHash::Sha1 => { + let mut hasher = Sha1::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha1, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha256, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha384 => { + let mut hasher = Sha384::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha384, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha512 => { + let mut hasher = Sha512::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha512, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + }; + + // Sign data based on computed padding and return buffer + private_key.sign(padding, &digest_in)? + } + Algorithm::Ecdsa => { + let curve: &EcdsaSigningAlgorithm = + args.named_curve.ok_or_else(not_supported)?.try_into()?; + + let key_pair = EcdsaKeyPair::from_pkcs8(curve, &*args.key.data)?; + // We only support P256-SHA256 & P384-SHA384. These are recommended signature pairs. + // https://briansmith.org/rustdoc/ring/signature/index.html#statics + if let Some(hash) = args.hash { + match hash { + CryptoHash::Sha256 | CryptoHash::Sha384 => (), + _ => return Err(type_error("Unsupported algorithm")), + } + }; + + let rng = RingRand::SystemRandom::new(); + let signature = key_pair.sign(&rng, data)?; + + // Signature data as buffer. + signature.as_ref().to_vec() + } + Algorithm::Hmac => { + let hash: HmacAlgorithm = args.hash.ok_or_else(not_supported)?.into(); + + let key = HmacKey::new(hash, &*args.key.data); + + let signature = ring::hmac::sign(&key, data); + signature.as_ref().to_vec() + } + _ => return Err(type_error("Unsupported algorithm".to_string())), + }; + + Ok(signature.into()) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifyArg { + key: KeyData, + algorithm: Algorithm, + salt_length: Option<u32>, + hash: Option<CryptoHash>, + signature: ZeroCopyBuf, +} + +pub async fn op_crypto_verify_key( + _state: Rc<RefCell<OpState>>, + args: VerifyArg, + zero_copy: Option<ZeroCopyBuf>, +) -> Result<bool, AnyError> { + let zero_copy = zero_copy.ok_or_else(null_opbuf)?; + let data = &*zero_copy; + let algorithm = args.algorithm; + + let verification = match algorithm { + Algorithm::RsassaPkcs1v15 => { + let public_key: RsaPublicKey = + RsaPrivateKey::from_pkcs8_der(&*args.key.data)?.to_public_key(); + let (padding, hashed) = match args + .hash + .ok_or_else(|| type_error("Missing argument hash".to_string()))? + { + CryptoHash::Sha1 => { + let mut hasher = Sha1::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA1), + }, + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA2_256), + }, + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha384 => { + let mut hasher = Sha384::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA2_384), + }, + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha512 => { + let mut hasher = Sha512::new(); + hasher.update(&data); + ( + PaddingScheme::PKCS1v15Sign { + hash: Some(rsa::hash::Hash::SHA2_512), + }, + hasher.finalize()[..].to_vec(), + ) + } + }; + + public_key + .verify(padding, &hashed, &*args.signature) + .is_ok() + } + Algorithm::RsaPss => { + let salt_len = args + .salt_length + .ok_or_else(|| type_error("Missing argument saltLength".to_string()))? + as usize; + let public_key: RsaPublicKey = + RsaPrivateKey::from_pkcs8_der(&*args.key.data)?.to_public_key(); + + let rng = OsRng; + let (padding, hashed) = match args + .hash + .ok_or_else(|| type_error("Missing argument hash".to_string()))? + { + CryptoHash::Sha1 => { + let mut hasher = Sha1::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha1, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha256, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha384 => { + let mut hasher = Sha384::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha384, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + CryptoHash::Sha512 => { + let mut hasher = Sha512::new(); + hasher.update(&data); + ( + PaddingScheme::new_pss_with_salt::<Sha512, _>(rng, salt_len), + hasher.finalize()[..].to_vec(), + ) + } + }; + + public_key + .verify(padding, &hashed, &*args.signature) + .is_ok() + } + Algorithm::Hmac => { + let hash: HmacAlgorithm = args.hash.ok_or_else(not_supported)?.into(); + let key = HmacKey::new(hash, &*args.key.data); + ring::hmac::verify(&key, data, &*args.signature).is_ok() + } + _ => return Err(type_error("Unsupported algorithm".to_string())), + }; + + Ok(verification) +} + +pub fn op_crypto_random_uuid( + state: &mut OpState, + _: (), + _: (), +) -> Result<String, AnyError> { + let maybe_seeded_rng = state.try_borrow_mut::<StdRng>(); + let uuid = if let Some(seeded_rng) = maybe_seeded_rng { + let mut bytes = [0u8; 16]; + seeded_rng.fill(&mut bytes); + uuid::Builder::from_bytes(bytes) + .set_version(uuid::Version::Random) + .build() + } else { + uuid::Uuid::new_v4() + }; + + Ok(uuid.to_string()) +} + +pub async fn op_crypto_subtle_digest( + _state: Rc<RefCell<OpState>>, + algorithm: CryptoHash, + data: Option<ZeroCopyBuf>, +) -> Result<ZeroCopyBuf, AnyError> { + let input = data.ok_or_else(null_opbuf)?; + let output = tokio::task::spawn_blocking(move || { + digest::digest(algorithm.into(), &input) + .as_ref() + .to_vec() + .into() + }) + .await?; + + Ok(output) +} + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_crypto.d.ts") +} |