@sideband/secure-relay 0.2.2 → 0.3.0

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.
Files changed (46) hide show
  1. package/README.md +6 -4
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/constants.d.ts +49 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +51 -0
  6. package/dist/constants.js.map +1 -0
  7. package/dist/crypto.d.ts +70 -0
  8. package/dist/crypto.d.ts.map +1 -0
  9. package/dist/crypto.js +144 -0
  10. package/dist/crypto.js.map +1 -0
  11. package/dist/frame.d.ts +219 -0
  12. package/dist/frame.d.ts.map +1 -0
  13. package/dist/frame.js +554 -0
  14. package/dist/frame.js.map +1 -0
  15. package/dist/handshake.d.ts +39 -0
  16. package/dist/handshake.d.ts.map +1 -0
  17. package/dist/handshake.js +93 -0
  18. package/dist/handshake.js.map +1 -0
  19. package/dist/index.d.ts +46 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +12 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/replay.d.ts +32 -0
  24. package/dist/replay.d.ts.map +1 -0
  25. package/dist/replay.js +88 -0
  26. package/dist/replay.js.map +1 -0
  27. package/dist/session.d.ts +67 -0
  28. package/dist/session.d.ts.map +1 -0
  29. package/dist/session.js +122 -0
  30. package/dist/session.js.map +1 -0
  31. package/dist/types.d.ts +120 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +81 -0
  34. package/dist/types.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/constants.ts +3 -3
  37. package/src/crypto.test.ts +5 -5
  38. package/src/frame.test.ts +113 -47
  39. package/src/frame.ts +119 -86
  40. package/src/handshake.test.ts +29 -41
  41. package/src/handshake.ts +25 -27
  42. package/src/index.ts +4 -10
  43. package/src/integration.test.ts +97 -138
  44. package/src/session.test.ts +12 -10
  45. package/src/types.ts +4 -14
  46. /package/{dist/LICENSE → LICENSE} +0 -0
package/src/handshake.ts CHANGED
@@ -27,19 +27,6 @@ import type {
27
27
  } from "./types.js";
28
28
  import { SbrpError, SbrpErrorCode } from "./types.js";
29
29
 
30
- /** Result of a successful daemon handshake */
31
- export interface DaemonHandshakeResult {
32
- sessionKeys: SessionKeys;
33
- ephemeralKeyPair: EphemeralKeyPair;
34
- signature: Uint8Array;
35
- }
36
-
37
- /** Result of a successful client handshake */
38
- export interface ClientHandshakeResult {
39
- sessionKeys: SessionKeys;
40
- ephemeralKeyPair: EphemeralKeyPair;
41
- }
42
-
43
30
  /**
44
31
  * Create a handshake init message (client side).
45
32
  *
@@ -73,7 +60,7 @@ export function processHandshakeInit(
73
60
  init: HandshakeInit,
74
61
  daemonId: DaemonId,
75
62
  identityKeyPair: IdentityKeyPair,
76
- ): { message: HandshakeAccept; result: DaemonHandshakeResult } {
63
+ ): { message: HandshakeAccept; sessionKeys: SessionKeys } {
77
64
  const ephemeralKeyPair = generateEphemeralKeyPair();
78
65
 
79
66
  // Sign ephemeral key with context binding
@@ -97,20 +84,18 @@ export function processHandshakeInit(
97
84
  );
98
85
  const sessionKeys = deriveSessionKeys(sharedSecret, transcriptHash);
99
86
 
100
- // Best-effort zeroize shared secret
87
+ // Best-effort zeroize secrets
101
88
  zeroize(sharedSecret);
89
+ zeroize(ephemeralKeyPair.privateKey);
102
90
 
103
91
  return {
104
92
  message: {
105
93
  type: "handshake.accept",
94
+ identityPublicKey: identityKeyPair.publicKey,
106
95
  acceptPublicKey: ephemeralKeyPair.publicKey,
107
96
  signature,
108
97
  },
109
- result: {
110
- sessionKeys,
111
- ephemeralKeyPair,
112
- signature,
113
- },
98
+ sessionKeys,
114
99
  };
115
100
  }
116
101
 
@@ -123,14 +108,29 @@ export function processHandshakeInit(
123
108
  * NOTE: Callers MUST enforce a 30-second handshake timeout per SBRP §1.4.
124
109
  * This function does not track time; timeout enforcement is a transport concern.
125
110
  *
126
- * @throws {SbrpError} with code HandshakeFailed if signature verification fails
111
+ * @param ephemeralKeyPair The privateKey is zeroized in-place after key derivation.
112
+ * @throws {SbrpError} IdentityKeyChanged if advertised key doesn't match pinned key
113
+ * @throws {SbrpError} HandshakeFailed if signature verification fails
127
114
  */
