@hpke/chacha20poly1305
Version:
A Hybrid Public Key Encryption (HPKE) module extension for ChaCha20/Poly1305
127 lines (126 loc) • 4.64 kB
JavaScript
/**
* 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.