@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 +37 -0
- package/package.json +1 -4
- package/src/constants.ts +2 -2
- package/src/handshake.ts +6 -0
- package/src/session.test.ts +10 -10
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.
|
|
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 =
|
|
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(
|
package/src/session.test.ts
CHANGED
|
@@ -185,16 +185,16 @@ describe("session", () => {
|
|
|
185
185
|
const daemonSession = createDaemonSession(sessionKeys);
|
|
186
186
|
const clientSession = createClientSession(clientId, sessionKeys);
|
|
187
187
|
|
|
188
|
-
// Encrypt
|
|
188
|
+
// Encrypt 200 messages to advance the window (default window is 128)
|
|
189
189
|
const messages: EncryptedMessage[] = [];
|
|
190
|
-
for (let i = 0; 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
|
|
197
|
-
for (let 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
|
|
220
|
+
// Encrypt 200 messages to advance the window (default window is 128)
|
|
221
221
|
const messages: EncryptedMessage[] = [];
|
|
222
|
-
for (let i = 0; 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
|
|
229
|
-
for (let 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
|
|