@sideband/secure-relay 0.0.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/LICENSE +190 -0
- package/README.md +86 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +35 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto.d.ts +70 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +145 -0
- package/dist/crypto.js.map +1 -0
- package/dist/handshake.d.ts +42 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +83 -0
- package/dist/handshake.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +32 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +89 -0
- package/dist/replay.js.map +1 -0
- package/dist/session.d.ts +67 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +123 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/constants.ts +49 -0
- package/src/crypto.ts +234 -0
- package/src/handshake.ts +169 -0
- package/src/index.ts +132 -0
- package/src/replay.ts +115 -0
- package/src/session.ts +196 -0
- package/src/types.ts +104 -0
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
/**
|
|
4
|
+
* Cryptographic primitives for Sideband Relay Protocol (SBRP).
|
|
5
|
+
*
|
|
6
|
+
* Uses @noble/curves for Ed25519/X25519, @noble/ciphers for ChaCha20-Poly1305,
|
|
7
|
+
* and @noble/hashes for SHA-256/HKDF.
|
|
8
|
+
*/
|
|
9
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha";
|
|
10
|
+
import { ed25519 } from "@noble/curves/ed25519";
|
|
11
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
12
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
13
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
14
|
+
import { concatBytes, randomBytes } from "@noble/hashes/utils";
|
|
15
|
+
import { AUTH_TAG_LENGTH, DIRECTION_CLIENT_TO_DAEMON, DIRECTION_DAEMON_TO_CLIENT, NONCE_LENGTH, SBRP_HANDSHAKE_CONTEXT, SBRP_SESSION_KEYS_INFO, SBRP_TRANSCRIPT_CONTEXT, SESSION_KEYS_LENGTH, SYMMETRIC_KEY_LENGTH, } from "./constants.js";
|
|
16
|
+
import { Direction } from "./types.js";
|
|
17
|
+
const textEncoder = new TextEncoder();
|
|
18
|
+
/** Generate a new Ed25519 identity keypair */
|
|
19
|
+
export function generateIdentityKeyPair() {
|
|
20
|
+
const privateKey = ed25519.utils.randomPrivateKey();
|
|
21
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
22
|
+
return { publicKey, privateKey };
|
|
23
|
+
}
|
|
24
|
+
/** Generate a new X25519 ephemeral keypair */
|
|
25
|
+
export function generateEphemeralKeyPair() {
|
|
26
|
+
const privateKey = x25519.utils.randomPrivateKey();
|
|
27
|
+
const publicKey = x25519.getPublicKey(privateKey);
|
|
28
|
+
return { publicKey, privateKey };
|
|
29
|
+
}
|
|
30
|
+
/** Compute SHA-256 fingerprint of an identity public key */
|
|
31
|
+
export function computeFingerprint(identityPublicKey) {
|
|
32
|
+
const hash = sha256(identityPublicKey);
|
|
33
|
+
const hex = Array.from(hash)
|
|
34
|
+
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
|
|
35
|
+
.join(":");
|
|
36
|
+
return `SHA256:${hex}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create the signature payload for handshake authentication.
|
|
40
|
+
*
|
|
41
|
+
* payload = SHA256("sbrp-v1-handshake" || daemonId || browserPublicKey || daemonEphemeralPublicKey)
|
|
42
|
+
*/
|
|
43
|
+
export function createSignaturePayload(daemonId, browserPublicKey, daemonEphemeralPublicKey) {
|
|
44
|
+
return sha256(concatBytes(textEncoder.encode(SBRP_HANDSHAKE_CONTEXT), textEncoder.encode(daemonId), browserPublicKey, daemonEphemeralPublicKey));
|
|
45
|
+
}
|
|
46
|
+
/** Sign a payload with an Ed25519 identity private key */
|
|
47
|
+
export function signPayload(payload, identityPrivateKey) {
|
|
48
|
+
return ed25519.sign(payload, identityPrivateKey);
|
|
49
|
+
}
|
|
50
|
+
/** Verify an Ed25519 signature */
|
|
51
|
+
export function verifySignature(payload, signature, identityPublicKey) {
|
|
52
|
+
return ed25519.verify(signature, payload, identityPublicKey);
|
|
53
|
+
}
|
|
54
|
+
/** Compute X25519 shared secret */
|
|
55
|
+
export function computeSharedSecret(myPrivateKey, peerPublicKey) {
|
|
56
|
+
return x25519.getSharedSecret(myPrivateKey, peerPublicKey);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create the transcript hash for key derivation.
|
|
60
|
+
*
|
|
61
|
+
* transcript = SHA256("sbrp-v1-transcript" || daemonId || browserPublicKey || daemonPublicKey || signature)
|
|
62
|
+
*/
|
|
63
|
+
export function createTranscriptHash(daemonId, browserPublicKey, daemonPublicKey, signature) {
|
|
64
|
+
return sha256(concatBytes(textEncoder.encode(SBRP_TRANSCRIPT_CONTEXT), textEncoder.encode(daemonId), browserPublicKey, daemonPublicKey, signature));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Derive traffic keys using HKDF-SHA256.
|
|
68
|
+
*
|
|
69
|
+
* Keys are derived with transcript hash as salt to bind to the authenticated session.
|
|
70
|
+
*/
|
|
71
|
+
export function deriveTrafficKeys(sharedSecret, transcriptHash) {
|
|
72
|
+
const keys = hkdf(sha256, sharedSecret, transcriptHash, SBRP_SESSION_KEYS_INFO, SESSION_KEYS_LENGTH);
|
|
73
|
+
return {
|
|
74
|
+
clientToDaemon: keys.slice(0, SYMMETRIC_KEY_LENGTH),
|
|
75
|
+
daemonToClient: keys.slice(SYMMETRIC_KEY_LENGTH, SESSION_KEYS_LENGTH),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Construct a nonce for ChaCha20-Poly1305.
|
|
80
|
+
*
|
|
81
|
+
* Nonce format (12 bytes):
|
|
82
|
+
* - Bytes 0-3: Direction (0x00000001 = client→daemon, 0x00000002 = daemon→client)
|
|
83
|
+
* - Bytes 4-11: Sequence number (big-endian uint64)
|
|
84
|
+
*/
|
|
85
|
+
export function constructNonce(direction, seq) {
|
|
86
|
+
const nonce = new Uint8Array(NONCE_LENGTH);
|
|
87
|
+
const directionBytes = direction === Direction.ClientToDaemon
|
|
88
|
+
? DIRECTION_CLIENT_TO_DAEMON
|
|
89
|
+
: DIRECTION_DAEMON_TO_CLIENT;
|
|
90
|
+
nonce.set(directionBytes, 0);
|
|
91
|
+
// Write sequence number as big-endian uint64 (bytes 4-11)
|
|
92
|
+
const view = new DataView(nonce.buffer);
|
|
93
|
+
view.setBigUint64(4, seq, false); // false = big-endian
|
|
94
|
+
return nonce;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Encrypt a message using ChaCha20-Poly1305.
|
|
98
|
+
*
|
|
99
|
+
* Returns: nonce (12 bytes) || ciphertext || authTag (16 bytes)
|
|
100
|
+
*/
|
|
101
|
+
export function encrypt(key, direction, seq, plaintext) {
|
|
102
|
+
const nonce = constructNonce(direction, seq);
|
|
103
|
+
const cipher = chacha20poly1305(key, nonce);
|
|
104
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
105
|
+
return concatBytes(nonce, ciphertext);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decrypt a message using ChaCha20-Poly1305.
|
|
109
|
+
*
|
|
110
|
+
* Input format: nonce (12 bytes) || ciphertext || authTag (16 bytes)
|
|
111
|
+
* Returns the plaintext, or throws on decryption failure.
|
|
112
|
+
*/
|
|
113
|
+
export function decrypt(key, data) {
|
|
114
|
+
if (data.length < NONCE_LENGTH + AUTH_TAG_LENGTH) {
|
|
115
|
+
throw new Error("Invalid encrypted message: too short");
|
|
116
|
+
}
|
|
117
|
+
const nonce = data.slice(0, NONCE_LENGTH);
|
|
118
|
+
const ciphertext = data.slice(NONCE_LENGTH);
|
|
119
|
+
const cipher = chacha20poly1305(key, nonce);
|
|
120
|
+
return cipher.decrypt(ciphertext);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Extract sequence number from encrypted message data.
|
|
124
|
+
*
|
|
125
|
+
* Reads bytes 4-11 of the nonce as big-endian uint64.
|
|
126
|
+
*/
|
|
127
|
+
export function extractSequence(data) {
|
|
128
|
+
if (data.length < NONCE_LENGTH) {
|
|
129
|
+
throw new Error("Invalid encrypted message: too short");
|
|
130
|
+
}
|
|
131
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
132
|
+
return view.getBigUint64(4, false); // false = big-endian
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Best-effort zeroization of sensitive key material.
|
|
136
|
+
*
|
|
137
|
+
* Note: JavaScript/GC limitations mean this is not guaranteed to prevent
|
|
138
|
+
* key material from remaining in memory.
|
|
139
|
+
*/
|
|
140
|
+
export function zeroize(data) {
|
|
141
|
+
data.fill(0);
|
|
142
|
+
}
|
|
143
|
+
/** Generate random bytes */
|
|
144
|
+
export { randomBytes };
|
|
145
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DaemonId, EphemeralKeyPair, HandshakeAccept, HandshakeInit, IdentityKeyPair, TrafficKeys } from "./types.js";
|
|
2
|
+
/** Result of a successful daemon handshake */
|
|
3
|
+
export interface DaemonHandshakeResult {
|
|
4
|
+
trafficKeys: TrafficKeys;
|
|
5
|
+
ephemeralKeyPair: EphemeralKeyPair;
|
|
6
|
+
signature: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
/** Result of a successful client handshake */
|
|
9
|
+
export interface ClientHandshakeResult {
|
|
10
|
+
trafficKeys: TrafficKeys;
|
|
11
|
+
ephemeralKeyPair: EphemeralKeyPair;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a handshake init message (browser side).
|
|
15
|
+
*
|
|
16
|
+
* Generates an ephemeral X25519 keypair for this session.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createHandshakeInit(): {
|
|
19
|
+
message: HandshakeInit;
|
|
20
|
+
ephemeralKeyPair: EphemeralKeyPair;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Process handshake init and create accept message (daemon side).
|
|
24
|
+
*
|
|
25
|
+
* 1. Generate ephemeral X25519 keypair
|
|
26
|
+
* 2. Sign ephemeral public key with identity key (context-bound)
|
|
27
|
+
* 3. Derive traffic keys
|
|
28
|
+
*/
|
|
29
|
+
export declare function processHandshakeInit(init: HandshakeInit, daemonId: DaemonId, identityKeyPair: IdentityKeyPair): {
|
|
30
|
+
message: HandshakeAccept;
|
|
31
|
+
result: DaemonHandshakeResult;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Process handshake accept message (client side).
|
|
35
|
+
*
|
|
36
|
+
* 1. Verify signature using PINNED identity key (TOFU)
|
|
37
|
+
* 2. Derive traffic keys using same transcript hash as daemon
|
|
38
|
+
*
|
|
39
|
+
* @throws {SbrpError} with code HandshakeFailed if signature verification fails
|
|
40
|
+
*/
|
|
41
|
+
export declare function processHandshakeAccept(accept: HandshakeAccept, daemonId: DaemonId, pinnedIdentityPublicKey: Uint8Array, ephemeralKeyPair: EphemeralKeyPair): ClientHandshakeResult;
|
|
42
|
+
//# sourceMappingURL=handshake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,QAAQ,EACR,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,YAAY,CAAC;AAGpB,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,WAAW,CAAC;IACzB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,WAAW,CAAC;IACzB,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI;IACrC,OAAO,EAAE,aAAa,CAAC;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;CACpC,CASA;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,QAAQ,EAClB,eAAe,EAAE,eAAe,GAC/B;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAA;CAAE,CAuC7D;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,QAAQ,EAClB,uBAAuB,EAAE,UAAU,EACnC,gBAAgB,EAAE,gBAAgB,GACjC,qBAAqB,CAyCvB"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
/**
|
|
4
|
+
* E2EE handshake protocol for Sideband Relay Protocol (SBRP).
|
|
5
|
+
*
|
|
6
|
+
* Implements the authenticated key exchange between browser and daemon,
|
|
7
|
+
* with Ed25519 signatures for MITM protection.
|
|
8
|
+
*/
|
|
9
|
+
import { computeSharedSecret, createSignaturePayload, createTranscriptHash, deriveTrafficKeys, generateEphemeralKeyPair, signPayload, verifySignature, zeroize, } from "./crypto.js";
|
|
10
|
+
import { SbrpError, SbrpErrorCode } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Create a handshake init message (browser side).
|
|
13
|
+
*
|
|
14
|
+
* Generates an ephemeral X25519 keypair for this session.
|
|
15
|
+
*/
|
|
16
|
+
export function createHandshakeInit() {
|
|
17
|
+
const ephemeralKeyPair = generateEphemeralKeyPair();
|
|
18
|
+
return {
|
|
19
|
+
message: {
|
|
20
|
+
type: "handshake.init",
|
|
21
|
+
browserPublicKey: ephemeralKeyPair.publicKey,
|
|
22
|
+
},
|
|
23
|
+
ephemeralKeyPair,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Process handshake init and create accept message (daemon side).
|
|
28
|
+
*
|
|
29
|
+
* 1. Generate ephemeral X25519 keypair
|
|
30
|
+
* 2. Sign ephemeral public key with identity key (context-bound)
|
|
31
|
+
* 3. Derive traffic keys
|
|
32
|
+
*/
|
|
33
|
+
export function processHandshakeInit(init, daemonId, identityKeyPair) {
|
|
34
|
+
const ephemeralKeyPair = generateEphemeralKeyPair();
|
|
35
|
+
// Sign ephemeral key with context binding
|
|
36
|
+
const signaturePayload = createSignaturePayload(daemonId, init.browserPublicKey, ephemeralKeyPair.publicKey);
|
|
37
|
+
const signature = signPayload(signaturePayload, identityKeyPair.privateKey);
|
|
38
|
+
// Derive traffic keys
|
|
39
|
+
const sharedSecret = computeSharedSecret(ephemeralKeyPair.privateKey, init.browserPublicKey);
|
|
40
|
+
const transcriptHash = createTranscriptHash(daemonId, init.browserPublicKey, ephemeralKeyPair.publicKey, signature);
|
|
41
|
+
const trafficKeys = deriveTrafficKeys(sharedSecret, transcriptHash);
|
|
42
|
+
// Best-effort zeroize shared secret
|
|
43
|
+
zeroize(sharedSecret);
|
|
44
|
+
return {
|
|
45
|
+
message: {
|
|
46
|
+
type: "handshake.accept",
|
|
47
|
+
daemonPublicKey: ephemeralKeyPair.publicKey,
|
|
48
|
+
signature,
|
|
49
|
+
},
|
|
50
|
+
result: {
|
|
51
|
+
trafficKeys,
|
|
52
|
+
ephemeralKeyPair,
|
|
53
|
+
signature,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Process handshake accept message (client side).
|
|
59
|
+
*
|
|
60
|
+
* 1. Verify signature using PINNED identity key (TOFU)
|
|
61
|
+
* 2. Derive traffic keys using same transcript hash as daemon
|
|
62
|
+
*
|
|
63
|
+
* @throws {SbrpError} with code HandshakeFailed if signature verification fails
|
|
64
|
+
*/
|
|
65
|
+
export function processHandshakeAccept(accept, daemonId, pinnedIdentityPublicKey, ephemeralKeyPair) {
|
|
66
|
+
// Verify daemon signature using PINNED key (not relay-provided!)
|
|
67
|
+
const signaturePayload = createSignaturePayload(daemonId, ephemeralKeyPair.publicKey, accept.daemonPublicKey);
|
|
68
|
+
const valid = verifySignature(signaturePayload, accept.signature, pinnedIdentityPublicKey);
|
|
69
|
+
if (!valid) {
|
|
70
|
+
throw new SbrpError(SbrpErrorCode.HandshakeFailed, "Signature verification failed");
|
|
71
|
+
}
|
|
72
|
+
// Derive traffic keys using same transcript hash as daemon
|
|
73
|
+
const sharedSecret = computeSharedSecret(ephemeralKeyPair.privateKey, accept.daemonPublicKey);
|
|
74
|
+
const transcriptHash = createTranscriptHash(daemonId, ephemeralKeyPair.publicKey, accept.daemonPublicKey, accept.signature);
|
|
75
|
+
const trafficKeys = deriveTrafficKeys(sharedSecret, transcriptHash);
|
|
76
|
+
// Best-effort zeroize shared secret
|
|
77
|
+
zeroize(sharedSecret);
|
|
78
|
+
return {
|
|
79
|
+
trafficKeys,
|
|
80
|
+
ephemeralKeyPair,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,6CAA6C;AAE7C;;;;;GAKG;AAEH,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,EACjB,wBAAwB,EACxB,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,aAAa,CAAC;AASrB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAetD;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IAIjC,MAAM,gBAAgB,GAAG,wBAAwB,EAAE,CAAC;IACpD,OAAO;QACL,OAAO,EAAE;YACP,IAAI,EAAE,gBAAgB;YACtB,gBAAgB,EAAE,gBAAgB,CAAC,SAAS;SAC7C;QACD,gBAAgB;KACjB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAmB,EACnB,QAAkB,EAClB,eAAgC;IAEhC,MAAM,gBAAgB,GAAG,wBAAwB,EAAE,CAAC;IAEpD,0CAA0C;IAC1C,MAAM,gBAAgB,GAAG,sBAAsB,CAC7C,QAAQ,EACR,IAAI,CAAC,gBAAgB,EACrB,gBAAgB,CAAC,SAAS,CAC3B,CAAC;IACF,MAAM,SAAS,GAAG,WAAW,CAAC,gBAAgB,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IAE5E,sBAAsB;IACtB,MAAM,YAAY,GAAG,mBAAmB,CACtC,gBAAgB,CAAC,UAAU,EAC3B,IAAI,CAAC,gBAAgB,CACtB,CAAC;IACF,MAAM,cAAc,GAAG,oBAAoB,CACzC,QAAQ,EACR,IAAI,CAAC,gBAAgB,EACrB,gBAAgB,CAAC,SAAS,EAC1B,SAAS,CACV,CAAC;IACF,MAAM,WAAW,GAAG,iBAAiB,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAEpE,oCAAoC;IACpC,OAAO,CAAC,YAAY,CAAC,CAAC;IAEtB,OAAO;QACL,OAAO,EAAE;YACP,IAAI,EAAE,kBAAkB;YACxB,eAAe,EAAE,gBAAgB,CAAC,SAAS;YAC3C,SAAS;SACV;QACD,MAAM,EAAE;YACN,WAAW;YACX,gBAAgB;YAChB,SAAS;SACV;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAuB,EACvB,QAAkB,EAClB,uBAAmC,EACnC,gBAAkC;IAElC,iEAAiE;IACjE,MAAM,gBAAgB,GAAG,sBAAsB,CAC7C,QAAQ,EACR,gBAAgB,CAAC,SAAS,EAC1B,MAAM,CAAC,eAAe,CACvB,CAAC;IAEF,MAAM,KAAK,GAAG,eAAe,CAC3B,gBAAgB,EAChB,MAAM,CAAC,SAAS,EAChB,uBAAuB,CACxB,CAAC;IAEF,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,SAAS,CACjB,aAAa,CAAC,eAAe,EAC7B,+BAA+B,CAChC,CAAC;IACJ,CAAC;IAED,2DAA2D;IAC3D,MAAM,YAAY,GAAG,mBAAmB,CACtC,gBAAgB,CAAC,UAAU,EAC3B,MAAM,CAAC,eAAe,CACvB,CAAC;IACF,MAAM,cAAc,GAAG,oBAAoB,CACzC,QAAQ,EACR,gBAAgB,CAAC,SAAS,EAC1B,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,SAAS,CACjB,CAAC;IACF,MAAM,WAAW,GAAG,iBAAiB,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IAEpE,oCAAoC;IACpC,OAAO,CAAC,YAAY,CAAC,CAAC;IAEtB,OAAO;QACL,WAAW;QACX,gBAAgB;KACjB,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sideband/secure-relay
|
|
3
|
+
*
|
|
4
|
+
* Sideband Relay Protocol (SBRP) implementation for E2EE communication
|
|
5
|
+
* between daemons and clients via a relay server.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Ed25519 identity signatures for MITM protection
|
|
9
|
+
* - X25519 ephemeral key exchange for forward secrecy
|
|
10
|
+
* - ChaCha20-Poly1305 authenticated encryption
|
|
11
|
+
* - TOFU (Trust On First Use) identity pinning
|
|
12
|
+
* - Bitmap-based replay protection
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Daemon side: generate identity keypair on first run
|
|
17
|
+
* const identity = generateIdentityKeyPair();
|
|
18
|
+
*
|
|
19
|
+
* // Client side: create handshake init
|
|
20
|
+
* const { message: init, ephemeralKeyPair } = createHandshakeInit();
|
|
21
|
+
*
|
|
22
|
+
* // Daemon side: process init and create accept
|
|
23
|
+
* const { message: accept, result } = processHandshakeInit(init, daemonId, identity);
|
|
24
|
+
* const clientSession = createClientSession(clientId, result.trafficKeys);
|
|
25
|
+
*
|
|
26
|
+
* // Client side: process accept (with TOFU-pinned identity)
|
|
27
|
+
* const { trafficKeys } = processHandshakeAccept(accept, daemonId, pinnedKey, ephemeralKeyPair);
|
|
28
|
+
* const daemonConn = createDaemonConnection(trafficKeys);
|
|
29
|
+
*
|
|
30
|
+
* // Encrypt/decrypt messages
|
|
31
|
+
* const encrypted = encryptClientToDaemon(daemonConn, plaintext);
|
|
32
|
+
* const decrypted = decryptClientToDaemon(clientSession, encrypted);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export type { ClientId, DaemonId, EncryptedMessage, EphemeralKeyPair, HandshakeAccept, HandshakeInit, IdentityKeyPair, PinnedIdentity, TrafficKeys, } from "./types.js";
|
|
36
|
+
export { asClientId, asDaemonId, Direction, SbrpError, SbrpErrorCode } from "./types.js";
|
|
37
|
+
export { AUTH_TAG_LENGTH, DEFAULT_REPLAY_WINDOW_SIZE, DIRECTION_CLIENT_TO_DAEMON, DIRECTION_DAEMON_TO_CLIENT, ED25519_PRIVATE_KEY_LENGTH, ED25519_PUBLIC_KEY_LENGTH, ED25519_SIGNATURE_LENGTH, NONCE_LENGTH, SBRP_HANDSHAKE_CONTEXT, SBRP_SESSION_KEYS_INFO, SBRP_TRANSCRIPT_CONTEXT, SESSION_KEYS_LENGTH, SYMMETRIC_KEY_LENGTH, X25519_PRIVATE_KEY_LENGTH, X25519_PUBLIC_KEY_LENGTH, } from "./constants.js";
|
|
38
|
+
export { computeFingerprint, computeSharedSecret, constructNonce, createSignaturePayload, createTranscriptHash, decrypt, deriveTrafficKeys, encrypt, extractSequence, generateEphemeralKeyPair, generateIdentityKeyPair, randomBytes, signPayload, verifySignature, zeroize, } from "./crypto.js";
|
|
39
|
+
export type { ClientHandshakeResult, DaemonHandshakeResult, } from "./handshake.js";
|
|
40
|
+
export { createHandshakeInit, processHandshakeAccept, processHandshakeInit, } from "./handshake.js";
|
|
41
|
+
export type { ReplayWindow } from "./replay.js";
|
|
42
|
+
export { checkAndUpdateReplay, createReplayWindow, isValidSequence, resetReplayWindow, } from "./replay.js";
|
|
43
|
+
export type { ClientSession, DaemonConnection } from "./session.js";
|
|
44
|
+
export { clearClientSession, clearDaemonConnection, createClientSession, createDaemonConnection, decryptClientToDaemon, decryptDaemonToClient, encryptClientToDaemon, encryptDaemonToClient, } from "./session.js";
|
|
45
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAGH,YAAY,EACV,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGzF,OAAO,EACL,eAAe,EACf,0BAA0B,EAC1B,0BAA0B,EAC1B,0BAA0B,EAC1B,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,OAAO,EACP,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACvB,WAAW,EACX,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAGrB,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
export { asClientId, asDaemonId, Direction, SbrpError, SbrpErrorCode } from "./types.js";
|
|
4
|
+
// Constants
|
|
5
|
+
export { AUTH_TAG_LENGTH, DEFAULT_REPLAY_WINDOW_SIZE, DIRECTION_CLIENT_TO_DAEMON, DIRECTION_DAEMON_TO_CLIENT, ED25519_PRIVATE_KEY_LENGTH, ED25519_PUBLIC_KEY_LENGTH, ED25519_SIGNATURE_LENGTH, NONCE_LENGTH, SBRP_HANDSHAKE_CONTEXT, SBRP_SESSION_KEYS_INFO, SBRP_TRANSCRIPT_CONTEXT, SESSION_KEYS_LENGTH, SYMMETRIC_KEY_LENGTH, X25519_PRIVATE_KEY_LENGTH, X25519_PUBLIC_KEY_LENGTH, } from "./constants.js";
|
|
6
|
+
// Crypto primitives
|
|
7
|
+
export { computeFingerprint, computeSharedSecret, constructNonce, createSignaturePayload, createTranscriptHash, decrypt, deriveTrafficKeys, encrypt, extractSequence, generateEphemeralKeyPair, generateIdentityKeyPair, randomBytes, signPayload, verifySignature, zeroize, } from "./crypto.js";
|
|
8
|
+
export { createHandshakeInit, processHandshakeAccept, processHandshakeInit, } from "./handshake.js";
|
|
9
|
+
export { checkAndUpdateReplay, createReplayWindow, isValidSequence, resetReplayWindow, } from "./replay.js";
|
|
10
|
+
export { clearClientSession, clearDaemonConnection, createClientSession, createDaemonConnection, decryptClientToDaemon, decryptDaemonToClient, encryptClientToDaemon, encryptDaemonToClient, } from "./session.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,6CAA6C;AAkD7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzF,YAAY;AACZ,OAAO,EACL,eAAe,EACf,0BAA0B,EAC1B,0BAA0B,EAC1B,0BAA0B,EAC1B,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AAExB,oBAAoB;AACpB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,OAAO,EACP,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACvB,WAAW,EACX,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,aAAa,CAAC;AAQrB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAKrB,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,cAAc,CAAC"}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Replay window state */
|
|
2
|
+
export interface ReplayWindow {
|
|
3
|
+
/** Highest accepted sequence number */
|
|
4
|
+
maxSeen: bigint;
|
|
5
|
+
/** Bitmap: bit i set = sequence (maxSeen - i) was seen */
|
|
6
|
+
bitmap: bigint;
|
|
7
|
+
/** Window size in bits */
|
|
8
|
+
windowSize: bigint;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create a new replay window.
|
|
12
|
+
*
|
|
13
|
+
* @param windowSize - Window size in bits (default: 64)
|
|
14
|
+
*/
|
|
15
|
+
export declare function createReplayWindow(windowSize?: bigint): ReplayWindow;
|
|
16
|
+
/**
|
|
17
|
+
* Check if a sequence number is valid (not a replay) and update the window.
|
|
18
|
+
*
|
|
19
|
+
* @returns true if the sequence is valid and accepted, false if it's a replay
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkAndUpdateReplay(seq: bigint, window: ReplayWindow): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a sequence number would be valid without updating the window.
|
|
24
|
+
*
|
|
25
|
+
* Useful for pre-validation before decryption.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isValidSequence(seq: bigint, window: ReplayWindow): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Reset the replay window to initial state.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resetReplayWindow(window: ReplayWindow): void;
|
|
32
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAaA,0BAA0B;AAC1B,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,GAAE,MAAmC,GAC9C,YAAY,CAMd;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,GACnB,OAAO,CAqCT;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAgB1E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAG5D"}
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
/**
|
|
4
|
+
* Bitmap-based replay protection for Sideband Relay Protocol (SBRP).
|
|
5
|
+
*
|
|
6
|
+
* Uses a sliding window to track seen sequence numbers and prevent
|
|
7
|
+
* replay attacks while avoiding memory exhaustion from attacker-controlled
|
|
8
|
+
* sequence numbers.
|
|
9
|
+
*/
|
|
10
|
+
import { DEFAULT_REPLAY_WINDOW_SIZE } from "./constants.js";
|
|
11
|
+
/**
|
|
12
|
+
* Create a new replay window.
|
|
13
|
+
*
|
|
14
|
+
* @param windowSize - Window size in bits (default: 64)
|
|
15
|
+
*/
|
|
16
|
+
export function createReplayWindow(windowSize = DEFAULT_REPLAY_WINDOW_SIZE) {
|
|
17
|
+
return {
|
|
18
|
+
maxSeen: -1n, // No messages seen yet
|
|
19
|
+
bitmap: 0n,
|
|
20
|
+
windowSize,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a sequence number is valid (not a replay) and update the window.
|
|
25
|
+
*
|
|
26
|
+
* @returns true if the sequence is valid and accepted, false if it's a replay
|
|
27
|
+
*/
|
|
28
|
+
export function checkAndUpdateReplay(seq, window) {
|
|
29
|
+
// First message ever
|
|
30
|
+
if (window.maxSeen === -1n) {
|
|
31
|
+
window.maxSeen = seq;
|
|
32
|
+
window.bitmap = 1n;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (seq > window.maxSeen) {
|
|
36
|
+
// New high sequence - shift window
|
|
37
|
+
const shift = seq - window.maxSeen;
|
|
38
|
+
if (shift >= window.windowSize) {
|
|
39
|
+
// Sequence is far ahead, reset bitmap
|
|
40
|
+
window.bitmap = 1n;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
window.bitmap = (window.bitmap << shift) | 1n;
|
|
44
|
+
}
|
|
45
|
+
window.maxSeen = seq;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
// Sequence is within or before the window
|
|
49
|
+
const diff = window.maxSeen - seq;
|
|
50
|
+
if (diff >= window.windowSize) {
|
|
51
|
+
// Too old, outside window
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const mask = 1n << diff;
|
|
55
|
+
if (window.bitmap & mask) {
|
|
56
|
+
// Already seen (replay)
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
// Mark as seen
|
|
60
|
+
window.bitmap |= mask;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if a sequence number would be valid without updating the window.
|
|
65
|
+
*
|
|
66
|
+
* Useful for pre-validation before decryption.
|
|
67
|
+
*/
|
|
68
|
+
export function isValidSequence(seq, window) {
|
|
69
|
+
if (window.maxSeen === -1n) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (seq > window.maxSeen) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const diff = window.maxSeen - seq;
|
|
76
|
+
if (diff >= window.windowSize) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const mask = 1n << diff;
|
|
80
|
+
return (window.bitmap & mask) === 0n;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Reset the replay window to initial state.
|
|
84
|
+
*/
|
|
85
|
+
export function resetReplayWindow(window) {
|
|
86
|
+
window.maxSeen = -1n;
|
|
87
|
+
window.bitmap = 0n;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,6CAA6C;AAE7C;;;;;;GAMG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAY5D;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,aAAqB,0BAA0B;IAE/C,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,EAAE,uBAAuB;QACrC,MAAM,EAAE,EAAE;QACV,UAAU;KACX,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAW,EACX,MAAoB;IAEpB,qBAAqB;IACrB,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC;QACrB,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QACzB,mCAAmC;QACnC,MAAM,KAAK,GAAG,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC;QACnC,IAAI,KAAK,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YAC/B,sCAAsC;YACtC,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QAChD,CAAC;QACD,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0CAA0C;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC;IAClC,IAAI,IAAI,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC9B,0BAA0B;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC;IACxB,IAAI,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QACzB,wBAAwB;QACxB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,eAAe;IACf,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC;IACtB,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,MAAoB;IAC/D,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,EAAE,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC;IAClC,IAAI,IAAI,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC;IACxB,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;AACvC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAoB;IACpD,MAAM,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;IACrB,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ReplayWindow } from "./replay.js";
|
|
2
|
+
import type { ClientId, EncryptedMessage, TrafficKeys } from "./types.js";
|
|
3
|
+
/** Crypto state for one direction of communication (traffic key, counters, replay) */
|
|
4
|
+
interface ChannelState {
|
|
5
|
+
trafficKey: Uint8Array;
|
|
6
|
+
sendSeq: bigint;
|
|
7
|
+
recvWindow: ReplayWindow;
|
|
8
|
+
}
|
|
9
|
+
/** Client session (daemon-side state for each connected client) */
|
|
10
|
+
export interface ClientSession {
|
|
11
|
+
clientId: ClientId;
|
|
12
|
+
clientToDaemon: ChannelState;
|
|
13
|
+
daemonToClient: ChannelState;
|
|
14
|
+
}
|
|
15
|
+
/** Daemon connection (client-side state for communicating with daemon) */
|
|
16
|
+
export interface DaemonConnection {
|
|
17
|
+
clientToDaemon: ChannelState;
|
|
18
|
+
daemonToClient: ChannelState;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a client session (daemon side).
|
|
22
|
+
*
|
|
23
|
+
* Used by daemon to manage state for each connected client.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createClientSession(clientId: ClientId, trafficKeys: TrafficKeys): ClientSession;
|
|
26
|
+
/**
|
|
27
|
+
* Create a daemon connection (client side).
|
|
28
|
+
*
|
|
29
|
+
* Used by client to communicate with daemon.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createDaemonConnection(trafficKeys: TrafficKeys): DaemonConnection;
|
|
32
|
+
/**
|
|
33
|
+
* Encrypt a message from client to daemon.
|
|
34
|
+
*/
|
|
35
|
+
export declare function encryptClientToDaemon(conn: DaemonConnection, plaintext: Uint8Array): EncryptedMessage;
|
|
36
|
+
/**
|
|
37
|
+
* Encrypt a message from daemon to client.
|
|
38
|
+
*/
|
|
39
|
+
export declare function encryptDaemonToClient(session: ClientSession, plaintext: Uint8Array): EncryptedMessage;
|
|
40
|
+
/**
|
|
41
|
+
* Decrypt a message received by daemon from client.
|
|
42
|
+
*
|
|
43
|
+
* @throws {SbrpError} with code SequenceError if replay detected
|
|
44
|
+
* @throws {SbrpError} with code DecryptFailed if decryption fails
|
|
45
|
+
*/
|
|
46
|
+
export declare function decryptClientToDaemon(session: ClientSession, message: EncryptedMessage): Uint8Array;
|
|
47
|
+
/**
|
|
48
|
+
* Decrypt a message received by client from daemon.
|
|
49
|
+
*
|
|
50
|
+
* @throws {SbrpError} with code SequenceError if replay detected
|
|
51
|
+
* @throws {SbrpError} with code DecryptFailed if decryption fails
|
|
52
|
+
*/
|
|
53
|
+
export declare function decryptDaemonToClient(conn: DaemonConnection, message: EncryptedMessage): Uint8Array;
|
|
54
|
+
/**
|
|
55
|
+
* Clear all key material from a client session.
|
|
56
|
+
*
|
|
57
|
+
* Best-effort zeroization (JS/GC limitations apply).
|
|
58
|
+
*/
|
|
59
|
+
export declare function clearClientSession(session: ClientSession): void;
|
|
60
|
+
/**
|
|
61
|
+
* Clear all key material from a daemon connection.
|
|
62
|
+
*
|
|
63
|
+
* Best-effort zeroization (JS/GC limitations apply).
|
|
64
|
+
*/
|
|
65
|
+
export declare function clearDaemonConnection(conn: DaemonConnection): void;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG1E,sFAAsF;AACtF,UAAU,YAAY;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,YAAY,CAAC;CAC1B;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,cAAc,EAAE,YAAY,CAAC;IAC7B,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,YAAY,CAAC;IAC7B,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,WAAW,GACvB,aAAa,CAcf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,WAAW,GACvB,gBAAgB,CAalB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,gBAAgB,EACtB,SAAS,EAAE,UAAU,GACpB,gBAAgB,CAUlB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,UAAU,GACpB,gBAAgB,CAUlB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,gBAAgB,GACxB,UAAU,CAeZ;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,gBAAgB,GACxB,UAAU,CAeZ;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAG/D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAGlE"}
|