@ccmsg/protocol 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/common/auth.ts +44 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/protocol",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Wire contract (schema + types + op attribute table) shared by the ccmsg daemon and web UI",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -92,6 +92,13 @@ export const RegisterClaims = Type.Object(
92
92
  expires_at: Timestamp,
93
93
  /** Names this registration, so it can be spent once. */
94
94
  jti: Type.String({ minLength: 1 }),
95
+ /** The WebAuthn user handle for this subject: sixteen random bytes the
96
+ * issuing instance settles on once per `sub`. The page creates the
97
+ * credential against it, the record keeps it, and an assertion that names a
98
+ * handle is held to it. It is here rather than left to the page because the
99
+ * authenticator stores it beyond this instance's reach — a second value for
100
+ * one person would be a second account on their device. */
101
+ user_id: Base64Url,
95
102
  /** What the administrator who issued the URL wrote down about who it was
96
103
  * for. Their words, not the holder's — the label the person gives their
97
104
  * own device is `device_label` on the record, and the two are worth telling
@@ -242,7 +249,16 @@ export const AuthRefreshResponse = response("auth_refresh", AuthRefreshResult);
242
249
  * verification, the record lookup — the receiving instance does itself. */
243
250
  export const AuthResolveArgs = Type.Union(
244
251
  [
245
- Type.Object({ kind: Type.Literal("register"), token: Type.String({ minLength: 1 }) }),
252
+ Type.Object({
253
+ kind: Type.Literal("register"),
254
+ token: Type.String({ minLength: 1 }),
255
+ /** The digits the person typed, forwarded unchecked. The issuer holds
256
+ * both the code and the count of attempts against it, so it is the only
257
+ * one that can refuse a wrong one and retire the URL after enough of
258
+ * them; a receiver that judged the code itself would let an attacker
259
+ * spread guesses across instances without any of them counting. */
260
+ code: Type.String({ pattern: "^[0-9]{6}$" }),
261
+ }),
246
262
  Type.Object({ kind: Type.Literal("challenge"), challenge: Base64Url }),
247
263
  ],
248
264
  { $id: "AuthResolveArgs" },
@@ -300,7 +316,9 @@ export const CredentialRecord = Type.Object(
300
316
  credential_id: Base64Url,
301
317
  /** The public key, COSE-encoded. */
302
318
  public_key: Base64Url,
303
- /** The `user.id` this credential was created against. */
319
+ /** The `user_id` of the registration's claims, which is what the credential
320
+ * was created against and what an assertion naming a handle is checked
321
+ * against. */
304
322
  user_handle: Base64Url,
305
323
  /** The relying party this credential was created under, as the claims of
306
324
  * the registration that made it stated. Written by the registration and not
@@ -355,6 +373,30 @@ export const TokenFamily = Type.Object(
355
373
  refresh: Type.Object({ value: Base64Url, expires_at: Timestamp }),
356
374
  /** The generation before the current one, while the grace for it lasts. */
357
375
  previous_refresh: Type.Optional(Type.Object({ value: Base64Url, expires_at: Timestamp })),
376
+ /** What every generation retired before that was, kept only as a digest and
377
+ * only until the value itself would have expired.
378
+ *
379
+ * Recognising a replay takes remembering the value, but holding it is what
380
+ * the family is trying to protect — these travel to every instance, and a
381
+ * retired token still inside its lifetime would be a live secret copied
382
+ * around for no purpose it could serve. A digest answers the one question
383
+ * asked of it, that a value presented now was once issued here and is no
384
+ * longer, which fails the whole family.
385
+ *
386
+ * Written by the `iss` alone, like the rest of the family, and replicated,
387
+ * so the memory survives that instance restarting and holds wherever the
388
+ * reused value is presented. */
389
+ retired: Type.Optional(
390
+ Type.Array(
391
+ Type.Object({
392
+ /** sha256 of the retired value, lowercase hex. */
393
+ hash: Type.String({ pattern: "^[0-9a-f]{64}$" }),
394
+ /** When the value would have expired, after which remembering it
395
+ * refuses nothing that its own expiry would not. */
396
+ expires_at: Timestamp,
397
+ }),
398
+ ),
399
+ ),
358
400
  },
359
401
  { $id: "TokenFamily" },
360
402
  );