@sideband/secure-relay 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,43 @@ Identity keys use trust-on-first-use (TOFU) pinning:
86
86
  - Never accept key changes silently — `identity_key_changed` indicates potential MITM
87
87
  - On mismatch, present both fingerprints and require explicit user approval
88
88
 
89
+ ### Detecting Identity Key Changes
90
+
91
+ Compare the daemon's current identity key against your stored pin before handshake:
92
+
93
+ ```ts
94
+ import {
95
+ processHandshakeAccept,
96
+ computeFingerprint,
97
+ SbrpError,
98
+ SbrpErrorCode,
99
+ } from "@sideband/secure-relay";
100
+
101
+ // Load pinned key from storage (null on first connection)
102
+ const pinnedKey = await storage.get(`tofu:${daemonId}`);
103
+
104
+ if (pinnedKey && !equalBytes(pinnedKey, currentIdentityKey)) {
105
+ // Key changed — potential MITM attack
106
+ throw new SbrpError(
107
+ SbrpErrorCode.IdentityKeyChanged,
108
+ `Identity key changed for ${daemonId}. ` +
109
+ `Expected: ${computeFingerprint(pinnedKey)}, ` +
110
+ `Got: ${computeFingerprint(currentIdentityKey)}`,
111
+ );
112
+ }
113
+
114
+ // First connection: pin the key after successful handshake
115
+ const result = processHandshakeAccept(
116
+ accept,
117
+ daemonId,
118
+ currentIdentityKey,
119
+ ephemeralKeyPair,
120
+ );
121
+ if (!pinnedKey) {
122
+ await storage.set(`tofu:${daemonId}`, currentIdentityKey);
123
+ }
124
+ ```
125
+
89
126
  ## Error Handling
90
127
 
91
128
  All errors throw `SbrpError` with a specific `code`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideband/secure-relay",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Secure Relay Protocol (SBRP): E2EE handshake, session encryption, and TOFU identity pinning for relay-mediated communication.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -49,9 +49,6 @@
49
49
  "@noble/curves": "^1.8.1",
50
50
  "@noble/hashes": "^1.7.1"
51
51
  },
52
- "devDependencies": {
53
- "@types/bun": "^1.3.3"
54
- },
55
52
  "files": [
56
53
  "dist",
57
54
  "src"
package/src/constants.ts CHANGED
@@ -40,8 +40,8 @@ export const NONCE_LENGTH = 12;
40
40
  /** Length of Poly1305 auth tag in bytes */
41
41
  export const AUTH_TAG_LENGTH = 16;
42
42
 
43
- /** Default replay window size (bits) */
44
- export const DEFAULT_REPLAY_WINDOW_SIZE = 64n;
43
+ /** Default replay window size (bits). Spec requires ≥64, recommends ≥128. */
44
+ export const DEFAULT_REPLAY_WINDOW_SIZE = 128n;
45
45
 
46
46
  /** Direction bytes in nonce (4 bytes, big-endian) */
47
47
  export const DIRECTION_CLIENT_TO_DAEMON = new Uint8Array([0, 0, 0, 1]);
package/src/handshake.ts CHANGED
@@ -65,6 +65,9 @@ export function createHandshakeInit(): {
65
65
  * 1. Generate ephemeral X25519 keypair
66
66
  * 2. Sign ephemeral public key with identity key (context-bound)
67
67
  * 3. Derive session keys
68
+ *
69
+ * NOTE: Callers MUST enforce a 30-second handshake timeout per SBRP §1.4.
70
+ * This function does not track time; timeout enforcement is a transport concern.
68
71
  */
69
72
  export function processHandshakeInit(
70
73
  init: HandshakeInit,
@@ -117,6 +120,9 @@ export function processHandshakeInit(
117
120
  * 1. Verify signature using PINNED identity key (TOFU)
118
121
  * 2. Derive session keys using same transcript hash as daemon
119
122
  *
123
+ * NOTE: Callers MUST enforce a 30-second handshake timeout per SBRP §1.4.
124
+ * This function does not track time; timeout enforcement is a transport concern.
125
+ *
120
126
  * @throws {SbrpError} with code HandshakeFailed if signature verification fails
121
127
  */
122
128
  export function processHandshakeAccept(
@@ -185,16 +185,16 @@ describe("session", () => {
185
185
  const daemonSession = createDaemonSession(sessionKeys);
186
186
  const clientSession = createClientSession(clientId, sessionKeys);
187
187
 
188
- // Encrypt 100 messages to advance the window
188
+ // Encrypt 200 messages to advance the window (default window is 128)
189
189
  const messages: EncryptedMessage[] = [];
190
- for (let i = 0; i < 100; i++) {
190
+ for (let i = 0; i < 200; i++) {
191
191
  messages.push(
192
- encryptClientToDaemon(daemonSession, new Uint8Array([i])),
192
+ encryptClientToDaemon(daemonSession, new Uint8Array([i % 256])),
193
193
  );
194
194
  }
195
195
 
196
- // Decrypt latest 64 (within window)
197
- for (let i = 99; i >= 36; i--) {
196
+ // Decrypt latest 128 (within window)
197
+ for (let i = 199; i >= 72; i--) {
198
198
  decryptClientToDaemon(clientSession, messages[i]);
199
199
  }
200
200
 
@@ -217,16 +217,16 @@ describe("session", () => {
217
217
  const daemonSession = createDaemonSession(sessionKeys);
218
218
  const clientSession = createClientSession(clientId, sessionKeys);
219
219
 
220
- // Encrypt 100 messages to advance the window
220
+ // Encrypt 200 messages to advance the window (default window is 128)
221
221
  const messages: EncryptedMessage[] = [];
222
- for (let i = 0; i < 100; i++) {
222
+ for (let i = 0; i < 200; i++) {
223
223
  messages.push(
224
- encryptDaemonToClient(clientSession, new Uint8Array([i])),
224
+ encryptDaemonToClient(clientSession, new Uint8Array([i % 256])),
225
225
  );
226
226
  }
227
227
 
228
- // Decrypt latest 64 (within window)
229
- for (let i = 99; i >= 36; i--) {
228
+ // Decrypt latest 128 (within window)
229
+ for (let i = 199; i >= 72; i--) {
230
230
  decryptDaemonToClient(daemonSession, messages[i]);
231
231
  }
232
232