release(v4.0.0): Shade GA — V3.x consolidation + audit prep
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled

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>
This commit is contained in:
2026-05-03 18:35:35 +02:00
parent 8b055912b7
commit e6fdf31b49
298 changed files with 37909 additions and 256 deletions

20
test-vectors/backup.json Normal file
View File

@@ -0,0 +1,20 @@
{
"version": 2,
"vectors": [
{
"description": "Backup v1: HKDF(passphrase_utf8, salt, info=\"ShadeBackupKey\", 32) -> backupKey",
"passphrase": "correct-horse-battery-staple",
"salt": "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5",
"info": "ShadeBackupKey",
"backupKey": "ee14b1292de8761db52cfdb5e4e24d8c165d5f2a5268c8d0ac6da51e291d027f"
},
{
"description": "Backup v1: AES-256-GCM(backupKey, plaintext, no AAD) with deterministic nonce",
"backupKey": "ee14b1292de8761db52cfdb5e4e24d8c165d5f2a5268c8d0ac6da51e291d027f",
"nonce": "c7c7c7c7c7c7c7c7c7c7c7c7",
"plaintext": "7b2276657273696f6e223a312c226964656e74697479223a6e756c6c2c2273657373696f6e73223a5b5d7d",
"plaintextUtf8": "{\"version\":1,\"identity\":null,\"sessions\":[]}",
"ciphertext": "2605b2494fff14d4151e1a22da0740f4d13631a046498f588cf281febe40f1671f4e07978ea183b20ebfbe9ca2e638f06c9d443e550a587e2460d1"
}
]
}

View File

