waterplane 0.1.16 → 0.1.18
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/CHANGELOG.md +28 -0
- package/NOTICES.md +18 -7
- package/README.md +43 -0
- package/SECURITY.md +196 -12
- package/node_modules/@waterplane/shared/dist/desktopfiles.d.ts +24 -2
- package/node_modules/@waterplane/shared/dist/desktopfiles.js +73 -3
- package/node_modules/@waterplane/shared/dist/desktopfiles.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/hosts.d.ts +31 -0
- package/node_modules/@waterplane/shared/dist/hosts.js +46 -1
- package/node_modules/@waterplane/shared/dist/hosts.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/hosturl.d.ts +55 -0
- package/node_modules/@waterplane/shared/dist/hosturl.js +85 -0
- package/node_modules/@waterplane/shared/dist/hosturl.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/index.d.ts +4 -0
- package/node_modules/@waterplane/shared/dist/index.js +4 -0
- package/node_modules/@waterplane/shared/dist/index.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/installrow.d.ts +6 -1
- package/node_modules/@waterplane/shared/dist/installrow.js +20 -1
- package/node_modules/@waterplane/shared/dist/installrow.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/lanroute.d.ts +5 -0
- package/node_modules/@waterplane/shared/dist/lanroute.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/pairing.d.ts +96 -2
- package/node_modules/@waterplane/shared/dist/pairing.js +71 -3
- package/node_modules/@waterplane/shared/dist/pairing.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/pake.d.ts +131 -0
- package/node_modules/@waterplane/shared/dist/pake.js +272 -0
- package/node_modules/@waterplane/shared/dist/pake.js.map +1 -0
- package/node_modules/@waterplane/shared/dist/peer.d.ts +63 -0
- package/node_modules/@waterplane/shared/dist/peer.js +41 -0
- package/node_modules/@waterplane/shared/dist/peer.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/plugin/api.d.ts +15 -0
- package/node_modules/@waterplane/shared/dist/plugin/version.d.ts +5 -1
- package/node_modules/@waterplane/shared/dist/plugin/version.js +5 -1
- package/node_modules/@waterplane/shared/dist/plugin/version.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.d.ts +7 -2
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js +6 -3
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/waiting.d.ts +64 -0
- package/node_modules/@waterplane/shared/dist/waiting.js +86 -0
- package/node_modules/@waterplane/shared/dist/waiting.js.map +1 -0
- package/node_modules/@waterplane/shared/dist/wire/messages/hostcore.d.ts +3 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.d.ts +2 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.js +2 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/wire/repo.d.ts +25 -1
- package/node_modules/@waterplane/shared/dist/wire/repo.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/wire/sysstat.d.ts +7 -0
- package/node_modules/@waterplane/shared/dist/wire/telemetry.d.ts +3 -0
- package/node_modules/@waterplane/shared/package.json +3 -0
- package/npm-shrinkwrap.json +30 -2
- package/package.json +2 -1
- package/packages/extensions/desktop/helper/Capture.swift +245 -0
- package/packages/extensions/desktop/helper/H264.swift +226 -0
- package/packages/extensions/desktop/helper/Info.plist +20 -0
- package/packages/extensions/desktop/helper/Input.swift +195 -0
- package/packages/extensions/desktop/helper/Jpeg.swift +81 -0
- package/packages/extensions/desktop/helper/PROTOCOL.md +366 -0
- package/packages/extensions/desktop/helper/Params.swift +133 -0
- package/packages/extensions/desktop/helper/Probe.swift +119 -0
- package/packages/extensions/desktop/helper/Protocol.swift +224 -0
- package/packages/extensions/desktop/helper/Request.swift +51 -0
- package/packages/extensions/desktop/helper/Responsibility.swift +133 -0
- package/packages/extensions/desktop/helper/Server.swift +334 -0
- package/packages/extensions/desktop/helper/Synthetic.swift +281 -0
- package/packages/extensions/desktop/helper/build.sh +53 -0
- package/packages/extensions/desktop/helper/main.swift +79 -0
- package/packages/extensions/desktop/helper/selftest-driver +626 -0
- package/packages/extensions/desktop/helper/selftest.sh +16 -0
- package/packages/extensions/desktop/helper-win/Capture.cs +299 -0
- package/packages/extensions/desktop/helper-win/H264.cs +340 -0
- package/packages/extensions/desktop/helper-win/Input.cs +257 -0
- package/packages/extensions/desktop/helper-win/Jpeg.cs +75 -0
- package/packages/extensions/desktop/helper-win/Main.cs +118 -0
- package/packages/extensions/desktop/helper-win/Native.cs +276 -0
- package/packages/extensions/desktop/helper-win/Params.cs +138 -0
- package/packages/extensions/desktop/helper-win/Protocol.cs +206 -0
- package/packages/extensions/desktop/helper-win/Server.cs +304 -0
- package/packages/extensions/desktop/helper-win/Synthetic.cs +88 -0
- package/packages/extensions/desktop/helper-win/build.sh +57 -0
- package/packages/extensions/desktop/helper-win/selftest.sh +36 -0
- package/packages/extensions/desktop/waterplane-plugin.json +24 -0
- package/packages/host/dist/alwayson.js +1 -1
- package/packages/host/dist/alwayson.js.map +1 -1
- package/packages/host/dist/attentionstore.d.ts +4 -0
- package/packages/host/dist/attentionstore.js +17 -0
- package/packages/host/dist/attentionstore.js.map +1 -1
- package/packages/host/dist/auth.d.ts +28 -0
- package/packages/host/dist/auth.js +65 -2
- package/packages/host/dist/auth.js.map +1 -1
- package/packages/host/dist/cli/pair.d.ts +4 -0
- package/packages/host/dist/cli/pair.js +18 -5
- package/packages/host/dist/cli/pair.js.map +1 -1
- package/packages/host/dist/der.d.ts +19 -0
- package/packages/host/dist/der.js +51 -0
- package/packages/host/dist/der.js.map +1 -0
- package/packages/host/dist/doctor.d.ts +2 -0
- package/packages/host/dist/doctor.js +10 -2
- package/packages/host/dist/doctor.js.map +1 -1
- package/packages/host/dist/extensions/desktop/host/auto.d.mts +71 -0
- package/packages/host/dist/extensions/desktop/host/auto.mjs +114 -0
- package/packages/host/dist/extensions/desktop/host/auto.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/desktop.d.mts +51 -0
- package/packages/host/dist/extensions/desktop/host/desktop.mjs +113 -0
- package/packages/host/dist/extensions/desktop/host/desktop.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.d.mts +71 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.mjs +160 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/index.d.mts +4 -0
- package/packages/host/dist/extensions/desktop/host/index.mjs +88 -0
- package/packages/host/dist/extensions/desktop/host/index.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/knobs.d.mts +52 -0
- package/packages/host/dist/extensions/desktop/host/knobs.mjs +81 -0
- package/packages/host/dist/extensions/desktop/host/knobs.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/probe.d.mts +57 -0
- package/packages/host/dist/extensions/desktop/host/probe.mjs +119 -0
- package/packages/host/dist/extensions/desktop/host/probe.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/records.d.mts +40 -0
- package/packages/host/dist/extensions/desktop/host/records.mjs +101 -0
- package/packages/host/dist/extensions/desktop/host/records.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/session.d.mts +86 -0
- package/packages/host/dist/extensions/desktop/host/session.mjs +318 -0
- package/packages/host/dist/extensions/desktop/host/session.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/setup.d.mts +28 -0
- package/packages/host/dist/extensions/desktop/host/setup.mjs +217 -0
- package/packages/host/dist/extensions/desktop/host/setup.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/status.d.mts +27 -0
- package/packages/host/dist/extensions/desktop/host/status.mjs +62 -0
- package/packages/host/dist/extensions/desktop/host/status.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/shared/wire.d.mts +206 -0
- package/packages/host/dist/extensions/desktop/shared/wire.mjs +174 -0
- package/packages/host/dist/extensions/desktop/shared/wire.mjs.map +1 -0
- package/packages/host/dist/git.d.ts +23 -6
- package/packages/host/dist/git.js +54 -26
- package/packages/host/dist/git.js.map +1 -1
- package/packages/host/dist/gitfetchfail.d.ts +24 -0
- package/packages/host/dist/gitfetchfail.js +62 -0
- package/packages/host/dist/gitfetchfail.js.map +1 -0
- package/packages/host/dist/gitlog.d.ts +4 -0
- package/packages/host/dist/gitlog.js +8 -3
- package/packages/host/dist/gitlog.js.map +1 -1
- package/packages/host/dist/gitnotify.d.ts +7 -1
- package/packages/host/dist/gitnotify.js +61 -1
- package/packages/host/dist/gitnotify.js.map +1 -1
- package/packages/host/dist/host.js +5 -1
- package/packages/host/dist/host.js.map +1 -1
- package/packages/host/dist/hoststore.js +4 -4
- package/packages/host/dist/hoststore.js.map +1 -1
- package/packages/host/dist/lantls.d.ts +145 -0
- package/packages/host/dist/lantls.js +337 -0
- package/packages/host/dist/lantls.js.map +1 -0
- package/packages/host/dist/main.js +7 -2
- package/packages/host/dist/main.js.map +1 -1
- package/packages/host/dist/net.d.ts +7 -0
- package/packages/host/dist/net.js +9 -0
- package/packages/host/dist/net.js.map +1 -1
- package/packages/host/dist/origin.d.ts +4 -0
- package/packages/host/dist/origin.js +6 -0
- package/packages/host/dist/origin.js.map +1 -1
- package/packages/host/dist/pair.d.ts +54 -5
- package/packages/host/dist/pair.js +139 -34
- package/packages/host/dist/pair.js.map +1 -1
- package/packages/host/dist/pairpake.d.ts +58 -0
- package/packages/host/dist/pairpake.js +208 -0
- package/packages/host/dist/pairpake.js.map +1 -0
- package/packages/host/dist/passive.d.ts +2 -0
- package/packages/host/dist/passive.js +4 -0
- package/packages/host/dist/passive.js.map +1 -1
- package/packages/host/dist/peers/client.d.ts +3 -1
- package/packages/host/dist/peers/client.js +4 -0
- package/packages/host/dist/peers/client.js.map +1 -1
- package/packages/host/dist/peers/routes.d.ts +6 -1
- package/packages/host/dist/peers/routes.js +39 -0
- package/packages/host/dist/peers/routes.js.map +1 -1
- package/packages/host/dist/plugins/pluginapi.js +13 -0
- package/packages/host/dist/plugins/pluginapi.js.map +1 -1
- package/packages/host/dist/plugins/pluginregistry.d.ts +6 -0
- package/packages/host/dist/plugins/pluginregistry.js.map +1 -1
- package/packages/host/dist/prepushcheck.d.ts +34 -0
- package/packages/host/dist/prepushcheck.js +157 -0
- package/packages/host/dist/prepushcheck.js.map +1 -0
- package/packages/host/dist/prs.js +2 -1
- package/packages/host/dist/prs.js.map +1 -1
- package/packages/host/dist/routes/apigitcheck.d.ts +2 -0
- package/packages/host/dist/routes/apigitcheck.js +29 -0
- package/packages/host/dist/routes/apigitcheck.js.map +1 -0
- package/packages/host/dist/routes/apihost.js +2 -0
- package/packages/host/dist/routes/apihost.js.map +1 -1
- package/packages/host/dist/routes/apiio.js +2 -1
- package/packages/host/dist/routes/apiio.js.map +1 -1
- package/packages/host/dist/routes/apirepofiles.js +2 -1
- package/packages/host/dist/routes/apirepofiles.js.map +1 -1
- package/packages/host/dist/routes/apisystem.js +21 -3
- package/packages/host/dist/routes/apisystem.js.map +1 -1
- package/packages/host/dist/server/http.js +31 -5
- package/packages/host/dist/server/http.js.map +1 -1
- package/packages/host/dist/server/plugins.d.ts +13 -0
- package/packages/host/dist/server/plugins.js +26 -0
- package/packages/host/dist/server/plugins.js.map +1 -1
- package/packages/host/dist/server/wsops.js +3 -1
- package/packages/host/dist/server/wsops.js.map +1 -1
- package/packages/host/dist/sysstat.js +10 -1
- package/packages/host/dist/sysstat.js.map +1 -1
- package/packages/host/dist/updatekey.d.ts +22 -0
- package/packages/host/dist/updatekey.js +140 -0
- package/packages/host/dist/updatekey.js.map +1 -0
- package/packages/host/dist/webbundle.d.ts +15 -6
- package/packages/host/dist/webbundle.js +44 -7
- package/packages/host/dist/webbundle.js.map +1 -1
- package/packages/host/dist/wincpu.d.ts +13 -0
- package/packages/host/dist/wincpu.js +101 -0
- package/packages/host/dist/wincpu.js.map +1 -0
- package/packages/host/package.json +2 -1
- package/packages/web/dist/assets/demo-BLBaz4G_.js +26 -0
- package/packages/web/dist/assets/icons-D8RlmSLR.js +1 -0
- package/packages/web/dist/assets/index-BrvcQFBj.js +170 -0
- package/packages/web/dist/assets/index-Dx3VHnr3.css +1 -0
- package/packages/web/dist/assets/noble-DV-r0OkE.js +1 -0
- package/packages/web/dist/assets/{react-CoSyT_xY.js → react-CjmvHigY.js} +2 -2
- package/packages/web/dist/assets/rolldown-runtime-4tVxZq34.js +1 -0
- package/packages/web/dist/assets/shared-BsWIJLE-.js +51 -0
- package/packages/web/dist/assets/sheets-C7CXUU6R.js +250 -0
- package/packages/web/dist/assets/xterm-BHsKrDzh.js +16 -0
- package/packages/web/dist/build-id.txt +1 -1
- package/packages/web/dist/buildstamp.json +1 -1
- package/packages/web/dist/index.html +9 -9
- package/packages/web/dist/sw.js +2 -2
- package/release.json +7 -3
- package/packages/web/dist/assets/demo-DkCtg-SJ.js +0 -26
- package/packages/web/dist/assets/icons-B48F-4nA.js +0 -1
- package/packages/web/dist/assets/index-CpdyS6Xe.css +0 -1
- package/packages/web/dist/assets/index-EYc28CfH.js +0 -170
- package/packages/web/dist/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
- package/packages/web/dist/assets/shared-DjRRLGfw.js +0 -50
- package/packages/web/dist/assets/sheets-ByWNPa9p.js +0 -248
- package/packages/web/dist/assets/xterm-C8zLTNGk.js +0 -16
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* the APK); the installed app is reached through the DEEP LINK
|
|
10
10
|
* (`<scheme>://pair?u=<base>&c=<code>`). Both parse to the same `{base, code}`.
|
|
11
11
|
*
|
|
12
|
+
* The deep link may also carry, each additive so a shipped page reads `u` and `c` and ignores them:
|
|
13
|
+
* `s=1` (pair by PAKE only, `pake.ts`; never fall back to a plain redeem), `k=<32 hex>` (`pairCommit`:
|
|
14
|
+
* what the seal must match, so someone who has the code still cannot be this desktop) and
|
|
15
|
+
* `l=<plain LAN base>` (so a phone with no tailnet need not type the address). `u` stays plain, and
|
|
16
|
+
* TLS facts ride only inside the seal: a shipped APK could not reach a TLS address.
|
|
17
|
+
*
|
|
12
18
|
* The base goes through `normalizeHostBase`, so a scanned or pasted link can
|
|
13
19
|
* never aim the app at a `javascript:` or `data:` base.
|
|
14
20
|
*/
|
|
@@ -32,8 +38,28 @@ export declare function formatPairCode(code: string): string;
|
|
|
32
38
|
export interface PairTarget {
|
|
33
39
|
base: string;
|
|
34
40
|
code: string;
|
|
41
|
+
/** `s=1`: this link pairs by PAKE or not at all. Absent: nobody asked for that. */
|
|
42
|
+
secure?: boolean;
|
|
43
|
+
/** `k=`: what the seal must match (`pairCommit`). */
|
|
44
|
+
commit?: string;
|
|
45
|
+
/** `l=`: the plain LAN base. */
|
|
46
|
+
lan?: string;
|
|
35
47
|
}
|
|
36
|
-
|
|
48
|
+
/** What a deep link adds to `u` and `c`. */
|
|
49
|
+
export type PairLinkFacts = Pick<PairTarget, "secure" | "commit" | "lan">;
|
|
50
|
+
export declare function pairDeepLink(base: string, code: string, facts?: PairLinkFacts): string;
|
|
51
|
+
/**
|
|
52
|
+
* `k=`: the first 32 hex digits (128 bits) of SHA-256(DER ‖ PIN), lowercase, where DER is the update key's SPKI
|
|
53
|
+
* DER (the base64 of `updateKey`, decoded) and PIN the 32 bytes of the TLS pin (the hex of `tls.spki`,
|
|
54
|
+
* decoded). A desktop with no TLS listener has no PIN, so the hash covers the DER alone. Null when
|
|
55
|
+
* either input is malformed, so a phone reads that as a mismatch and never as a pass. 128 bits because
|
|
56
|
+
* the value is the same for every code of a desktop and a link is shared as text: a shorter one could
|
|
57
|
+
* be ground offline once and reused against any later code.
|
|
58
|
+
*/
|
|
59
|
+
export declare function pairCommit(updateKey: unknown, spki?: unknown): Promise<string | null>;
|
|
60
|
+
/** The update key's fingerprint (8a): lowercase hex SHA-256 of the SPKI DER. What the desktop prints
|
|
61
|
+
* beside the code, so the native dialog has something to compare against. */
|
|
62
|
+
export declare function updateKeyFingerprint(updateKey: unknown): Promise<string | null>;
|
|
37
63
|
export declare function pairLandingUrl(base: string, code: string): string;
|
|
38
64
|
/** Parse `<scheme>://pair?u=<base>&c=<code>` (current or a legacy scheme), or `null`. */
|
|
39
65
|
export declare function parsePairDeepLink(raw: unknown): PairTarget | null;
|
|
@@ -79,8 +105,18 @@ export interface PairMintResponse {
|
|
|
79
105
|
landingUrl: string;
|
|
80
106
|
deepLink: string;
|
|
81
107
|
base: string;
|
|
82
|
-
/** Plain-HTTP LAN base, when the host has a LAN interface bound.
|
|
108
|
+
/** Plain-HTTP LAN base, when the host has a LAN interface bound. Equal to `base` on a desktop with
|
|
109
|
+
* no tailnet, and then `landingUrl` is a plain address: opening it burns the code. */
|
|
83
110
|
lanBase?: string;
|
|
111
|
+
/** The deep link says `s=1`. Absent with no password set, because PAKE refuses every attempt then. */
|
|
112
|
+
secure?: boolean;
|
|
113
|
+
/** The deep link's `k=`. */
|
|
114
|
+
commit?: string;
|
|
115
|
+
/** Lowercase hex SHA-256 of the update key's DER, printed beside the code. */
|
|
116
|
+
updateKeyFingerprint?: string;
|
|
117
|
+
/** The same link with `u=` the LAN address, for a shipped APK that ignores `l=`. Only when `base`
|
|
118
|
+
* is another address. */
|
|
119
|
+
lanDeepLink?: string;
|
|
84
120
|
}
|
|
85
121
|
/** `POST /api/pair/redeem` */
|
|
86
122
|
export interface PairRedeemRequest {
|
|
@@ -117,4 +153,62 @@ export type PairRedeemResponse = {
|
|
|
117
153
|
error: PairRedeemError;
|
|
118
154
|
message: string;
|
|
119
155
|
};
|
|
156
|
+
/** `POST /api/pair/pake/start`. Carries no `Authorization`. */
|
|
157
|
+
export interface PakeStartRequest {
|
|
158
|
+
sidP: string;
|
|
159
|
+
ya: string;
|
|
160
|
+
}
|
|
161
|
+
/** `POST /api/pair/pake/finish`, also with no `Authorization`. `confirm[i]` and `box[i]` answer
|
|
162
|
+
* `yb[i]`; the phone cannot know which code it holds, so it answers all of them. */
|
|
163
|
+
export interface PakeFinishRequest {
|
|
164
|
+
sidP: string;
|
|
165
|
+
confirm: string[];
|
|
166
|
+
box: string[];
|
|
167
|
+
}
|
|
168
|
+
/** What a `box` holds. `presented` is the old token a re-pair retires, sealed so a relay never sees
|
|
169
|
+
* it, and so it can never ride in a header on these routes. */
|
|
170
|
+
export interface PakeBoxBody {
|
|
171
|
+
deviceName: string;
|
|
172
|
+
presented?: string;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Why a PAKE route refused. `closed`, `no-password` and `no-codes` come from a host that DOES speak
|
|
176
|
+
* PAKE, so a phone never answers them with a plain redeem. `unknown` is a miss; `no-session` is a
|
|
177
|
+
* `finish` with nothing to finish, its 60 seconds up or its one try spent. None of them is a 404,
|
|
178
|
+
* which stays what a host with no PAKE at all may answer.
|
|
179
|
+
*/
|
|
180
|
+
export type PakeError = PairRedeemError | "closed" | "no-codes" | "no-session";
|
|
181
|
+
export type PakeRefusal = {
|
|
182
|
+
ok: false;
|
|
183
|
+
error: PakeError;
|
|
184
|
+
message: string;
|
|
185
|
+
};
|
|
186
|
+
export type PakeStartResponse = {
|
|
187
|
+
ok: true;
|
|
188
|
+
nh: string;
|
|
189
|
+
yb: string[];
|
|
190
|
+
} | PakeRefusal;
|
|
191
|
+
export type PakeFinishResponse = {
|
|
192
|
+
ok: true;
|
|
193
|
+
seal: string;
|
|
194
|
+
} | PakeRefusal;
|
|
195
|
+
/** Inside the seal: `redeemPair`'s answer and what only a sealed reply may carry. The seal opening
|
|
196
|
+
* proves the host knew the code, which is why the LAN identity may go to a LAN redeemer here. */
|
|
197
|
+
export type PakeSealedReply = Extract<PairRedeemResponse, {
|
|
198
|
+
ok: true;
|
|
199
|
+
}> & {
|
|
200
|
+
hostId: string;
|
|
201
|
+
/** Absent with no TLS listener. `spki` is lowercase hex. */
|
|
202
|
+
tls?: {
|
|
203
|
+
port: number;
|
|
204
|
+
spki: string;
|
|
205
|
+
};
|
|
206
|
+
/** Base64 SPKI DER (8a). */
|
|
207
|
+
updateKey?: string;
|
|
208
|
+
lanIdentity: {
|
|
209
|
+
hostId: string;
|
|
210
|
+
secret: string;
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
export declare function pakeErrorText(error: PakeError): string;
|
|
120
214
|
export declare function pairRedeemErrorText(error: PairRedeemError): string;
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* the APK); the installed app is reached through the DEEP LINK
|
|
10
10
|
* (`<scheme>://pair?u=<base>&c=<code>`). Both parse to the same `{base, code}`.
|
|
11
11
|
*
|
|
12
|
+
* The deep link may also carry, each additive so a shipped page reads `u` and `c` and ignores them:
|
|
13
|
+
* `s=1` (pair by PAKE only, `pake.ts`; never fall back to a plain redeem), `k=<32 hex>` (`pairCommit`:
|
|
14
|
+
* what the seal must match, so someone who has the code still cannot be this desktop) and
|
|
15
|
+
* `l=<plain LAN base>` (so a phone with no tailnet need not type the address). `u` stays plain, and
|
|
16
|
+
* TLS facts ride only inside the seal: a shipped APK could not reach a TLS address.
|
|
17
|
+
*
|
|
12
18
|
* The base goes through `normalizeHostBase`, so a scanned or pasted link can
|
|
13
19
|
* never aim the app at a `javascript:` or `data:` base.
|
|
14
20
|
*/
|
|
@@ -16,6 +22,7 @@ import { normalizeHostBase } from "./hosturl.js";
|
|
|
16
22
|
import { BRAND, LEGACY } from "./brand.js";
|
|
17
23
|
import { randomFromAlphabet, defaultRandomBytes } from "./hosts.js";
|
|
18
24
|
import { trimEndChars } from "./lineartrim.js";
|
|
25
|
+
import { pakeDecode, sha256 } from "./pake.js";
|
|
19
26
|
/** 32 characters (a power of two: uniform from a byte), no 0, O, 1 or I. */
|
|
20
27
|
export const PAIR_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
21
28
|
export const PAIR_CODE_LEN = 8;
|
|
@@ -46,8 +53,44 @@ export function normalizePairCode(raw) {
|
|
|
46
53
|
export function formatPairCode(code) {
|
|
47
54
|
return code.length === PAIR_CODE_LEN ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
const PAIR_COMMIT_HEX = 32;
|
|
57
|
+
export function pairDeepLink(base, code, facts = {}) {
|
|
58
|
+
const extra = [
|
|
59
|
+
facts.secure ? "&s=1" : "",
|
|
60
|
+
facts.commit ? `&k=${facts.commit}` : "",
|
|
61
|
+
facts.lan ? `&l=${encodeURIComponent(facts.lan)}` : "",
|
|
62
|
+
].join("");
|
|
63
|
+
return `${PAIR_SCHEME}://pair?u=${encodeURIComponent(base)}&c=${encodeURIComponent(code)}${extra}`;
|
|
64
|
+
}
|
|
65
|
+
const hex = (b) => Array.from(b, (c) => c.toString(16).padStart(2, "0")).join("");
|
|
66
|
+
function hexBytes(s) {
|
|
67
|
+
if (s.length % 2 !== 0 || !/^[0-9a-f]*$/.test(s))
|
|
68
|
+
return null;
|
|
69
|
+
return Uint8Array.from(s.match(/../g) ?? [], (x) => parseInt(x, 16));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `k=`: the first 32 hex digits (128 bits) of SHA-256(DER ‖ PIN), lowercase, where DER is the update key's SPKI
|
|
73
|
+
* DER (the base64 of `updateKey`, decoded) and PIN the 32 bytes of the TLS pin (the hex of `tls.spki`,
|
|
74
|
+
* decoded). A desktop with no TLS listener has no PIN, so the hash covers the DER alone. Null when
|
|
75
|
+
* either input is malformed, so a phone reads that as a mismatch and never as a pass. 128 bits because
|
|
76
|
+
* the value is the same for every code of a desktop and a link is shared as text: a shorter one could
|
|
77
|
+
* be ground offline once and reused against any later code.
|
|
78
|
+
*/
|
|
79
|
+
export async function pairCommit(updateKey, spki) {
|
|
80
|
+
const der = pakeDecode(updateKey);
|
|
81
|
+
const pin = spki === undefined ? new Uint8Array(0) : typeof spki === "string" ? hexBytes(spki) : null;
|
|
82
|
+
if (!der || der.length === 0 || !pin || (spki !== undefined && pin.length !== 32))
|
|
83
|
+
return null;
|
|
84
|
+
const both = new Uint8Array(der.length + pin.length);
|
|
85
|
+
both.set(der);
|
|
86
|
+
both.set(pin, der.length);
|
|
87
|
+
return hex(await sha256(both)).slice(0, PAIR_COMMIT_HEX);
|
|
88
|
+
}
|
|
89
|
+
/** The update key's fingerprint (8a): lowercase hex SHA-256 of the SPKI DER. What the desktop prints
|
|
90
|
+
* beside the code, so the native dialog has something to compare against. */
|
|
91
|
+
export async function updateKeyFingerprint(updateKey) {
|
|
92
|
+
const der = pakeDecode(updateKey);
|
|
93
|
+
return der && der.length > 0 ? hex(await sha256(der)) : null;
|
|
51
94
|
}
|
|
52
95
|
export function pairLandingUrl(base, code) {
|
|
53
96
|
return `${trimEndChars(base, "/")}/pair/${code}`;
|
|
@@ -69,7 +112,20 @@ export function parsePairDeepLink(raw) {
|
|
|
69
112
|
}
|
|
70
113
|
const base = normalizeHostBase(params.get("u") ?? "");
|
|
71
114
|
const code = normalizePairCode(params.get("c"));
|
|
72
|
-
|
|
115
|
+
if (!base || !code)
|
|
116
|
+
return null;
|
|
117
|
+
// A commitment that cannot be read is refused rather than dropped: dropped, it would pass as a
|
|
118
|
+
// link that made no promise.
|
|
119
|
+
const commit = params.get("k");
|
|
120
|
+
if (commit !== null && !new RegExp(`^[0-9a-f]{${PAIR_COMMIT_HEX}}$`).test(commit))
|
|
121
|
+
return null;
|
|
122
|
+
const lan = normalizeHostBase(params.get("l") ?? "");
|
|
123
|
+
return {
|
|
124
|
+
base, code,
|
|
125
|
+
...(params.get("s") === "1" ? { secure: true } : {}),
|
|
126
|
+
...(commit !== null ? { commit } : {}),
|
|
127
|
+
...(lan ? { lan } : {}),
|
|
128
|
+
};
|
|
73
129
|
}
|
|
74
130
|
/** The code in a landing path: `/pair/<code>` (page) or `/pair/<code>/apk`. */
|
|
75
131
|
export function pairPathCode(pathname) {
|
|
@@ -112,6 +168,18 @@ export function parsePairInput(raw) {
|
|
|
112
168
|
export function pairExpired(expiresAt, now) {
|
|
113
169
|
return now >= expiresAt;
|
|
114
170
|
}
|
|
171
|
+
export function pakeErrorText(error) {
|
|
172
|
+
switch (error) {
|
|
173
|
+
case "closed":
|
|
174
|
+
return "Too many wrong attempts closed encrypted pairing for this code. Show a new code on the desktop.";
|
|
175
|
+
case "no-codes":
|
|
176
|
+
return "The desktop has no pairing code showing. Show a new code and try again.";
|
|
177
|
+
case "no-session":
|
|
178
|
+
return "That pairing attempt ran out of time. Try again.";
|
|
179
|
+
default:
|
|
180
|
+
return pairRedeemErrorText(error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
115
183
|
export function pairRedeemErrorText(error) {
|
|
116
184
|
switch (error) {
|
|
117
185
|
case "bad-request":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing a phone over a network nobody trusts: CPace over ristretto255, keyed by the 8-character
|
|
3
|
+
* pairing code. The phone and the host both run THIS file, so there is one reading of every byte.
|
|
4
|
+
*
|
|
5
|
+
* phone host
|
|
6
|
+
* sidP (16 random bytes), a
|
|
7
|
+
* g = H2C(lp(S), lp(sidP)) S = SHA-256(utf8(code)), see below
|
|
8
|
+
* Ya = a·g ── start {sidP, ya} ──▶ for each code i it may offer (at most 5):
|
|
9
|
+
* b_i, Yb_i = b_i·g_i
|
|
10
|
+
* ◀── {nh, yb: [Yb_i]} ──
|
|
11
|
+
* TH_i = SHA-512(lp(DST), lp(sidP), lp(nh), lp(Ya), lp(Yb_i))
|
|
12
|
+
* Z_i = a·Yb_i = b_i·Ya
|
|
13
|
+
* K_phone, K_box, K_seal = HKDF-SHA256(ikm Z_i, salt TH_i, info = the label, 32 bytes each)
|
|
14
|
+
* confirm_i = HMAC-SHA256(K_phone_i, TH_i)
|
|
15
|
+
* box_i = nonce96 ‖ AES-256-GCM(K_box_i, json, aad TH_i)
|
|
16
|
+
* ── finish {sidP, confirm, box} ──▶ one confirm of one code, or a miss
|
|
17
|
+
* ◀── {seal} ── nonce96 ‖ AES-256-GCM(K_seal, json, aad TH)
|
|
18
|
+
*
|
|
19
|
+
* The phone proves first, so a wrong guess is detected and counted by the host; the seal opening is
|
|
20
|
+
* what authenticates the host. A relay gets one online guess per attempt and learns nothing it can
|
|
21
|
+
* test offline: without the code it cannot compute g.
|
|
22
|
+
*
|
|
23
|
+
* **`S`, not the code itself.** The host keeps a code only as its SHA-256 (`PairStore`: no live
|
|
24
|
+
* credential sits in its memory as typed) and the generator depends on the phone's fresh `sidP`, so
|
|
25
|
+
* it cannot be worked out at mint time. Both ends therefore hash first. `S` is one-to-one with the
|
|
26
|
+
* code, so the password space and the guess count are the same as hashing the code directly.
|
|
27
|
+
*
|
|
28
|
+
* **Encodings.** `lp(x)` is a 4-byte big-endian length, then `x`. A code goes in as its normalized
|
|
29
|
+
* form (`normalizePairCode`). Every binary field on the wire is standard padded base64, decoded
|
|
30
|
+
* strictly (`pakeDecode`), so one value has one spelling.
|
|
31
|
+
*
|
|
32
|
+
* `@noble/curves` is loaded by dynamic import, so the phone's main chunk never carries it and it
|
|
33
|
+
* costs nothing until somebody pairs. HKDF, HMAC, AES-GCM and the hashes are WebCrypto, which node
|
|
34
|
+
* and the APK's WebView both have; a plain-http browser page does not, and `PakeFailure` says so.
|
|
35
|
+
*/
|
|
36
|
+
import { type RandomBytes } from "./hosts.js";
|
|
37
|
+
/** The domain separator: frozen, and not the brand, because both ends of a pairing must agree on it
|
|
38
|
+
* across a rename. */
|
|
39
|
+
export declare const PAKE_DST = "waterplane-cpace-v1";
|
|
40
|
+
/** The most codes a host offers, and the most shares a phone will answer. */
|
|
41
|
+
export declare const PAKE_MAX_OFFERS = 5;
|
|
42
|
+
export declare const PAKE_SID_BYTES = 16;
|
|
43
|
+
export declare const PAKE_NONCE_BYTES = 16;
|
|
44
|
+
export declare const PAKE_POINT_BYTES = 32;
|
|
45
|
+
export declare const PAKE_CONFIRM_BYTES = 32;
|
|
46
|
+
export declare const PAKE_TH_BYTES = 64;
|
|
47
|
+
/** A value that cannot be used: a bad point, a malformed field, no WebCrypto. Never a wrong code. */
|
|
48
|
+
export declare class PakeFailure extends Error {
|
|
49
|
+
}
|
|
50
|
+
/** `x` with its 4-byte big-endian length in front. */
|
|
51
|
+
export declare function lp(x: Uint8Array): Uint8Array;
|
|
52
|
+
/** Standard padded base64. */
|
|
53
|
+
export declare function pakeEncode(b: Uint8Array): string;
|
|
54
|
+
/** Strict base64: only the one spelling `pakeEncode` writes, optionally of exactly `len` bytes. */
|
|
55
|
+
export declare function pakeDecode(raw: unknown, len?: number): Uint8Array | null;
|
|
56
|
+
export declare function sha256(b: Uint8Array): Promise<Uint8Array>;
|
|
57
|
+
/** Whether `b` is a share `decodeShare` accepts. */
|
|
58
|
+
export declare function pakeShareOk(b: Uint8Array): Promise<boolean>;
|
|
59
|
+
/** What goes in for the code: SHA-256 of its normalized form (the header says why). */
|
|
60
|
+
export declare function pakeSecret(code: string): Promise<Uint8Array>;
|
|
61
|
+
/** A fresh scalar and its share on the generator for `secret` and `sidP`. */
|
|
62
|
+
export declare function pakeShare(secret: Uint8Array, sidP: Uint8Array, rand?: RandomBytes): Promise<{
|
|
63
|
+
scalar: bigint;
|
|
64
|
+
share: Uint8Array;
|
|
65
|
+
}>;
|
|
66
|
+
/** `TH`: everything both ends must have seen alike. */
|
|
67
|
+
export declare function pakeTranscript(sidP: Uint8Array, nh: Uint8Array, ya: Uint8Array, yb: Uint8Array): Promise<Uint8Array>;
|
|
68
|
+
/** `Z`: my scalar on the peer's share. Throws on a bad share, and on an identity result. */
|
|
69
|
+
export declare function pakeSharedPoint(scalar: bigint, peerShare: Uint8Array): Promise<Uint8Array>;
|
|
70
|
+
export type PakeLabel = "K_phone" | "K_box" | "K_seal";
|
|
71
|
+
export interface PakeKeys {
|
|
72
|
+
kPhone: Uint8Array;
|
|
73
|
+
kBox: Uint8Array;
|
|
74
|
+
kSeal: Uint8Array;
|
|
75
|
+
}
|
|
76
|
+
export declare function pakeKeys(z: Uint8Array, th: Uint8Array): Promise<PakeKeys>;
|
|
77
|
+
/** `HMAC-SHA256(K_phone, TH)`: the phone's proof that it holds the code. */
|
|
78
|
+
export declare function pakeConfirm(kPhone: Uint8Array, th: Uint8Array): Promise<Uint8Array>;
|
|
79
|
+
/** The phone's words to the host: `{deviceName, presented}`, under `K_box`. */
|
|
80
|
+
export declare const pakeBox: (kBox: Uint8Array, th: Uint8Array, value: unknown, rand?: RandomBytes) => Promise<string>;
|
|
81
|
+
export declare const pakeOpenBox: (kBox: Uint8Array, th: Uint8Array, raw: unknown) => Promise<unknown>;
|
|
82
|
+
/** The host's reply, under `K_seal`. A random 96-bit nonce, `TH` as the additional data. */
|
|
83
|
+
export declare const pakeSeal: (kSeal: Uint8Array, th: Uint8Array, value: unknown, rand?: RandomBytes) => Promise<string>;
|
|
84
|
+
export declare const pakeOpenSeal: (kSeal: Uint8Array, th: Uint8Array, raw: unknown) => Promise<unknown>;
|
|
85
|
+
/** One code offered to one attempt: what `finish` needs, and no way back to the code. */
|
|
86
|
+
export interface PakeOffer {
|
|
87
|
+
yb: Uint8Array;
|
|
88
|
+
th: Uint8Array;
|
|
89
|
+
confirm: Uint8Array;
|
|
90
|
+
kBox: Uint8Array;
|
|
91
|
+
kSeal: Uint8Array;
|
|
92
|
+
}
|
|
93
|
+
/** `secret`'s answer to a `start`. Throws `PakeFailure` on a bad `ya`. */
|
|
94
|
+
export declare function pakeHostOffer(secret: Uint8Array, sidP: Uint8Array, ya: Uint8Array, nh: Uint8Array, rand?: RandomBytes): Promise<PakeOffer>;
|
|
95
|
+
export interface PakePhoneState {
|
|
96
|
+
sidP: Uint8Array;
|
|
97
|
+
ya: Uint8Array;
|
|
98
|
+
scalar: bigint;
|
|
99
|
+
}
|
|
100
|
+
/** `code` is normalized (`normalizePairCode`). The state stays on the phone. */
|
|
101
|
+
export declare function pakePhoneStart(code: string, rand?: RandomBytes): Promise<{
|
|
102
|
+
state: PakePhoneState;
|
|
103
|
+
request: {
|
|
104
|
+
sidP: string;
|
|
105
|
+
ya: string;
|
|
106
|
+
};
|
|
107
|
+
}>;
|
|
108
|
+
/** What opens the seal: one candidate per share, because only the host knows which code hit. */
|
|
109
|
+
export interface PakeCandidate {
|
|
110
|
+
kSeal: Uint8Array;
|
|
111
|
+
th: Uint8Array;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Answer a `start` reply. Throws `PakeFailure` on more shares than a host may offer, on none, and on
|
|
115
|
+
* any bad share, because a host that sends one is not worth a guess. `box` is the words the host
|
|
116
|
+
* needs (`PakeBoxBody`), one copy per candidate.
|
|
117
|
+
*/
|
|
118
|
+
export declare function pakePhoneFinish(state: PakePhoneState, reply: {
|
|
119
|
+
nh: unknown;
|
|
120
|
+
yb: unknown;
|
|
121
|
+
}, box: unknown, rand?: RandomBytes): Promise<{
|
|
122
|
+
request: {
|
|
123
|
+
sidP: string;
|
|
124
|
+
confirm: string[];
|
|
125
|
+
box: string[];
|
|
126
|
+
};
|
|
127
|
+
candidates: PakeCandidate[];
|
|
128
|
+
}>;
|
|
129
|
+
/** Open a seal against every candidate. Null: no candidate opens it, so this was not the host that
|
|
130
|
+
* knew the code, and nothing in it may be believed. */
|
|
131
|
+
export declare function pakePhoneOpen(candidates: readonly PakeCandidate[], seal: unknown): Promise<unknown>;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing a phone over a network nobody trusts: CPace over ristretto255, keyed by the 8-character
|
|
3
|
+
* pairing code. The phone and the host both run THIS file, so there is one reading of every byte.
|
|
4
|
+
*
|
|
5
|
+
* phone host
|
|
6
|
+
* sidP (16 random bytes), a
|
|
7
|
+
* g = H2C(lp(S), lp(sidP)) S = SHA-256(utf8(code)), see below
|
|
8
|
+
* Ya = a·g ── start {sidP, ya} ──▶ for each code i it may offer (at most 5):
|
|
9
|
+
* b_i, Yb_i = b_i·g_i
|
|
10
|
+
* ◀── {nh, yb: [Yb_i]} ──
|
|
11
|
+
* TH_i = SHA-512(lp(DST), lp(sidP), lp(nh), lp(Ya), lp(Yb_i))
|
|
12
|
+
* Z_i = a·Yb_i = b_i·Ya
|
|
13
|
+
* K_phone, K_box, K_seal = HKDF-SHA256(ikm Z_i, salt TH_i, info = the label, 32 bytes each)
|
|
14
|
+
* confirm_i = HMAC-SHA256(K_phone_i, TH_i)
|
|
15
|
+
* box_i = nonce96 ‖ AES-256-GCM(K_box_i, json, aad TH_i)
|
|
16
|
+
* ── finish {sidP, confirm, box} ──▶ one confirm of one code, or a miss
|
|
17
|
+
* ◀── {seal} ── nonce96 ‖ AES-256-GCM(K_seal, json, aad TH)
|
|
18
|
+
*
|
|
19
|
+
* The phone proves first, so a wrong guess is detected and counted by the host; the seal opening is
|
|
20
|
+
* what authenticates the host. A relay gets one online guess per attempt and learns nothing it can
|
|
21
|
+
* test offline: without the code it cannot compute g.
|
|
22
|
+
*
|
|
23
|
+
* **`S`, not the code itself.** The host keeps a code only as its SHA-256 (`PairStore`: no live
|
|
24
|
+
* credential sits in its memory as typed) and the generator depends on the phone's fresh `sidP`, so
|
|
25
|
+
* it cannot be worked out at mint time. Both ends therefore hash first. `S` is one-to-one with the
|
|
26
|
+
* code, so the password space and the guess count are the same as hashing the code directly.
|
|
27
|
+
*
|
|
28
|
+
* **Encodings.** `lp(x)` is a 4-byte big-endian length, then `x`. A code goes in as its normalized
|
|
29
|
+
* form (`normalizePairCode`). Every binary field on the wire is standard padded base64, decoded
|
|
30
|
+
* strictly (`pakeDecode`), so one value has one spelling.
|
|
31
|
+
*
|
|
32
|
+
* `@noble/curves` is loaded by dynamic import, so the phone's main chunk never carries it and it
|
|
33
|
+
* costs nothing until somebody pairs. HKDF, HMAC, AES-GCM and the hashes are WebCrypto, which node
|
|
34
|
+
* and the APK's WebView both have; a plain-http browser page does not, and `PakeFailure` says so.
|
|
35
|
+
*/
|
|
36
|
+
import { defaultRandomBytes } from "./hosts.js";
|
|
37
|
+
/** The domain separator: frozen, and not the brand, because both ends of a pairing must agree on it
|
|
38
|
+
* across a rename. */
|
|
39
|
+
// eslint-disable-next-line local/no-brand-literal -- a wire constant, frozen across any rename
|
|
40
|
+
export const PAKE_DST = "waterplane-cpace-v1";
|
|
41
|
+
/** The most codes a host offers, and the most shares a phone will answer. */
|
|
42
|
+
export const PAKE_MAX_OFFERS = 5;
|
|
43
|
+
export const PAKE_SID_BYTES = 16;
|
|
44
|
+
export const PAKE_NONCE_BYTES = 16;
|
|
45
|
+
export const PAKE_POINT_BYTES = 32;
|
|
46
|
+
export const PAKE_CONFIRM_BYTES = 32;
|
|
47
|
+
export const PAKE_TH_BYTES = 64;
|
|
48
|
+
const GCM_NONCE_BYTES = 12;
|
|
49
|
+
const KEY_BYTES = 32;
|
|
50
|
+
/** A value that cannot be used: a bad point, a malformed field, no WebCrypto. Never a wrong code. */
|
|
51
|
+
export class PakeFailure extends Error {
|
|
52
|
+
}
|
|
53
|
+
const text = new TextEncoder();
|
|
54
|
+
function subtle() {
|
|
55
|
+
const s = globalThis.crypto?.subtle;
|
|
56
|
+
if (!s)
|
|
57
|
+
throw new PakeFailure("This page has no WebCrypto, so it cannot pair securely. Open it over https or use the app.");
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
const own = (b) => new Uint8Array(b);
|
|
61
|
+
/** `x` with its 4-byte big-endian length in front. */
|
|
62
|
+
export function lp(x) {
|
|
63
|
+
const out = new Uint8Array(4 + x.length);
|
|
64
|
+
new DataView(out.buffer).setUint32(0, x.length);
|
|
65
|
+
out.set(x, 4);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function concat(...parts) {
|
|
69
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
70
|
+
let at = 0;
|
|
71
|
+
for (const p of parts) {
|
|
72
|
+
out.set(p, at);
|
|
73
|
+
at += p.length;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** Standard padded base64. */
|
|
78
|
+
export function pakeEncode(b) {
|
|
79
|
+
let s = "";
|
|
80
|
+
for (const c of b)
|
|
81
|
+
s += String.fromCharCode(c);
|
|
82
|
+
return btoa(s);
|
|
83
|
+
}
|
|
84
|
+
/** Strict base64: only the one spelling `pakeEncode` writes, optionally of exactly `len` bytes. */
|
|
85
|
+
export function pakeDecode(raw, len) {
|
|
86
|
+
if (typeof raw !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(raw))
|
|
87
|
+
return null;
|
|
88
|
+
let bin;
|
|
89
|
+
try {
|
|
90
|
+
bin = atob(raw);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const out = new Uint8Array(bin.length);
|
|
96
|
+
for (let i = 0; i < bin.length; i++)
|
|
97
|
+
out[i] = bin.charCodeAt(i);
|
|
98
|
+
if (pakeEncode(out) !== raw)
|
|
99
|
+
return null;
|
|
100
|
+
return len === undefined || out.length === len ? out : null;
|
|
101
|
+
}
|
|
102
|
+
export async function sha256(b) {
|
|
103
|
+
return new Uint8Array(await subtle().digest("SHA-256", own(b)));
|
|
104
|
+
}
|
|
105
|
+
async function sha512(b) {
|
|
106
|
+
return new Uint8Array(await subtle().digest("SHA-512", own(b)));
|
|
107
|
+
}
|
|
108
|
+
let group = null;
|
|
109
|
+
function ristretto() {
|
|
110
|
+
group ??= import("@noble/curves/ed25519.js").then(({ ristretto255, ristretto255_hasher }) => ({
|
|
111
|
+
fromBytes: (b) => ristretto255.Point.fromBytes(b),
|
|
112
|
+
hash: (msg) => ristretto255_hasher.hashToCurve(msg, { DST: text.encode(PAKE_DST) }),
|
|
113
|
+
order: ristretto255.Point.Fn.ORDER,
|
|
114
|
+
}));
|
|
115
|
+
return group;
|
|
116
|
+
}
|
|
117
|
+
/** A share off the wire: 32 bytes, a canonical encoding, never the identity. */
|
|
118
|
+
async function decodeShare(b) {
|
|
119
|
+
if (b.length !== PAKE_POINT_BYTES)
|
|
120
|
+
throw new PakeFailure("A key share is the wrong length.");
|
|
121
|
+
const g = await ristretto();
|
|
122
|
+
let p;
|
|
123
|
+
try {
|
|
124
|
+
p = g.fromBytes(b);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new PakeFailure("A key share is not a canonical group element.");
|
|
128
|
+
}
|
|
129
|
+
if (p.is0())
|
|
130
|
+
throw new PakeFailure("A key share is the identity element.");
|
|
131
|
+
return p;
|
|
132
|
+
}
|
|
133
|
+
/** Whether `b` is a share `decodeShare` accepts. */
|
|
134
|
+
export async function pakeShareOk(b) {
|
|
135
|
+
try {
|
|
136
|
+
await decodeShare(b);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
if (e instanceof PakeFailure)
|
|
141
|
+
return false;
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** What goes in for the code: SHA-256 of its normalized form (the header says why). */
|
|
146
|
+
export function pakeSecret(code) {
|
|
147
|
+
return sha256(text.encode(code));
|
|
148
|
+
}
|
|
149
|
+
async function randomScalar(rand) {
|
|
150
|
+
const { order } = await ristretto();
|
|
151
|
+
for (;;) {
|
|
152
|
+
// 512 bits reduced mod a 253-bit order: no bias worth naming.
|
|
153
|
+
let n = 0n;
|
|
154
|
+
for (const c of rand(64))
|
|
155
|
+
n = (n << 8n) | BigInt(c);
|
|
156
|
+
const s = n % order;
|
|
157
|
+
if (s !== 0n)
|
|
158
|
+
return s;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** A fresh scalar and its share on the generator for `secret` and `sidP`. */
|
|
162
|
+
export async function pakeShare(secret, sidP, rand = defaultRandomBytes) {
|
|
163
|
+
const g = await ristretto();
|
|
164
|
+
const scalar = await randomScalar(rand);
|
|
165
|
+
return { scalar, share: g.hash(concat(lp(secret), lp(sidP))).multiply(scalar).toBytes() };
|
|
166
|
+
}
|
|
167
|
+
/** `TH`: everything both ends must have seen alike. */
|
|
168
|
+
export function pakeTranscript(sidP, nh, ya, yb) {
|
|
169
|
+
return sha512(concat(lp(text.encode(PAKE_DST)), lp(sidP), lp(nh), lp(ya), lp(yb)));
|
|
170
|
+
}
|
|
171
|
+
/** `Z`: my scalar on the peer's share. Throws on a bad share, and on an identity result. */
|
|
172
|
+
export async function pakeSharedPoint(scalar, peerShare) {
|
|
173
|
+
const z = (await decodeShare(peerShare)).multiply(scalar);
|
|
174
|
+
if (z.is0())
|
|
175
|
+
throw new PakeFailure("The shared point is the identity element.");
|
|
176
|
+
return z.toBytes();
|
|
177
|
+
}
|
|
178
|
+
async function hkdf(z, th, label) {
|
|
179
|
+
const s = subtle();
|
|
180
|
+
const key = await s.importKey("raw", own(z), "HKDF", false, ["deriveBits"]);
|
|
181
|
+
return new Uint8Array(await s.deriveBits({ name: "HKDF", hash: "SHA-256", salt: own(th), info: text.encode(label) }, key, KEY_BYTES * 8));
|
|
182
|
+
}
|
|
183
|
+
export async function pakeKeys(z, th) {
|
|
184
|
+
const [kPhone, kBox, kSeal] = await Promise.all([hkdf(z, th, "K_phone"), hkdf(z, th, "K_box"), hkdf(z, th, "K_seal")]);
|
|
185
|
+
return { kPhone, kBox, kSeal };
|
|
186
|
+
}
|
|
187
|
+
/** `HMAC-SHA256(K_phone, TH)`: the phone's proof that it holds the code. */
|
|
188
|
+
export async function pakeConfirm(kPhone, th) {
|
|
189
|
+
const s = subtle();
|
|
190
|
+
const key = await s.importKey("raw", own(kPhone), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
191
|
+
return new Uint8Array(await s.sign("HMAC", key, own(th)));
|
|
192
|
+
}
|
|
193
|
+
async function encryptJson(k, th, value, rand) {
|
|
194
|
+
const s = subtle();
|
|
195
|
+
const key = await s.importKey("raw", own(k), "AES-GCM", false, ["encrypt"]);
|
|
196
|
+
const iv = own(rand(GCM_NONCE_BYTES));
|
|
197
|
+
const ct = await s.encrypt({ name: "AES-GCM", iv, additionalData: own(th) }, key, own(text.encode(JSON.stringify(value))));
|
|
198
|
+
return pakeEncode(concat(iv, new Uint8Array(ct)));
|
|
199
|
+
}
|
|
200
|
+
/** The JSON inside, or null for anything that does not authenticate. */
|
|
201
|
+
async function decryptJson(k, th, raw) {
|
|
202
|
+
const b = pakeDecode(raw);
|
|
203
|
+
if (!b || b.length <= GCM_NONCE_BYTES)
|
|
204
|
+
return null;
|
|
205
|
+
const s = subtle();
|
|
206
|
+
const key = await s.importKey("raw", own(k), "AES-GCM", false, ["decrypt"]);
|
|
207
|
+
try {
|
|
208
|
+
const pt = await s.decrypt({ name: "AES-GCM", iv: own(b.subarray(0, GCM_NONCE_BYTES)), additionalData: own(th) }, key, own(b.subarray(GCM_NONCE_BYTES)));
|
|
209
|
+
return JSON.parse(new TextDecoder().decode(pt));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** The phone's words to the host: `{deviceName, presented}`, under `K_box`. */
|
|
216
|
+
export const pakeBox = (kBox, th, value, rand = defaultRandomBytes) => encryptJson(kBox, th, value, rand);
|
|
217
|
+
export const pakeOpenBox = (kBox, th, raw) => decryptJson(kBox, th, raw);
|
|
218
|
+
/** The host's reply, under `K_seal`. A random 96-bit nonce, `TH` as the additional data. */
|
|
219
|
+
export const pakeSeal = (kSeal, th, value, rand = defaultRandomBytes) => encryptJson(kSeal, th, value, rand);
|
|
220
|
+
export const pakeOpenSeal = (kSeal, th, raw) => decryptJson(kSeal, th, raw);
|
|
221
|
+
/** `secret`'s answer to a `start`. Throws `PakeFailure` on a bad `ya`. */
|
|
222
|
+
export async function pakeHostOffer(secret, sidP, ya, nh, rand = defaultRandomBytes) {
|
|
223
|
+
const { scalar, share: yb } = await pakeShare(secret, sidP, rand);
|
|
224
|
+
const z = await pakeSharedPoint(scalar, ya);
|
|
225
|
+
const th = await pakeTranscript(sidP, nh, ya, yb);
|
|
226
|
+
const { kPhone, kBox, kSeal } = await pakeKeys(z, th);
|
|
227
|
+
return { yb, th, confirm: await pakeConfirm(kPhone, th), kBox, kSeal };
|
|
228
|
+
}
|
|
229
|
+
/** `code` is normalized (`normalizePairCode`). The state stays on the phone. */
|
|
230
|
+
export async function pakePhoneStart(code, rand = defaultRandomBytes) {
|
|
231
|
+
const sidP = rand(PAKE_SID_BYTES);
|
|
232
|
+
const { scalar, share: ya } = await pakeShare(await pakeSecret(code), sidP, rand);
|
|
233
|
+
return { state: { sidP, ya, scalar }, request: { sidP: pakeEncode(sidP), ya: pakeEncode(ya) } };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Answer a `start` reply. Throws `PakeFailure` on more shares than a host may offer, on none, and on
|
|
237
|
+
* any bad share, because a host that sends one is not worth a guess. `box` is the words the host
|
|
238
|
+
* needs (`PakeBoxBody`), one copy per candidate.
|
|
239
|
+
*/
|
|
240
|
+
export async function pakePhoneFinish(state, reply, box, rand = defaultRandomBytes) {
|
|
241
|
+
const nh = pakeDecode(reply.nh, PAKE_NONCE_BYTES);
|
|
242
|
+
if (!nh)
|
|
243
|
+
throw new PakeFailure("The desktop's reply is malformed.");
|
|
244
|
+
if (!Array.isArray(reply.yb) || reply.yb.length === 0 || reply.yb.length > PAKE_MAX_OFFERS)
|
|
245
|
+
throw new PakeFailure("The desktop offered the wrong number of key shares.");
|
|
246
|
+
const confirm = [];
|
|
247
|
+
const boxes = [];
|
|
248
|
+
const candidates = [];
|
|
249
|
+
for (const raw of reply.yb) {
|
|
250
|
+
const yb = pakeDecode(raw, PAKE_POINT_BYTES);
|
|
251
|
+
if (!yb)
|
|
252
|
+
throw new PakeFailure("A key share is malformed.");
|
|
253
|
+
const z = await pakeSharedPoint(state.scalar, yb);
|
|
254
|
+
const th = await pakeTranscript(state.sidP, nh, state.ya, yb);
|
|
255
|
+
const { kPhone, kBox, kSeal } = await pakeKeys(z, th);
|
|
256
|
+
confirm.push(pakeEncode(await pakeConfirm(kPhone, th)));
|
|
257
|
+
boxes.push(await pakeBox(kBox, th, box, rand));
|
|
258
|
+
candidates.push({ kSeal, th });
|
|
259
|
+
}
|
|
260
|
+
return { request: { sidP: pakeEncode(state.sidP), confirm, box: boxes }, candidates };
|
|
261
|
+
}
|
|
262
|
+
/** Open a seal against every candidate. Null: no candidate opens it, so this was not the host that
|
|
263
|
+
* knew the code, and nothing in it may be believed. */
|
|
264
|
+
export async function pakePhoneOpen(candidates, seal) {
|
|
265
|
+
for (const c of candidates) {
|
|
266
|
+
const v = await pakeOpenSeal(c.kSeal, c.th, seal);
|
|
267
|
+
if (v !== null)
|
|
268
|
+
return v;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=pake.js.map
|