128
115
  export function processHandshakeAccept(
129
116
  accept: HandshakeAccept,
130
117
  daemonId: DaemonId,
131
118
  pinnedIdentityPublicKey: Uint8Array,
132
119
  ephemeralKeyPair: EphemeralKeyPair,
133
- ): ClientHandshakeResult {
120
+ ): SessionKeys {
121
+ // Reject if advertised identity key doesn't match pinned key.
122
+ // Signature is verified against pinnedIdentityPublicKey, but an attacker
123
+ // could swap the advertised field to mislead higher layers.
124
+ if (
125
+ accept.identityPublicKey.length !== pinnedIdentityPublicKey.length ||
126
+ !accept.identityPublicKey.every((b, i) => b === pinnedIdentityPublicKey[i])
127
+ ) {
128
+ throw new SbrpError(
129
+ SbrpErrorCode.IdentityKeyChanged,
130
+ "Advertised identity key does not match pinned key",
131
+ );
132
+ }
133
+
134
134
  // Verify daemon signature using PINNED key (not relay-provided!)
135
135
  const signaturePayload = createSignaturePayload(
136
136
  daemonId,
@@ -164,11 +164,9 @@ export function processHandshakeAccept(
164
164
  );
165
165
  const sessionKeys = deriveSessionKeys(sharedSecret, transcriptHash);
166
166
 
167
- // Best-effort zeroize shared secret
167
+ // Best-effort zeroize secrets
168
168
  zeroize(sharedSecret);
169
+ zeroize(ephemeralKeyPair.privateKey);
169
170
 
170
- return {
171
- sessionKeys,
172
- ephemeralKeyPair,
173
- };
171
+ return sessionKeys;
174
172
  }
package/src/index.ts CHANGED
@@ -22,12 +22,12 @@
22
22
  * const { message: init, ephemeralKeyPair } = createHandshakeInit();
23
23
  *
24
24
  * // Daemon side: process init and create accept
25
- * const { message: accept, result } = processHandshakeInit(init, daemonId, identity);
26
- * const clientSession = createClientSession(clientId, result.sessionKeys);
25
+ * const { message: accept, sessionKeys } = processHandshakeInit(init, daemonId, identity);
26
+ * const clientSession = createClientSession(clientId, sessionKeys);
27
27
  *
28
28
  * // Client side: process accept (with TOFU-pinned identity)
29
- * const { sessionKeys } = processHandshakeAccept(accept, daemonId, pinnedKey, ephemeralKeyPair);
30
- * const daemonSession = createDaemonSession(sessionKeys);
29
+ * const clientKeys = processHandshakeAccept(accept, daemonId, pinnedKey, ephemeralKeyPair);
30
+ * const daemonSession = createDaemonSession(clientKeys);
31
31
  *
32
32
  * // Encrypt/decrypt messages
33
33
  * const encrypted = encryptClientToDaemon(daemonSession, plaintext);
@@ -44,7 +44,6 @@ export type {
44
44
  HandshakeAccept,
45
45
  HandshakeInit,
46
46
  IdentityKeyPair,
47
- PinnedIdentity,
48
47
  SessionId,
49
48
  SessionKeys,
50
49
  } from "./types.js";
@@ -106,11 +105,6 @@ export {
106
105
  } from "./crypto.js";
107
106
 
108
107
  // Handshake
109
- export type {
110
- ClientHandshakeResult,
111
- DaemonHandshakeResult,
112
- } from "./handshake.js";
113
-
114
108
  export {
115
109
  createHandshakeInit,
116
110
  processHandshakeAccept,