Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/core/embedding/bitstream/preamble.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Prepends a fixed high-entropy sync preamble to a bitstream.
*
* @param payloadBits - Bit array of 0/1 values to append after the preamble.
* @returns A new Uint8Array containing the preamble followed by `payloadBits`.
*/

export function injectPreamble(payloadBits: Uint8Array) {
const PREAMBLE = new Uint8Array([
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1,
0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0,
1, 0, 1, 1, 1,
]);

const totalBits = PREAMBLE.length + payloadBits.length;
const syncStream = new Uint8Array(totalBits);

syncStream.set(PREAMBLE, 0);
syncStream.set(payloadBits, PREAMBLE.length);

return syncStream;
}
30 changes: 30 additions & 0 deletions src/core/embedding/bitstream/serialiser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Serializes an array of byte shards into a flat bit array.
*
* Each input shard is processed in order and each byte is expanded into eight elements
* containing `0` or `1`. Bits for a single byte are emitted from least-significant to
* most-significant (LSB first).
*
* @param interleadShards - Array of `Uint8Array` shards whose bytes will be unpacked into bits
* @returns A `Uint8Array` of `0`/`1` values representing the concatenated bits of all input bytes
*/

export function serialiseBits(interleadShards: Uint8Array[]): Uint8Array {
const totalBytes = interleadShards.reduce(
(acc, shard) => acc + shard.length,
0,
);
const bitstream = new Uint8Array(totalBytes * 8);

let bitIndex = 0;
for (const shard of interleadShards) {
for (let i = 0; i < shard.length; i++) {
const byte = shard[i];

for (let shift = 0; shift < 8; shift++) {
bitstream[bitIndex++] = (byte! >> shift) & 1;
}
}
}
return bitstream;
}
23 changes: 23 additions & 0 deletions src/core/embedding/crypto/aes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createCipheriv, randomBytes } from "node:crypto";

/**
* Encrypts a payload using AES-256-GCM and returns a single packet containing the nonce, ciphertext, and authentication tag.
*
* @param framedPayload - Plaintext bytes to encrypt.
* @param key - 32-byte AES-256 key.
* @returns A Uint8Array structured as [nonce (12 bytes) | ciphertext | authTag (16 bytes)].
*/

export function encryptPayload(framedPayload: Uint8Array, key: Uint8Array) {
const nonce = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, nonce);

const cipherText = Buffer.concat([
cipher.update(framedPayload),
cipher.final(),
]);
const authTag = cipher.getAuthTag();

const encryptedPacket = Buffer.concat([nonce, cipherText, authTag]);
return new Uint8Array(encryptedPacket);
}
31 changes: 31 additions & 0 deletions src/core/embedding/crypto/keyDerivation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { pbkdf2 } from "node:crypto";
import { promisify } from "node:util";

const pbkdf2Async = promisify(pbkdf2);

/**
* Derives a 256-bit cryptographic key from a password using PBKDF2.
*
* @param password - The human-readable password to derive the key from.
* @param salt - A cryptographic salt (unique and unpredictable) used during derivation.
* @returns The derived 32-byte (256-bit) key as a `Uint8Array`.
* @throws If key derivation fails.
*/
export async function deriveKey(password: string, salt: Uint8Array) {
const iterations = 600_000;
const keyLength = 32;
const digest = "sha256";

try {
const derivedKey = await pbkdf2Async(
password,
salt,
iterations,
keyLength,
digest,
);
return new Uint8Array(derivedKey);
} catch (error) {
throw new Error("Failed to derive key");
}
}
31 changes: 31 additions & 0 deletions src/core/embedding/fec/interleave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Distributes input shards so that shards from the same Reed–Solomon block are separated in the output order.
*
* @param shards - Input shard buffers in their original (block-consecutive) order.
* @param dataShards - Number of data shards per Reed–Solomon block.
* @param parityShards - Number of parity shards per Reed–Solomon block.
* @returns The same shard buffers reordered into an interleaved array so consecutive physical errors affect different RS blocks.
*/

export function interleave(
shards: Uint8Array[],
dataShards: number,
parityShards: number,
) {
const totalShardsPerBlock = dataShards + parityShards;
const numBlocks = Math.ceil(shards.length / totalShardsPerBlock);
const interleaved: Uint8Array[] = new Array(shards.length);

let index = 0;
for (let col = 0; col < totalShardsPerBlock; col++) {
for (let row = 0; row < numBlocks; row++) {
const sourceIdx = row * totalShardsPerBlock + col;

if (sourceIdx < shards.length) {
interleaved[index++] = shards[sourceIdx]!;
}
}
}

return interleaved;
}
86 changes: 86 additions & 0 deletions src/core/embedding/fec/readSolomon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ReedSolomonErasure } from "@subspace/reed-solomon-erasure.wasm";
import fs from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
let rsInstance: ReedSolomonErasure | null = null;

