summaryrefslogtreecommitdiff
path: root/extensions/crypto/00_crypto.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/crypto/00_crypto.js')
-rw-r--r--extensions/crypto/00_crypto.js675
1 files changed, 675 insertions, 0 deletions
diff --git a/extensions/crypto/00_crypto.js b/extensions/crypto/00_crypto.js
new file mode 100644
index 000000000..a6c6c0722
--- /dev/null
+++ b/extensions/crypto/00_crypto.js
@@ -0,0 +1,675 @@
+// 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" />
+/// <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;
+
+ // P-521 is not yet supported.
+ const supportedNamedCurves = ["P-256", "P-384"];
+
+ const simpleAlgorithmDictionaries = {
+ RsaHashedKeyGenParams: { hash: "HashAlgorithmIdentifier" },
+ EcKeyGenParams: {},
+ HmacKeyGenParams: { hash: "HashAlgorithmIdentifier" },
+ RsaPssParams: {},
+ EcdsaParams: { 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,
+ },
+ };
+
+ // 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 (key.toLowerCase() === algName.toLowerCase()) {
+ 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(
+ (ArrayBuffer.isView(idlValue) ? idlValue.buffer : idlValue).slice(
+ 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;
+ }
+
+ // Should match op_crypto_subtle_digest() in extensions/crypto/lib.rs
+ function digestToId(name) {
+ switch (name) {
+ case "SHA-1":
+ return 0;
+ case "SHA-256":
+ return 1;
+ case "SHA-384":
+ return 2;
+ case "SHA-512":
+ return 3;
+ }
+ }
+
+ 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 [Symbol.toStringTag]() {
+ return "CryptoKey";
+ }
+
+ [Symbol.for("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
+ // TODO(littledivy): When the need arises, make `b` a list.
+ /**
+ * @param {string[]} a
+ * @param {string} b
+ * @returns
+ */
+ function usageIntersection(a, b) {
+ return a.includes(b) ? [b] : [];
+ }
+
+ // 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 (ArrayBuffer.isView(data)) {
+ data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ } else {
+ data = new Uint8Array(data);
+ }
+ data = data.slice();
+
+ algorithm = normalizeAlgorithm(algorithm, "digest");
+
+ const result = await core.opAsync(
+ "op_crypto_subtle_digest",
+ digestToId(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 (ArrayBuffer.isView(data)) {
+ data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ } else {
+ data = new Uint8Array(data);
+ }
+ data = data.slice();
+
+ // 2.
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, "sign");
+
+ const handle = key[_handle];
+ const keyData = KEY_STORE.get(handle);
+
+ // 8.
+ if (normalizedAlgorithm.name !== key[_algorithm].name) {
+ throw new DOMException(
+ "Signing algorithm doesn't match key algorithm.",
+ "InvalidAccessError",
+ );
+ }
+
+ // 9.
+ if (!key[_usages].includes("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 (!supportedNamedCurves.includes(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} 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;
+ }
+ }
+
+ async function generateKey(normalizedAlgorithm, extractable, usages) {
+ switch (normalizedAlgorithm.name) {
+ case "RSASSA-PKCS1-v1_5":
+ case "RSA-PSS": {
+ // 1.
+ if (usages.find((u) => !["sign", "verify"].includes(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 = {};
+ KEY_STORE.set(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 (usages.find((u) => !["sign", "verify"].includes(u)) !== undefined) {
+ throw new DOMException("Invalid key usages", "SyntaxError");
+ }
+
+ // 2-3.
+ const handle = {};
+ if (supportedNamedCurves.includes(normalizedAlgorithm.namedCurve)) {
+ const keyData = await core.opAsync("op_crypto_generate_key", {
+ name: "ECDSA",
+ namedCurve: normalizedAlgorithm.namedCurve,
+ });
+ KEY_STORE.set(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 (
+ usages.find((u) => !["sign", "verify"].includes(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 = {};
+ KEY_STORE.set(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 Int16Array ||
+ arrayBufferView instanceof Uint16Array ||
+ arrayBufferView instanceof Int32Array ||
+ arrayBufferView instanceof Uint32Array ||
+ arrayBufferView instanceof Uint8ClampedArray
+ )
+ ) {
+ 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 [Symbol.toStringTag]() {
+ return "Crypto";
+ }
+
+ [Symbol.for("Deno.customInspect")](inspect) {
+ return `${this.constructor.name} ${inspect({})}`;
+ }
+ }
+
+ webidl.configurePrototype(Crypto);
+
+ window.__bootstrap.crypto = {
+ SubtleCrypto,
+ crypto: webidl.createBranded(Crypto),
+ Crypto,
+ CryptoKey,
+ };
+})(this);