UNPKG

@hpke/chacha20poly1305

Version:

A Hybrid Public Key Encryption (HPKE) module extension for ChaCha20/Poly1305

127 lines (126 loc) 4.64 kB
/** * This file is based on noble-ciphers (https://github.com/paulmillr/noble-ciphers). * * noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) * * The original file is located at: * https://github.com/paulmillr/noble-ciphers/blob/749cdf9cd07ebdd19e9b957d0f172f1045179695/src/utils.ts */ /** * Utilities for hex, bytes, CSPRNG. * @module */ import { abytes, aexists, anumber, aoutput, clean, copyBytes, createView, isLE, numberToBigint, u32, } from "@hpke/common"; export { abytes, aexists, anumber, aoutput, clean, copyBytes, createView, isLE, u32, }; /** Asserts something is boolean. */ export function abool(b) { if (typeof b !== "boolean") throw new Error(`boolean expected, not ${b}`); } /** Cast u8 / u16 / u32 to u8. */ export function u8(arr) { return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); } /** * Wraps a cipher: validates args, ensures encrypt() can only be called once. * @__NO_SIDE_EFFECTS__ */ // deno-lint-ignore no-explicit-any export const wrapCipher = (params, constructor) => { // deno-lint-ignore no-explicit-any function wrappedCipher(key, ...args) { // Validate key abytes(key, undefined, "key"); // Big-Endian hardware is rare. Just in case someone still decides to run ciphers: if (!isLE) { throw new Error("Non little-endian hardware is not yet supported"); } // Validate nonce if nonceLength is present if (params.nonceLength !== undefined) { const nonce = args[0]; abytes(nonce, params.varSizeNonce ? undefined : params.nonceLength, "nonce"); } // Validate AAD if tagLength present const tagl = params.tagLength; if (tagl && args[1] !== undefined) abytes(args[1], undefined, "AAD"); const cipher = constructor(key, ...args); const checkOutput = (fnLength, output) => { if (output !== undefined) { if (fnLength !== 2) throw new Error("cipher output not supported"); abytes(output, undefined, "output"); } }; // Create wrapped cipher with validation and single-use encryption let called = false; const wrCipher = { encrypt(data, output) { if (called) { throw new Error("cannot encrypt() twice with same key + nonce"); } called = true; abytes(data); checkOutput(cipher.encrypt.length, output); return cipher.encrypt(data, output); }, decrypt(data, output) { abytes(data); if (tagl && data.length < tagl) { throw new Error('"ciphertext" expected length bigger than tagLength=' + tagl); } checkOutput(cipher.decrypt.length, output); return cipher.decrypt(data, output); }, }; return wrCipher; } Object.assign(wrappedCipher, params); return wrappedCipher; }; export function checkOpts(defaults, opts) { if (opts == null || typeof opts !== "object") { throw new Error("options must be defined"); } const merged = Object.assign(defaults, opts); return merged; } /** Compares 2 uint8array-s in kinda constant time. */ export function equalBytes(a, b) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; return diff === 0; } /** * By default, returns u8a of length. * When out is available, it checks it for validity and uses it. */ export function getOutput(expectedLength, out, onlyAligned = true) { if (out === undefined) return new Uint8Array(expectedLength); if (out.length !== expectedLength) { throw new Error('"output" expected Uint8Array of length ' + expectedLength + ", got: " + out.length); } if (onlyAligned && !isAligned32(out)) { throw new Error("invalid output, must be aligned"); } return out; } export function u64Lengths(dataLength, aadLength, isLE) { abool(isLE); const num = new Uint8Array(16); const view = createView(num); view.setBigUint64(0, numberToBigint(aadLength), isLE); view.setBigUint64(8, numberToBigint(dataLength), isLE); return num; } // Is byte array aligned to 4 byte offset (u32)? export function isAligned32(bytes) { return bytes.byteOffset % 4 === 0; } // copy bytes to new u8a (aligned). Because Buffer.slice is broken. // Re-exported from @hpke/common.