Files
Shade/scripts/generate-vectors.ts

686 lines
24 KiB
TypeScript
Raw Permalink Normal View History

feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
#!/usr/bin/env bun
/**
* Generate cross-platform test vectors from the TypeScript implementation.
*
* The output JSON files are loaded by BOTH the TypeScript and Kotlin test
* suites. Any divergence between platforms fails CI immediately.
*
* Schema: every file is `{ "version": <int>, "vectors": [...] }`.
* Bump `VECTOR_FILE_VERSION` whenever the vector schema (NOT just values)
* changes, so downstream consumers can fail loudly on mismatch.
*
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
* Usage: bun run scripts/generate-vectors.ts
*/
import { writeFileSync } from 'fs';
import { join } from 'path';
import { SubtleCryptoProvider } from '../packages/shade-crypto-web/src/index.js';
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
import { computeFingerprint } from '../packages/shade-core/src/fingerprint.js';
import { kdfChainKey, kdfRootKey, deriveInitialRootKey } from '../packages/shade-core/src/keys.js';
import { encodeEnvelope, encodeStreamChunk, decodeStreamChunk } from '../packages/shade-proto/src/index.js';
import type { StreamChunkWire } from '../packages/shade-proto/src/index.js';
import type { ShadeEnvelope, RatchetMessage, PreKeyMessage } from '../packages/shade-core/src/index.js';
import {
deriveStreamKey,
deriveLaneKey,
buildChunkNonce,
buildChunkAad,
aesGcmEncryptWithNonce,
} from '../packages/shade-streams/src/index.js';
const VECTOR_FILE_VERSION = 2;
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
const crypto = new SubtleCryptoProvider();
const OUT_DIR = join(import.meta.dir, '..', 'test-vectors');
function hex(bytes: Uint8Array): string {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
function fromHex(str: string): Uint8Array {
const bytes = new Uint8Array(str.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(str.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
interface Vector {
description: string;
[key: string]: unknown;
}
// AES-GCM with caller-supplied nonce. The CryptoProvider interface picks a
// random nonce internally, so vector generation goes around it via SubtleCrypto
// directly — same primitive `@shade/streams` already uses.
async function aesGcmEncryptDeterministic(
key: Uint8Array,
nonce: Uint8Array,
plaintext: Uint8Array,
aad: Uint8Array,
): Promise<Uint8Array> {
const subtle = globalThis.crypto.subtle;
const aesKey = await subtle.importKey(
'raw',
key as unknown as ArrayBuffer,
'AES-GCM',
false,
['encrypt'],
);
const out = await subtle.encrypt(
{ name: 'AES-GCM', iv: nonce as unknown as ArrayBuffer, additionalData: aad as unknown as ArrayBuffer },
aesKey,
plaintext as unknown as ArrayBuffer,
);
return new Uint8Array(out);
}
// Mirror of `encodeHeader` in @shade/core/ratchet.ts — kept inline to avoid
// exporting an internal symbol just for tests.
function encodeRatchetHeader(
dhPublicKey: Uint8Array,
previousCounter: number,
counter: number,
): Uint8Array {
const buf = new Uint8Array(40);
buf.set(dhPublicKey, 0);
const view = new DataView(buf.buffer);
view.setUint32(32, previousCounter, false);
view.setUint32(36, counter, false);
return buf;
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
}
// ─── HKDF vectors ───────────────────────────────────────────
async function generateHkdfVectors(): Promise<Vector[]> {
const cases = [
{ ikm: '01'.repeat(32), salt: '02'.repeat(32), info: 'test', length: 32 },
{ ikm: 'ab'.repeat(32), salt: '00'.repeat(32), info: 'ShadeRootRatchet', length: 64 },
{ ikm: 'cd'.repeat(32), salt: '00'.repeat(32), info: 'ShadeX3DH', length: 32 },
];
const vectors: Vector[] = [];
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
for (const c of cases) {
const out = await crypto.hkdf(
fromHex(c.ikm),
fromHex(c.salt),
new TextEncoder().encode(c.info),
c.length,
);
vectors.push({
description: `HKDF-SHA256 with ikm=${c.ikm.slice(0, 8)}... info="${c.info}"`,
ikm: c.ikm,
salt: c.salt,
info: c.info,
length: c.length,
output: hex(out),
});
}
return vectors;
}
// ─── KDF chain vectors ─────────────────────────────────────
async function generateKdfChainVectors(): Promise<Vector[]> {
const rootKey = new Uint8Array(32).fill(0x11);
const dhOutput = new Uint8Array(32).fill(0x22);
const rootResult = await kdfRootKey(crypto, rootKey, dhOutput);
const chainKey = new Uint8Array(32).fill(0x33);
const chainResult = await kdfChainKey(crypto, chainKey);
return [
{
description: 'Root key ratchet: kdfRootKey',
rootKey: hex(rootKey),
dhOutput: hex(dhOutput),
newRootKey: hex(rootResult.newRootKey),
chainKey: hex(rootResult.chainKey),
},
{
description: 'Chain key ratchet: kdfChainKey',
chainKey: hex(chainKey),
newChainKey: hex(chainResult.newChainKey),
messageKey: hex(chainResult.messageKey),
},
];
}
// ─── X3DH initial root key ─────────────────────────────────
async function generateX3DHVectors(): Promise<Vector[]> {
const secrets = [
new Uint8Array(32).fill(0xaa),
new Uint8Array(32).fill(0xbb),
new Uint8Array(32).fill(0xcc),
];
const rootKey3 = await deriveInitialRootKey(crypto, secrets);
const secrets4 = [...secrets, new Uint8Array(32).fill(0xdd)];
const rootKey4 = await deriveInitialRootKey(crypto, secrets4);
return [
{
description: 'X3DH initial root key with 3 DH outputs (no one-time prekey)',
secrets: secrets.map(hex),
rootKey: hex(rootKey3),
},
{
description: 'X3DH initial root key with 4 DH outputs (with one-time prekey)',
secrets: secrets4.map(hex),
rootKey: hex(rootKey4),
},
];
}
// ─── Fingerprint vectors ───────────────────────────────────
async function generateFingerprintVectors(): Promise<Vector[]> {
const cases = [
{ sig: '01'.repeat(32), dh: '02'.repeat(32) },
{ sig: 'ab'.repeat(32), dh: 'cd'.repeat(32) },
];
const vectors: Vector[] = [];
for (const c of cases) {
const fp = await computeFingerprint(crypto, fromHex(c.sig), fromHex(c.dh));
vectors.push({
description: `Fingerprint for signing=${c.sig.slice(0, 8)}... dh=${c.dh.slice(0, 8)}...`,
signingKey: c.sig,
dhKey: c.dh,
fingerprint: fp,
});
}
return vectors;
}
// ─── Wire format vectors ───────────────────────────────────
async function generateWireFormatVectors(): Promise<Vector[]> {
const ratchetMsg: RatchetMessage = {
dhPublicKey: new Uint8Array(32).fill(0x11),
previousCounter: 42,
counter: 7,
ciphertext: new Uint8Array(16).fill(0x22),
nonce: new Uint8Array(12).fill(0x33),
};
const envelopeRatchet: ShadeEnvelope = {
type: 'ratchet',
content: ratchetMsg,
timestamp: 0,
senderAddress: '',
};
const bytesRatchet = encodeEnvelope(envelopeRatchet);
const innerRatchet: RatchetMessage = {
dhPublicKey: new Uint8Array(32).fill(0x44),
previousCounter: 0,
counter: 0,
ciphertext: new Uint8Array(8).fill(0x55),
nonce: new Uint8Array(12).fill(0x66),
};
const preKeyMsgWithOTPK: PreKeyMessage = {
registrationId: 0x12345678,
preKeyId: 99,
signedPreKeyId: 1,
ephemeralKey: new Uint8Array(32).fill(0x77),
identityDHKey: new Uint8Array(32).fill(0x88),
message: innerRatchet,
};
const envelopePreKey: ShadeEnvelope = {
type: 'prekey',
content: preKeyMsgWithOTPK,
timestamp: 0,
senderAddress: '',
};
const bytesPreKey = encodeEnvelope(envelopePreKey);
const preKeyMsgNoOTPK: PreKeyMessage = {
registrationId: 1,
preKeyId: undefined,
signedPreKeyId: 1,
ephemeralKey: new Uint8Array(32).fill(0x99),
identityDHKey: new Uint8Array(32).fill(0xaa),
message: innerRatchet,
};
const bytesPreKeyNoOTPK = encodeEnvelope({
type: 'prekey',
content: preKeyMsgNoOTPK,
timestamp: 0,
senderAddress: '',
});
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
return [
{
description: 'Wire format: RatchetMessage encoding (wire VERSION 0x02 — u32 length-prefixed)',
kind: 'ratchet',
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
message: {
dhPublicKey: hex(ratchetMsg.dhPublicKey),
previousCounter: ratchetMsg.previousCounter,
counter: ratchetMsg.counter,
ciphertext: hex(ratchetMsg.ciphertext),
nonce: hex(ratchetMsg.nonce),
},
encoded: hex(bytesRatchet),
},
{
description: 'Wire format: PreKeyMessage with one-time prekey (wire 0x02 type 0x01)',
kind: 'prekey',
message: {
registrationId: preKeyMsgWithOTPK.registrationId,
preKeyId: preKeyMsgWithOTPK.preKeyId ?? null,
signedPreKeyId: preKeyMsgWithOTPK.signedPreKeyId,
ephemeralKey: hex(preKeyMsgWithOTPK.ephemeralKey),
identityDHKey: hex(preKeyMsgWithOTPK.identityDHKey),
inner: {
dhPublicKey: hex(innerRatchet.dhPublicKey),
previousCounter: innerRatchet.previousCounter,
counter: innerRatchet.counter,
ciphertext: hex(innerRatchet.ciphertext),
nonce: hex(innerRatchet.nonce),
},
},
encoded: hex(bytesPreKey),
},
{
description: 'Wire format: PreKeyMessage without one-time prekey (preKeyId=null encoded as 0xFFFFFFFF)',
kind: 'prekey',
message: {
registrationId: preKeyMsgNoOTPK.registrationId,
preKeyId: null,
signedPreKeyId: preKeyMsgNoOTPK.signedPreKeyId,
ephemeralKey: hex(preKeyMsgNoOTPK.ephemeralKey),
identityDHKey: hex(preKeyMsgNoOTPK.identityDHKey),
inner: {
dhPublicKey: hex(innerRatchet.dhPublicKey),
previousCounter: innerRatchet.previousCounter,
counter: innerRatchet.counter,
ciphertext: hex(innerRatchet.ciphertext),
nonce: hex(innerRatchet.nonce),
},
},
encoded: hex(bytesPreKeyNoOTPK),
},
];
}
// ─── Ratchet step vectors ──────────────────────────────────
//
// A ratchet "encrypt step" is fully deterministic given (rootKey, dhSendPriv,
// dhRemotePub, plaintext, fixed nonce, counters). The vector records every
// intermediate derivation so each implementation can verify byte-parity at
// every layer (kdfRootKey → kdfChainKey → header AAD → AES-GCM ciphertext) and
// also verify decrypt(ciphertext, nonce, aad, messageKey) === plaintext.
async function generateRatchetStepVectors(): Promise<Vector[]> {
// Deterministic inputs
const rootKey = new Uint8Array(32).fill(0xa1);
const dhSendPriv = new Uint8Array(32).fill(0xb2);
const dhSendPub = new Uint8Array(32).fill(0xb3); // not used in derivation, only AAD
const dhRemotePub = new Uint8Array(32).fill(0xc4);
const plaintext = new TextEncoder().encode('Shade ratchet roundtrip vector v1');
const fixedNonce = new Uint8Array(12).fill(0x5e);
const previousCounter = 2;
const counter = 0;
// Step 1: DH between local send priv and remote pub
const dhOutput = await crypto.x25519(dhSendPriv, dhRemotePub);
// Step 2: kdfRootKey to advance root + get chain key
const { newRootKey, chainKey } = await kdfRootKey(crypto, rootKey, dhOutput);
// Step 3: kdfChainKey to derive messageKey
const { newChainKey, messageKey } = await kdfChainKey(crypto, chainKey);
// Step 4: Header AAD bytes
const aad = encodeRatchetHeader(dhSendPub, previousCounter, counter);
// Step 5: AES-GCM with deterministic nonce
const ciphertext = await aesGcmEncryptDeterministic(messageKey, fixedNonce, plaintext, aad);
return [
{
description: 'Ratchet step: deterministic encrypt (kdfRootKey + kdfChainKey + AES-GCM with fixed nonce)',
inputs: {
rootKey: hex(rootKey),
dhSendPrivateKey: hex(dhSendPriv),
dhSendPublicKey: hex(dhSendPub),
dhRemotePublicKey: hex(dhRemotePub),
previousCounter,
counter,
plaintext: hex(plaintext),
nonce: hex(fixedNonce),
},
derived: {
dhOutput: hex(dhOutput),
newRootKey: hex(newRootKey),
chainKey: hex(chainKey),
newChainKey: hex(newChainKey),
messageKey: hex(messageKey),
aad: hex(aad),
},
ciphertext: hex(ciphertext),
},
];
}
// ─── Streams 0x11 vectors ──────────────────────────────────
//
// Covers the @shade/streams primitives that V3.5 §3 (M-Cross 3) requires
// Kotlin to mirror byte-for-byte: HKDF labels with embedded NULs, u32-be
// laneId encoding inside the lane-key info, deterministic (laneId, seq)
// chunk nonces, the 29-byte chunk AAD, end-to-end chunk encrypt/decrypt,
// and the wire 0x11 envelope encode/decode.
async function generateStreamsVectors(): Promise<Vector[]> {
const streamSecret = new Uint8Array(32).fill(0xa1);
const streamId = new Uint8Array(16).fill(0xb2);
const streamKey = await deriveStreamKey(crypto, streamSecret, streamId);
const laneIdsForKeys = [0, 1, 2, 0xffff_ffff];
const laneKeyVectors: Array<{ laneId: number; laneKey: string }> = [];
for (const laneId of laneIdsForKeys) {
const laneKey = await deriveLaneKey(crypto, streamKey, streamId, laneId);
laneKeyVectors.push({ laneId, laneKey: hex(laneKey) });
}
const noncePairs: Array<{ laneId: number; seq: bigint }> = [
{ laneId: 0, seq: 0n },
{ laneId: 0, seq: 1n },
{ laneId: 1, seq: 0n },
{ laneId: 0xffff_ffff, seq: 0xffff_ffff_ffff_fffen },
];
const nonceVectors = noncePairs.map((p) => ({
laneId: p.laneId,
seq: p.seq.toString(),
nonce: hex(buildChunkNonce(p.laneId, p.seq)),
}));
const aadCases: Array<{ laneId: number; seq: bigint; isLast: boolean }> = [
{ laneId: 0, seq: 0n, isLast: false },
{ laneId: 1, seq: 7n, isLast: true },
{ laneId: 0xffff_ffff, seq: 0xffff_ffff_ffff_fffen, isLast: false },
];
const aadVectors = aadCases.map((c) => ({
laneId: c.laneId,
seq: c.seq.toString(),
isLast: c.isLast,
aad: hex(buildChunkAad(streamId, c.laneId, c.seq, c.isLast)),
}));
// End-to-end chunk encrypt with lane 0, seq 0, isLast=true
const laneId = 0;
const seq = 0n;
const isLast = true;
const laneKey = await deriveLaneKey(crypto, streamKey, streamId, laneId);
const nonce = buildChunkNonce(laneId, seq);
const aad = buildChunkAad(streamId, laneId, seq, isLast);
const plaintext = new TextEncoder().encode('Shade streams 0x11 chunk vector');
const ciphertext = await aesGcmEncryptWithNonce(laneKey, nonce, plaintext, aad);
// Wire 0x11 envelope (extra-aad field = 0 bytes per current spec)
const wire: StreamChunkWire = {
streamId,
laneId,
seq,
isLast,
nonce,
aad: new Uint8Array(0),
ciphertext,
};
const wireBytes = encodeStreamChunk(wire);
// Sanity: roundtrip-decode locally so the recorded bytes are always parseable
const decoded = decodeStreamChunk(wireBytes);
if (hex(decoded.ciphertext) !== hex(ciphertext)) {
throw new Error('streams wire 0x11 roundtrip diverged in generator');
}
return [
{
description: 'deriveStreamKey: HKDF(streamSecret, salt=streamId, info="shade-stream/v1\\0master")',
streamSecret: hex(streamSecret),
streamId: hex(streamId),
streamKey: hex(streamKey),
},
{
description: 'deriveLaneKey: HKDF(streamKey, salt=streamId, info="shade-stream/v1\\0lane\\0" || u32_be(laneId))',
streamKey: hex(streamKey),
streamId: hex(streamId),
lanes: laneKeyVectors,
},
{
description: 'buildChunkNonce(laneId, seq): u32_be(laneId) || u64_be(seq)',
nonces: nonceVectors,
},
{
description: 'buildChunkAad(streamId, laneId, seq, isLast): streamId(16) || u32_be(laneId) || u64_be(seq) || u8(isLast)',
streamId: hex(streamId),
cases: aadVectors,
},
{
description: 'End-to-end chunk encrypt: AES-256-GCM(laneKey, nonce, plaintext, aad)',
laneId,
seq: seq.toString(),
isLast,
laneKey: hex(laneKey),
nonce: hex(nonce),
aad: hex(aad),
plaintext: hex(plaintext),
ciphertext: hex(ciphertext),
},
{
description: 'Wire 0x11 stream-chunk envelope encode/decode',
streamId: hex(streamId),
laneId,
seq: seq.toString(),
isLast,
nonce: hex(nonce),
extraAad: hex(new Uint8Array(0)),
ciphertext: hex(ciphertext),
encoded: hex(wireBytes),
},
];
}
// ─── Backup format vectors ─────────────────────────────────
//
// Backup v1 derives an AES-256-GCM key from `(passphrase, salt)` via
// HKDF-SHA256 with info `"ShadeBackupKey"`, then encrypts the payload.
// The vector pins the HKDF output and an end-to-end encrypt/decrypt for
// a known plaintext + fixed nonce.
async function generateBackupVectors(): Promise<Vector[]> {
const passphrase = 'correct-horse-battery-staple';
const salt = new Uint8Array(32).fill(0xa5);
const info = new TextEncoder().encode('ShadeBackupKey');
const backupKey = await crypto.hkdf(
new TextEncoder().encode(passphrase),
salt,
info,
32,
);
const plaintext = new TextEncoder().encode(
JSON.stringify({ version: 1, identity: null, sessions: [] }),
);
const fixedNonce = new Uint8Array(12).fill(0xc7);
const ciphertext = await aesGcmEncryptDeterministic(
backupKey,
fixedNonce,
plaintext,
new Uint8Array(0),
);
return [
{
description: 'Backup v1: HKDF(passphrase_utf8, salt, info="ShadeBackupKey", 32) -> backupKey',
passphrase,
salt: hex(salt),
info: 'ShadeBackupKey',
backupKey: hex(backupKey),
},
{
description: 'Backup v1: AES-256-GCM(backupKey, plaintext, no AAD) with deterministic nonce',
backupKey: hex(backupKey),
nonce: hex(fixedNonce),
plaintext: hex(plaintext),
plaintextUtf8: new TextDecoder().decode(plaintext),
ciphertext: hex(ciphertext),
},
];
}
// ─── Group sender-keys vectors ─────────────────────────────
//
// Sender-key step pins three things:
// 1. The 12-byte sender header AAD (`u16_be(gLen) || g || u16_be(sLen) || s || u32_be(iter)`)
// 2. The chain-key advance (kdfChainKey) producing (newChainKey, messageKey)
// 3. AES-256-GCM encrypt with deterministic nonce + Ed25519 signature
// over `aad || ciphertext`. Ed25519 is deterministic so the signature
// bytes are byte-parity-checkable cross-platform.
async function generateGroupVectors(): Promise<Vector[]> {
// Static Ed25519 keypair (RFC 8032 §7.1 test vector 1)
const signingPrivateKey = fromHex(
'9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60',
);
const signingPublicKey = fromHex(
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a',
);
const groupId = 'group:42';
const senderAddress = 'alice@example.com';
const iteration = 5;
const chainKey = new Uint8Array(32).fill(0x9b);
const enc = new TextEncoder();
const gBytes = enc.encode(groupId);
const sBytes = enc.encode(senderAddress);
const aad = new Uint8Array(2 + gBytes.length + 2 + sBytes.length + 4);
const aadView = new DataView(aad.buffer);
let off = 0;
aadView.setUint16(off, gBytes.length, false); off += 2;
aad.set(gBytes, off); off += gBytes.length;
aadView.setUint16(off, sBytes.length, false); off += 2;
aad.set(sBytes, off); off += sBytes.length;
aadView.setUint32(off, iteration, false);
const { newChainKey, messageKey } = await kdfChainKey(crypto, chainKey);
const fixedNonce = new Uint8Array(12).fill(0x7d);
const plaintext = enc.encode('hello group');
const ciphertext = await aesGcmEncryptDeterministic(messageKey, fixedNonce, plaintext, aad);
const signed = new Uint8Array(aad.length + ciphertext.length);
signed.set(aad, 0);
signed.set(ciphertext, aad.length);
const signature = await crypto.sign(signingPrivateKey, signed);
// Sanity: verify with the matching public key in the generator
const ok = await crypto.verify(signingPublicKey, signed, signature);
if (!ok) throw new Error('group sender-key signature verify failed in generator');
return [
{
description: 'Sender header AAD: u16_be(gLen) || g || u16_be(sLen) || s || u32_be(iter)',
groupId,
senderAddress,
iteration,
aad: hex(aad),
},
{
description: 'Sender-key step: kdfChainKey + deterministic AES-GCM + Ed25519 sign(aad || ct)',
chainKey: hex(chainKey),
groupId,
senderAddress,
iteration,
plaintext: hex(plaintext),
nonce: hex(fixedNonce),
signingPrivateKey: hex(signingPrivateKey),
signingPublicKey: hex(signingPublicKey),
newChainKey: hex(newChainKey),
messageKey: hex(messageKey),
aad: hex(aad),
ciphertext: hex(ciphertext),
signature: hex(signature),
},
];
}
// ─── Storage-encryption HKDF parity ────────────────────────
//
// `test-vectors/storage-encryption.json` already exists (V3.2). It pins
// scrypt params + HKDF info templates + AAD templates. The Kotlin port
// will need scrypt (likely via Bouncy Castle) before the full file can
// be consumed; for now this generator emits a sub-vector covering only
// the HKDF-storage-key + HKDF-field-key + deterministic-nonce derivations
// — those Tink already supports. Bumps the `_ts_subset_version`.
async function generateStorageEncryptionSubset(): Promise<Vector[]> {
const masterKey = new Uint8Array(32);
for (let i = 0; i < 32; i++) masterKey[i] = i + 1;
const storageInfo = new TextEncoder().encode('shade-storage-v1');
const storageKey = await crypto.hkdf(masterKey, new Uint8Array(0), storageInfo, 32);
const fieldCases = [
{ table: 'sessions', column: 'session' },
{ table: 'identity', column: 'identity' },
{ table: 'trusted_identities', column: 'trusted_identity' },
];
const fieldKeys: Array<{ table: string; column: string; fieldKey: string }> = [];
for (const c of fieldCases) {
const info = new TextEncoder().encode(`shade-field-v1:${c.table}:${c.column}`);
const k = await crypto.hkdf(storageKey, new Uint8Array(0), info, 32);
fieldKeys.push({ table: c.table, column: c.column, fieldKey: hex(k) });
}
const rowKey = new Uint8Array(32).fill(0xcd);
const nonceCases = [
{ table: 'sessions', pk: 'alice' },
{ table: 'sessions', pk: 'bob' },
{ table: 'identity', pk: '1' },
];
const nonces: Array<{ table: string; pk: string; nonce: string }> = [];
for (const c of nonceCases) {
const info = new TextEncoder().encode(`shade-row-nonce-v1:${c.table}:${c.pk}`);
const n = await crypto.hkdf(rowKey, new Uint8Array(0), info, 12);
nonces.push({ table: c.table, pk: c.pk, nonce: hex(n) });
}
return [
{
description: 'Storage HKDF: storageKey = HKDF(masterKey, salt=0, info="shade-storage-v1", 32)',
masterKey: hex(masterKey),
storageKey: hex(storageKey),
},
{
description: 'Storage HKDF: fieldKey = HKDF(storageKey, salt=0, info="shade-field-v1:{table}:{column}", 32)',
storageKey: hex(storageKey),
fields: fieldKeys,
},
{
description: 'Storage HKDF: rowNonce = HKDF(rowKey, salt=0, info="shade-row-nonce-v1:{table}:{pk}", 12)',
rowKey: hex(rowKey),
nonces,
},
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
];
}
async function main() {
console.log('Generating cross-platform test vectors…');
const files: Array<[string, { vectors: Vector[] }]> = [
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
['hkdf.json', { vectors: await generateHkdfVectors() }],
['kdf-chain.json', { vectors: await generateKdfChainVectors() }],
['x3dh.json', { vectors: await generateX3DHVectors() }],
['fingerprint.json', { vectors: await generateFingerprintVectors() }],
['wire-format.json', { vectors: await generateWireFormatVectors() }],
['ratchet-step.json', { vectors: await generateRatchetStepVectors() }],
['streams.json', { vectors: await generateStreamsVectors() }],
['backup.json', { vectors: await generateBackupVectors() }],
['group.json', { vectors: await generateGroupVectors() }],
['storage-hkdf.json', { vectors: await generateStorageEncryptionSubset() }],
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
];
for (const [name, data] of files) {
const path = join(OUT_DIR, name);
const versioned = { version: VECTOR_FILE_VERSION, ...data };
writeFileSync(path, JSON.stringify(versioned, null, 2) + '\n');
console.log(`${name} (v${VECTOR_FILE_VERSION}, ${data.vectors.length} vectors)`);
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors Phase C complete: Shade now has a Kotlin implementation with byte-for-byte compatibility to the TypeScript core, verified by shared test vectors. M-Cross 1: shade-android Kotlin module - build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization - Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.) - CryptoProvider interface - TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC) - KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey) with the same info strings and salts as @shade/core - Fingerprint (safety number) computation matching TS exactly - X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing - Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt, ratchetDecrypt, DH ratchet step, skipped key cache - Wire format matching @shade/proto byte-for-byte - StorageProvider interface + MemoryStorage impl - High-level ShadeSessionManager mirroring @shade/core's API M-Cross 2: Cross-platform test vectors - scripts/generate-vectors.ts emits JSON fixtures from the TS implementation - Vectors cover: HKDF, KDF chain (root + chain), X3DH root key, fingerprint computation, wire format encoding - packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS produces the same output as the committed vectors - android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt loads the SAME JSON and verifies Kotlin produces identical bytes M-Cross 3: Nova Android migration plan - android/shade-android/MIGRATION-NOVA.md — concrete steps to replace Nova's static PushKeyStore AES with Shade sessions - Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate) - Smoke test recipe for end-to-end TS → Kotlin push flow 251 tests passing on the TS side. Kotlin tests run via Gradle when the Android SDK is available; the vectors guarantee they'll pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00
}
console.log('Done.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});