import { b64Decode, b64Encode } from "encoding/base64"; import { encodeTextAsBinary } from "encoding/text"; export interface KeyDeriverI { deriveKey( password: string, derivedKeyAlgorithm: AesKeyAlgorithm ): Promise; } /** * Creates an instance of a KeyDeriver. * This can be used to derive an encryption key from a given password. * * Note: You probably don't want to instantiate this directly, but rather * through a KeyDeriverConfig. * * @constructor * @param {object} algorithm The raw SubtleCrypto derivation algorithm object. */ function KeyDeriver(algorithm: Pbkdf2Params): KeyDeriverI { const keyDerivationAlgorithm = Object.freeze(Object.assign({}, algorithm)); return { async deriveKey( password: string, derivedKeyAlgorithm: AesKeyAlgorithm ): Promise { // Convert the password into something that we can use to derive the key const baseKey = await crypto.subtle.importKey( "raw", encodeTextAsBinary(password), keyDerivationAlgorithm, false, ["deriveKey"] ); const derivedKey = await crypto.subtle.deriveKey( keyDerivationAlgorithm, baseKey, derivedKeyAlgorithm, false, ["wrapKey", "unwrapKey"] ); return derivedKey; } }; } export interface KeyDerivationConfigParams { algorithm_name: string; iterations: number; hash: AlgorithmIdentifier; salt: string; } export interface KeyDerivationConfig { toJSON(): KeyDerivationConfigParams; buildDeriver(): KeyDeriverI; } /** * Creates an instance of a KeyDerivationConfig. * This is used represent the config required to build a KeyDeriver that will * be able to consistently reproduce the same key from a given password, even * if the defaults change. * * If a JSON (decoded) config is provided, then the config will be loaded from it. * If no config is provided, then a config will be generated. This can be exported * with the toJSON method. * * @constructor * @param {object} configJSON A JSON (decoded) config that should be loaded */ export function KeyDerivationConfig( configJSON?: KeyDerivationConfigParams ): KeyDerivationConfig { function generateSalt(): Uint8Array { // Salt length should be at least 64 bits for PBKDF2, we'll use 128 bits to be safe // https://security.stackexchange.com/questions/17994/with-pbkdf2-what-is-an-optimal-hash-size-in-bytes-what-about-the-size-of-the-s const saltBits = 128; // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues : // The Crypto.getRandomValues() method lets you get cryptographically strong random values. const outputArray = new Uint8Array(saltBits / 8); crypto.getRandomValues(outputArray); return outputArray; } function generateAlgorithm(): Pbkdf2Params { return { name: "PBKDF2", // Great discussion of iterations here: // https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pkbdf2-sha256 // Basically, go with as many iterations as possible while still providing tolerable performance. // Could be user configurable one day. iterations: 10 ** 6, hash: "SHA-512", salt: generateSalt() }; } function exportAlgorithm( algorithm: Pbkdf2Params ): KeyDerivationConfigParams { return { // eslint-disable-next-line @typescript-eslint/camelcase algorithm_name: algorithm.name, iterations: algorithm.iterations, hash: algorithm.hash, salt: b64Encode(algorithm.salt as Uint8Array) }; } function importAlgorithm(config: KeyDerivationConfigParams): Pbkdf2Params { return { name: config.algorithm_name, iterations: config.iterations, hash: config.hash, salt: b64Decode(config.salt) }; } const algorithm = configJSON !== undefined ? importAlgorithm(configJSON) : generateAlgorithm(); return { toJSON(): KeyDerivationConfigParams { return exportAlgorithm(algorithm); }, buildDeriver(): KeyDeriverI { return KeyDeriver(algorithm); } }; }