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.
Files changed (235) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/NOTICES.md +18 -7
  3. package/README.md +43 -0
  4. package/SECURITY.md +196 -12
  5. package/node_modules/@waterplane/shared/dist/desktopfiles.d.ts +24 -2
  6. package/node_modules/@waterplane/shared/dist/desktopfiles.js +73 -3
  7. package/node_modules/@waterplane/shared/dist/desktopfiles.js.map +1 -1
  8. package/node_modules/@waterplane/shared/dist/hosts.d.ts +31 -0
  9. package/node_modules/@waterplane/shared/dist/hosts.js +46 -1
  10. package/node_modules/@waterplane/shared/dist/hosts.js.map +1 -1
  11. package/node_modules/@waterplane/shared/dist/hosturl.d.ts +55 -0
  12. package/node_modules/@waterplane/shared/dist/hosturl.js +85 -0
  13. package/node_modules/@waterplane/shared/dist/hosturl.js.map +1 -1
  14. package/node_modules/@waterplane/shared/dist/index.d.ts +4 -0
  15. package/node_modules/@waterplane/shared/dist/index.js +4 -0
  16. package/node_modules/@waterplane/shared/dist/index.js.map +1 -1
  17. package/node_modules/@waterplane/shared/dist/installrow.d.ts +6 -1
  18. package/node_modules/@waterplane/shared/dist/installrow.js +20 -1
  19. package/node_modules/@waterplane/shared/dist/installrow.js.map +1 -1
  20. package/node_modules/@waterplane/shared/dist/lanroute.d.ts +5 -0
  21. package/node_modules/@waterplane/shared/dist/lanroute.js.map +1 -1
  22. package/node_modules/@waterplane/shared/dist/pairing.d.ts +96 -2
  23. package/node_modules/@waterplane/shared/dist/pairing.js +71 -3
  24. package/node_modules/@waterplane/shared/dist/pairing.js.map +1 -1
  25. package/node_modules/@waterplane/shared/dist/pake.d.ts +131 -0
  26. package/node_modules/@waterplane/shared/dist/pake.js +272 -0
  27. package/node_modules/@waterplane/shared/dist/pake.js.map +1 -0
  28. package/node_modules/@waterplane/shared/dist/peer.d.ts +63 -0
  29. package/node_modules/@waterplane/shared/dist/peer.js +41 -0
  30. package/node_modules/@waterplane/shared/dist/peer.js.map +1 -1
  31. package/node_modules/@waterplane/shared/dist/plugin/api.d.ts +15 -0
  32. package/node_modules/@waterplane/shared/dist/plugin/version.d.ts +5 -1
  33. package/node_modules/@waterplane/shared/dist/plugin/version.js +5 -1
  34. package/node_modules/@waterplane/shared/dist/plugin/version.js.map +1 -1
  35. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.d.ts +7 -2
  36. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js +6 -3
  37. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js.map +1 -1
  38. package/node_modules/@waterplane/shared/dist/waiting.d.ts +64 -0
  39. package/node_modules/@waterplane/shared/dist/waiting.js +86 -0
  40. package/node_modules/@waterplane/shared/dist/waiting.js.map +1 -0
  41. package/node_modules/@waterplane/shared/dist/wire/messages/hostcore.d.ts +3 -0
  42. package/node_modules/@waterplane/shared/dist/wire/notify.d.ts +2 -0
  43. package/node_modules/@waterplane/shared/dist/wire/notify.js +2 -0
  44. package/node_modules/@waterplane/shared/dist/wire/notify.js.map +1 -1
  45. package/node_modules/@waterplane/shared/dist/wire/repo.d.ts +25 -1
  46. package/node_modules/@waterplane/shared/dist/wire/repo.js.map +1 -1
  47. package/node_modules/@waterplane/shared/dist/wire/sysstat.d.ts +7 -0
  48. package/node_modules/@waterplane/shared/dist/wire/telemetry.d.ts +3 -0
  49. package/node_modules/@waterplane/shared/package.json +3 -0
  50. package/npm-shrinkwrap.json +30 -2
  51. package/package.json +2 -1
  52. package/packages/extensions/desktop/helper/Capture.swift +245 -0
  53. package/packages/extensions/desktop/helper/H264.swift +226 -0
  54. package/packages/extensions/desktop/helper/Info.plist +20 -0
  55. package/packages/extensions/desktop/helper/Input.swift +195 -0
  56. package/packages/extensions/desktop/helper/Jpeg.swift +81 -0
  57. package/packages/extensions/desktop/helper/PROTOCOL.md +366 -0
  58. package/packages/extensions/desktop/helper/Params.swift +133 -0
  59. package/packages/extensions/desktop/helper/Probe.swift +119 -0
  60. package/packages/extensions/desktop/helper/Protocol.swift +224 -0
  61. package/packages/extensions/desktop/helper/Request.swift +51 -0
  62. package/packages/extensions/desktop/helper/Responsibility.swift +133 -0
  63. package/packages/extensions/desktop/helper/Server.swift +334 -0
  64. package/packages/extensions/desktop/helper/Synthetic.swift +281 -0
  65. package/packages/extensions/desktop/helper/build.sh +53 -0
  66. package/packages/extensions/desktop/helper/main.swift +79 -0
  67. package/packages/extensions/desktop/helper/selftest-driver +626 -0
  68. package/packages/extensions/desktop/helper/selftest.sh +16 -0
  69. package/packages/extensions/desktop/helper-win/Capture.cs +299 -0
  70. package/packages/extensions/desktop/helper-win/H264.cs +340 -0
  71. package/packages/extensions/desktop/helper-win/Input.cs +257 -0
  72. package/packages/extensions/desktop/helper-win/Jpeg.cs +75 -0
  73. package/packages/extensions/desktop/helper-win/Main.cs +118 -0
  74. package/packages/extensions/desktop/helper-win/Native.cs +276 -0
  75. package/packages/extensions/desktop/helper-win/Params.cs +138 -0
  76. package/packages/extensions/desktop/helper-win/Protocol.cs +206 -0
  77. package/packages/extensions/desktop/helper-win/Server.cs +304 -0
  78. package/packages/extensions/desktop/helper-win/Synthetic.cs +88 -0
  79. package/packages/extensions/desktop/helper-win/build.sh +57 -0
  80. package/packages/extensions/desktop/helper-win/selftest.sh +36 -0
  81. package/packages/extensions/desktop/waterplane-plugin.json +24 -0
  82. package/packages/host/dist/alwayson.js +1 -1
  83. package/packages/host/dist/alwayson.js.map +1 -1
  84. package/packages/host/dist/attentionstore.d.ts +4 -0
  85. package/packages/host/dist/attentionstore.js +17 -0
  86. package/packages/host/dist/attentionstore.js.map +1 -1
  87. package/packages/host/dist/auth.d.ts +28 -0
  88. package/packages/host/dist/auth.js +65 -2
  89. package/packages/host/dist/auth.js.map +1 -1
  90. package/packages/host/dist/cli/pair.d.ts +4 -0
  91. package/packages/host/dist/cli/pair.js +18 -5
  92. package/packages/host/dist/cli/pair.js.map +1 -1
  93. package/packages/host/dist/der.d.ts +19 -0
  94. package/packages/host/dist/der.js +51 -0
  95. package/packages/host/dist/der.js.map +1 -0
  96. package/packages/host/dist/doctor.d.ts +2 -0
  97. package/packages/host/dist/doctor.js +10 -2
  98. package/packages/host/dist/doctor.js.map +1 -1
  99. package/packages/host/dist/extensions/desktop/host/auto.d.mts +71 -0
  100. package/packages/host/dist/extensions/desktop/host/auto.mjs +114 -0
  101. package/packages/host/dist/extensions/desktop/host/auto.mjs.map +1 -0
  102. package/packages/host/dist/extensions/desktop/host/desktop.d.mts +51 -0
  103. package/packages/host/dist/extensions/desktop/host/desktop.mjs +113 -0
  104. package/packages/host/dist/extensions/desktop/host/desktop.mjs.map +1 -0
  105. package/packages/host/dist/extensions/desktop/host/helperbin.d.mts +71 -0
  106. package/packages/host/dist/extensions/desktop/host/helperbin.mjs +160 -0
  107. package/packages/host/dist/extensions/desktop/host/helperbin.mjs.map +1 -0
  108. package/packages/host/dist/extensions/desktop/host/index.d.mts +4 -0
  109. package/packages/host/dist/extensions/desktop/host/index.mjs +88 -0
  110. package/packages/host/dist/extensions/desktop/host/index.mjs.map +1 -0
  111. package/packages/host/dist/extensions/desktop/host/knobs.d.mts +52 -0
  112. package/packages/host/dist/extensions/desktop/host/knobs.mjs +81 -0
  113. package/packages/host/dist/extensions/desktop/host/knobs.mjs.map +1 -0
  114. package/packages/host/dist/extensions/desktop/host/probe.d.mts +57 -0
  115. package/packages/host/dist/extensions/desktop/host/probe.mjs +119 -0
  116. package/packages/host/dist/extensions/desktop/host/probe.mjs.map +1 -0
  117. package/packages/host/dist/extensions/desktop/host/records.d.mts +40 -0
  118. package/packages/host/dist/extensions/desktop/host/records.mjs +101 -0
  119. package/packages/host/dist/extensions/desktop/host/records.mjs.map +1 -0
  120. package/packages/host/dist/extensions/desktop/host/session.d.mts +86 -0
  121. package/packages/host/dist/extensions/desktop/host/session.mjs +318 -0
  122. package/packages/host/dist/extensions/desktop/host/session.mjs.map +1 -0
  123. package/packages/host/dist/extensions/desktop/host/setup.d.mts +28 -0
  124. package/packages/host/dist/extensions/desktop/host/setup.mjs +217 -0
  125. package/packages/host/dist/extensions/desktop/host/setup.mjs.map +1 -0
  126. package/packages/host/dist/extensions/desktop/host/status.d.mts +27 -0
  127. package/packages/host/dist/extensions/desktop/host/status.mjs +62 -0
  128. package/packages/host/dist/extensions/desktop/host/status.mjs.map +1 -0
  129. package/packages/host/dist/extensions/desktop/shared/wire.d.mts +206 -0
  130. package/packages/host/dist/extensions/desktop/shared/wire.mjs +174 -0
  131. package/packages/host/dist/extensions/desktop/shared/wire.mjs.map +1 -0
  132. package/packages/host/dist/git.d.ts +23 -6
  133. package/packages/host/dist/git.js +54 -26
  134. package/packages/host/dist/git.js.map +1 -1
  135. package/packages/host/dist/gitfetchfail.d.ts +24 -0
  136. package/packages/host/dist/gitfetchfail.js +62 -0
  137. package/packages/host/dist/gitfetchfail.js.map +1 -0
  138. package/packages/host/dist/gitlog.d.ts +4 -0
  139. package/packages/host/dist/gitlog.js +8 -3
  140. package/packages/host/dist/gitlog.js.map +1 -1
  141. package/packages/host/dist/gitnotify.d.ts +7 -1
  142. package/packages/host/dist/gitnotify.js +61 -1
  143. package/packages/host/dist/gitnotify.js.map +1 -1
  144. package/packages/host/dist/host.js +5 -1
  145. package/packages/host/dist/host.js.map +1 -1
  146. package/packages/host/dist/hoststore.js +4 -4
  147. package/packages/host/dist/hoststore.js.map +1 -1
  148. package/packages/host/dist/lantls.d.ts +145 -0
  149. package/packages/host/dist/lantls.js +337 -0
  150. package/packages/host/dist/lantls.js.map +1 -0
  151. package/packages/host/dist/main.js +7 -2
  152. package/packages/host/dist/main.js.map +1 -1
  153. package/packages/host/dist/net.d.ts +7 -0
  154. package/packages/host/dist/net.js +9 -0
  155. package/packages/host/dist/net.js.map +1 -1
  156. package/packages/host/dist/origin.d.ts +4 -0
  157. package/packages/host/dist/origin.js +6 -0
  158. package/packages/host/dist/origin.js.map +1 -1
  159. package/packages/host/dist/pair.d.ts +54 -5
  160. package/packages/host/dist/pair.js +139 -34
  161. package/packages/host/dist/pair.js.map +1 -1
  162. package/packages/host/dist/pairpake.d.ts +58 -0
  163. package/packages/host/dist/pairpake.js +208 -0
  164. package/packages/host/dist/pairpake.js.map +1 -0
  165. package/packages/host/dist/passive.d.ts +2 -0
  166. package/packages/host/dist/passive.js +4 -0
  167. package/packages/host/dist/passive.js.map +1 -1
  168. package/packages/host/dist/peers/client.d.ts +3 -1
  169. package/packages/host/dist/peers/client.js +4 -0
  170. package/packages/host/dist/peers/client.js.map +1 -1
  171. package/packages/host/dist/peers/routes.d.ts +6 -1
  172. package/packages/host/dist/peers/routes.js +39 -0
  173. package/packages/host/dist/peers/routes.js.map +1 -1
  174. package/packages/host/dist/plugins/pluginapi.js +13 -0
  175. package/packages/host/dist/plugins/pluginapi.js.map +1 -1
  176. package/packages/host/dist/plugins/pluginregistry.d.ts +6 -0
  177. package/packages/host/dist/plugins/pluginregistry.js.map +1 -1
  178. package/packages/host/dist/prepushcheck.d.ts +34 -0
  179. package/packages/host/dist/prepushcheck.js +157 -0
  180. package/packages/host/dist/prepushcheck.js.map +1 -0
  181. package/packages/host/dist/prs.js +2 -1
  182. package/packages/host/dist/prs.js.map +1 -1
  183. package/packages/host/dist/routes/apigitcheck.d.ts +2 -0
  184. package/packages/host/dist/routes/apigitcheck.js +29 -0
  185. package/packages/host/dist/routes/apigitcheck.js.map +1 -0
  186. package/packages/host/dist/routes/apihost.js +2 -0
  187. package/packages/host/dist/routes/apihost.js.map +1 -1
  188. package/packages/host/dist/routes/apiio.js +2 -1
  189. package/packages/host/dist/routes/apiio.js.map +1 -1
  190. package/packages/host/dist/routes/apirepofiles.js +2 -1
  191. package/packages/host/dist/routes/apirepofiles.js.map +1 -1
  192. package/packages/host/dist/routes/apisystem.js +21 -3
  193. package/packages/host/dist/routes/apisystem.js.map +1 -1
  194. package/packages/host/dist/server/http.js +31 -5
  195. package/packages/host/dist/server/http.js.map +1 -1
  196. package/packages/host/dist/server/plugins.d.ts +13 -0
  197. package/packages/host/dist/server/plugins.js +26 -0
  198. package/packages/host/dist/server/plugins.js.map +1 -1
  199. package/packages/host/dist/server/wsops.js +3 -1
  200. package/packages/host/dist/server/wsops.js.map +1 -1
  201. package/packages/host/dist/sysstat.js +10 -1
  202. package/packages/host/dist/sysstat.js.map +1 -1
  203. package/packages/host/dist/updatekey.d.ts +22 -0
  204. package/packages/host/dist/updatekey.js +140 -0
  205. package/packages/host/dist/updatekey.js.map +1 -0
  206. package/packages/host/dist/webbundle.d.ts +15 -6
  207. package/packages/host/dist/webbundle.js +44 -7
  208. package/packages/host/dist/webbundle.js.map +1 -1
  209. package/packages/host/dist/wincpu.d.ts +13 -0
  210. package/packages/host/dist/wincpu.js +101 -0
  211. package/packages/host/dist/wincpu.js.map +1 -0
  212. package/packages/host/package.json +2 -1
  213. package/packages/web/dist/assets/demo-BLBaz4G_.js +26 -0
  214. package/packages/web/dist/assets/icons-D8RlmSLR.js +1 -0
  215. package/packages/web/dist/assets/index-BrvcQFBj.js +170 -0
  216. package/packages/web/dist/assets/index-Dx3VHnr3.css +1 -0
  217. package/packages/web/dist/assets/noble-DV-r0OkE.js +1 -0
  218. package/packages/web/dist/assets/{react-CoSyT_xY.js → react-CjmvHigY.js} +2 -2
  219. package/packages/web/dist/assets/rolldown-runtime-4tVxZq34.js +1 -0
  220. package/packages/web/dist/assets/shared-BsWIJLE-.js +51 -0
  221. package/packages/web/dist/assets/sheets-C7CXUU6R.js +250 -0
  222. package/packages/web/dist/assets/xterm-BHsKrDzh.js +16 -0
  223. package/packages/web/dist/build-id.txt +1 -1
  224. package/packages/web/dist/buildstamp.json +1 -1
  225. package/packages/web/dist/index.html +9 -9
  226. package/packages/web/dist/sw.js +2 -2
  227. package/release.json +7 -3
  228. package/packages/web/dist/assets/demo-DkCtg-SJ.js +0 -26
  229. package/packages/web/dist/assets/icons-B48F-4nA.js +0 -1
  230. package/packages/web/dist/assets/index-CpdyS6Xe.css +0 -1
  231. package/packages/web/dist/assets/index-EYc28CfH.js +0 -170
  232. package/packages/web/dist/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
  233. package/packages/web/dist/assets/shared-DjRRLGfw.js +0 -50
  234. package/packages/web/dist/assets/sheets-ByWNPa9p.js +0 -248
  235. 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
- export declare function pairDeepLink(base: string, code: string): string;
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
- export function pairDeepLink(base, code) {
50
- return `${PAIR_SCHEME}://pair?u=${encodeURIComponent(base)}&c=${encodeURIComponent(code)}`;
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
- return base && code ? { base, code } : null;
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":"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"}
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