mirror of
https://github.com/denoland/std.git
synced 2024-11-22 04:59:05 +00:00
BREAKING(crypto): remove KeyStack (#4916)
This commit is contained in:
parent
f94f8f3580
commit
1dd988ce66
@ -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"
|
||||
|
@ -21,5 +21,4 @@
|
||||
*/
|
||||
|
||||
export * from "./crypto.ts";
|
||||
export * from "./unstable_keystack.ts";
|
||||
export * from "./timing_safe_equal.ts";
|
||||
|
@ -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)
|
||||
}`;
|
||||
}
|
||||
}
|
@ -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 } ] ] }`,
|
||||
);
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue
Block a user