BREAKING(crypto): remove KeyStack (#4916)

This commit is contained in:
Yoshiya Hinosawa 2024-05-31 18:02:56 +09:00 committed by GitHub
parent f94f8f3580
commit 1dd988ce66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1 additions and 560 deletions

View File

@ -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"

View File

@ -21,5 +21,4 @@
*/
export * from "./crypto.ts";
export * from "./unstable_keystack.ts";
export * from "./timing_safe_equal.ts";

View File

@ -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<CryptoKey> {
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<ArrayBuffer> {
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<boolean> {
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<Key, CryptoKey>();
#keys: Key[];
async #toCryptoKey(key: Key): Promise<CryptoKey> {
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<Key>) {
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<string> {
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<boolean> {
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<number> {
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)
}`;
}
}

View File

@ -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 } ] ] }`,
);
},
});