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.
|
|
|
|
|
*
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
* 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';
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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';
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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';
|
2026-05-09 17:38:15 +02:00
|
|
|
import {
|
|
|
|
|
deriveBlobSlotId,
|
|
|
|
|
deriveBlobKey,
|
|
|
|
|
deriveBlobSigningSeed,
|
|
|
|
|
} from '../packages/shade-storage-encrypted/src/crypto.js';
|
|
|
|
|
import { ed25519PublicKeyFromSeed } from '../packages/shade-crypto-web/src/index.js';
|
|
|
|
|
import {
|
|
|
|
|
canonicalApprovalSigningBytes,
|
|
|
|
|
signProxyApproval,
|
|
|
|
|
buildApprovalRequest,
|
|
|
|
|
type ApprovalRequestFrame,
|
|
|
|
|
} from '../packages/shade-sdk/src/index.js';
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
|
|
|
|
|
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;
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
[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 },
|
|
|
|
|
];
|
|
|
|
|
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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);
|
|
|
|
|
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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 [
|
|
|
|
|
{
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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),
|
|
|
|
|
},
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
{
|
|
|
|
|
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
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 17:38:15 +02:00
|
|
|
async function generateBlobVectors(): Promise<Vector[]> {
|
|
|
|
|
// Three (master, app) cases. The first two share a master with
|
|
|
|
|
// different app namespaces, exercising the namespace separation;
|
|
|
|
|
// the third uses a different master entirely.
|
|
|
|
|
const cases: Array<{ masterKey: Uint8Array; app: string }> = [
|
|
|
|
|
{ masterKey: new Uint8Array(32).map((_, i) => i + 1), app: 'prism-profile-v1' },
|
|
|
|
|
{ masterKey: new Uint8Array(32).map((_, i) => i + 1), app: 'test-namespace' },
|
|
|
|
|
{ masterKey: new Uint8Array(32).fill(0xff), app: 'prism-profile-v1' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const kdf = cases.map((c) => {
|
|
|
|
|
const slotId = deriveBlobSlotId(c.masterKey, c.app);
|
|
|
|
|
const blobKey = deriveBlobKey(c.masterKey, c.app);
|
|
|
|
|
const signingSeed = deriveBlobSigningSeed(c.masterKey, c.app);
|
|
|
|
|
const ownerPubkey = ed25519PublicKeyFromSeed(signingSeed);
|
|
|
|
|
return {
|
|
|
|
|
description: `V4.9 blob KDF (master + app="${c.app}")`,
|
|
|
|
|
masterKey: hex(c.masterKey),
|
|
|
|
|
app: c.app,
|
|
|
|
|
slotId: hex(slotId),
|
|
|
|
|
blobKey: hex(blobKey),
|
|
|
|
|
signingSeed: hex(signingSeed),
|
|
|
|
|
ownerPubkey: hex(ownerPubkey),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Three deterministic AEAD round-trips: pinned key, pinned nonce,
|
|
|
|
|
// pinned plaintext. The wire form is `nonce || ct||tag`.
|
|
|
|
|
const aeadCases = [
|
|
|
|
|
{
|
|
|
|
|
key: new Uint8Array(32).fill(0xab),
|
|
|
|
|
nonce: new Uint8Array(12).fill(0x01),
|
|
|
|
|
slotIdHex: '00'.repeat(32),
|
|
|
|
|
plaintext: new TextEncoder().encode('hello shade-blob-v1'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: new Uint8Array(32).map((_, i) => i),
|
|
|
|
|
nonce: new Uint8Array(12).map((_, i) => 0xa0 + i),
|
|
|
|
|
slotIdHex: 'ff'.repeat(32),
|
|
|
|
|
plaintext: new TextEncoder().encode(
|
|
|
|
|
'{"version":1,"hosts":[],"clients":[],"trustedApproverFingerprints":[],"updatedAt":1}',
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const aead = await Promise.all(
|
|
|
|
|
aeadCases.map(async (c) => {
|
|
|
|
|
const aad = new TextEncoder().encode(`shade-profile-aad-v1:${c.slotIdHex}`);
|
|
|
|
|
const ctTag = await aesGcmEncryptDeterministic(c.key, c.nonce, c.plaintext, aad);
|
|
|
|
|
const wire = new Uint8Array(c.nonce.length + ctTag.length);
|
|
|
|
|
wire.set(c.nonce, 0);
|
|
|
|
|
wire.set(ctTag, c.nonce.length);
|
|
|
|
|
return {
|
|
|
|
|
description: 'V4.9 blob AEAD: nonce || AES-256-GCM(key, plaintext, aad="shade-profile-aad-v1:<slotIdHex>")',
|
|
|
|
|
key: hex(c.key),
|
|
|
|
|
nonce: hex(c.nonce),
|
|
|
|
|
slotIdHex: c.slotIdHex,
|
|
|
|
|
plaintext: hex(c.plaintext),
|
|
|
|
|
wire: hex(wire),
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [...kdf, ...aead];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function generateApprovalVectors(): Promise<Vector[]> {
|
|
|
|
|
// Pinned signing-payload bytes for canonical approval. Length-
|
|
|
|
|
// prefixed UTF-8 with u16 BE lengths — Kotlin/Swift implementations
|
|
|
|
|
// produce byte-identical input by spec.
|
|
|
|
|
const cases = [
|
|
|
|
|
{
|
|
|
|
|
domain: 'shade-link-approve-v1',
|
|
|
|
|
requestId: 'aabbccddeeff00112233445566778899',
|
|
|
|
|
hostFingerprint: '11111 22222 33333 44444',
|
|
|
|
|
requestingDeviceFingerprint: '55555 66666 77777 88888',
|
|
|
|
|
decision: 'approve' as const,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
domain: 'shade-link-approve-v1',
|
|
|
|
|
requestId: 'aabbccddeeff00112233445566778899',
|
|
|
|
|
hostFingerprint: '11111 22222 33333 44444',
|
|
|
|
|
requestingDeviceFingerprint: '55555 66666 77777 88888',
|
|
|
|
|
decision: 'reject' as const,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
domain: 'prism-link-approve-v1',
|
|
|
|
|
requestId: '00000000000000000000000000000000',
|
|
|
|
|
hostFingerprint: 'a',
|
|
|
|
|
requestingDeviceFingerprint: 'b',
|
|
|
|
|
decision: 'approve' as const,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const payloads = cases.map((c) => ({
|
|
|
|
|
description: 'V4.10 approval signing payload (length-prefixed u16 BE UTF-8)',
|
|
|
|
|
domain: c.domain,
|
|
|
|
|
requestId: c.requestId,
|
|
|
|
|
hostFingerprint: c.hostFingerprint,
|
|
|
|
|
requestingDeviceFingerprint: c.requestingDeviceFingerprint,
|
|
|
|
|
decision: c.decision,
|
|
|
|
|
signingPayload: hex(canonicalApprovalSigningBytes(c)),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Pinned end-to-end sign + verify: deterministic seed → pubkey →
|
|
|
|
|
// sign(payload) → verify against the pubkey. Lets the Kotlin port
|
|
|
|
|
// assert the exact 64-byte signature without re-running RNG.
|
|
|
|
|
const seed = new Uint8Array(32).map((_, i) => 0x10 + i);
|
|
|
|
|
const pubkey = ed25519PublicKeyFromSeed(seed);
|
|
|
|
|
const fakeReq: ApprovalRequestFrame = {
|
|
|
|
|
kind: 'approvalNeeded',
|
|
|
|
|
requestId: 'cafebabe1234567890abcdef00112233',
|
|
|
|
|
hostAddress: 'device:host',
|
|
|
|
|
hostFingerprint: 'host-fp',
|
|
|
|
|
requestingDevice: { fingerprint: 'req-fp', receivedAt: 1 },
|
|
|
|
|
expiresAt: 9_999_999_999_999,
|
|
|
|
|
domain: 'shade-link-approve-v1',
|
|
|
|
|
};
|
|
|
|
|
const signed = await signProxyApproval({
|
|
|
|
|
request: fakeReq,
|
|
|
|
|
decision: 'approve',
|
|
|
|
|
approverFingerprint: 'approver-fp',
|
|
|
|
|
approverSigningKey: seed,
|
|
|
|
|
crypto,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const e2e = {
|
|
|
|
|
description: 'V4.10 approval Ed25519 sign/verify (deterministic seed)',
|
|
|
|
|
seed: hex(seed),
|
|
|
|
|
publicKey: hex(pubkey),
|
|
|
|
|
request: {
|
|
|
|
|
requestId: fakeReq.requestId,
|
|
|
|
|
hostFingerprint: fakeReq.hostFingerprint,
|
|
|
|
|
requestingDeviceFingerprint: fakeReq.requestingDevice.fingerprint,
|
|
|
|
|
decision: 'approve',
|
|
|
|
|
domain: fakeReq.domain,
|
|
|
|
|
},
|
|
|
|
|
signingPayload: hex(
|
|
|
|
|
canonicalApprovalSigningBytes({
|
|
|
|
|
domain: fakeReq.domain,
|
|
|
|
|
requestId: fakeReq.requestId,
|
|
|
|
|
hostFingerprint: fakeReq.hostFingerprint,
|
|
|
|
|
requestingDeviceFingerprint: fakeReq.requestingDevice.fingerprint,
|
|
|
|
|
decision: 'approve',
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
signature: signed.signature,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Sanity self-check at generation time so a silently broken sign
|
|
|
|
|
// path can't ship vectors that "verify" themselves.
|
|
|
|
|
void buildApprovalRequest; // imported but unused — keeps the symbol live
|
|
|
|
|
|
|
|
|
|
return [...payloads, e2e];
|
|
|
|
|
}
|
|
|
|
|
|
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…');
|
|
|
|
|
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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() }],
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
['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() }],
|
2026-05-09 17:38:15 +02:00
|
|
|
['blob.json', { vectors: await generateBlobVectors() }],
|
|
|
|
|
['approval.json', { vectors: await generateApprovalVectors() }],
|
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);
|
release(v4.0.0): Shade GA — V3.x consolidation + audit prep
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire
format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers
byte-for-byte. The version bump is semantic: audit-cycle complete,
opt-in surface fully exposed, threat model refreshed for every new
surface.
Highlights:
- All 24 @shade/* packages bumped to 4.0.0 in lockstep.
- CHANGELOG 4.0.0 section is the canonical manifest of what landed.
- THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12
Web-Worker boundary) + residual-risks table refreshed.
- OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox,
bridge, observer, /metrics, /healthz, /ready.
- MIGRATION 0.3.x → 4.0 documented + smoke-tested against
shade migrate-storage on a real SQLite DB.
- docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer.
- scripts/soak.ts harness for the GA-stable 2-week soak window.
- All V*.md plans archived under docs/archive/ with Status: Done.
- Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen
non-realtime stack.
Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green.
Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports
version 4.0.0 on /health.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:35:35 +02:00
|
|
|
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);
|
|
|
|
|
});
|