Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.
Merged
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
Binary file modified coupling_runtime.exp
Binary file not shown.
Binary file modified coupling_runtime.lib
Binary file not shown.
3 changes: 3 additions & 0 deletions src/couplingSeed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ process.env.COUPLING_SERVICE_SECRET = process.env.COUPLING_SERVICE_SECRET || 'se
process.env.COUPLING_WM_MASTER_KEY =
'00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff';

const { config } = await import('./config');
config.wmMasterKeyHex = process.env.COUPLING_WM_MASTER_KEY;

const { deriveCouplingSeedHex, isCouplingSeedConfigured, COUPLING_SEED_HEX_LENGTH } = await import(
'./couplingSeed'
);
Expand Down
10 changes: 5 additions & 5 deletions src/ffi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ afterEach(() => {

describe('normalizeTokenHexForFfi', () => {
it('lowercases valid token hex before sending it to the native runtime', () => {
expect(normalizeTokenHexForFfi('ABCDEF123456')).toBe('abcdef123456');
expect(normalizeTokenHexForFfi('ABCDEF1234567890')).toBe('abcdef1234567890');
});

it('rejects non-hex token values', () => {
Expand All @@ -37,12 +37,12 @@ describe('normalizeTokenHexForFfi', () => {
);
});

it('rejects token values outside the supported native bounds', () => {
it('rejects token values outside the seeded v2 token width', () => {
expect(() => normalizeTokenHexForFfi('abcd')).toThrow(
'tokenHex must be between 8 and 64 hex characters'
'tokenHex must be exactly 16 hex characters'
);
expect(() => normalizeTokenHexForFfi('a'.repeat(66))).toThrow(
'tokenHex must be between 8 and 64 hex characters'
expect(() => normalizeTokenHexForFfi('a'.repeat(32))).toThrow(
'tokenHex must be exactly 16 hex characters'
);
});

Expand Down
210 changes: 27 additions & 183 deletions src/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ import { createHash } from 'node:crypto';
import { existsSync, readFileSync } from 'node:fs';
import { config } from './config';

export type CouplingAssetType = 'png' | 'fbx' | 'text';
export type CouplingAssetType = 'png' | 'fbx';

export type CouplingDecodeInput = {
filePath: string;
assetType: CouplingAssetType;
expectedTokenLength?: number;
// Per-job seed (64 hex) that placed the v2 mark. Required to decode a v2 image/mesh mark; when
// absent, only the legacy (pre-seed) decoders are attempted.
seedHex?: string;
// Per-job seed (64 hex) that placed the mark — required. Decode is seed-only; there is no blind path.
seedHex: string;
};

export type CouplingEncodeInput = {
filePath: string;
assetType: Exclude<CouplingAssetType, 'text'>;
assetType: CouplingAssetType;
tokenHex: string;
// Per-job seed (64 hex) from the server; the sole placement secret (no key is baked into the client).
seedHex: string;
Expand All @@ -26,21 +24,11 @@ export type CouplingDecodeResult = {
decoderKind: string;
tokenHex: string;
tokenLength: number;
// 2 = v2 (seeded DCT/vertex-norm), 1 = legacy/text. Lets forensics tell which engine matched.
// Always 2 (the v2 seeded engine). Retained for forensic reporting / future versioning.
wmVersion: number;
};

type NativeSymbols = {
xg_0115: (filePathUtf8: number, tokenHexUtf8: number) => number;
xg_0118: (filePathUtf8: number, outHexUtf8: number, outCap: number) => number;
xg_0119: (filePathUtf8: number, outHexUtf8: number, outCap: number) => number;
xg_0120: (filePathUtf8: number, tokenHexUtf8: number) => number;
xg_0121: (
filePathUtf8: number,
expectedHexLength: number,
outHexUtf8: number,
outCap: number
) => number;
// v2 image (DCT-domain QIM, all raster formats). Encode(path, token, seed): 0 ok, 1 skip, <0 err.
xg_0122: (filePathUtf8: number, tokenHexUtf8: number, seedHexUtf8: number) => number;
xg_0123: (filePathUtf8: number, seedHexUtf8: number, outHexUtf8: number, outCap: number) => number;
Expand All @@ -54,62 +42,10 @@ type NativeLibrary = {
close(): void;
};

const MIN_TOKEN_HEX_LENGTH = 8;
const MAX_TOKEN_HEX_LENGTH = 64;
const SEEDED_TOKEN_HEX_LENGTH = 16;
const OUTPUT_CAPACITY = MAX_TOKEN_HEX_LENGTH + 1;
const TOKEN_HEX_LENGTH = 16;
const OUTPUT_CAPACITY = TOKEN_HEX_LENGTH + 1;
const HEX_RE = /^[0-9a-f]+$/;

const PNG_ERRORS: Record<number, string> = {
[-1]: 'native sodium initialization failed',
[-2]: 'native PNG decoder received invalid arguments',
[-3]: 'native PNG decoder could not read the asset bytes',
[-4]: 'native PNG decoder requires a non-empty PNG file',
[-5]: 'native PNG decoder rejected the PNG signature',
[-6]: 'native PNG decoder found malformed PNG chunks',
[-7]: 'native PNG decoder could not unmask lane 0',
[-8]: 'native PNG decoder could not unmask lane 1',
[-9]: 'native PNG decoder could not unmask lane 2',
[-10]: 'native PNG decoder could not reach token quorum',
};

const TEXT_ERRORS: Record<number, string> = {
[-1]: 'native text decoder failed sodium initialization',
[-2]: 'native text decoder received invalid arguments',
[-3]: 'native text decoder could not read the asset bytes',
[-4]: 'native text decoder only supports files between 1 byte and 8 MiB',
[-5]: 'native text decoder could not unmask lane 0',
[-6]: 'native text decoder could not unmask lane 1',
[-7]: 'native text decoder could not unmask lane 2',
[-8]: 'native text decoder could not reach token quorum',
};

const FBX_ERRORS: Record<number, string> = {
[-1]: 'native FBX decoder failed sodium initialization',
[-2]: 'native FBX decoder received invalid arguments',
[-3]: 'native FBX decoder expected an even token length between 8 and 64',
[-4]: 'native FBX decoder output buffer was too small',
[-5]: 'native FBX decoder could not read the asset bytes',
[-6]: 'native FBX decoder only supports files between 1 byte and 64 MiB',
[-7]: 'native FBX decoder rejected binary bytes while using the text path',
[-8]: 'native FBX decoder found no carrier data',
[-9]: 'native FBX decoder did not find all three carrier lanes',
[-10]: 'native FBX decoder could not parse carrier values',
[-11]: 'native FBX decoder could not allocate its extraction buffer',
[-12]: 'native FBX decoder does not have enough carrier data for the requested token length',
[-13]: 'native FBX decoder could not build lane permutations',
[-14]: 'native FBX decoder failed while converting extracted bits to hex',
[-41]: 'native FBX decoder could not parse binary FBX nodes',
[-42]: 'native FBX decoder could not collect binary FBX carrier data',
[-43]: 'native FBX decoder did not find all three binary FBX carrier lanes',
[-44]: 'native FBX decoder could not convert binary FBX carrier values',
[-45]: 'native FBX decoder could not allocate its binary extraction buffer',
[-46]: 'native FBX decoder does not have enough binary carrier data for the requested token length',
[-47]: 'native FBX decoder could not build binary lane permutations',
[-48]: 'native FBX decoder failed while converting binary extracted bits to hex',
[-49]: 'native FBX decoder could not build binary lane seed material',
};

export class NativeCouplingError extends Error {
readonly code: number;
readonly decoderKind: string;
Expand Down Expand Up @@ -179,26 +115,6 @@ function loadNativeLibrary(): NativeLibrary {
verifyConfiguredDllIntegrity();
try {
nativeLibrary = dlopen(config.dllPath, {
xg_0115: {
args: ['ptr', 'ptr'],
returns: 'i32',
},
xg_0118: {
args: ['ptr', 'ptr', 'u32'],
returns: 'i32',
},
xg_0119: {
args: ['ptr', 'ptr', 'u32'],
returns: 'i32',
},
xg_0120: {
args: ['ptr', 'ptr'],
returns: 'i32',
},
xg_0121: {
args: ['ptr', 'u32', 'ptr', 'u32'],
returns: 'i32',
},
xg_0122: {
args: ['ptr', 'ptr', 'ptr'],
returns: 'i32',
Expand Down Expand Up @@ -232,6 +148,9 @@ function decodeBufferToHex(buffer: Uint8Array): string {
if (!HEX_RE.test(tokenHex)) {
throw new Error('Coupling runtime returned invalid token data');
}
if (tokenHex.length !== TOKEN_HEX_LENGTH) {
throw new Error('Coupling runtime returned token data with invalid length');
}
return tokenHex;
}

Expand All @@ -243,11 +162,8 @@ export function normalizeTokenHexForFfi(tokenHex: string): string {
if (normalized.length % 2 !== 0) {
throw new Error('tokenHex must contain an even number of hex characters');
}
if (
normalized.length < MIN_TOKEN_HEX_LENGTH ||
normalized.length > MAX_TOKEN_HEX_LENGTH
) {
throw new Error('tokenHex must be between 8 and 64 hex characters');
if (normalized.length !== TOKEN_HEX_LENGTH) {
throw new Error('tokenHex must be exactly 16 hex characters');
}
return normalized;
}
Expand All @@ -266,92 +182,31 @@ function toNativeUtf8Buffer(value: string): Buffer {
return Buffer.from(`${value}\0`, 'utf8');
}

function getDefaultExpectedTokenLength(assetType: CouplingAssetType): number | undefined {
if (assetType === 'fbx') {
return 32;
}
return undefined;
}

export function getDecoderKind(assetType: CouplingAssetType): string {
switch (assetType) {
case 'png':
return 'coupling-png-ffi';
case 'fbx':
return 'coupling-fbx-ffi';
case 'text':
return 'coupling-text-ffi';
}
}

function getErrorMap(assetType: CouplingAssetType): Record<number, string> {
switch (assetType) {
case 'png':
return PNG_ERRORS;
case 'fbx':
return FBX_ERRORS;
case 'text':
return TEXT_ERRORS;
}
return assetType === 'png' ? 'coupling-png-v2-ffi' : 'coupling-fbx-v2-ffi';
}

// Seed-only v2 decode: the per-job seed that placed the mark is required, and there is no blind or
// legacy fallback. A miss (wrong seed, tampered, or unmarked asset) throws NativeCouplingError.
function callNativeDecoder(input: CouplingDecodeInput): CouplingDecodeResult {
const library = loadNativeLibrary();
const output = new Uint8Array(OUTPUT_CAPACITY);
const outputPointer = ptr(output);
const filePathBuffer = toNativeUtf8Buffer(input.filePath);
const filePathPointer = ptr(filePathBuffer);
const filePathPointer = ptr(toNativeUtf8Buffer(input.filePath));
const seedPointer = ptr(toNativeUtf8Buffer(normalizeSeedHexForFfi(input.seedHex)));
const decoderKind = getDecoderKind(input.assetType);
const seedHex = input.seedHex ? normalizeSeedHexForFfi(input.seedHex) : undefined;

// A v2 mark can only be read with the per-job seed that placed it; without one we go straight to
// the legacy decoders.
const seedPointer = seedHex ? ptr(toNativeUtf8Buffer(seedHex)) : null;

let exitCode: number;
if (input.assetType === 'png') {
if (seedPointer !== null) {
exitCode = library.symbols.xg_0123(filePathPointer, seedPointer, outputPointer, output.byteLength);
if (exitCode === 0) {
const tokenHex = decodeBufferToHex(output);
return { decoderKind: 'coupling-png-v2-ffi', tokenHex, tokenLength: tokenHex.length, wmVersion: 2 };
}
}
exitCode = library.symbols.xg_0118(filePathPointer, outputPointer, output.byteLength);
} else if (input.assetType === 'text') {
exitCode = library.symbols.xg_0119(filePathPointer, outputPointer, output.byteLength);
} else {
if (seedPointer !== null) {
exitCode = library.symbols.xg_0125(filePathPointer, seedPointer, outputPointer, output.byteLength);
if (exitCode === 0) {
const tokenHex = decodeBufferToHex(output);
return { decoderKind: 'coupling-fbx-v2-ffi', tokenHex, tokenLength: tokenHex.length, wmVersion: 2 };
}
}
const expectedTokenLength = input.expectedTokenLength ?? getDefaultExpectedTokenLength('fbx');
if (!expectedTokenLength) {
throw new Error(`Missing expected token length for ${decoderKind}`);
}
exitCode = library.symbols.xg_0121(
filePathPointer,
expectedTokenLength,
outputPointer,
output.byteLength
);
}

const exitCode =
input.assetType === 'png'
? library.symbols.xg_0123(filePathPointer, seedPointer, outputPointer, output.byteLength)
: library.symbols.xg_0125(filePathPointer, seedPointer, outputPointer, output.byteLength);

if (exitCode !== 0) {
const errorMessage = getErrorMap(input.assetType)[exitCode] ?? `native decoder failed with code ${exitCode}`;
throw new NativeCouplingError(decoderKind, exitCode, errorMessage);
throw new NativeCouplingError(decoderKind, exitCode, `native v2 decoder failed with code ${exitCode}`);
}

const tokenHex = decodeBufferToHex(output);
return {
decoderKind,
tokenHex,
tokenLength: tokenHex.length,
wmVersion: 1,
};
return { decoderKind, tokenHex, tokenLength: tokenHex.length, wmVersion: 2 };
}

export function decodeCouplingAsset(input: CouplingDecodeInput): CouplingDecodeResult {
Expand All @@ -365,23 +220,12 @@ export function encodeCouplingAsset(input: CouplingEncodeInput): void {
const filePathPointer = ptr(toNativeUtf8Buffer(input.filePath));
const tokenHexPointer = ptr(toNativeUtf8Buffer(tokenHex));
const seedHexPointer = ptr(toNativeUtf8Buffer(seedHex));
const useSeededV2 = tokenHex.length === SEEDED_TOKEN_HEX_LENGTH;
const decoderKind =
input.assetType === 'png'
? useSeededV2
? 'coupling-png-v2-encode-ffi'
: 'coupling-png-legacy-encode-ffi'
: useSeededV2
? 'coupling-fbx-v2-encode-ffi'
: 'coupling-fbx-legacy-encode-ffi';
input.assetType === 'png' ? 'coupling-png-v2-encode-ffi' : 'coupling-fbx-v2-encode-ffi';
const exitCode =
input.assetType === 'png'
? useSeededV2
? library.symbols.xg_0122(filePathPointer, tokenHexPointer, seedHexPointer)
: library.symbols.xg_0115(filePathPointer, tokenHexPointer)
: useSeededV2
? library.symbols.xg_0124(filePathPointer, tokenHexPointer, seedHexPointer)
: library.symbols.xg_0120(filePathPointer, tokenHexPointer);
? library.symbols.xg_0122(filePathPointer, tokenHexPointer, seedHexPointer)
: library.symbols.xg_0124(filePathPointer, tokenHexPointer, seedHexPointer);
// 0 = applied, 1 = skipped (uncarryable asset — never an error, never blocks), <0 = real failure.
if (exitCode !== 0 && exitCode !== 1) {
throw new NativeCouplingError(
Expand Down
4 changes: 2 additions & 2 deletions src/materialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ describe('materializeProtectedPayload', () => {
}
});

it('materializes legacy-length coupling job tokens without forcing v2 encoding', async () => {
it('materializes coupling job assets with v2 seeded embedding', async () => {
const workspaceDir = await mkdtemp(path.join(tmpdir(), 'coupling-materialize-'));
try {
const fixture = await createMaterializationFixture(workspaceDir);
Expand All @@ -411,7 +411,7 @@ describe('materializeProtectedPayload', () => {
globalThis.fetch = createMaterializationFetchStub({
descriptor: fixture.descriptor,
contentKeyBase64: fixture.contentKeyBase64,
couplingJobs: [{ assetPath: 'Assets/Protected/artifact.png', tokenHex: 'a'.repeat(32) }],
couplingJobs: [{ assetPath: 'Assets/Protected/artifact.png', tokenHex: 'a'.repeat(16) }],
licenseSubject: 'lic-test',
redeemCounts,
receiptCounts,
Expand Down
27 changes: 27 additions & 0 deletions src/nativeExports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'bun:test';
import path from 'node:path';

describe('Windows native export ordinals', () => {
it('preserves surviving public DLL ordinals after removing legacy exports', async () => {
const defPath = path.resolve(import.meta.dir, '..', 'yucp_coupling', 'yucp_coupling.def');
const def = await Bun.file(defPath).text();

expect(def).toContain('xg_0122 @4');
expect(def).toContain('xg_0123 @5');
expect(def).toContain('xg_0124 @6');
expect(def).toContain('xg_0125 @7');
});

it('preserves surviving runtime-helper ordinals after removing legacy exports', async () => {
const defPath = path.resolve(
import.meta.dir,
'..',
'yucp_coupling',
'yucp_coupling.runtime-helper.def'
);
const def = await Bun.file(defPath).text();

expect(def).toContain('xg_0122 @3');
expect(def).toContain('xg_0124 @4');
});
});
Loading