@@ -1,4 +1,5 @@
{
"version": 2,
"vectors": [
{
"description": "Fingerprint for signing=01010101... dh=02020202...",

28
test-vectors/group.json Normal file
View File

@@ -0,0 +1,28 @@
{
"version": 2,
"vectors": [
{
"description": "Sender header AAD: u16_be(gLen) || g || u16_be(sLen) || s || u32_be(iter)",
"groupId": "group:42",
"senderAddress": "alice@example.com",
"iteration": 5,
"aad": "000867726f75703a34320011616c696365406578616d706c652e636f6d00000005"
},
{
"description": "Sender-key step: kdfChainKey + deterministic AES-GCM + Ed25519 sign(aad || ct)",
"chainKey": "9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b9b",
"groupId": "group:42",
"senderAddress": "alice@example.com",
"iteration": 5,
"plaintext": "68656c6c6f2067726f7570",
"nonce": "7d7d7d7d7d7d7d7d7d7d7d7d",
"signingPrivateKey": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
"signingPublicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"newChainKey": "2c0682d93e1a4a746afc72507774ce592f9730e64435e8503fb8dff9f0372e11",
"messageKey": "dc53b8b0848902f22d548e84a7daf779ff3695f42ee6caa8c4ebac4d70907354",
"aad": "000867726f75703a34320011616c696365406578616d706c652e636f6d00000005",
"ciphertext": "415321ea34c712b032779852d8d834b9bd4d7bf7e982514f73031f",
"signature": "05a860496422e5267818a773e8ba74eac75e3ed0120f4fe2662b782888a74ceadcc57d77a68b7870ca5ad9cf66822e46598b527fc6428d4db11014adff18630a"
}
]
}

View File

@@ -1,4 +1,5 @@
{
"version": 2,
"vectors": [
{
"description": "HKDF-SHA256 with ikm=01010101... info=\"test\"",

View File

@@ -1,4 +1,5 @@
{
"version": 2,
"vectors": [
{
"description": "Root key ratchet: kdfRootKey",

View File

@@ -0,0 +1,27 @@
{
"version": 2,
"vectors": [
{
"description": "Ratchet step: deterministic encrypt (kdfRootKey + kdfChainKey + AES-GCM with fixed nonce)",
"inputs": {
"rootKey": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"dhSendPrivateKey": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"dhSendPublicKey": "b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3",
"dhRemotePublicKey": "c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4",
"previousCounter": 2,
"counter": 0,
"plaintext": "5368616465207261746368657420726f756e647472697020766563746f72207631",
"nonce": "5e5e5e5e5e5e5e5e5e5e5e5e"
},
"derived": {
"dhOutput": "e68094b458c45f0f179bb4fb662f6e705b92d27be634632314080027faf53d17",
"newRootKey": "ebfa04f9adcb4821c7c3468186973ce69ace5669cd4017a85fd1b38b8662a6ec",
"chainKey": "b546ecd7e160947d022da8f0f4b3c898d2c5798e935ef731533d0ffabe67ee80",
"newChainKey": "5509f8a8bd2d67d9e94137c5eb07dde4ca380bccd795c29ee0e1e0189e8a3de8",
"messageKey": "f4be2749a29ff011798f2f97f503dd3e044557321f9466d32de1c6c7e60f889d",
"aad": "b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b30000000200000000"
},
"ciphertext": "28e2943c856c38933c59dc5421b4cc099b5dd2966e038aa35f44049e9a1ea57490617fea550e6bdbf7db342f3f5b82a377"
}
]
}

View File

@@ -0,0 +1,60 @@
{
"version": "shade-storage-v1",
"description": "At-rest storage encryption test vectors. Cross-implementation parity check for V3.2 (TS) and V3.5 (Android).",
"kdf": {
"scrypt": {
"passphrase": "correct-horse-battery-staple",
"salt_hex": "00112233445566778899aabbccddeeff",
"N": 1024,
"r": 8,
"p": 1,
"dkLen": 32,
"expected_master_key_hex": "aee2dc14f3a46c563f8906a9c8777f167c868dc06015a983fdf2dbba078a3597"
},
"hkdf_storage_key": {
"info": "shade-storage-v1",
"master_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
"expected_storage_key_hex": "059a250e15aa02952ab977f441e217cfcd4a6d9be8b51cf93d001a90eeb6accc"
},
"hkdf_field_key": {
"info_template": "shade-field-v1:{table}:{column}",
"storage_key_hex_filled_with": "ab",
"expected_field_key_hex_for_sessions_session": "cbe428b4e8be2d7c4cd707dbac7e02881f2da34ee5b00bdc9bc1ebf2f096087a",
"samples": [
{ "table": "sessions", "column": "session" },
{ "table": "identity", "column": "identity" },
{ "table": "trusted_identities","column": "trusted_identity" }
]
},
"deterministic_nonce": {
"info_template": "shade-row-nonce-v1:{table}:{pk}",
"row_key_hex_filled_with": "cd",
"expected_nonce_hex_for_sessions_alice": "f72f291a2d3cd0ba652b60c5",
"samples": [
{ "table": "sessions", "pk": "alice" },
{ "table": "sessions", "pk": "bob" },
{ "table": "identity", "pk": "1" }
]
}
},
"aead": {
"algorithm": "AES-256-GCM",
"wire_format": "nonce(12) || ciphertext || tag(16)",
"aad_template": "shade-aad-v1|{table}|{column}|{pk}",
"round_trips": [
{
"table": "sessions",
"column": "session",
"pk": "alice",
"plaintext_utf8": "{\"rootKey\":\"AAAA\"}"
},
{
"table": "identity",
"column": "identity",
"pk": "1",
"plaintext_utf8": "{\"signingPublicKey\":\"BBBB\"}"
}
]
},
"_note": "Fields marked RUNTIME are computed by the test harness in packages/shade-storage-encrypted/tests/test-vectors.test.ts. Android (V3.5) consumes the same JSON and must produce byte-identical KDF outputs."
}

View File

@@ -0,0 +1,52 @@
{
"version": 2,
"vectors": [
{
"description": "Storage HKDF: storageKey = HKDF(masterKey, salt=0, info=\"shade-storage-v1\", 32)",
"masterKey": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
"storageKey": "059a250e15aa02952ab977f441e217cfcd4a6d9be8b51cf93d001a90eeb6accc"
},
{
"description": "Storage HKDF: fieldKey = HKDF(storageKey, salt=0, info=\"shade-field-v1:{table}:{column}\", 32)",
"storageKey": "059a250e15aa02952ab977f441e217cfcd4a6d9be8b51cf93d001a90eeb6accc",
"fields": [
{
"table": "sessions",
"column": "session",
"fieldKey": "0d1a61a8208d5374cc925039d336aace2cca57e10776e0d8a3a873638d6dc592"
},
{
"table": "identity",
"column": "identity",
"fieldKey": "bad14e2d54916c687de89c06d2efbc7fedfd3e5d1cbd2075afebc3f14e7c8917"
},
{
"table": "trusted_identities",
"column": "trusted_identity",
"fieldKey": "8e32c2d3d74c70fb5108793e8449cb0a4a151d57f8302fb61448814188da139d"
}
]
},
{
"description": "Storage HKDF: rowNonce = HKDF(rowKey, salt=0, info=\"shade-row-nonce-v1:{table}:{pk}\", 12)",
"rowKey": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
"nonces": [
{
"table": "sessions",
"pk": "alice",
"nonce": "f72f291a2d3cd0ba652b60c5"
},
{
"table": "sessions",
"pk": "bob",
"nonce": "0f8922ef1e99d0bf8ff74a9a"
},
{
"table": "identity",
"pk": "1",
"nonce": "8daf77c7b3fbaffc3ddcdb14"
}
]
}
]
}

105
test-vectors/streams.json Normal file
View File

@@ -0,0 +1,105 @@
{
"version": 2,
"vectors": [
{
"description": "deriveStreamKey: HKDF(streamSecret, salt=streamId, info=\"shade-stream/v1\\0master\")",
"streamSecret": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"streamId": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"streamKey": "2eb635cee7797336215657bd84a60238b49053d7fd1a2696b4cb18046b6dfbf5"
},
{
"description": "deriveLaneKey: HKDF(streamKey, salt=streamId, info=\"shade-stream/v1\\0lane\\0\" || u32_be(laneId))",
"streamKey": "2eb635cee7797336215657bd84a60238b49053d7fd1a2696b4cb18046b6dfbf5",
"streamId": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"lanes": [
{
"laneId": 0,
"laneKey": "0849f549230b1522e00fa50199bd336cbccd896293a6d42e9fced63665a471f0"
},
{
"laneId": 1,
"laneKey": "74d4b6384626b751581c94f589c34846f67b5e198b12960c45da147d2913fa0b"
},
{
"laneId": 2,
"laneKey": "cad987dbb6b8b72b13ad125116e6de6c3c39f2f38bca522fb7d129fb1ab38860"
},
{
"laneId": 4294967295,
"laneKey": "f88038166f595cfcd85d47b5a05038ea00e097768ff8a0e8f6eaad8651e62163"
}
]
},
{
"description": "buildChunkNonce(laneId, seq): u32_be(laneId) || u64_be(seq)",
"nonces": [
{
"laneId": 0,
"seq": "0",
"nonce": "000000000000000000000000"
},
{
"laneId": 0,
"seq": "1",
"nonce": "000000000000000000000001"
},
{
"laneId": 1,
"seq": "0",
"nonce": "000000010000000000000000"
},
{
"laneId": 4294967295,
"seq": "18446744073709551614",
"nonce": "fffffffffffffffffffffffe"
}
]
},
{
"description": "buildChunkAad(streamId, laneId, seq, isLast): streamId(16) || u32_be(laneId) || u64_be(seq) || u8(isLast)",
"streamId": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"cases": [
{
"laneId": 0,
"seq": "0",
"isLast": false,
"aad": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b200000000000000000000000000"
},
{
"laneId": 1,
"seq": "7",
"isLast": true,
"aad": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b200000001000000000000000701"
},
{
"laneId": 4294967295,
"seq": "18446744073709551614",
"isLast": false,
"aad": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2fffffffffffffffffffffffe00"
}
]
},
{
"description": "End-to-end chunk encrypt: AES-256-GCM(laneKey, nonce, plaintext, aad)",
"laneId": 0,
"seq": "0",
"isLast": true,
"laneKey": "0849f549230b1522e00fa50199bd336cbccd896293a6d42e9fced63665a471f0",
"nonce": "000000000000000000000000",
"aad": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b200000000000000000000000001",
"plaintext": "53686164652073747265616d732030783131206368756e6b20766563746f72",
"ciphertext": "060928337f09ea3d6b979b00d6db008f0f0f885b72684b9042ec3368c55fb9fdd7ee2b94f3f4c08061c12bbc3607c8"
},
{
"description": "Wire 0x11 stream-chunk envelope encode/decode",
"streamId": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"laneId": 0,
"seq": "0",
"isLast": true,
"nonce": "000000000000000000000000",
"extraAad": "",
"ciphertext": "060928337f09ea3d6b979b00d6db008f0f0f885b72684b9042ec3368c55fb9fdd7ee2b94f3f4c08061c12bbc3607c8",
"encoded": "0211b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b200000000000000000000000001000000000000000000000000000000000000002f060928337f09ea3d6b979b00d6db008f0f0f885b72684b9042ec3368c55fb9fdd7ee2b94f3f4c08061c12bbc3607c8"
}
]
}

View File

@@ -1,7 +1,9 @@
{
"version": 2,
"vectors": [
{
"description": "Wire format: RatchetMessage encoding (wire VERSION 0x02 — u32 length-prefixed)",
"kind": "ratchet",
"message": {
"dhPublicKey": "1111111111111111111111111111111111111111111111111111111111111111",
"previousCounter": 42,
@@ -10,6 +12,44 @@
"nonce": "333333333333333333333333"
},
"encoded": "02020000002011111111111111111111111111111111111111111111111111111111111111110000002a0000000700000010222222222222222222222222222222220000000c333333333333333333333333"
},
{
"description": "Wire format: PreKeyMessage with one-time prekey (wire 0x02 type 0x01)",
"kind": "prekey",
"message": {
"registrationId": 305419896,
"preKeyId": 99,
"signedPreKeyId": 1,
"ephemeralKey": "7777777777777777777777777777777777777777777777777777777777777777",
"identityDHKey": "8888888888888888888888888888888888888888888888888888888888888888",
"inner": {
"dhPublicKey": "4444444444444444444444444444444444444444444444444444444444444444",
"previousCounter": 0,
"counter": 0,
"ciphertext": "5555555555555555",
"nonce": "666666666666666666666666"
}
},
"encoded": "02011234567800000063000000010000002077777777777777777777777777777777777777777777777777777777777777770000002088888888888888888888888888888888888888888888888888888888888888880000004800000020444444444444444444444444444444444444444444444444444444444444444400000000000000000000000855555555555555550000000c666666666666666666666666"
},
{
"description": "Wire format: PreKeyMessage without one-time prekey (preKeyId=null encoded as 0xFFFFFFFF)",
"kind": "prekey",
"message": {
"registrationId": 1,
"preKeyId": null,
"signedPreKeyId": 1,
"ephemeralKey": "9999999999999999999999999999999999999999999999999999999999999999",
"identityDHKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"inner": {
"dhPublicKey": "4444444444444444444444444444444444444444444444444444444444444444",
"previousCounter": 0,
"counter": 0,
"ciphertext": "5555555555555555",
"nonce": "666666666666666666666666"
}
},
"encoded": "020100000001ffffffff0000000100000020999999999999999999999999999999999999999999999999999999999999999900000020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000004800000020444444444444444444444444444444444444444444444444444444444444444400000000000000000000000855555555555555550000000c666666666666666666666666"
}
]
}

View File

@@ -1,4 +1,5 @@
{
"version": 2,
"vectors": [
{
"description": "X3DH initial root key with 3 DH outputs (no one-time prekey)",