From abdc6cb6a79c5f72b0e1cae6664d2bf806624d09 Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Fri, 31 May 2024 13:47:44 +0900 Subject: [PATCH 1/2] BREAKING(crypto): remove KeyStack --- crypto/mod.ts | 1 - crypto/unstable_keystack.ts | 285 ------------------------------- crypto/unstable_keystack_test.ts | 272 ----------------------------- 3 files changed, 558 deletions(-) delete mode 100644 crypto/unstable_keystack.ts delete mode 100644 crypto/unstable_keystack_test.ts diff --git a/crypto/mod.ts b/crypto/mod.ts index 49a68871565d..eccb9df010bc 100644 --- a/crypto/mod.ts +++ b/crypto/mod.ts @@ -21,5 +21,4 @@ */ export * from "./crypto.ts"; -export * from "./unstable_keystack.ts"; export * from "./timing_safe_equal.ts"; diff --git a/crypto/unstable_keystack.ts b/crypto/unstable_keystack.ts deleted file mode 100644 index 93936dbcd698..000000000000 --- a/crypto/unstable_keystack.ts +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// This module is browser compatible. - -/** - * Provides the {@linkcode KeyStack} class which implements the - * {@linkcode KeyRing} interface for managing rotatable keys. - * - * @module - */ - -import { timingSafeEqual } from "./timing_safe_equal.ts"; -import { encodeBase64Url } from "@std/encoding/base64url"; - -/** Types of data that can be signed cryptographically. */ -export type Data = string | number[] | ArrayBuffer | Uint8Array; - -/** Types of keys that can be used to sign data. */ -export type Key = string | number[] | ArrayBuffer | Uint8Array; - -const encoder = new TextEncoder(); - -function importKey(key: Key): Promise { - if (typeof key === "string") { - key = encoder.encode(key); - } else if (Array.isArray(key)) { - key = new Uint8Array(key); - } - return crypto.subtle.importKey( - "raw", - key, - { - name: "HMAC", - hash: { name: "SHA-256" }, - }, - true, - ["sign", "verify"], - ); -} - -function sign(data: Data, key: CryptoKey): Promise { - if (typeof data === "string") { - data = encoder.encode(data); - } else if (Array.isArray(data)) { - data = Uint8Array.from(data); - } - return crypto.subtle.sign("HMAC", key, data); -} - -/** - * Compare two strings, Uint8Arrays, ArrayBuffers, or arrays of numbers in a - * way that avoids timing based attacks on the comparisons on the values. - * - * The function will return `true` if the values match, or `false`, if they - * do not match. - * - * This was inspired by https://github.com/suryagh/tsscmp which provides a - * timing safe string comparison to avoid timing attacks as described in - * https://codahale.com/a-lesson-in-timing-attacks/. - */ -async function compare(a: Data, b: Data): Promise { - const key = new Uint8Array(32); - globalThis.crypto.getRandomValues(key); - const cryptoKey = await importKey(key); - const [ah, bh] = await Promise.all([ - sign(a, cryptoKey), - sign(b, cryptoKey), - ]); - return timingSafeEqual(ah, bh); -} - -/** - * A cryptographic key chain which allows signing of data to prevent tampering, - * but also allows for easy key rotation without needing to re-sign the data. - * - * Data is signed as SHA256 HMAC. - * - * This was inspired by - * {@linkcode https://www.npmjs.com/package/keygrip | npm:keygrip}. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { assert } from "@std/assert/assert"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * const digest = await keyStack.sign("some data"); - * - * const rotatedStack = new KeyStack(["deno", "says", "hello", "world"]); - * assert(await rotatedStack.verify("some data", digest)); - * ``` - */ -export class KeyStack { - #cryptoKeys = new Map(); - #keys: Key[]; - - async #toCryptoKey(key: Key): Promise { - if (!this.#cryptoKeys.has(key)) { - this.#cryptoKeys.set(key, await importKey(key)); - } - return this.#cryptoKeys.get(key)!; - } - - /** - * Number of keys - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { assertEquals } from "@std/assert/assert-equals"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * assertEquals(keyStack.length, 2); - * ``` - * - * @returns The length of the key stack. - */ - get length(): number { - return this.#keys.length; - } - - /** - * Constructs a new instance. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { assertInstanceOf } from "@std/assert/assert-instance-of"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * assertInstanceOf(keyStack, KeyStack); - * ``` - * - * @param keys An iterable of keys, of which the index 0 will be used to sign - * data, but verification can happen against any key. - */ - constructor(keys: Iterable) { - const values = Array.isArray(keys) ? keys : [...keys]; - if (!(values.length)) { - throw new TypeError("keys must contain at least one value"); - } - this.#keys = values; - } - - /** - * Take `data` and return a SHA256 HMAC digest that uses the current 0 index - * of the `keys` passed to the constructor. This digest is in the form of a - * URL safe base64 encoded string. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { assert } from "@std/assert/assert"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * const digest = await keyStack.sign("some data"); - * - * const rotatedStack = new KeyStack(["deno", "says", "hello", "world"]); - * assert(await rotatedStack.verify("some data", digest)); - * ``` - * - * @param data The data to sign. - * @returns A URL safe base64 encoded string of the SHA256 HMAC digest. - */ - async sign(data: Data): Promise { - const key = await this.#toCryptoKey(this.#keys[0]!); - return encodeBase64Url(await sign(data, key)); - } - - /** - * Given `data` and a `digest`, verify that one of the `keys` provided the - * constructor was used to generate the `digest`. Returns `true` if one of - * the keys was used, otherwise `false`. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { assert } from "@std/assert/assert"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * const digest = await keyStack.sign("some data"); - * - * const rotatedStack = new KeyStack(["deno", "says", "hello", "world"]); - * assert(await rotatedStack.verify("some data", digest)); - * ``` - * - * @param data The data to verify. - * @param digest The digest to verify. - * @returns `true` if the digest was generated by one of the keys, otherwise - */ - async verify(data: Data, digest: string): Promise { - return (await this.indexOf(data, digest)) > -1; - } - - /** - * Given `data` and a `digest`, return the current index of the key in the - * `keys` passed the constructor that was used to generate the digest. If no - * key can be found, the method returns `-1`. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { assertEquals } from "@std/assert/assert-equals"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * const digest = await keyStack.sign("some data"); - * - * const rotatedStack = new KeyStack(["deno", "says", "hello", "world"]); - * assertEquals(await rotatedStack.indexOf("some data", digest), 2); - * ``` - * - * @param data The data to verify. - * @param digest The digest to verify. - * @returns The index of the key that was used to generate the digest. - */ - async indexOf(data: Data, digest: string): Promise { - for (let i = 0; i < this.#keys.length; i++) { - const key = this.#keys[i] as Key; - const cryptoKey = await this.#toCryptoKey(key); - if ( - await compare(digest, encodeBase64Url(await sign(data, cryptoKey))) - ) { - return i; - } - } - return -1; - } - - /** - * Custom output for {@linkcode Deno.inspect}. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * console.log(Deno.inspect(keyStack)); - * ``` - * - * @param inspect The inspect function. - * @returns A string representation of the key stack. - */ - [Symbol.for("Deno.customInspect")]( - inspect: (value: unknown) => string, - ): string { - const { length } = this; - return `${this.constructor.name} ${inspect({ length })}`; - } - - /** - * Custom output for Node's - * {@linkcode https://nodejs.org/api/util.html#utilinspectobject-options | util.inspect}. - * - * @example Usage - * ```ts - * import { KeyStack } from "@std/crypto/unstable-keystack"; - * import { inspect } from "node:util"; - * - * const keyStack = new KeyStack(["hello", "world"]); - * console.log(inspect(keyStack)); - * ``` - * - * @param depth The depth to inspect. - * @param options The options to inspect. - * @param inspect The inspect function. - * @returns A string representation of the key stack. - */ - [Symbol.for("nodejs.util.inspect.custom")]( - depth: number, - // deno-lint-ignore no-explicit-any - options: any, - inspect: (value: unknown, options?: unknown) => string, - ): string { - if (depth < 0) { - return options.stylize(`[${this.constructor.name}]`, "special"); - } - - const newOptions = Object.assign({}, options, { - depth: options.depth === null ? null : options.depth - 1, - }); - const { length } = this; - return `${options.stylize(this.constructor.name, "special")} ${ - inspect({ length }, newOptions) - }`; - } -} diff --git a/crypto/unstable_keystack_test.ts b/crypto/unstable_keystack_test.ts deleted file mode 100644 index f391c54c0e5e..000000000000 --- a/crypto/unstable_keystack_test.ts +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -import { assert, assertEquals, assertThrows } from "@std/assert"; - -import { KeyStack } from "./unstable_keystack.ts"; - -Deno.test({ - name: "KeyStack() throws on empty keys", - fn() { - assertThrows( - () => new KeyStack([]), - TypeError, - "keys must contain at least one value", - ); - }, -}); - -Deno.test({ - name: "keyStack.sign() handles single key", - async fn() { - const keys = ["hello"]; - const keyStack = new KeyStack(keys); - const actual = await keyStack.sign("world"); - const expected = "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ"; - assertEquals(actual, expected); - }, -}); - -Deno.test({ - name: "keyStack.sign() handles two keys, first key used", - async fn() { - const keys = ["hello", "world"]; - const keyStack = new KeyStack(keys); - const actual = await keyStack.sign("world"); - const expected = "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ"; - assertEquals(actual, expected); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles single key", - async fn() { - const keys = ["hello"]; - const keyStack = new KeyStack(keys); - const digest = await keyStack.sign("world"); - assert(await keyStack.verify("world", digest)); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles single key verify invalid", - async fn() { - const keys = ["hello"]; - const keyStack = new KeyStack(keys); - const digest = await keyStack.sign("world"); - assert(!await keyStack.verify("worlds", digest)); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles two keys", - async fn() { - const keys = ["hello", "world"]; - const keyStack = new KeyStack(keys); - const digest = await keyStack.sign("world"); - assert(await keyStack.verify("world", digest)); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles unshift key", - async fn() { - const keys = ["hello"]; - const keyStack = new KeyStack(keys); - const digest = await keyStack.sign("world"); - keys.unshift("world"); - assertEquals(keys, ["world", "hello"]); - assert(await keyStack.verify("world", digest)); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles shift key", - async fn() { - const keys = ["hello", "world"]; - const keyStack = new KeyStack(keys); - const digest = await keyStack.sign("world"); - assertEquals(keys.shift(), "hello"); - assertEquals(keys, ["world"]); - assert(!await keyStack.verify("world", digest)); - }, -}); - -Deno.test({ - name: "keyStack.indexOf() handles single key", - async fn() { - const keys = ["hello"]; - const keyStack = new KeyStack(keys); - assertEquals( - await keyStack.indexOf( - "world", - "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ", - ), - 0, - ); - }, -}); - -Deno.test({ - name: "keyStack.indexOf() handles two keys index 0", - async fn() { - const keys = ["hello", "world"]; - const keyStack = new KeyStack(keys); - assertEquals( - await keyStack.indexOf( - "world", - "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ", - ), - 0, - ); - }, -}); - -Deno.test({ - name: "keyStack.indexOf() handles two keys index 1", - async fn() { - const keys = ["world", "hello"]; - const keyStack = new KeyStack(keys); - assertEquals( - await keyStack.indexOf( - "world", - "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ", - ), - 1, - ); - }, -}); - -Deno.test({ - name: "keyStack.indexOf() handles two keys not found", - async fn() { - const keys = ["world", "hello"]; - const keyStack = new KeyStack(keys); - assertEquals( - await keyStack.indexOf( - "hello", - "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ", - ), - -1, - ); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles number array key", - async fn() { - const keys = [[212, 213]]; - const keyStack = new KeyStack(keys); - assert(await keyStack.verify("hello", await keyStack.sign("hello"))); - }, -}); - -Deno.test({ - name: "keyStack.verify() handles Uint8Array key", - async fn() { - const keys = [new Uint8Array([212, 213])]; - const keyStack = new KeyStack(keys); - assert(await keyStack.verify("hello", await keyStack.sign("hello"))); - }, -}); - -Deno.test({ - name: "verify() handles ArrayBuffer key", - async fn() { - const key = new ArrayBuffer(2); - const dataView = new DataView(key); - dataView.setInt8(0, 212); - dataView.setInt8(1, 213); - const keys = [key]; - const keyStack = new KeyStack(keys); - assert(await keyStack.verify("hello", await keyStack.sign("hello"))); - }, -}); - -Deno.test({ - name: "verify() handles number array data", - async fn() { - const keys = [[212, 213]]; - const keyStack = new KeyStack(keys); - assert(await keyStack.verify([212, 213], await keyStack.sign([212, 213]))); - }, -}); - -Deno.test({ - name: "verify() handles Uint8Array data", - async fn() { - const keys = [[212, 213]]; - const keyStack = new KeyStack(keys); - assert( - await keyStack.verify( - new Uint8Array([212, 213]), - await keyStack.sign(new Uint8Array([212, 213])), - ), - ); - }, -}); - -Deno.test({ - name: "verify() handles ArrayBuffer data", - async fn() { - const keys = [[212, 213]]; - const keyStack = new KeyStack(keys); - const data1 = new ArrayBuffer(2); - const dataView1 = new DataView(data1); - dataView1.setInt8(0, 212); - dataView1.setInt8(1, 213); - const data2 = new ArrayBuffer(2); - const dataView2 = new DataView(data2); - dataView2.setInt8(0, 212); - dataView2.setInt8(1, 213); - assert(await keyStack.verify(data2, await keyStack.sign(data1))); - }, -}); - -Deno.test({ - name: "verify() handles user iterable keys", - async fn() { - const keys = new Set(["hello", "world"]); - const keyStack = new KeyStack(keys); - const actual = await keyStack.sign("world"); - const expected = "8ayXAutfryPKKRpNxG3t3u4qeMza8KQSvtdxTP_7HMQ"; - assertEquals(actual, expected); - }, -}); - -Deno.test({ - name: "KeyStack() handles inspection in Deno", - fn() { - assertEquals( - Deno.inspect(new KeyStack(["abcdef"])), - `KeyStack { length: 1 }`, - ); - }, -}); - -Deno.test({ - name: "KeyStack() handles inspection in Node", - async fn() { - const { inspect } = await import("node:util"); - - const keyStack = new KeyStack(["abcdef"]); - - // Needs to overwrite Deno.customInspect symbol to enable Node's inspect - // deno-lint-ignore no-explicit-any - (keyStack as any)[Symbol.for("Deno.customInspect")] = undefined; - - assertEquals( - inspect(keyStack), - `KeyStack { length: 1 }`, - ); - // Check the short form - assertEquals( - inspect({ stack: [[keyStack]] }), - `{ stack: [ [ [KeyStack] ] ] }`, - ); - // Check the case when depth is null - assertEquals( - inspect({ stack: [[keyStack]] }, { depth: null }), - `{ stack: [ [ KeyStack { length: 1 } ] ] }`, - ); - }, -}); From 3cc31bcddb2db0f95410c30ac215d81c02b0be22 Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Fri, 31 May 2024 17:27:42 +0900 Subject: [PATCH 2/2] remove from export map --- crypto/deno.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crypto/deno.json b/crypto/deno.json index d2e6204e95f7..1713d18e1890 100644 --- a/crypto/deno.json +++ b/crypto/deno.json @@ -4,8 +4,7 @@ "exports": { ".": "./mod.ts", "./crypto": "./crypto.ts", - "./timing-safe-equal": "./timing_safe_equal.ts", - "./unstable-keystack": "./unstable_keystack.ts" + "./timing-safe-equal": "./timing_safe_equal.ts" }, "exclude": [ "_wasm/target"