/**
* Lazily initializes and returns a singleton ReedSolomonErasure engine loaded from the package's WASM binary.
*
* Loads "reed_solomon_erasure_bg.wasm" from the installed @subspace/reed-solomon-erasure.wasm package, caches the created instance, and reuses it for subsequent calls. Throws an Error if the wasm file is not found at the expected location.
*
* @returns A cached or newly created ReedSolomonErasure instance
*/
async function getRSEngine(): Promise<ReedSolomonErasure> {
if (!rsInstance) {
const pkgPath = require.resolve("@subspace/reed-solomon-erasure.wasm");
const pkgDir = path.dirname(pkgPath);

const wasmPath = path.join(pkgDir, "reed_solomon_erasure_bg.wasm");
if (!fs.existsSync(wasmPath)) {
throw new Error(`WASM file missing! Looked for it at: ${wasmPath}`);
}

const wasmBuffer = fs.readFileSync(wasmPath);
rsInstance = ReedSolomonErasure.fromBytes(wasmBuffer);
}
return rsInstance;
}

/**
* Encode an array of packets into data and parity shards using Reed–Solomon FEC.
*
* Encodes the input packets in sequential blocks of `dataShards`, producing `dataShards + parityShards`
* fixed-size shards per block and returning them as a flat array (data shards followed by parity shards
* for each block).
*
* @param packets - Array of equal-length `Uint8Array` packets to encode. An empty array returns an empty result.
* @param dataShards - Number of data shards per block.
* @param parityShards - Number of parity shards to produce per block (default: 3).
* @returns A flat `Uint8Array[]` containing all encoded shards; for each input block the first `dataShards`
* are the original data shards (padded as needed) followed by `parityShards` parity shards.
* @throws Error if the underlying WASM Reed–Solomon encoder returns a non-OK result.
*/

export async function applyFEC(
packets: Uint8Array[],
dataShards: number,
parityShards: number = 3,
): Promise<Uint8Array[]> {
if (packets.length === 0) return [];

const rs = await getRSEngine();
const shardLength = packets[0]!.length;
const encodedStream: Uint8Array[] = [];

for (let i = 0; i < packets.length; i += dataShards) {
const block = packets.slice(i, i + dataShards);

const totalShards = dataShards + parityShards;
const contiguousBuffer = new Uint8Array(totalShards * shardLength);

for (let j = 0; j < dataShards; j++) {
if (block[j]) {
contiguousBuffer.set(block[j]!, j * shardLength);
}
}

const result = rs.encode(contiguousBuffer, dataShards, parityShards);

if (result !== ReedSolomonErasure.RESULT_OK) {
throw new Error(`WASM FEC Encoding failed with internal code: ${result}`);
}

for (let j = 0; j < totalShards; j++) {
const shard = contiguousBuffer.slice(
j * shardLength,
(j + 1) * shardLength,
);
encodedStream.push(shard);
}
}

return encodedStream;
}
41 changes: 41 additions & 0 deletions src/core/embedding/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { randomBytes } from "node:crypto";
import { framePayload } from "./payload/framing.js";
import { loadFileToUint8 } from "./payload/Uint8FileReader.js";
import { deriveKey } from "./crypto/keyDerivation.js";
import { encryptPayload } from "./crypto/aes.js";
import { packetize } from "./payload/packer.js";
import { applyFEC } from "./fec/readSolomon.js";
import { interleave } from "./fec/interleave.js";
import { serialiseBits } from "./bitstream/serialiser.js";
import { injectPreamble } from "./bitstream/preamble.js";

