Skip to main content
Module

x/oak/keyStack.ts

A middleware framework for handling HTTP with Deno 🐿️ 🦕
Extremely Popular
Go to Latest
File
// Copyright 2018-2022 the oak authors. All rights reserved. MIT license.
// This was inspired by [keygrip](https://github.com/crypto-utils/keygrip/)// which allows signing of data (cookies) to prevent tampering, but also allows// for easy key rotation without needing to resign the data.
import { timingSafeEqual } from "./deps.ts";import { encodeBase64Safe, importKey, sign } from "./util.ts";import type { Data, Key } from "./types.d.ts";
/** 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 = await sign(a, cryptoKey); const bh = await sign(b, cryptoKey); return timingSafeEqual(ah, bh);}
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)!; }
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 array of keys, of which the index 0 will be used to sign * data, but verification can happen against any key. */ constructor(keys: Key[]) { if (!(0 in keys)) { throw new TypeError("keys must contain at least one value"); } this.#keys = keys; }
/** 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 encodeBase64Safe(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 cryptoKey = await this.#toCryptoKey(this.#keys[i]); if ( await compare(digest, encodeBase64Safe(await sign(data, cryptoKey))) ) { return i; } } return -1; }
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) { const { length } = this; return `${this.constructor.name} ${inspect({ length })}`; }
[Symbol.for("nodejs.util.inspect.custom")]( depth: number, // deno-lint-ignore no-explicit-any options: any, inspect: (value: unknown, options?: unknown) => 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) }`; }}