/** * Key derivation and PRNG for watermark embedding * * Uses a simple but effective approach: derive deterministic * pseudo-random sequences from a secret key using a seeded PRNG. * For browser compatibility, we use a pure-JS implementation. */ /** * Simple hash function (djb2 variant) for string to 32-bit seed */ function hashString(str: string): number { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0; } return hash >>> 0; } /** * Mix two 32-bit values (MurmurHash3 finalizer) */ function mix(h: number): number { h = ((h >>> 16) ^ h) * 0x45d9f3b | 0; h = ((h >>> 16) ^ h) * 0x45d9f3b | 0; h = (h >>> 16) ^ h; return h >>> 0; } /** * Seeded PRNG (xorshift128) * Produces deterministic sequences from a seed */ export class SeededPRNG { private s0: number; private s1: number; private s2: number; private s3: number; constructor(seed: number) { // Initialize state from seed using splitmix32 this.s0 = seed | 0; this.s1 = mix(seed + 1); this.s2 = mix(seed + 2); this.s3 = mix(seed + 3); // Warm up for (let i = 0; i < 20; i++) this.next(); } /** Get next random 32-bit unsigned integer */ next(): number { let t = this.s3; const s = this.s0; this.s3 = this.s2; this.s2 = this.s1; this.s1 = s; t ^= t << 11; t ^= t >>> 8; this.s0 = t ^ s ^ (s >>> 19); return this.s0 >>> 0; } /** Get random float in [0, 1) */ nextFloat(): number { return this.next() / 0x100000000; } /** Get random float in [min, max) */ nextRange(min: number, max: number): number { return min + this.nextFloat() * (max - min); } /** Get random integer in [0, max) */ nextInt(max: number): number { return (this.next() % max) >>> 0; } } /** * Derive a seed from key + purpose string */ export function deriveSeed(key: string, purpose: string): number { const combined = key + ':' + purpose; return mix(hashString(combined)); } /** * Generate dither values for DM-QIM embedding * Returns array of dither values in [-delta/2, +delta/2] */ export function generateDithers(key: string, count: number, delta: number): Float64Array { const prng = new SeededPRNG(deriveSeed(key, 'dither')); const dithers = new Float64Array(count); for (let i = 0; i < count; i++) { dithers[i] = prng.nextRange(-delta / 2, delta / 2); } return dithers; } /** * Generate a permutation (Fisher-Yates) for bit interleaving */ export function generatePermutation(key: string, length: number): Uint32Array { const prng = new SeededPRNG(deriveSeed(key, 'permutation')); const perm = new Uint32Array(length); for (let i = 0; i < length; i++) perm[i] = i; for (let i = length - 1; i > 0; i--) { const j = prng.nextInt(i + 1); const tmp = perm[i]; perm[i] = perm[j]; perm[j] = tmp; } return perm; } /** * Generate inverse permutation */ export function generateInversePermutation(key: string, length: number): Uint32Array { const perm = generatePermutation(key, length); const inv = new Uint32Array(length); for (let i = 0; i < length; i++) { inv[perm[i]] = i; } return inv; }