@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/session.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
/**
|
|
4
|
+
* Session management for Sideband Relay Protocol (SBRP).
|
|
5
|
+
*
|
|
6
|
+
* Handles encrypted message sending/receiving with proper key selection,
|
|
7
|
+
* sequence number tracking, and replay protection.
|
|
8
|
+
*/
|
|
9
|
+
import { decrypt, encrypt, extractSequence, zeroize } from "./crypto.js";
|
|
10
|
+
import { checkAndUpdateReplay, createReplayWindow, } from "./replay.js";
|
|
11
|
+
import { Direction, SbrpError, SbrpErrorCode } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Create a client session (daemon side).
|
|
14
|
+
*
|
|
15
|
+
* Used by daemon to manage state for each connected client.
|
|
16
|
+
*/
|
|
17
|
+
export function createClientSession(clientId, trafficKeys) {
|
|
18
|
+
return {
|
|
19
|
+
clientId,
|
|
20
|
+
clientToDaemon: {
|
|
21
|
+
trafficKey: trafficKeys.clientToDaemon,
|
|
22
|
+
sendSeq: 0n,
|
|
23
|
+
recvWindow: createReplayWindow(),
|
|
24
|
+
},
|
|
25
|
+
daemonToClient: {
|
|
26
|
+
trafficKey: trafficKeys.daemonToClient,
|
|
27
|
+
sendSeq: 0n,
|
|
28
|
+
recvWindow: createReplayWindow(),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a daemon connection (client side).
|
|
34
|
+
*
|
|
35
|
+
* Used by client to communicate with daemon.
|
|
36
|
+
*/
|
|
37
|
+
export function createDaemonConnection(trafficKeys) {
|
|
38
|
+
return {
|
|
39
|
+
clientToDaemon: {
|
|
40
|
+
trafficKey: trafficKeys.clientToDaemon,
|
|
41
|
+
sendSeq: 0n,
|
|
42
|
+
recvWindow: createReplayWindow(),
|
|
43
|
+
},
|
|
44
|
+
daemonToClient: {
|
|
45
|
+
trafficKey: trafficKeys.daemonToClient,
|
|
46
|
+
sendSeq: 0n,
|
|
47
|
+
recvWindow: createReplayWindow(),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Encrypt a message from client to daemon.
|
|
53
|
+
*/
|
|
54
|
+
export function encryptClientToDaemon(conn, plaintext) {
|
|
55
|
+
const seq = conn.clientToDaemon.sendSeq++;
|
|
56
|
+
const data = encrypt(conn.clientToDaemon.trafficKey, Direction.ClientToDaemon, seq, plaintext);
|
|
57
|
+
return { type: "encrypted", seq, data };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Encrypt a message from daemon to client.
|
|
61
|
+
*/
|
|
62
|
+
export function encryptDaemonToClient(session, plaintext) {
|
|
63
|
+
const seq = session.daemonToClient.sendSeq++;
|
|
64
|
+
const data = encrypt(session.daemonToClient.trafficKey, Direction.DaemonToClient, seq, plaintext);
|
|
65
|
+
return { type: "encrypted", seq, data };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decrypt a message received by daemon from client.
|
|
69
|
+
*
|
|
70
|
+
* @throws {SbrpError} with code SequenceError if replay detected
|
|
71
|
+
* @throws {SbrpError} with code DecryptFailed if decryption fails
|
|
72
|
+
*/
|
|
73
|
+
export function decryptClientToDaemon(session, message) {
|
|
74
|
+
// Check replay protection
|
|
75
|
+
const seq = extractSequence(message.data);
|
|
76
|
+
if (!checkAndUpdateReplay(seq, session.clientToDaemon.recvWindow)) {
|
|
77
|
+
throw new SbrpError(SbrpErrorCode.SequenceError, "Sequence number outside valid window or replay detected");
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return decrypt(session.clientToDaemon.trafficKey, message.data);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new SbrpError(SbrpErrorCode.DecryptFailed, "Message decryption failed");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decrypt a message received by client from daemon.
|
|
88
|
+
*
|
|
89
|
+
* @throws {SbrpError} with code SequenceError if replay detected
|
|
90
|
+
* @throws {SbrpError} with code DecryptFailed if decryption fails
|
|
91
|
+
*/
|
|
92
|
+
export function decryptDaemonToClient(conn, message) {
|
|
93
|
+
// Check replay protection
|
|
94
|
+
const seq = extractSequence(message.data);
|
|
95
|
+
if (!checkAndUpdateReplay(seq, conn.daemonToClient.recvWindow)) {
|
|
96
|
+
throw new SbrpError(SbrpErrorCode.SequenceError, "Sequence number outside valid window or replay detected");
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return decrypt(conn.daemonToClient.trafficKey, message.data);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new SbrpError(SbrpErrorCode.DecryptFailed, "Message decryption failed");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Clear all key material from a client session.
|
|
107
|
+
*
|
|
108
|
+
* Best-effort zeroization (JS/GC limitations apply).
|
|
109
|
+
*/
|
|
110
|
+
export function clearClientSession(session) {
|
|
111
|
+
zeroize(session.clientToDaemon.trafficKey);
|
|
112
|
+
zeroize(session.daemonToClient.trafficKey);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Clear all key material from a daemon connection.
|
|
116
|
+
*
|
|
117
|
+
* Best-effort zeroization (JS/GC limitations apply).
|
|
118
|
+
*/
|
|
119
|
+
export function clearDaemonConnection(conn) {
|
|
120
|
+
zeroize(conn.clientToDaemon.trafficKey);
|
|
121
|
+
zeroize(conn.daemonToClient.trafficKey);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Sideband Relay Protocol (SBRP).
|
|
3
|
+
*/
|
|
4
|
+
/** Branded type for daemon identifiers */
|
|
5
|
+
export type DaemonId = string & {
|
|
6
|
+
readonly __brand: "DaemonId";
|
|
7
|
+
};
|
|
8
|
+
/** Branded type for client session identifiers (relay-assigned) */
|
|
9
|
+
export type ClientId = string & {
|
|
10
|
+
readonly __brand: "ClientId";
|
|
11
|
+
};
|
|
12
|
+
/** Ed25519 identity keypair for daemon authentication */
|
|
13
|
+
export interface IdentityKeyPair {
|
|
14
|
+
publicKey: Uint8Array;
|
|
15
|
+
privateKey: Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
/** X25519 ephemeral keypair for key exchange */
|
|
18
|
+
export interface EphemeralKeyPair {
|
|
19
|
+
publicKey: Uint8Array;
|
|
20
|
+
privateKey: Uint8Array;
|
|
21
|
+
}
|
|
22
|
+
/** Pinned identity for TOFU verification (browser-side storage) */
|
|
23
|
+
export interface PinnedIdentity {
|
|
24
|
+
daemonId: DaemonId;
|
|
25
|
+
identityPublicKey: Uint8Array;
|
|
26
|
+
firstSeen: Date;
|
|
27
|
+
lastSeen: Date;
|
|
28
|
+
}
|
|
29
|
+
/** Traffic keys derived from handshake (directional symmetric keys) */
|
|
30
|
+
export interface TrafficKeys {
|
|
31
|
+
/** Key for encrypting client→daemon messages */
|
|
32
|
+
clientToDaemon: Uint8Array;
|
|
33
|
+
/** Key for encrypting daemon→client messages */
|
|
34
|
+
daemonToClient: Uint8Array;
|
|
35
|
+
}
|
|
36
|
+
/** Handshake init message (browser → daemon) */
|
|
37
|
+
export interface HandshakeInit {
|
|
38
|
+
type: "handshake.init";
|
|
39
|
+
browserPublicKey: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
/** Handshake accept message (daemon → browser) */
|
|
42
|
+
export interface HandshakeAccept {
|
|
43
|
+
type: "handshake.accept";
|
|
44
|
+
daemonPublicKey: Uint8Array;
|
|
45
|
+
signature: Uint8Array;
|
|
46
|
+
}
|
|
47
|
+
/** Encrypted message envelope */
|
|
48
|
+
export interface EncryptedMessage {
|
|
49
|
+
type: "encrypted";
|
|
50
|
+
seq: bigint;
|
|
51
|
+
data: Uint8Array;
|
|
52
|
+
}
|
|
53
|
+
/** Direction of message flow (used in nonce construction) */
|
|
54
|
+
export declare const enum Direction {
|
|
55
|
+
ClientToDaemon = 1,
|
|
56
|
+
DaemonToClient = 2
|
|
57
|
+
}
|
|
58
|
+
/** SBRP error codes */
|
|
59
|
+
export declare const enum SbrpErrorCode {
|
|
60
|
+
Unauthorized = "unauthorized",
|
|
61
|
+
DaemonNotFound = "daemon_not_found",
|
|
62
|
+
DaemonOffline = "daemon_offline",
|
|
63
|
+
DaemonNotOwned = "daemon_not_owned",
|
|
64
|
+
IdentityKeyChanged = "identity_key_changed",
|
|
65
|
+
HandshakeFailed = "handshake_failed",
|
|
66
|
+
DecryptFailed = "decrypt_failed",
|
|
67
|
+
SequenceError = "sequence_error",
|
|
68
|
+
RateLimited = "rate_limited"
|
|
69
|
+
}
|
|
70
|
+
/** SBRP-specific error */
|
|
71
|
+
export declare class SbrpError extends Error {
|
|
72
|
+
readonly code: SbrpErrorCode;
|
|
73
|
+
constructor(code: SbrpErrorCode, message: string);
|
|
74
|
+
}
|
|
75
|
+
/** Type guard for DaemonId */
|
|
76
|
+
export declare function asDaemonId(value: string): DaemonId;
|
|
77
|
+
/** Type guard for ClientId */
|
|
78
|
+
export declare function asClientId(value: string): ClientId;
|
|
79
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AAEH,0CAA0C;AAC1C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAEjE,mEAAmE;AACnE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAC;AAEjE,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,iBAAiB,EAAE,UAAU,CAAC;IAC9B,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,IAAI,CAAC;CAChB;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,cAAc,EAAE,UAAU,CAAC;IAC3B,gDAAgD;IAChD,cAAc,EAAE,UAAU,CAAC;CAC5B;AAED,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,gBAAgB,CAAC;IACvB,gBAAgB,EAAE,UAAU,CAAC;CAC9B;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,eAAe,EAAE,UAAU,CAAC;IAC5B,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,iCAAiC;AACjC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,6DAA6D;AAC7D,0BAAkB,SAAS;IACzB,cAAc,IAAI;IAClB,cAAc,IAAI;CACnB;AAED,uBAAuB;AACvB,0BAAkB,aAAa;IAC7B,YAAY,iBAAiB;IAC7B,cAAc,qBAAqB;IACnC,aAAa,mBAAmB;IAChC,cAAc,qBAAqB;IACnC,kBAAkB,yBAAyB;IAC3C,eAAe,qBAAqB;IACpC,aAAa,mBAAmB;IAChC,aAAa,mBAAmB;IAChC,WAAW,iBAAiB;CAC7B;AAED,0BAA0B;AAC1B,qBAAa,SAAU,SAAQ,KAAK;aAEhB,IAAI,EAAE,aAAa;gBAAnB,IAAI,EAAE,aAAa,EACnC,OAAO,EAAE,MAAM;CAKlB;AAED,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAElD;AAED,8BAA8B;AAC9B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAElD"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
/** Direction of message flow (used in nonce construction) */
|
|
4
|
+
export var Direction;
|
|
5
|
+
(function (Direction) {
|
|
6
|
+
Direction[Direction["ClientToDaemon"] = 1] = "ClientToDaemon";
|
|
7
|
+
Direction[Direction["DaemonToClient"] = 2] = "DaemonToClient";
|
|
8
|
+
})(Direction || (Direction = {}));
|
|
9
|
+
/** SBRP error codes */
|
|
10
|
+
export var SbrpErrorCode;
|
|
11
|
+
(function (SbrpErrorCode) {
|
|
12
|
+
SbrpErrorCode["Unauthorized"] = "unauthorized";
|
|
13
|
+
SbrpErrorCode["DaemonNotFound"] = "daemon_not_found";
|
|
14
|
+
SbrpErrorCode["DaemonOffline"] = "daemon_offline";
|
|
15
|
+
SbrpErrorCode["DaemonNotOwned"] = "daemon_not_owned";
|
|
16
|
+
SbrpErrorCode["IdentityKeyChanged"] = "identity_key_changed";
|
|
17
|
+
SbrpErrorCode["HandshakeFailed"] = "handshake_failed";
|
|
18
|
+
SbrpErrorCode["DecryptFailed"] = "decrypt_failed";
|
|
19
|
+
SbrpErrorCode["SequenceError"] = "sequence_error";
|
|
20
|
+
SbrpErrorCode["RateLimited"] = "rate_limited";
|
|
21
|
+
})(SbrpErrorCode || (SbrpErrorCode = {}));
|
|
22
|
+
/** SBRP-specific error */
|
|
23
|
+
export class SbrpError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.name = "SbrpError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Type guard for DaemonId */
|
|
32
|
+
export function asDaemonId(value) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/** Type guard for ClientId */
|
|
36
|
+
export function asClientId(value) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,6CAA6C;AA4D7C,6DAA6D;AAC7D,MAAM,CAAN,IAAkB,SAGjB;AAHD,WAAkB,SAAS;IACzB,6DAAkB,CAAA;IAClB,6DAAkB,CAAA;AACpB,CAAC,EAHiB,SAAS,KAAT,SAAS,QAG1B;AAED,uBAAuB;AACvB,MAAM,CAAN,IAAkB,aAUjB;AAVD,WAAkB,aAAa;IAC7B,8CAA6B,CAAA;IAC7B,oDAAmC,CAAA;IACnC,iDAAgC,CAAA;IAChC,oDAAmC,CAAA;IACnC,4DAA2C,CAAA;IAC3C,qDAAoC,CAAA;IACpC,iDAAgC,CAAA;IAChC,iDAAgC,CAAA;IAChC,6CAA4B,CAAA;AAC9B,CAAC,EAViB,aAAa,KAAb,aAAa,QAU9B;AAED,0BAA0B;AAC1B,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEhB;IADlB,YACkB,IAAmB,EACnC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,SAAI,GAAJ,IAAI,CAAe;QAInC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAED,8BAA8B;AAC9B,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAiB,CAAC;AAC3B,CAAC;AAED,8BAA8B;AAC9B,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAiB,CAAC;AAC3B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sideband/secure-relay",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Secure Relay Protocol (SBRP): E2EE handshake, session encryption, and TOFU identity pinning for relay-mediated communication.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/sidebandtech/sideband.git",
|
|
9
|
+
"directory": "packages/secure-relay"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/sidebandtech/sideband/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://sideband.tech",
|
|
15
|
+
"author": "Sideband <hello@sideband.tech>",
|
|
16
|
+
"contributors": [
|
|
17
|
+
"Konstantin Tarkus <koistya@kriasoft.com>"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"sideband",
|
|
21
|
+
"secure-relay",
|
|
22
|
+
"relay-protocol",
|
|
23
|
+
"e2ee",
|
|
24
|
+
"end-to-end-encryption",
|
|
25
|
+
"authenticated-encryption",
|
|
26
|
+
"identity-pinning",
|
|
27
|
+
"tofu",
|
|
28
|
+
"zero-trust",
|
|
29
|
+
"secure-websocket",
|
|
30
|
+
"agent-communication",
|
|
31
|
+
"browser-to-daemon",
|
|
32
|
+
"remote-daemon",
|
|
33
|
+
"encryption",
|
|
34
|
+
"x25519",
|
|
35
|
+
"ed25519",
|
|
36
|
+
"chacha20-poly1305"
|
|
37
|
+
],
|
|
38
|
+
"type": "module",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"bun": "./src/index.ts",
|
|
44
|
+
"default": "./dist/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@noble/ciphers": "^1.2.1",
|
|
50
|
+
"@noble/curves": "^1.8.1",
|
|
51
|
+
"@noble/hashes": "^1.7.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "^1.3.3"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist",
|
|
58
|
+
"src"
|
|
59
|
+
],
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Protocol constants for Sideband Relay Protocol (SBRP).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Domain separation context for handshake signature payload */
|
|
9
|
+
export const SBRP_HANDSHAKE_CONTEXT = "sbrp-v1-handshake";
|
|
10
|
+
|
|
11
|
+
/** Domain separation context for transcript hash (HKDF salt) */
|
|
12
|
+
export const SBRP_TRANSCRIPT_CONTEXT = "sbrp-v1-transcript";
|
|
13
|
+
|
|
14
|
+
/** HKDF info string for session key derivation */
|
|
15
|
+
export const SBRP_SESSION_KEYS_INFO = "sbrp-session-keys";
|
|
16
|
+
|
|
17
|
+
/** Length of session keys in bytes (browserToDaemon + daemonToBrowser) */
|
|
18
|
+
export const SESSION_KEYS_LENGTH = 64;
|
|
19
|
+
|
|
20
|
+
/** Length of a single symmetric key in bytes */
|
|
21
|
+
export const SYMMETRIC_KEY_LENGTH = 32;
|
|
22
|
+
|
|
23
|
+
/** Length of Ed25519 public key in bytes */
|
|
24
|
+
export const ED25519_PUBLIC_KEY_LENGTH = 32;
|
|
25
|
+
|
|
26
|
+
/** Length of Ed25519 private key seed in bytes */
|
|
27
|
+
export const ED25519_PRIVATE_KEY_LENGTH = 32;
|
|
28
|
+
|
|
29
|
+
/** Length of Ed25519 signature in bytes */
|
|
30
|
+
export const ED25519_SIGNATURE_LENGTH = 64;
|
|
31
|
+
|
|
32
|
+
/** Length of X25519 public key in bytes */
|
|
33
|
+
export const X25519_PUBLIC_KEY_LENGTH = 32;
|
|
34
|
+
|
|
35
|
+
/** Length of X25519 private key in bytes */
|
|
36
|
+
export const X25519_PRIVATE_KEY_LENGTH = 32;
|
|
37
|
+
|
|
38
|
+
/** Length of ChaCha20-Poly1305 nonce in bytes */
|
|
39
|
+
export const NONCE_LENGTH = 12;
|
|
40
|
+
|
|
41
|
+
/** Length of Poly1305 auth tag in bytes */
|
|
42
|
+
export const AUTH_TAG_LENGTH = 16;
|
|
43
|
+
|
|
44
|
+
/** Default replay window size (bits) */
|
|
45
|
+
export const DEFAULT_REPLAY_WINDOW_SIZE = 64n;
|
|
46
|
+
|
|
47
|
+
/** Direction bytes in nonce (4 bytes, big-endian) */
|
|
48
|
+
export const DIRECTION_CLIENT_TO_DAEMON = new Uint8Array([0, 0, 0, 1]);
|
|
49
|
+
export const DIRECTION_DAEMON_TO_CLIENT = new Uint8Array([0, 0, 0, 2]);
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-present Sideband
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cryptographic primitives for Sideband Relay Protocol (SBRP).
|
|
6
|
+
*
|
|
7
|
+
* Uses @noble/curves for Ed25519/X25519, @noble/ciphers for ChaCha20-Poly1305,
|
|
8
|
+
* and @noble/hashes for SHA-256/HKDF.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha";
|
|
12
|
+
import { ed25519 } from "@noble/curves/ed25519";
|
|
13
|
+
import { x25519 } from "@noble/curves/ed25519";
|
|
14
|
+
import { hkdf } from "@noble/hashes/hkdf";
|
|
15
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
16
|
+
import { concatBytes, randomBytes } from "@noble/hashes/utils";
|
|
17
|
+
import {
|
|
18
|
+
AUTH_TAG_LENGTH,
|
|
19
|
+
DIRECTION_CLIENT_TO_DAEMON,
|
|
20
|
+
DIRECTION_DAEMON_TO_CLIENT,
|
|
21
|
+
NONCE_LENGTH,
|
|
22
|
+
SBRP_HANDSHAKE_CONTEXT,
|
|
23
|
+
SBRP_SESSION_KEYS_INFO,
|
|
24
|
+
SBRP_TRANSCRIPT_CONTEXT,
|
|
25
|
+
SESSION_KEYS_LENGTH,
|
|
26
|
+
SYMMETRIC_KEY_LENGTH,
|
|
27
|
+
} from "./constants.js";
|
|
28
|
+
import type {
|
|
29
|
+
DaemonId,
|
|
30
|
+
EphemeralKeyPair,
|
|
31
|
+
IdentityKeyPair,
|
|
32
|
+
SessionKeys,
|
|
33
|
+
} from "./types.js";
|
|
34
|
+
import { Direction } from "./types.js";
|
|
35
|
+
|
|
36
|
+
const textEncoder = new TextEncoder();
|
|
37
|
+
|
|
38
|
+
/** Generate a new Ed25519 identity keypair */
|
|
39
|
+
export function generateIdentityKeyPair(): IdentityKeyPair {
|
|
40
|
+
const privateKey = ed25519.utils.randomPrivateKey();
|
|
41
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
42
|
+
return { publicKey, privateKey };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Generate a new X25519 ephemeral keypair */
|
|
46
|
+
export function generateEphemeralKeyPair(): EphemeralKeyPair {
|
|
47
|
+
const privateKey = x25519.utils.randomPrivateKey();
|
|
48
|
+
const publicKey = x25519.getPublicKey(privateKey);
|
|
49
|
+
return { publicKey, privateKey };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compute SHA-256 fingerprint of an identity public key */
|
|
53
|
+
export function computeFingerprint(identityPublicKey: Uint8Array): string {
|
|
54
|
+
const hash = sha256(identityPublicKey);
|
|
55
|
+
const hex = Array.from(hash)
|
|
56
|
+
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
|
|
57
|
+
.join(":");
|
|
58
|
+
return `SHA256:${hex}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create the signature payload for handshake authentication.
|
|
63
|
+
*
|
|
64
|
+
* payload = SHA256("sbrp-v1-handshake" || daemonId || clientPublicKey || daemonEphemeralPublicKey)
|
|
65
|
+
*/
|
|
66
|
+
export function createSignaturePayload(
|
|
67
|
+
daemonId: DaemonId,
|
|
68
|
+
clientPublicKey: Uint8Array,
|
|
69
|
+
daemonEphemeralPublicKey: Uint8Array,
|
|
70
|
+
): Uint8Array {
|
|
71
|
+
return sha256(
|
|
72
|
+
concatBytes(
|
|
73
|
+
textEncoder.encode(SBRP_HANDSHAKE_CONTEXT),
|
|
74
|
+
textEncoder.encode(daemonId),
|
|
75
|
+
clientPublicKey,
|
|
76
|
+
daemonEphemeralPublicKey,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Sign a payload with an Ed25519 identity private key */
|
|
82
|
+
export function signPayload(
|
|
83
|
+
payload: Uint8Array,
|
|
84
|
+
identityPrivateKey: Uint8Array,
|
|
85
|
+
): Uint8Array {
|
|
86
|
+
return ed25519.sign(payload, identityPrivateKey);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Verify an Ed25519 signature */
|
|
90
|
+
export function verifySignature(
|
|
91
|
+
payload: Uint8Array,
|
|
92
|
+
signature: Uint8Array,
|
|
93
|
+
identityPublicKey: Uint8Array,
|
|
94
|
+
): boolean {
|
|
95
|
+
return ed25519.verify(signature, payload, identityPublicKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Compute X25519 shared secret */
|
|
99
|
+
export function computeSharedSecret(
|
|
100
|
+
myPrivateKey: Uint8Array,
|
|
101
|
+
peerPublicKey: Uint8Array,
|
|
102
|
+
): Uint8Array {
|
|
103
|
+
return x25519.getSharedSecret(myPrivateKey, peerPublicKey);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create the transcript hash for key derivation.
|
|
108
|
+
*
|
|
109
|
+
* transcript = SHA256("sbrp-v1-transcript" || daemonId || clientPublicKey || daemonPublicKey || signature)
|
|
110
|
+
*/
|
|
111
|
+
export function createTranscriptHash(
|
|
112
|
+
daemonId: DaemonId,
|
|
113
|
+
clientPublicKey: Uint8Array,
|
|
114
|
+
daemonPublicKey: Uint8Array,
|
|
115
|
+
signature: Uint8Array,
|
|
116
|
+
): Uint8Array {
|
|
117
|
+
return sha256(
|
|
118
|
+
concatBytes(
|
|
119
|
+
textEncoder.encode(SBRP_TRANSCRIPT_CONTEXT),
|
|
120
|
+
textEncoder.encode(daemonId),
|
|
121
|
+
clientPublicKey,
|
|
122
|
+
daemonPublicKey,
|
|
123
|
+
signature,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Derive session keys using HKDF-SHA256.
|
|
130
|
+
*
|
|
131
|
+
* Keys are derived with transcript hash as salt to bind to the authenticated session.
|
|
132
|
+
*/
|
|
133
|
+
export function deriveSessionKeys(
|
|
134
|
+
sharedSecret: Uint8Array,
|
|
135
|
+
transcriptHash: Uint8Array,
|
|
136
|
+
): SessionKeys {
|
|
137
|
+
const keys = hkdf(
|
|
138
|
+
sha256,
|
|
139
|
+
sharedSecret,
|
|
140
|
+
transcriptHash,
|
|
141
|
+
SBRP_SESSION_KEYS_INFO,
|
|
142
|
+
SESSION_KEYS_LENGTH,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
clientToDaemon: keys.slice(0, SYMMETRIC_KEY_LENGTH),
|
|
147
|
+
daemonToClient: keys.slice(SYMMETRIC_KEY_LENGTH, SESSION_KEYS_LENGTH),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Construct a nonce for ChaCha20-Poly1305.
|
|
153
|
+
*
|
|
154
|
+
* Nonce format (12 bytes):
|
|
155
|
+
* - Bytes 0-3: Direction (0x00000001 = client→daemon, 0x00000002 = daemon→client)
|
|
156
|
+
* - Bytes 4-11: Sequence number (big-endian uint64)
|
|
157
|
+
*/
|
|
158
|
+
export function constructNonce(direction: Direction, seq: bigint): Uint8Array {
|
|
159
|
+
const nonce = new Uint8Array(NONCE_LENGTH);
|
|
160
|
+
const directionBytes =
|
|
161
|
+
direction === Direction.ClientToDaemon
|
|
162
|
+
? DIRECTION_CLIENT_TO_DAEMON
|
|
163
|
+
: DIRECTION_DAEMON_TO_CLIENT;
|
|
164
|
+
|
|
165
|
+
nonce.set(directionBytes, 0);
|
|
166
|
+
|
|
167
|
+
// Write sequence number as big-endian uint64 (bytes 4-11)
|
|
168
|
+
const view = new DataView(nonce.buffer);
|
|
169
|
+
view.setBigUint64(4, seq, false); // false = big-endian
|
|
170
|
+
|
|
171
|
+
return nonce;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Encrypt a message using ChaCha20-Poly1305.
|
|
176
|
+
*
|
|
177
|
+
* Returns: nonce (12 bytes) || ciphertext || authTag (16 bytes)
|
|
178
|
+
*/
|
|
179
|
+
export function encrypt(
|
|
180
|
+
key: Uint8Array,
|
|
181
|
+
direction: Direction,
|
|
182
|
+
seq: bigint,
|
|
183
|
+
plaintext: Uint8Array,
|
|
184
|
+
): Uint8Array {
|
|
185
|
+
const nonce = constructNonce(direction, seq);
|
|
186
|
+
const cipher = chacha20poly1305(key, nonce);
|
|
187
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
188
|
+
|
|
189
|
+
return concatBytes(nonce, ciphertext);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Decrypt a message using ChaCha20-Poly1305.
|
|
194
|
+
*
|
|
195
|
+
* Input format: nonce (12 bytes) || ciphertext || authTag (16 bytes)
|
|
196
|
+
* Returns the plaintext, or throws on decryption failure.
|
|
197
|
+
*/
|
|
198
|
+
export function decrypt(key: Uint8Array, data: Uint8Array): Uint8Array {
|
|
199
|
+
if (data.length < NONCE_LENGTH + AUTH_TAG_LENGTH) {
|
|
200
|
+
throw new Error("Invalid encrypted message: too short");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const nonce = data.slice(0, NONCE_LENGTH);
|
|
204
|
+
const ciphertext = data.slice(NONCE_LENGTH);
|
|
205
|
+
|
|
206
|
+
const cipher = chacha20poly1305(key, nonce);
|
|
207
|
+
return cipher.decrypt(ciphertext);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract sequence number from encrypted message data.
|
|
212
|
+
*
|
|
213
|
+
* Reads bytes 4-11 of the nonce as big-endian uint64.
|
|
214
|
+
*/
|
|
215
|
+
export function extractSequence(data: Uint8Array): bigint {
|
|
216
|
+
if (data.length < NONCE_LENGTH) {
|
|
217
|
+
throw new Error("Invalid encrypted message: too short");
|
|
218
|
+
}
|
|
219
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
220
|
+
return view.getBigUint64(4, false); // false = big-endian
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Best-effort zeroization of sensitive key material.
|
|
225
|
+
*
|
|
226
|
+
* Note: JavaScript/GC limitations mean this is not guaranteed to prevent
|
|
227
|
+
* key material from remaining in memory.
|
|
228
|
+
*/
|
|
229
|
+
export function zeroize(data: Uint8Array): void {
|
|
230
|
+
data.fill(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Generate random bytes */
|
|
234
|
+
export { randomBytes };
|