deno.land / std@0.224.0 / crypto / unstable_keystack.ts

unstable_keystack.ts
View Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// 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 "../encoding/base64url.ts";
/** 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 {@link https://github.com/crypto-utils/keygrip/ | keygrip}. * * @example * ```ts * import { KeyStack } from "https://deno.land/std@$STD_VERSION/crypto/unstable_keystack.ts"; * * const keyStack = new KeyStack(["hello", "world"]); * const digest = await keyStack.sign("some data"); * * const rotatedStack = new KeyStack(["deno", "says", "hello", "world"]); * await rotatedStack.verify("some data", digest); // true * ``` */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 */ get length(): number { return this.#keys.length; }
/** * A class which accepts an array of keys that are used to sign and verify * data and allows easy key rotation without invalidation of previously signed * data. * * @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. */ 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`. */ 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`. */ 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}. */ [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}. */ [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) }`; }}
std

Version Info

Tagged at
6 months ago