@moxxy/e2e 0.27.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.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/bytes.d.ts +31 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +140 -0
- package/dist/bytes.js.map +1 -0
- package/dist/channel.d.ts +27 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +89 -0
- package/dist/channel.js.map +1 -0
- package/dist/frame.d.ts +16 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +59 -0
- package/dist/frame.js.map +1 -0
- package/dist/handshake.d.ts +39 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +98 -0
- package/dist/handshake.js.map +1 -0
- package/dist/identity.d.ts +23 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +57 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +10 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +45 -0
- package/dist/node.js.map +1 -0
- package/package.json +62 -0
- package/src/bytes.test.ts +71 -0
- package/src/bytes.ts +146 -0
- package/src/channel.test.ts +154 -0
- package/src/channel.ts +124 -0
- package/src/frame.test.ts +63 -0
- package/src/frame.ts +56 -0
- package/src/handshake.test.ts +56 -0
- package/src/handshake.ts +136 -0
- package/src/identity.test.ts +54 -0
- package/src/identity.ts +73 -0
- package/src/index.ts +46 -0
- package/src/node.test.ts +38 -0
- package/src/node.ts +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @moxxy/e2e
|
|
2
|
+
|
|
3
|
+
End-to-end-encrypted secure channel for the **proxy** tunnel. It lets a phone and
|
|
4
|
+
a locally-running agent talk through an untrusted relay (`<uuid>.proxy.moxxy.ai`)
|
|
5
|
+
such that the relay sees only ciphertext it can neither read nor forge.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
The relay terminates the outer TLS, so on its own it would see plaintext — the
|
|
10
|
+
same position ngrok/cloudflared are in today. This package closes that gap with an
|
|
11
|
+
application-layer handshake **inside** the tunnel, authenticated out-of-band via
|
|
12
|
+
the fingerprint printed in the QR code.
|
|
13
|
+
|
|
14
|
+
## Design
|
|
15
|
+
|
|
16
|
+
- **Identity** — the agent holds a long-lived Ed25519 keypair. Its public key is
|
|
17
|
+
the identity: the QR carries `fp = base64url(pubkey)`, and the subdomain is
|
|
18
|
+
`uuid = base32(sha256(pubkey))`. The phone recomputes the expected uuid from the
|
|
19
|
+
pinned `fp`, so the subdomain is verifiable, not just trusted. No accounts.
|
|
20
|
+
- **Handshake** — signed ephemeral ECDH (station-to-station-lite): the phone sends
|
|
21
|
+
an X25519 ephemeral; the agent replies with its own ephemeral, its identity key,
|
|
22
|
+
and an Ed25519 signature over the transcript. The phone pins the identity key
|
|
23
|
+
(constant-time) and verifies the signature, so a relay without the private key
|
|
24
|
+
cannot impersonate the agent. Ephemerals give forward secrecy.
|
|
25
|
+
- **Framing** — XChaCha20-Poly1305 per message, with a strictly-increasing
|
|
26
|
+
sequence number bound as AAD, so the relay cannot replay, reorder, or tamper.
|
|
27
|
+
Directional keys prevent reflection.
|
|
28
|
+
|
|
29
|
+
The phone is authenticated to the agent by the existing bearer token, which now
|
|
30
|
+
travels encrypted inside this channel.
|
|
31
|
+
|
|
32
|
+
## Entry points
|
|
33
|
+
|
|
34
|
+
- `@moxxy/e2e` — pure JS (`@noble/*`), **no Node built-ins**, bundles under
|
|
35
|
+
Metro/React Native. Identity math, handshake, framing, `SecureChannel`.
|
|
36
|
+
- `@moxxy/e2e/node` — `loadOrCreateIdentity()` (persists the secret key 0600 at
|
|
37
|
+
`~/.moxxy/proxy-identity.key`). Node only.
|
|
38
|
+
|
|
39
|
+
> RN note: `@noble`'s randomness uses WebCrypto `getRandomValues`; an Expo app
|
|
40
|
+
> must import a polyfill (`react-native-get-random-values`) at startup.
|
package/dist/bytes.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, dependency-free byte helpers shared by the handshake/frame/identity
|
|
3
|
+
* modules. Encodings are hand-rolled (not `node:buffer`) so the `.` export stays
|
|
4
|
+
* RN-safe — React Native has no global `Buffer`.
|
|
5
|
+
*/
|
|
6
|
+
/** Concatenate byte arrays into one fresh `Uint8Array`. */
|
|
7
|
+
export declare function concatBytes(...parts: readonly Uint8Array[]): Uint8Array;
|
|
8
|
+
/**
|
|
9
|
+
* Constant-time equality for two byte arrays. Returns false immediately on a
|
|
10
|
+
* length mismatch (lengths here are public — fixed-size keys), then XOR-folds
|
|
11
|
+
* every byte so the compare time doesn't depend on where the first difference
|
|
12
|
+
* is. Used for the pinned-fingerprint check, where a timing leak would let an
|
|
13
|
+
* attacker probe the expected key byte by byte.
|
|
14
|
+
*/
|
|
15
|
+
export declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
16
|
+
/** UTF-8 encode without depending on `Buffer` (TextEncoder is in Node + RN). */
|
|
17
|
+
export declare function utf8(s: string): Uint8Array;
|
|
18
|
+
/** UTF-8 decode (TextDecoder is in Node + modern RN/Hermes). */
|
|
19
|
+
export declare function utf8Decode(b: Uint8Array): string;
|
|
20
|
+
/**
|
|
21
|
+
* Big-endian 8-byte encoding of a non-negative JS number. Written as two 32-bit
|
|
22
|
+
* halves to avoid `BigInt`/`setBigUint64` (Hermes/older RN engines are spotty);
|
|
23
|
+
* safe for any sequence counter below `Number.MAX_SAFE_INTEGER`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function u64be(n: number): Uint8Array;
|
|
26
|
+
/** Decode a big-endian 8-byte counter back to a JS number. */
|
|
27
|
+
export declare function readU64be(b: Uint8Array, off?: number): number;
|
|
28
|
+
export declare function base64urlEncode(bytes: Uint8Array): string;
|
|
29
|
+
export declare function base64urlDecode(s: string): Uint8Array;
|
|
30
|
+
export declare function base32Encode(bytes: Uint8Array): string;
|
|
31
|
+
//# sourceMappingURL=bytes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAUvE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAKvE;AAED,gFAAgF;AAChF,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAE1C;AAED,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAgB3C;AAED,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,SAAI,GAAG,MAAM,CAYxD;AASD,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAYzD;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAmBrD;AAUD,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CActD"}
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, dependency-free byte helpers shared by the handshake/frame/identity
|
|
3
|
+
* modules. Encodings are hand-rolled (not `node:buffer`) so the `.` export stays
|
|
4
|
+
* RN-safe — React Native has no global `Buffer`.
|
|
5
|
+
*/
|
|
6
|
+
/** Concatenate byte arrays into one fresh `Uint8Array`. */
|
|
7
|
+
export function concatBytes(...parts) {
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (const p of parts)
|
|
10
|
+
total += p.length;
|
|
11
|
+
const out = new Uint8Array(total);
|
|
12
|
+
let off = 0;
|
|
13
|
+
for (const p of parts) {
|
|
14
|
+
out.set(p, off);
|
|
15
|
+
off += p.length;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Constant-time equality for two byte arrays. Returns false immediately on a
|
|
21
|
+
* length mismatch (lengths here are public — fixed-size keys), then XOR-folds
|
|
22
|
+
* every byte so the compare time doesn't depend on where the first difference
|
|
23
|
+
* is. Used for the pinned-fingerprint check, where a timing leak would let an
|
|
24
|
+
* attacker probe the expected key byte by byte.
|
|
25
|
+
*/
|
|
26
|
+
export function constantTimeEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length)
|
|
28
|
+
return false;
|
|
29
|
+
let diff = 0;
|
|
30
|
+
for (let i = 0; i < a.length; i++)
|
|
31
|
+
diff |= a[i] ^ b[i];
|
|
32
|
+
return diff === 0;
|
|
33
|
+
}
|
|
34
|
+
/** UTF-8 encode without depending on `Buffer` (TextEncoder is in Node + RN). */
|
|
35
|
+
export function utf8(s) {
|
|
36
|
+
return new TextEncoder().encode(s);
|
|
37
|
+
}
|
|
38
|
+
/** UTF-8 decode (TextDecoder is in Node + modern RN/Hermes). */
|
|
39
|
+
export function utf8Decode(b) {
|
|
40
|
+
return new TextDecoder().decode(b);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Big-endian 8-byte encoding of a non-negative JS number. Written as two 32-bit
|
|
44
|
+
* halves to avoid `BigInt`/`setBigUint64` (Hermes/older RN engines are spotty);
|
|
45
|
+
* safe for any sequence counter below `Number.MAX_SAFE_INTEGER`.
|
|
46
|
+
*/
|
|
47
|
+
export function u64be(n) {
|
|
48
|
+
if (!Number.isInteger(n) || n < 0 || n > Number.MAX_SAFE_INTEGER) {
|
|
49
|
+
throw new RangeError(`u64be: out of range: ${n}`);
|
|
50
|
+
}
|
|
51
|
+
const out = new Uint8Array(8);
|
|
52
|
+
const hi = Math.floor(n / 0x1_0000_0000);
|
|
53
|
+
const lo = n >>> 0;
|
|
54
|
+
out[0] = (hi >>> 24) & 0xff;
|
|
55
|
+
out[1] = (hi >>> 16) & 0xff;
|
|
56
|
+
out[2] = (hi >>> 8) & 0xff;
|
|
57
|
+
out[3] = hi & 0xff;
|
|
58
|
+
out[4] = (lo >>> 24) & 0xff;
|
|
59
|
+
out[5] = (lo >>> 16) & 0xff;
|
|
60
|
+
out[6] = (lo >>> 8) & 0xff;
|
|
61
|
+
out[7] = lo & 0xff;
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Decode a big-endian 8-byte counter back to a JS number. */
|
|
65
|
+
export function readU64be(b, off = 0) {
|
|
66
|
+
const hi = (b[off] << 24) |
|
|
67
|
+
(b[off + 1] << 16) |
|
|
68
|
+
(b[off + 2] << 8) |
|
|
69
|
+
b[off + 3];
|
|
70
|
+
const lo = (b[off + 4] << 24) |
|
|
71
|
+
(b[off + 5] << 16) |
|
|
72
|
+
(b[off + 6] << 8) |
|
|
73
|
+
b[off + 7];
|
|
74
|
+
return (hi >>> 0) * 0x1_0000_0000 + (lo >>> 0);
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// base64url (RFC 4648 §5, no padding) — used to carry the 32-byte Ed25519
|
|
78
|
+
// public-key fingerprint in the QR/connect URL.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
const B64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
81
|
+
export function base64urlEncode(bytes) {
|
|
82
|
+
let out = '';
|
|
83
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
84
|
+
const b0 = bytes[i];
|
|
85
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
86
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
|
|
87
|
+
out += B64URL[b0 >> 2];
|
|
88
|
+
out += B64URL[((b0 & 0x03) << 4) | (b1 >> 4)];
|
|
89
|
+
if (i + 1 < bytes.length)
|
|
90
|
+
out += B64URL[((b1 & 0x0f) << 2) | (b2 >> 6)];
|
|
91
|
+
if (i + 2 < bytes.length)
|
|
92
|
+
out += B64URL[b2 & 0x3f];
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
export function base64urlDecode(s) {
|
|
97
|
+
const lut = new Int16Array(128).fill(-1);
|
|
98
|
+
for (let i = 0; i < B64URL.length; i++)
|
|
99
|
+
lut[B64URL.charCodeAt(i)] = i;
|
|
100
|
+
const clean = s.trim();
|
|
101
|
+
const out = [];
|
|
102
|
+
let buf = 0;
|
|
103
|
+
let bits = 0;
|
|
104
|
+
for (const ch of clean) {
|
|
105
|
+
const code = ch.charCodeAt(0);
|
|
106
|
+
const v = code < 128 ? lut[code] : -1;
|
|
107
|
+
if (v === undefined || v < 0)
|
|
108
|
+
throw new Error('base64urlDecode: invalid character');
|
|
109
|
+
buf = (buf << 6) | v;
|
|
110
|
+
bits += 6;
|
|
111
|
+
if (bits >= 8) {
|
|
112
|
+
bits -= 8;
|
|
113
|
+
out.push((buf >> bits) & 0xff);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return Uint8Array.from(out);
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// base32 (RFC 4648 §6, lowercase, no padding) — used for the uuid subdomain
|
|
120
|
+
// label. Lowercase a-z + 2-7 is a valid DNS label, and the alphabet avoids the
|
|
121
|
+
// visually ambiguous 0/1/8/9.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
const B32 = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
124
|
+
export function base32Encode(bytes) {
|
|
125
|
+
let out = '';
|
|
126
|
+
let buf = 0;
|
|
127
|
+
let bits = 0;
|
|
128
|
+
for (const byte of bytes) {
|
|
129
|
+
buf = (buf << 8) | byte;
|
|
130
|
+
bits += 8;
|
|
131
|
+
while (bits >= 5) {
|
|
132
|
+
bits -= 5;
|
|
133
|
+
out += B32[(buf >> bits) & 0x1f];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (bits > 0)
|
|
137
|
+
out += B32[(buf << (5 - bits)) & 0x1f];
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=bytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Identity } from './identity.js';
|
|
2
|
+
/** A minimal duplex transport of discrete, ordered binary messages. */
|
|
3
|
+
export interface MessageTransport {
|
|
4
|
+
/** Send one binary message. */
|
|
5
|
+
send(data: Uint8Array): void;
|
|
6
|
+
/** Register the (single) handler for inbound binary messages. */
|
|
7
|
+
onMessage(handler: (data: Uint8Array) => void): void;
|
|
8
|
+
/** Register the (single) handler for transport close. */
|
|
9
|
+
onClose(handler: () => void): void;
|
|
10
|
+
/** Close the underlying transport. */
|
|
11
|
+
close(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface SecureChannel {
|
|
14
|
+
/** Encrypt and send one plaintext message. */
|
|
15
|
+
send(plaintext: Uint8Array): void;
|
|
16
|
+
/** Register the handler for decrypted inbound messages. Buffered until set. */
|
|
17
|
+
onMessage(handler: (plaintext: Uint8Array) => void): void;
|
|
18
|
+
/** Register a close handler (fires on transport close or fatal frame error). */
|
|
19
|
+
onClose(handler: () => void): void;
|
|
20
|
+
/** Tear down the channel and the underlying transport. */
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
|
23
|
+
/** Phone side: pin the agent's public key (from the QR `fp`) and connect. */
|
|
24
|
+
export declare function connectInitiator(transport: MessageTransport, pinnedPublicKey: Uint8Array): Promise<SecureChannel>;
|
|
25
|
+
/** Agent side: respond with a signed ServerHello and connect. */
|
|
26
|
+
export declare function connectResponder(transport: MessageTransport, identity: Identity): Promise<SecureChannel>;
|
|
27
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,+BAA+B;IAC/B,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAC7B,iEAAiE;IACjE,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACrD,yDAAyD;IACzD,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACnC,sCAAsC;IACtC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,IAAI,CAAC;IAClC,+EAA+E;IAC/E,SAAS,CAAC,OAAO,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IAC1D,gFAAgF;IAChF,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACnC,0DAA0D;IAC1D,KAAK,IAAI,IAAI,CAAC;CACf;AA4DD,6EAA6E;AAC7E,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,gBAAgB,EAC3B,eAAe,EAAE,UAAU,GAC1B,OAAO,CAAC,aAAa,CAAC,CAMxB;AAED,iEAAiE;AACjE,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,gBAAgB,EAC3B,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,aAAa,CAAC,CAKxB"}
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SecureChannel` ties the handshake + framing to a concrete message transport
|
|
3
|
+
* (a WebSocket, or anything that delivers discrete binary messages in order).
|
|
4
|
+
*
|
|
5
|
+
* It runs the two-message handshake first, then transparently seals everything
|
|
6
|
+
* the caller `send()`s and opens everything that arrives, emitting plaintext via
|
|
7
|
+
* `onMessage`. Both ends use the same channel object; only the constructor
|
|
8
|
+
* (`connectInitiator` for the phone, `connectResponder` for the agent shim)
|
|
9
|
+
* differs. The returned promise resolves once the channel is encrypted and
|
|
10
|
+
* ready — any handshake failure (length, pin mismatch, bad signature, early
|
|
11
|
+
* close) rejects it, and the caller must abort the connection.
|
|
12
|
+
*/
|
|
13
|
+
import { FrameOpener, FrameSealer } from './frame.js';
|
|
14
|
+
import { finishInitiator, respond, startInitiator, } from './handshake.js';
|
|
15
|
+
function makeChannel(transport, keys) {
|
|
16
|
+
const sealer = new FrameSealer(keys.sendKey);
|
|
17
|
+
const opener = new FrameOpener(keys.recvKey);
|
|
18
|
+
let onMsg = null;
|
|
19
|
+
let onClose = null;
|
|
20
|
+
const backlog = [];
|
|
21
|
+
let closed = false;
|
|
22
|
+
const close = () => {
|
|
23
|
+
if (closed)
|
|
24
|
+
return;
|
|
25
|
+
closed = true;
|
|
26
|
+
transport.close();
|
|
27
|
+
onClose?.();
|
|
28
|
+
};
|
|
29
|
+
transport.onMessage((data) => {
|
|
30
|
+
let pt;
|
|
31
|
+
try {
|
|
32
|
+
pt = opener.open(data);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A frame that fails to open is a tampering/replay attempt on the wire —
|
|
36
|
+
// tear the whole channel down rather than skipping the message.
|
|
37
|
+
close();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (onMsg)
|
|
41
|
+
onMsg(pt);
|
|
42
|
+
else
|
|
43
|
+
backlog.push(pt);
|
|
44
|
+
});
|
|
45
|
+
transport.onClose(() => {
|
|
46
|
+
if (closed)
|
|
47
|
+
return;
|
|
48
|
+
closed = true;
|
|
49
|
+
onClose?.();
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
send: (plaintext) => transport.send(sealer.seal(plaintext)),
|
|
53
|
+
onMessage: (handler) => {
|
|
54
|
+
onMsg = handler;
|
|
55
|
+
while (backlog.length > 0)
|
|
56
|
+
handler(backlog.shift());
|
|
57
|
+
},
|
|
58
|
+
onClose: (handler) => {
|
|
59
|
+
onClose = handler;
|
|
60
|
+
},
|
|
61
|
+
close,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Wait for exactly the next inbound transport message (the handshake reply),
|
|
66
|
+
* rejecting if the transport closes first. Used only during the handshake.
|
|
67
|
+
*/
|
|
68
|
+
function nextMessage(transport) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
transport.onMessage((data) => resolve(data));
|
|
71
|
+
transport.onClose(() => reject(new Error('proxy-e2e: transport closed during handshake')));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Phone side: pin the agent's public key (from the QR `fp`) and connect. */
|
|
75
|
+
export async function connectInitiator(transport, pinnedPublicKey) {
|
|
76
|
+
const { clientHello, state } = startInitiator();
|
|
77
|
+
const reply = nextMessage(transport);
|
|
78
|
+
transport.send(clientHello);
|
|
79
|
+
const keys = finishInitiator(await reply, state, pinnedPublicKey);
|
|
80
|
+
return makeChannel(transport, keys);
|
|
81
|
+
}
|
|
82
|
+
/** Agent side: respond with a signed ServerHello and connect. */
|
|
83
|
+
export async function connectResponder(transport, identity) {
|
|
84
|
+
const clientHello = await nextMessage(transport);
|
|
85
|
+
const { serverHello, keys } = respond(clientHello, identity);
|
|
86
|
+
transport.send(serverHello);
|
|
87
|
+
return makeChannel(transport, keys);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Seals outgoing plaintext into authenticated frames with a monotonic counter. */
|
|
2
|
+
export declare class FrameSealer {
|
|
3
|
+
private readonly key;
|
|
4
|
+
private seq;
|
|
5
|
+
constructor(key: Uint8Array);
|
|
6
|
+
seal(plaintext: Uint8Array): Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
/** Opens incoming frames, rejecting any whose sequence is not strictly increasing. */
|
|
9
|
+
export declare class FrameOpener {
|
|
10
|
+
private readonly key;
|
|
11
|
+
/** Highest sequence accepted so far; -1 means "nothing yet". */
|
|
12
|
+
private lastSeq;
|
|
13
|
+
constructor(key: Uint8Array);
|
|
14
|
+
open(frame: Uint8Array): Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=frame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../src/frame.ts"],"names":[],"mappings":"AAqBA,mFAAmF;AACnF,qBAAa,WAAW;IAEV,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,OAAO,CAAC,GAAG,CAAK;gBACa,GAAG,EAAE,UAAU;IAE5C,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU;CAOxC;AAED,sFAAsF;AACtF,qBAAa,WAAW;IAGV,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,gEAAgE;IAChE,OAAO,CAAC,OAAO,CAAM;gBACQ,GAAG,EAAE,UAAU;IAE5C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU;CAcpC"}
|
package/dist/frame.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-message AEAD framing for the post-handshake channel.
|
|
3
|
+
*
|
|
4
|
+
* Each frame: seq(8 BE) ‖ nonce(24) ‖ XChaCha20-Poly1305(key, nonce, aad=seq).
|
|
5
|
+
*
|
|
6
|
+
* The sequence number is bound as AAD and enforced strictly increasing by the
|
|
7
|
+
* receiver, so the relay (the adversary on the wire) cannot replay, reorder, or
|
|
8
|
+
* drop-and-resend a frame without the open failing. XChaCha's 192-bit random
|
|
9
|
+
* nonce means we never have to synchronise a nonce counter across the wire.
|
|
10
|
+
*
|
|
11
|
+
* A sealer/opener pair is one-directional; a SecureChannel keeps one of each,
|
|
12
|
+
* keyed by the directional keys from the handshake.
|
|
13
|
+
*/
|
|
14
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
15
|
+
import { randomBytes } from '@noble/hashes/utils.js';
|
|
16
|
+
import { concatBytes, readU64be, u64be } from './bytes.js';
|
|
17
|
+
const SEQ = 8;
|
|
18
|
+
const NONCE = 24;
|
|
19
|
+
const HEADER = SEQ + NONCE; // 32
|
|
20
|
+
/** Seals outgoing plaintext into authenticated frames with a monotonic counter. */
|
|
21
|
+
export class FrameSealer {
|
|
22
|
+
key;
|
|
23
|
+
seq = 0;
|
|
24
|
+
constructor(key) {
|
|
25
|
+
this.key = key;
|
|
26
|
+
}
|
|
27
|
+
seal(plaintext) {
|
|
28
|
+
const seqBytes = u64be(this.seq);
|
|
29
|
+
const nonce = randomBytes(NONCE);
|
|
30
|
+
const ct = xchacha20poly1305(this.key, nonce, seqBytes).encrypt(plaintext);
|
|
31
|
+
this.seq += 1;
|
|
32
|
+
return concatBytes(seqBytes, nonce, ct);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Opens incoming frames, rejecting any whose sequence is not strictly increasing. */
|
|
36
|
+
export class FrameOpener {
|
|
37
|
+
key;
|
|
38
|
+
/** Highest sequence accepted so far; -1 means "nothing yet". */
|
|
39
|
+
lastSeq = -1;
|
|
40
|
+
constructor(key) {
|
|
41
|
+
this.key = key;
|
|
42
|
+
}
|
|
43
|
+
open(frame) {
|
|
44
|
+
if (frame.length < HEADER)
|
|
45
|
+
throw new Error('proxy-e2e: frame too short');
|
|
46
|
+
const seqBytes = frame.slice(0, SEQ);
|
|
47
|
+
const seq = readU64be(seqBytes);
|
|
48
|
+
if (seq <= this.lastSeq) {
|
|
49
|
+
throw new Error(`proxy-e2e: out-of-order/replayed frame (seq ${seq} <= ${this.lastSeq})`);
|
|
50
|
+
}
|
|
51
|
+
const nonce = frame.slice(SEQ, HEADER);
|
|
52
|
+
const ct = frame.slice(HEADER);
|
|
53
|
+
// Throws if the tag (over ciphertext + seq AAD) doesn't verify.
|
|
54
|
+
const pt = xchacha20poly1305(this.key, nonce, seqBytes).decrypt(ct);
|
|
55
|
+
this.lastSeq = seq;
|
|
56
|
+
return pt;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=frame.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame.js","sourceRoot":"","sources":["../src/frame.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,GAAG,GAAG,CAAC,CAAC;AACd,MAAM,KAAK,GAAG,EAAE,CAAC;AACjB,MAAM,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,KAAK;AAEjC,mFAAmF;AACnF,MAAM,OAAO,WAAW;IAEO;IADrB,GAAG,GAAG,CAAC,CAAC;IAChB,YAA6B,GAAe;QAAf,QAAG,GAAH,GAAG,CAAY;IAAG,CAAC;IAEhD,IAAI,CAAC,SAAqB;QACxB,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC3E,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACd,OAAO,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,CAAC;CACF;AAED,sFAAsF;AACtF,MAAM,OAAO,WAAW;IAGO;IAF7B,gEAAgE;IACxD,OAAO,GAAG,CAAC,CAAC,CAAC;IACrB,YAA6B,GAAe;QAAf,QAAG,GAAH,GAAG,CAAY;IAAG,CAAC;IAEhD,IAAI,CAAC,KAAiB;QACpB,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QACzE,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACrC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,+CAA+C,GAAG,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACvC,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/B,gEAAgE;QAChE,MAAM,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC;QACnB,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const CLIENT_HELLO_LEN: number;
|
|
2
|
+
export declare const SERVER_HELLO_LEN: number;
|
|
3
|
+
/** Directional session keys, named from the owner's point of view. */
|
|
4
|
+
export interface SessionKeys {
|
|
5
|
+
/** Key for frames this side SENDS. */
|
|
6
|
+
readonly sendKey: Uint8Array;
|
|
7
|
+
/** Key for frames this side RECEIVES. */
|
|
8
|
+
readonly recvKey: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
/** Opaque initiator state carried between {@link startInitiator} and {@link finishInitiator}. */
|
|
11
|
+
export interface InitiatorState {
|
|
12
|
+
readonly ephSecret: Uint8Array;
|
|
13
|
+
readonly ephPublic: Uint8Array;
|
|
14
|
+
readonly clientNonce: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** Initiator (phone) step 1: produce the ClientHello and the state to finish later. */
|
|
17
|
+
export declare function startInitiator(): {
|
|
18
|
+
clientHello: Uint8Array;
|
|
19
|
+
state: InitiatorState;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Responder (agent) step: consume the ClientHello, produce the signed
|
|
23
|
+
* ServerHello, and derive the session keys. Throws on a malformed ClientHello.
|
|
24
|
+
*/
|
|
25
|
+
export declare function respond(clientHello: Uint8Array, identity: {
|
|
26
|
+
secretKey: Uint8Array;
|
|
27
|
+
publicKey: Uint8Array;
|
|
28
|
+
}): {
|
|
29
|
+
serverHello: Uint8Array;
|
|
30
|
+
keys: SessionKeys;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Initiator (phone) step 2: consume the ServerHello, verify the agent's pinned
|
|
34
|
+
* identity + signature, and derive the session keys. Throws on length mismatch,
|
|
35
|
+
* a fingerprint that doesn't match the pin, or an invalid signature — every
|
|
36
|
+
* failure means "this is not the agent the QR named", so the caller must abort.
|
|
37
|
+
*/
|
|
38
|
+
export declare function finishInitiator(serverHello: Uint8Array, state: InitiatorState, pinnedPublicKey: Uint8Array): SessionKeys;
|
|
39
|
+
//# sourceMappingURL=handshake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAmCA,eAAO,MAAM,gBAAgB,QAAmB,CAAC;AACjD,eAAO,MAAM,gBAAgB,QAAkC,CAAC;AAEhE,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAED,iGAAiG;AACjG,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC;CAClC;AAgBD,uFAAuF;AACvF,wBAAgB,cAAc,IAAI;IAAE,WAAW,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,CAOnF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACrB,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE;IAAE,SAAS,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,UAAU,CAAA;CAAE,GACzD;IAAE,WAAW,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAkBhD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,UAAU,EACvB,KAAK,EAAE,cAAc,EACrB,eAAe,EAAE,UAAU,GAC1B,WAAW,CAqBb"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proxy E2E handshake: an authenticated, forward-secret key agreement run
|
|
3
|
+
* as the first two messages inside the tunnel, before any session traffic.
|
|
4
|
+
*
|
|
5
|
+
* Shape (station-to-station-lite / signed ephemeral ECDH):
|
|
6
|
+
* initiator (phone) ──ClientHello──▶ responder (agent)
|
|
7
|
+
* ClientHello = ephI_pub(32) ‖ nC(16)
|
|
8
|
+
* responder ──ServerHello──▶ initiator
|
|
9
|
+
* ServerHello = ephR_pub(32) ‖ nS(16) ‖ idPub(32) ‖ sig(64)
|
|
10
|
+
* sig = Ed25519.sign(transcript, idSecret)
|
|
11
|
+
* transcript = LABEL ‖ ephI_pub ‖ ephR_pub ‖ nC ‖ nS
|
|
12
|
+
*
|
|
13
|
+
* The initiator pins `idPub` against the QR fingerprint (constant-time) and
|
|
14
|
+
* verifies `sig` over the transcript — so a relay (or anyone without the private
|
|
15
|
+
* key) cannot impersonate the agent. Both sides derive the session keys from the
|
|
16
|
+
* X25519 shared secret via HKDF; the ephemeral keys give forward secrecy. Keys
|
|
17
|
+
* are split per direction to prevent reflection.
|
|
18
|
+
*
|
|
19
|
+
* The phone is NOT authenticated by key here — it authenticates with the bearer
|
|
20
|
+
* token at the app layer (unchanged), which now travels encrypted inside this
|
|
21
|
+
* channel.
|
|
22
|
+
*/
|
|
23
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
24
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
25
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
26
|
+
import { randomBytes } from '@noble/hashes/utils.js';
|
|
27
|
+
import { concatBytes, constantTimeEqual, utf8 } from './bytes.js';
|
|
28
|
+
import { sign, verify } from './identity.js';
|
|
29
|
+
const LABEL = utf8('moxxy/proxy-e2e/v1');
|
|
30
|
+
const HS_NONCE = 16;
|
|
31
|
+
const X_PUB = 32;
|
|
32
|
+
const ID_PUB = 32;
|
|
33
|
+
const SIG = 64;
|
|
34
|
+
export const CLIENT_HELLO_LEN = X_PUB + HS_NONCE; // 48
|
|
35
|
+
export const SERVER_HELLO_LEN = X_PUB + HS_NONCE + ID_PUB + SIG; // 144
|
|
36
|
+
function transcript(ephIPub, ephRPub, nC, nS) {
|
|
37
|
+
return concatBytes(LABEL, ephIPub, ephRPub, nC, nS);
|
|
38
|
+
}
|
|
39
|
+
function deriveKeys(shared, nC, nS) {
|
|
40
|
+
const okm = hkdf(sha256, shared, concatBytes(nC, nS), LABEL, 64);
|
|
41
|
+
return { i2r: okm.slice(0, 32), r2i: okm.slice(32, 64) };
|
|
42
|
+
}
|
|
43
|
+
/** Initiator (phone) step 1: produce the ClientHello and the state to finish later. */
|
|
44
|
+
export function startInitiator() {
|
|
45
|
+
const { secretKey: ephSecret, publicKey: ephPublic } = x25519.keygen();
|
|
46
|
+
const clientNonce = randomBytes(HS_NONCE);
|
|
47
|
+
return {
|
|
48
|
+
clientHello: concatBytes(ephPublic, clientNonce),
|
|
49
|
+
state: { ephSecret, ephPublic, clientNonce },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Responder (agent) step: consume the ClientHello, produce the signed
|
|
54
|
+
* ServerHello, and derive the session keys. Throws on a malformed ClientHello.
|
|
55
|
+
*/
|
|
56
|
+
export function respond(clientHello, identity) {
|
|
57
|
+
if (clientHello.length !== CLIENT_HELLO_LEN) {
|
|
58
|
+
throw new Error(`proxy-e2e: bad ClientHello length ${clientHello.length}`);
|
|
59
|
+
}
|
|
60
|
+
const ephIPub = clientHello.slice(0, X_PUB);
|
|
61
|
+
const nC = clientHello.slice(X_PUB, X_PUB + HS_NONCE);
|
|
62
|
+
const { secretKey: ephSecret, publicKey: ephRPub } = x25519.keygen();
|
|
63
|
+
const nS = randomBytes(HS_NONCE);
|
|
64
|
+
const sig = sign(transcript(ephIPub, ephRPub, nC, nS), identity.secretKey);
|
|
65
|
+
const shared = x25519.getSharedSecret(ephSecret, ephIPub);
|
|
66
|
+
const { i2r, r2i } = deriveKeys(shared, nC, nS);
|
|
67
|
+
return {
|
|
68
|
+
serverHello: concatBytes(ephRPub, nS, identity.publicKey, sig),
|
|
69
|
+
keys: { sendKey: r2i, recvKey: i2r },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Initiator (phone) step 2: consume the ServerHello, verify the agent's pinned
|
|
74
|
+
* identity + signature, and derive the session keys. Throws on length mismatch,
|
|
75
|
+
* a fingerprint that doesn't match the pin, or an invalid signature — every
|
|
76
|
+
* failure means "this is not the agent the QR named", so the caller must abort.
|
|
77
|
+
*/
|
|
78
|
+
export function finishInitiator(serverHello, state, pinnedPublicKey) {
|
|
79
|
+
if (serverHello.length !== SERVER_HELLO_LEN) {
|
|
80
|
+
throw new Error(`proxy-e2e: bad ServerHello length ${serverHello.length}`);
|
|
81
|
+
}
|
|
82
|
+
let off = 0;
|
|
83
|
+
const ephRPub = serverHello.slice(off, (off += X_PUB));
|
|
84
|
+
const nS = serverHello.slice(off, (off += HS_NONCE));
|
|
85
|
+
const idPub = serverHello.slice(off, (off += ID_PUB));
|
|
86
|
+
const sig = serverHello.slice(off, (off += SIG));
|
|
87
|
+
if (!constantTimeEqual(idPub, pinnedPublicKey)) {
|
|
88
|
+
throw new Error('proxy-e2e: identity key does not match pinned fingerprint (possible spoofing)');
|
|
89
|
+
}
|
|
90
|
+
const t = transcript(state.ephPublic, ephRPub, state.clientNonce, nS);
|
|
91
|
+
if (!verify(sig, t, idPub)) {
|
|
92
|
+
throw new Error('proxy-e2e: handshake signature invalid (possible spoofing)');
|
|
93
|
+
}
|
|
94
|
+
const shared = x25519.getSharedSecret(state.ephSecret, ephRPub);
|
|
95
|
+
const { i2r, r2i } = deriveKeys(shared, state.clientNonce, nS);
|
|
96
|
+
return { sendKey: i2r, recvKey: r2i };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Number of base32 chars in the uuid label (16 × 5 = 80 bits of preimage strength). */
|
|
2
|
+
export declare const UUID_LABEL_LENGTH = 16;
|
|
3
|
+
export interface Identity {
|
|
4
|
+
/** Ed25519 secret key (32 bytes). Never leaves the agent. */
|
|
5
|
+
readonly secretKey: Uint8Array;
|
|
6
|
+
/** Ed25519 public key (32 bytes). The identity. */
|
|
7
|
+
readonly publicKey: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
/** Generate a fresh agent identity. */
|
|
10
|
+
export declare function generateIdentity(): Identity;
|
|
11
|
+
/** Recover the public key from a stored secret key. */
|
|
12
|
+
export declare function publicKeyFromSecret(secretKey: Uint8Array): Uint8Array;
|
|
13
|
+
/** The QR-carried fingerprint: base64url of the raw 32-byte public key. */
|
|
14
|
+
export declare function fingerprint(publicKey: Uint8Array): string;
|
|
15
|
+
/** Parse a fingerprint string back to the 32-byte public key (throws if malformed). */
|
|
16
|
+
export declare function publicKeyFromFingerprint(fp: string): Uint8Array;
|
|
17
|
+
/** Derive the stable uuid subdomain label from a public key. */
|
|
18
|
+
export declare function deriveUuid(publicKey: Uint8Array): string;
|
|
19
|
+
/** Sign a message with the identity's secret key (Ed25519). */
|
|
20
|
+
export declare function sign(message: Uint8Array, secretKey: Uint8Array): Uint8Array;
|
|
21
|
+
/** Verify an Ed25519 signature against a public key. */
|
|
22
|
+
export declare function verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean;
|
|
23
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAkBA,wFAAwF;AACxF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,MAAM,WAAW,QAAQ;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC;AAED,uCAAuC;AACvC,wBAAgB,gBAAgB,IAAI,QAAQ,CAG3C;AAED,uDAAuD;AACvD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAErE;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAEzD;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAI/D;AAED,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAExD;AAED,+DAA+D;AAC/D,wBAAgB,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,UAAU,CAE3E;AAED,wDAAwD;AACxD,wBAAgB,MAAM,CACpB,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,UAAU,GACpB,OAAO,CAMT"}
|