123456789101112131415161718192021222324import { blake3 } from "hash-wasm";25import { remainingBits } from "./cjk";2627export const PAYLOAD_VERSION = 0x03;28export const PAYLOAD_FLAGS = 0x01; 2930export const HEADER_LEN = 31;31export const NONCE_LEN = 12;32export const AUTH_TAG_LEN = 16;33export const CHECKSUM_LEN = 32;34export const SALT_LEN = 16;353637export const MIN_PAYLOAD_LEN = HEADER_LEN + NONCE_LEN + AUTH_TAG_LEN + CHECKSUM_LEN;38394041424344const OFFSET_VERSION = 0;45const OFFSET_ENTRY_COUNT = 1;46const OFFSET_FLAGS = 3;47const OFFSET_ARGON2_MEMORY = 4;48const OFFSET_ARGON2_TIME = 8;49const OFFSET_ARGON2_PARALLEL = 12;50const OFFSET_REMAINING_BITS = 13;51const OFFSET_SALT = 15;525354555657export interface HeaderFields {58 entryCount: number;59 argon2Memory: number;60 argon2Time: number;61 argon2Parallel: number;62 salt: Uint8Array;63 remainingBitsValue: number;64}6566676869export function buildHeader(fields: HeaderFields): Uint8Array {70 if (fields.salt.length !== SALT_LEN) {71 throw new Error(`SALT_LEN must be ${SALT_LEN}, got ${fields.salt.length}`);72 }73 if (fields.entryCount > 0xffff) {74 throw new Error("entryCount exceeds uint16 range");75 }7677 const header = new Uint8Array(HEADER_LEN);78 const view = new DataView(header.buffer);7980 header[OFFSET_VERSION] = PAYLOAD_VERSION;81 view.setUint16(OFFSET_ENTRY_COUNT, fields.entryCount, true);82 header[OFFSET_FLAGS] = PAYLOAD_FLAGS;83 view.setUint32(OFFSET_ARGON2_MEMORY, fields.argon2Memory, true);84 view.setUint32(OFFSET_ARGON2_TIME, fields.argon2Time, true);85 header[OFFSET_ARGON2_PARALLEL] = fields.argon2Parallel;86 view.setUint16(OFFSET_REMAINING_BITS, fields.remainingBitsValue, true);87 header.set(fields.salt, OFFSET_SALT);8889 return header;90}919293949596979899export function remainingBitsForCiphertextLen(ciphertextLen: number): number {100 return remainingBits(HEADER_LEN + NONCE_LEN + ciphertextLen + AUTH_TAG_LEN + CHECKSUM_LEN);101}102103104105106107108export async function finalisePayload(109 header: Uint8Array,110 nonce: Uint8Array,111 ciphertext: Uint8Array,112 authTag: Uint8Array113): Promise<Uint8Array> {114 if (header.length !== HEADER_LEN) throw new Error("header wrong length");115 if (nonce.length !== NONCE_LEN) throw new Error("nonce wrong length");116 if (authTag.length !== AUTH_TAG_LEN) throw new Error("authTag wrong length");117118 const withoutChecksum = concatBytes([header, nonce, ciphertext, authTag]);119 const checksum = await computeBlake3(withoutChecksum);120121 return concatBytes([withoutChecksum, checksum]);122}123124125126127128export interface ExtractedFields extends HeaderFields {129 nonce: Uint8Array;130 ciphertext: Uint8Array;131 authTag: Uint8Array;132 storedChecksum: Uint8Array;133 134 payloadForChecksum: Uint8Array;135}136137138139140141142export function extractFields(binary: Uint8Array): ExtractedFields {143 if (binary.length < MIN_PAYLOAD_LEN) {144 throw new Error("PAYLOAD_TOO_SHORT");145 }146147 const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);148149 const version = binary[OFFSET_VERSION];150 if (version !== PAYLOAD_VERSION) {151 throw new Error(`UNSUPPORTED_VERSION: ${version}`);152 }153154 const flags = binary[OFFSET_FLAGS];155 if (flags !== PAYLOAD_FLAGS) {156 throw new Error(`UNSUPPORTED_FLAGS: ${flags}`);157 }158159 const entryCount = view.getUint16(OFFSET_ENTRY_COUNT, true);160 const argon2Memory = view.getUint32(OFFSET_ARGON2_MEMORY, true);161 const argon2Time = view.getUint32(OFFSET_ARGON2_TIME, true);162 const argon2Parallel = binary[OFFSET_ARGON2_PARALLEL];163 const remainingBitsValue = view.getUint16(OFFSET_REMAINING_BITS, true);164 const salt = binary.slice(OFFSET_SALT, OFFSET_SALT + SALT_LEN);165166 const nonceStart = HEADER_LEN;167 const nonceEnd = nonceStart + NONCE_LEN;168 const nonce = binary.slice(nonceStart, nonceEnd);169170 const checksumStart = binary.length - CHECKSUM_LEN;171 const authTagStart = checksumStart - AUTH_TAG_LEN;172173 if (authTagStart < nonceEnd) {174 throw new Error("PAYLOAD_TOO_SHORT");175 }176177 const ciphertext = binary.slice(nonceEnd, authTagStart);178 const authTag = binary.slice(authTagStart, checksumStart);179 const storedChecksum = binary.slice(checksumStart);180 const payloadForChecksum = binary.slice(0, checksumStart);181182 return {183 entryCount,184 argon2Memory,185 argon2Time,186 argon2Parallel,187 salt,188 remainingBitsValue,189 nonce,190 ciphertext,191 authTag,192 storedChecksum,193 payloadForChecksum,194 };195}196197198199200201export async function computeBlake3(data: Uint8Array): Promise<Uint8Array> {202 const hex = await blake3(data);203 return hexToBytes(hex);204}205206207208209210211export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {212 if (a.length !== b.length) return false;213 let diff = 0;214 for (let i = 0; i < a.length; i++) {215 diff |= a[i] ^ b[i];216 }217 return diff === 0;218}219220221222223224export function concatBytes(chunks: Uint8Array[]): Uint8Array {225 let total = 0;226 for (const c of chunks) total += c.length;227 const out = new Uint8Array(total);228 let offset = 0;229 for (const c of chunks) {230 out.set(c, offset);231 offset += c.length;232 }233 return out;234}235236function hexToBytes(hex: string): Uint8Array {237 const out = new Uint8Array(hex.length / 2);238 for (let i = 0; i < hex.length; i += 2) {239 out[i / 2] = parseInt(hex.slice(i, i + 2), 16);240 }241 return out;242}