@camerontaylor/paseo-relay 0.8.0-fork.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/dist/base64.d.ts +3 -0
- package/dist/base64.js +17 -0
- package/dist/cloudflare-adapter.d.ts +76 -0
- package/dist/cloudflare-adapter.js +504 -0
- package/dist/crypto.d.ts +30 -0
- package/dist/crypto.js +145 -0
- package/dist/cutover-proxy.d.ts +6 -0
- package/dist/cutover-proxy.js +12 -0
- package/dist/e2ee.d.ts +5 -0
- package/dist/e2ee.js +3 -0
- package/dist/encrypted-channel.d.ts +86 -0
- package/dist/encrypted-channel.js +452 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +11 -0
- package/package.json +54 -0
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* E2EE crypto primitives using NaCl (tweetnacl).
|
|
4
|
+
*
|
|
5
|
+
* - Key exchange: Curve25519 (nacl.box.before)
|
|
6
|
+
* - Encryption: XSalsa20-Poly1305 (nacl.box.after / open.after)
|
|
7
|
+
*
|
|
8
|
+
* Bundle format (binary):
|
|
9
|
+
* [nonce (24 bytes)] [ciphertext...]
|
|
10
|
+
*
|
|
11
|
+
* The encrypted channel chooses the WebSocket representation. Crypto remains
|
|
12
|
+
* byte-oriented so frame kind is never inferred from plaintext contents.
|
|
13
|
+
*/
|
|
14
|
+
import nacl from "tweetnacl";
|
|
15
|
+
import { fromByteArray, toByteArray } from "base64-js";
|
|
16
|
+
const NONCE_LENGTH = nacl.box.nonceLength; // 24
|
|
17
|
+
const ZERO_X25519_SHARED_RESULT = new Uint8Array(nacl.box.sharedKeyLength);
|
|
18
|
+
let prngReady = false;
|
|
19
|
+
function getGlobalCrypto() {
|
|
20
|
+
const g = globalThis;
|
|
21
|
+
return g.crypto;
|
|
22
|
+
}
|
|
23
|
+
function ensurePrng() {
|
|
24
|
+
if (prngReady)
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
nacl.randomBytes(1);
|
|
28
|
+
prngReady = true;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// fallthrough
|
|
33
|
+
}
|
|
34
|
+
const cryptoObj = getGlobalCrypto();
|
|
35
|
+
if (cryptoObj?.getRandomValues) {
|
|
36
|
+
nacl.setPRNG((x, n) => {
|
|
37
|
+
const buf = new Uint8Array(n);
|
|
38
|
+
cryptoObj.getRandomValues(buf);
|
|
39
|
+
x.set(buf, 0);
|
|
40
|
+
});
|
|
41
|
+
prngReady = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
throw new Error("No secure PRNG available for tweetnacl (missing crypto.getRandomValues)");
|
|
45
|
+
}
|
|
46
|
+
function encodeBase64(bytes) {
|
|
47
|
+
return fromByteArray(bytes);
|
|
48
|
+
}
|
|
49
|
+
function decodeBase64(base64) {
|
|
50
|
+
return toByteArray(base64);
|
|
51
|
+
}
|
|
52
|
+
function decodePublicKeyBase64(base64) {
|
|
53
|
+
if (typeof base64 !== "string" ||
|
|
54
|
+
base64.length % 4 !== 0 ||
|
|
55
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64)) {
|
|
56
|
+
throw new Error("Invalid public key encoding");
|
|
57
|
+
}
|
|
58
|
+
const bytes = decodeBase64(base64);
|
|
59
|
+
if (encodeBase64(bytes) !== base64) {
|
|
60
|
+
throw new Error("Invalid public key encoding");
|
|
61
|
+
}
|
|
62
|
+
return bytes;
|
|
63
|
+
}
|
|
64
|
+
function toUint8(data) {
|
|
65
|
+
return typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
|
|
66
|
+
}
|
|
67
|
+
function toArrayBuffer(bytes) {
|
|
68
|
+
const out = new Uint8Array(bytes.byteLength);
|
|
69
|
+
out.set(bytes);
|
|
70
|
+
return out.buffer;
|
|
71
|
+
}
|
|
72
|
+
export function generateKeyPair() {
|
|
73
|
+
ensurePrng();
|
|
74
|
+
const { publicKey, secretKey } = nacl.box.keyPair();
|
|
75
|
+
return { publicKey, secretKey };
|
|
76
|
+
}
|
|
77
|
+
export function exportPublicKey(publicKey) {
|
|
78
|
+
if (!(publicKey instanceof Uint8Array) || publicKey.byteLength !== nacl.box.publicKeyLength) {
|
|
79
|
+
throw new Error(`Invalid public key length (expected ${nacl.box.publicKeyLength})`);
|
|
80
|
+
}
|
|
81
|
+
return encodeBase64(publicKey);
|
|
82
|
+
}
|
|
83
|
+
export function importPublicKey(base64) {
|
|
84
|
+
const bytes = decodePublicKeyBase64(base64);
|
|
85
|
+
if (bytes.byteLength !== nacl.box.publicKeyLength) {
|
|
86
|
+
throw new Error(`Invalid public key length (expected ${nacl.box.publicKeyLength})`);
|
|
87
|
+
}
|
|
88
|
+
return bytes;
|
|
89
|
+
}
|
|
90
|
+
export function exportSecretKey(secretKey) {
|
|
91
|
+
if (!(secretKey instanceof Uint8Array) || secretKey.byteLength !== nacl.box.secretKeyLength) {
|
|
92
|
+
throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
|
|
93
|
+
}
|
|
94
|
+
return encodeBase64(secretKey);
|
|
95
|
+
}
|
|
96
|
+
export function importSecretKey(base64) {
|
|
97
|
+
const bytes = decodeBase64(base64);
|
|
98
|
+
if (bytes.byteLength !== nacl.box.secretKeyLength) {
|
|
99
|
+
throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
|
|
100
|
+
}
|
|
101
|
+
return bytes;
|
|
102
|
+
}
|
|
103
|
+
export function deriveSharedKey(ourSecretKey, peerPublicKey) {
|
|
104
|
+
if (ourSecretKey.byteLength !== nacl.box.secretKeyLength) {
|
|
105
|
+
throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
|
|
106
|
+
}
|
|
107
|
+
if (peerPublicKey.byteLength !== nacl.box.publicKeyLength) {
|
|
108
|
+
throw new Error(`Invalid peer public key length (expected ${nacl.box.publicKeyLength})`);
|
|
109
|
+
}
|
|
110
|
+
const rawSharedResult = nacl.scalarMult(ourSecretKey, peerPublicKey);
|
|
111
|
+
const isAllZero = nacl.verify(rawSharedResult, ZERO_X25519_SHARED_RESULT);
|
|
112
|
+
rawSharedResult.fill(0);
|
|
113
|
+
if (isAllZero) {
|
|
114
|
+
throw new Error("Invalid peer public key");
|
|
115
|
+
}
|
|
116
|
+
return nacl.box.before(peerPublicKey, ourSecretKey);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Encrypts data and returns the binary bundle:
|
|
120
|
+
* [nonce (24)] [ciphertext...]
|
|
121
|
+
*/
|
|
122
|
+
export function encrypt(sharedKey, data) {
|
|
123
|
+
ensurePrng();
|
|
124
|
+
const nonce = nacl.randomBytes(NONCE_LENGTH);
|
|
125
|
+
const plaintext = toUint8(data);
|
|
126
|
+
const ciphertext = nacl.box.after(plaintext, nonce, sharedKey);
|
|
127
|
+
const out = new Uint8Array(nonce.byteLength + ciphertext.byteLength);
|
|
128
|
+
out.set(nonce, 0);
|
|
129
|
+
out.set(ciphertext, nonce.byteLength);
|
|
130
|
+
return toArrayBuffer(out);
|
|
131
|
+
}
|
|
132
|
+
export function decrypt(sharedKey, data) {
|
|
133
|
+
const bytes = new Uint8Array(data);
|
|
134
|
+
if (bytes.byteLength < NONCE_LENGTH) {
|
|
135
|
+
throw new Error("Ciphertext bundle too short");
|
|
136
|
+
}
|
|
137
|
+
const nonce = bytes.slice(0, NONCE_LENGTH);
|
|
138
|
+
const ciphertext = bytes.slice(NONCE_LENGTH);
|
|
139
|
+
const opened = nacl.box.open.after(ciphertext, nonce, sharedKey);
|
|
140
|
+
if (!opened) {
|
|
141
|
+
throw new Error("Decryption failed");
|
|
142
|
+
}
|
|
143
|
+
return toArrayBuffer(opened);
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createCutoverProxy(origin) {
|
|
2
|
+
const originUrl = new URL(origin);
|
|
3
|
+
return {
|
|
4
|
+
async fetch(request) {
|
|
5
|
+
const upstreamUrl = new URL(request.url);
|
|
6
|
+
upstreamUrl.protocol = originUrl.protocol;
|
|
7
|
+
upstreamUrl.host = originUrl.host;
|
|
8
|
+
return fetch(new Request(upstreamUrl, request));
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=cutover-proxy.js.map
|
package/dist/e2ee.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js";
|
|
2
|
+
export type { Transport, TransportMessage, EncryptedChannelEvents } from "./encrypted-channel.js";
|
|
3
|
+
export { generateKeyPair, exportPublicKey, importPublicKey, exportSecretKey, importSecretKey, } from "./crypto.js";
|
|
4
|
+
export type { KeyPair, SharedKey } from "./crypto.js";
|
|
5
|
+
//# sourceMappingURL=e2ee.d.ts.map
|
package/dist/e2ee.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted channel that wraps a WebSocket-like transport.
|
|
3
|
+
*
|
|
4
|
+
* Handles ECDH handshake and encrypts/decrypts all messages.
|
|
5
|
+
* Works identically for daemon and client sides.
|
|
6
|
+
*/
|
|
7
|
+
import { type KeyPair, type SharedKey } from "./crypto.js";
|
|
8
|
+
export interface Transport {
|
|
9
|
+
send(data: string | ArrayBuffer): void | Promise<void>;
|
|
10
|
+
close(code?: number, reason?: string): void;
|
|
11
|
+
onmessage: ((message: TransportMessage) => void) | null;
|
|
12
|
+
onclose: ((code: number, reason: string) => void) | null;
|
|
13
|
+
onerror: ((error: Error) => void) | null;
|
|
14
|
+
}
|
|
15
|
+
export interface TransportMessage {
|
|
16
|
+
data: string | ArrayBuffer;
|
|
17
|
+
isBinary: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface EncryptedChannelEvents {
|
|
20
|
+
onopen?: () => void;
|
|
21
|
+
onmessage?: (data: string | ArrayBuffer) => void;
|
|
22
|
+
onclose?: (code: number, reason: string) => void;
|
|
23
|
+
onerror?: (error: Error) => void;
|
|
24
|
+
}
|
|
25
|
+
type ChannelState = "connecting" | "handshaking" | "open" | "closed";
|
|
26
|
+
interface EncryptedChannelOptions {
|
|
27
|
+
/**
|
|
28
|
+
* If set, the channel can validate repeated plaintext `{type:"e2ee_hello"}`
|
|
29
|
+
* messages even after it is open.
|
|
30
|
+
*
|
|
31
|
+
* This is useful for robustness when the client retries the handshake
|
|
32
|
+
* (e.g., it didn't observe the daemon's `{type:"e2ee_ready"}` yet). In that case,
|
|
33
|
+
* the daemon should re-send `{type:"e2ee_ready"}` without changing keys.
|
|
34
|
+
*/
|
|
35
|
+
daemonKeyPair?: KeyPair;
|
|
36
|
+
binaryCiphertext?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function base64EncryptedWireByteLength(plaintextBytes: number): number;
|
|
39
|
+
export declare function maxBase64EncryptedPlaintextByteLength(wireBytes: number): number;
|
|
40
|
+
/**
|
|
41
|
+
* Creates an encrypted channel as the initiator (client).
|
|
42
|
+
*
|
|
43
|
+
* The client:
|
|
44
|
+
* 1. Receives daemon's public key via QR code
|
|
45
|
+
* 2. Generates own keypair
|
|
46
|
+
* 3. Sends e2ee_hello with own public key
|
|
47
|
+
* 4. Derives shared key and starts encrypted communication
|
|
48
|
+
*/
|
|
49
|
+
export declare function createClientChannel(transport: Transport, daemonPublicKeyB64: string, events?: EncryptedChannelEvents): Promise<EncryptedChannel>;
|
|
50
|
+
/**
|
|
51
|
+
* Creates an encrypted channel as the responder (daemon).
|
|
52
|
+
*
|
|
53
|
+
* The daemon:
|
|
54
|
+
* 1. Has pre-generated keypair (public key was in QR)
|
|
55
|
+
* 2. Waits for client's e2ee_hello with their public key
|
|
56
|
+
* 3. Derives shared key and starts encrypted communication
|
|
57
|
+
*/
|
|
58
|
+
export declare function createDaemonChannel(transport: Transport, daemonKeyPair: KeyPair, events?: EncryptedChannelEvents): Promise<EncryptedChannel>;
|
|
59
|
+
/**
|
|
60
|
+
* Encrypted channel that wraps a transport with E2EE.
|
|
61
|
+
*/
|
|
62
|
+
export declare class EncryptedChannel {
|
|
63
|
+
private transport;
|
|
64
|
+
private sharedKey;
|
|
65
|
+
private state;
|
|
66
|
+
private events;
|
|
67
|
+
private options;
|
|
68
|
+
private pendingSends;
|
|
69
|
+
private onOpenCallbacks;
|
|
70
|
+
private onCloseCallbacks;
|
|
71
|
+
constructor(transport: Transport, sharedKey: SharedKey, events?: EncryptedChannelEvents, options?: EncryptedChannelOptions);
|
|
72
|
+
setState(state: ChannelState): void;
|
|
73
|
+
private handleMessage;
|
|
74
|
+
send(data: string | ArrayBuffer): Promise<void>;
|
|
75
|
+
outboundWireByteLength(data: string | ArrayBuffer): number;
|
|
76
|
+
private flushPendingSends;
|
|
77
|
+
private handleDaemonRehello;
|
|
78
|
+
private sendReadyForRetry;
|
|
79
|
+
private rejectKeyRotation;
|
|
80
|
+
close(code?: number, reason?: string): void;
|
|
81
|
+
isOpen(): boolean;
|
|
82
|
+
onTransitionToOpen(cb: () => void): void;
|
|
83
|
+
onClose(cb: () => void): void;
|
|
84
|
+
}
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=encrypted-channel.d.ts.map
|