/**
* Prepare a file payload for transmission by framing, encrypting, packetizing, applying FEC and interleaving, serialising to bits, and injecting a preamble.
*
* @param filename - Path to the input file whose contents will be embedded and transmitted
* @param password - Password used to derive the encryption key for the payload
* @returns An object with:
* - `finalBitStream`: the prepared bitstream ready for transmission
* - `salt`: the 16-byte salt used for key derivation
*/
export async function preparePayload(filename: string, password: string) {
const rawBytes = await loadFileToUint8(filename);
const framed = framePayload(rawBytes, filename);

const salt = randomBytes(16);
const key = await deriveKey(password, salt);

const encrypted = encryptPayload(framed, key);

const packets = packetize(encrypted, 256);

const FEC_SHARDS = await applyFEC(packets, 6, 3);

const interleaved = interleave(FEC_SHARDS, 6, 3);

const payloadBits = serialiseBits(interleaved);

const finalBitStream = injectPreamble(payloadBits);

return { finalBitStream, salt };
}
18 changes: 18 additions & 0 deletions src/core/embedding/payload/Uint8FileReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
/**
* Load a file from the project's "data" directory and return its contents as a Uint8Array.
*
* @param filename - Name of the file located inside the project's "data" directory
* @returns A Uint8Array view over the file's contents
* @throws Error if the file cannot be read (message: "Failed to read file: <filepath>")
*/
export async function loadFileToUint8(filename: string): Promise<Uint8Array> {
const filepath = path.join(process.cwd(), "data", filename);
try {
const buffer = await readFile(filepath);
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
} catch (error) {
throw new Error(`Failed to read file: ${filepath}`);
}
}
33 changes: 33 additions & 0 deletions src/core/embedding/payload/framing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Wraps file bytes and filename with a 10-byte protocol header to produce a single packet.
*
* The resulting packet layout is: 10-byte header, filename bytes, then file bytes.
*
* @param filebytes - Raw file data to place into the packet
* @param filename - File name which will be UTF-8 encoded and included immediately after the header (encoded length must be <= 255)
* @returns A Uint8Array containing the header followed by the filename bytes and the file bytes
* @throws Error when the UTF-8 encoded `filename` is longer than 255 bytes
*/

export function framePayload(filebytes: Uint8Array, filename: string) {
const encoder = new TextEncoder();
const filenameBytes = encoder.encode(filename);

if (filenameBytes.length > 255) {
throw new Error("Filename too long");
}

const headerSize = 10;
const totalSize = headerSize + filenameBytes.length + filebytes.length;
const packet = new Uint8Array(totalSize);
const view = new DataView(packet.buffer);

view.setUint32(4, 0x44484944, false);
view.setUint32(4, 0x01);
view.setUint8(5, filenameBytes.length);
view.setUint32(6, filebytes.length, false);

packet.set(filenameBytes, headerSize);
packet.set(filebytes, headerSize + filenameBytes.length);
return packet;
}
35 changes: 35 additions & 0 deletions src/core/embedding/payload/packer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Breaks encrypted data into sequential frames, each prefixed with a 4-byte big-endian frame identifier.
*
* @param encryptedData - The encrypted payload to split into frames.
* @param frameSize - Maximum number of payload bytes per frame (default: 512). The produced frame length is 4 + chunk length.
* @returns An array of `Uint8Array` frames; each frame starts with a 4-byte big-endian frameId (starting at 0) followed by a slice of the input data.
*/

export function packetize(
encryptedData: Uint8Array,
frameSize: number = 512,
): Uint8Array[] {
const frames: Uint8Array[] = [];
const totalBytes = encryptedData.length;

let offset = 0;
let frameId = 0;

while (offset < totalBytes) {
const end = Math.min(offset + frameSize, totalBytes);
const chunk = encryptedData.slice(offset, end);

const frame = new Uint8Array(4 + chunk.length);
const view = new DataView(frame.buffer);

view.setUint32(0, frameId, false);
frame.set(chunk, 4);
frames.push(frame);

offset += frameSize;
frameId++;
}

return frames;
}
9 changes: 7 additions & 2 deletions src/core/profiler/freqBarkMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ export function computeBarkEnergy(powerSpectrum: Float32Array): Float32Array {
return barkEnergy;
}

/**
* Identify FFT bin indices whose power is below their corresponding Bark-band threshold.
*
* @param powerSpectrum - Power values for each FFT bin.
* @param thresholds - Threshold values for each Bark band (indexed by band); used to compare against each bin's power.
* @returns An array of FFT bin indices where the bin power is less than its Bark-band threshold.
*/
export function identifySafeBins(
powerSpectrum: Float32Array,
thresholds: Float32Array,
) {
// let count = 0;
const result: number[] = [];

for (let i = 0; i < powerSpectrum.length; i++) {
Expand All @@ -37,7 +43,6 @@ export function identifySafeBins(

if (binPower < bandThreshold) {
result.push(i);
// count++;
}
}
return result;
Expand Down
Loading