@interncom/diplomatic 0.13.6 → 0.14.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.
@@ -2,58 +2,94 @@ import { Enclave, type PasskeyPrfOpts } from "../shared/crypto/enclave";
2
2
  import { type SealedMasterKey } from "../shared/seed";
3
3
  import { type ValStat } from "../shared/valstat";
4
4
  import type { ISeedStore, SetSeedOpts } from "../types";
5
- import type { PrfRp } from "../shared/webauthn/prf";
6
- export type PrfSeedMeta = {
7
- /** AEAD-sealed master under KDF(PRF). */
5
+ import { type PrfRp } from "../shared/webauthn/prf";
6
+ import { type AuthenticatorAttachmentName, type EnrolledOs } from "../shared/webauthn/common";
7
+ /** Max bindings on one device. Keeps allowCredentials small. */
8
+ export declare const KEYRING_MAX = 8;
9
+ export type KeyringType = "prf";
10
+ /** One PRF binding (one binding key). Future types (shard, …) share this list. */
11
+ export type KeyringEntry = {
12
+ type: "prf";
8
13
  sealedMaster: SealedMasterKey;
9
- /** PRF salt used at wrap time. */
14
+ credId: Uint8Array;
15
+ /** blake3(master ‖ bindtag ‖ credId); check-only, not a display fingerprint. */
16
+ tag: Uint8Array;
17
+ userId?: Uint8Array;
18
+ nick?: string;
19
+ enrolledAt: number;
20
+ lastUsedAt?: number;
21
+ attachment?: AuthenticatorAttachmentName;
22
+ transports?: string[];
23
+ aaguid?: Uint8Array;
24
+ enrolledOs?: EnrolledOs;
25
+ };
26
+ /** Local keyring: one PRF salt, a flat list of typed bindings. */
27
+ export type Keyring = {
10
28
  salt: Uint8Array;
11
- /**
12
- * Last PRF passkey id (allowCredentials). The sealed-meta row itself is
13
- * the breadcrumb that this device enrolled PRF unlock.
14
- */
15
- credId?: Uint8Array;
29
+ entries: KeyringEntry[];
30
+ };
31
+ /** Public row no sealed bytes. */
32
+ export type KeyringRow = {
33
+ type: "prf";
34
+ credId: Uint8Array;
35
+ userId?: Uint8Array;
36
+ nick?: string;
37
+ enrolledAt: number;
38
+ lastUsedAt?: number;
39
+ attachment?: AuthenticatorAttachmentName;
40
+ transports?: string[];
41
+ aaguid?: Uint8Array;
42
+ enrolledOs?: EnrolledOs;
16
43
  };
17
44
  /**
18
- * Persist sealed-master metadata for cold start (e.g. protocol IDB seedMeta).
45
+ * Persist the keyring for cold start (e.g. protocol IDB).
19
46
  * Called with `undefined` when the store is wiped.
20
47
  */
21
- export type PersistPrfSeedMeta = (meta: PrfSeedMeta | undefined) => void | Promise<void>;
48
+ export type PersistKeyring = (ring: Keyring | undefined) => void | Promise<void>;
22
49
  export type PrfSeedStoreOpts = PrfRp & {
23
- /**
24
- * Required durable write path for sealed master + salt + credId.
25
- * Protocol IDB implements this via {@link IDBSeedStore.persistPrfMeta}.
26
- */
27
- persistMeta: PersistPrfSeedMeta;
28
- /** Previously persisted meta (cold start). Does not re-call persistMeta. */
29
- meta?: PrfSeedMeta;
50
+ persistKeyring: PersistKeyring;
51
+ keyring?: Keyring;
30
52
  };
31
53
  /**
32
- * Session + durable PRF-wrapped seed. Session secret is always {@link Enclave}.
54
+ * Session + durable PRF-bound seed. Session secret is always {@link Enclave}.
33
55
  *
34
56
  * - {@link save} is memory-only (holds enclave).
35
- * - {@link wrapAndSave} runs PRF UV inside Enclave and persists sealed meta.
57
+ * - {@link bindAndSave} creates a binding and appends/upserts a keyring entry.
36
58
  * - {@link unlock} runs PRF UV inside Enclave and returns a new enclave.
37
- *
38
- * Fallback when PRF is unavailable (not implemented yet): passphrase-sealed
39
- * meta with the same layout — no plain seed on disk either way.
40
59
  */
41
60
  export declare class PrfSeedStore implements ISeedStore {
42
61
  #private;
43
62
  constructor(opts: PrfSeedStoreOpts);
44
- get meta(): PrfSeedMeta | undefined;
45
- setMeta(meta: PrfSeedMeta | undefined): void;
63
+ get keyring(): Keyring | undefined;
64
+ /** Public rows (no sealed master). */
65
+ list(): KeyringRow[];
46
66
  save(enclave: Enclave, opts?: SetSeedOpts): Promise<Enclave>;
47
67
  /**
48
- * PRF UV inside Enclave, seal master, persist meta (no PRF leaves Enclave).
68
+ * PRF UV inside Enclave.bind, persist the entry.
69
+ * Always creates a new cred unless `credId` is passed (re-bind that cred).
70
+ * Existing tags are checked inside the enclave (one keyring, one master).
49
71
  */
50
- wrapAndSave(enclave: Enclave, opts?: Pick<PasskeyPrfOpts, "salt" | "credId" | "createCredIfNeeded" | "authenticatorAttachment" | "hints">): Promise<ValStat<Enclave>>;
72
+ bindAndSave(enclave: Enclave, opts?: Pick<PasskeyPrfOpts, "salt" | "credId" | "createCredIfNeeded" | "authenticatorAttachment" | "hints" | "excludeCredentials"> & {
73
+ nick?: string;
74
+ }): Promise<ValStat<Enclave>>;
51
75
  load(): Promise<Enclave | void>;
52
76
  /** Passkey UV inside Enclave → new session enclave. */
53
77
  unlock(): Promise<ValStat<Enclave>>;
78
+ /** Set a display nick (local only; does not update the binding key). */
79
+ rename(credId: Uint8Array, nick: string): Promise<ValStat<void>>;
54
80
  /**
55
- * Install sealed meta + session enclave (e.g. after pair-package open) and persist.
81
+ * Drop a binding from this browser. Does not delete the binding-key cred.
82
+ * Last entry clears the keyring.
56
83
  */
57
- adoptSealed(enclave: Enclave, meta: PrfSeedMeta): Promise<ValStat<Enclave>>;
84
+ remove(credId: Uint8Array): Promise<ValStat<void>>;
58
85
  wipe(): Promise<void>;
59
86
  }
87
+ export declare function cloneKeyring(r: Keyring): Keyring;
88
+ /** Read a keyring from IDB structured-clone output. */
89
+ export declare function decodeKeyring(raw: unknown): Keyring | undefined;
90
+ /** Cred ids of every keyring entry. */
91
+ export declare function keyringCredIds(ring: Keyring | undefined): Uint8Array[];
92
+ /** Inferred kind for UI. Not attested. */
93
+ export declare function keyringKind(row: KeyringRow): string;
94
+ /** Display label: nick, else inferred kind + short cred id. */
95
+ export declare function keyringLabel(row: KeyringRow): string;
@@ -5,20 +5,33 @@ import type { ICrypto, PublicKey } from "../types.ts";
5
5
  import { type ValStat } from "../valstat.ts";
6
6
  import { type DHKEReq, type DHKEResp, PairRequest } from "./pairing.ts";
7
7
  import { type LargeBlobCreateOpts, type LargeBlobRp } from "../webauthn/largeBlob.ts";
8
- import { type PrfCreateOpts, type PrfEvalOpts, type PrfRp } from "../webauthn/prf.ts";
8
+ import { type PrfCeremony, type PrfCreateOpts, type PrfEvalOpts, type PrfRp } from "../webauthn/prf.ts";
9
9
  export type { LargeBlobCreateOpts, LargeBlobRp };
10
10
  export type { PrfCreateOpts, PrfEvalOpts, PrfRp };
11
11
  /** Options for PRF seal/unseal ceremonies (no raw PRF bytes). */
12
12
  export type PasskeyPrfOpts = PrfCreateOpts & {
13
- credId?: Uint8Array;
13
+ credId?: Uint8Array | readonly Uint8Array[];
14
14
  /** Seal only: create a PRF credential first when no credId is known. */
15
15
  createCredIfNeeded?: boolean;
16
16
  };
17
- /** Durable PRF-sealed master + public meta (never includes PRF output). */
18
- export type PrfSealedMaster = {
17
+ /** One binding to try on unseal (cred id + sealed master). */
18
+ export type PrfBinding = {
19
+ sealedMaster: SealedMasterKey;
20
+ credId: Uint8Array;
21
+ };
22
+ /** Durable PRF-sealed master + public ceremony facts (never includes PRF output). */
23
+ export type PrfSealedMaster = PrfCeremony & {
19
24
  sealedMaster: SealedMasterKey;
20
25
  salt: Uint8Array;
26
+ };
27
+ /** One existing keyring member used to prove this enclave matches the list. */
28
+ export type BindPrior = {
21
29
  credId: Uint8Array;
30
+ tag: Uint8Array;
31
+ };
32
+ /** Seal + bind-tag from {@link Enclave.bind} (tag is not a global fingerprint). */
33
+ export type PrfBound = PrfSealedMaster & {
34
+ tag: Uint8Array;
22
35
  };
23
36
  export type EncryptCipher = {
24
37
  encrypt: (data: Uint8Array) => Promise<Uint8Array>;
@@ -46,7 +59,7 @@ export type Identity = {
46
59
  /** Per-bag KDM mixed with this identity's private key (see bag seal). */
47
60
  kdmFor: (msgHeadEnc: Uint8Array) => Promise<Uint8Array>;
48
61
  };
49
- declare const SEAL_WRAP_DOMAIN: Uint8Array<ArrayBufferLike>;
62
+ declare const SEAL_BIND_DOMAIN: Uint8Array<ArrayBufferLike>;
50
63
  declare const SEAL_KEY_LEN = 32;
51
64
  declare const SEAL_PRF_MIN_LEN = 16;
52
65
  export declare class Enclave {
@@ -69,11 +82,16 @@ export declare class Enclave {
69
82
  */
70
83
  sealWithPasskey(opts?: PasskeyPrfOpts): Promise<ValStat<PrfSealedMaster>>;
71
84
  /**
72
- * UV + PRF ceremony, then unseal durable master into a new Enclave.
73
- * Callers never supply PRF bytes only sealed meta + RP/salt/credId.
74
- * Returns the credential id used so stores can persist it (skip picker next time).
85
+ * UV + PRF seal, plus a per-cred bind tag (blake3(master domain credId)).
86
+ * Tag compute and compare stay in the enclave. If `prior` is non-empty, every
87
+ * stored tag must match this master or bind fails (one keyring, one master).
88
+ */
89
+ bind(opts?: PasskeyPrfOpts, prior?: readonly BindPrior[]): Promise<ValStat<PrfBound>>;
90
+ /**
91
+ * UV + PRF ceremony, then unseal one of `bindings` into a new Enclave.
92
+ * allowCredentials is the union of binding cred ids. Returns the asserted id.
75
93
  */
76
- static unsealWithPasskey(sealedMaster: SealedMasterKey, opts: PasskeyPrfOpts & {
94
+ static unsealWithPasskey(bindings: readonly PrfBinding[], opts: PasskeyPrfOpts & {
77
95
  salt: Uint8Array;
78
96
  }): Promise<ValStat<{
79
97
  enclave: Enclave;
@@ -135,4 +153,4 @@ export declare class Enclave {
135
153
  }
136
154
  /** Derive seal key from PRF output (domain-separated). Used by enclave seal/unseal. */
137
155
  export declare function sealKeyFromPrf(crypto: ICrypto, prf: Uint8Array, domain?: Uint8Array): Promise<ValStat<Uint8Array>>;
138
- export { MASTER_SEED_LEN, SEAL_KEY_LEN, SEAL_PRF_MIN_LEN, SEAL_WRAP_DOMAIN };
156
+ export { MASTER_SEED_LEN, SEAL_BIND_DOMAIN, SEAL_KEY_LEN, SEAL_PRF_MIN_LEN };
@@ -13,7 +13,7 @@ export type MasterSeed = Uint8Array & {
13
13
  };
14
14
  /**
15
15
  * Master seed sealed under KDF(PRF) (nonce‖ciphertext‖tag).
16
- * Useless without the PRF output that derives the wrap key.
16
+ * Useless without the PRF output that derives the binding key.
17
17
  */
18
18
  export type SealedMasterKey = Uint8Array & {
19
19
  readonly [sealedMasterKeySymbol]: true;
@@ -5,7 +5,7 @@ export declare const WEBAUTHN_CHAL_LEN = 32;
5
5
  export declare const COSE_ALG_ES256 = -7;
6
6
  export declare const COSE_ALG_EDDSA = -8;
7
7
  export declare const COSE_ALG_RS256 = -257;
8
- /** Prefer modern curves; include RS256 for broader authenticator coverage. */
8
+ /** Prefer modern curves; include RS256 for broader binding-key coverage. */
9
9
  export declare const WEBAUTHN_PUB_KEY_PARAMS: PublicKeyCredentialParameters[];
10
10
  /** UA preference for the WebAuthn picker. Not exclusive; omit attachment to allow both. */
11
11
  export type WebAuthnHint = "security-key" | "client-device" | "hybrid";
@@ -51,3 +51,13 @@ export declare function asPublicKeyCredential(cred: Credential | null): ValStat<
51
51
  * `extKey` e.g. `"extension:largeBlob"`, `"extension:prf"`.
52
52
  */
53
53
  export declare function webAuthnExtensionCapable(extKey: string): Promise<boolean>;
54
+ export type AuthenticatorAttachmentName = "platform" | "cross-platform";
55
+ export type EnrolledOs = "ios" | "macos" | "android" | "windows" | "linux" | "other";
56
+ /** Maps a WebAuthn attachment string to the two legal values. */
57
+ export declare function readAttachment(v: string | null | undefined): AuthenticatorAttachmentName | undefined;
58
+ /** Coarse OS of this browser (enrollment / display only). */
59
+ export declare function enrolledOsFromUA(ua: string): EnrolledOs;
60
+ /** `getTransports()` on an attestation response, if the UA exposes it. */
61
+ export declare function readTransports(pk: PublicKeyCredential): string[] | undefined;
62
+ /** AAGUID from attested credential data (create). Undefined if AT is absent. */
63
+ export declare function readAaguid(pk: PublicKeyCredential): Uint8Array | undefined;
@@ -1,5 +1,5 @@
1
1
  import { type ValStat } from "../valstat.ts";
2
- import { type WebAuthnRp } from "./common.ts";
2
+ import { type AuthenticatorAttachmentName, type WebAuthnRp } from "./common.ts";
3
3
  export type PrfRp = WebAuthnRp;
4
4
  export type PrfCreateOpts = PrfRp & {
5
5
  userName?: string;
@@ -7,22 +7,33 @@ export type PrfCreateOpts = PrfRp & {
7
7
  authenticatorAttachment?: "platform" | "cross-platform";
8
8
  /** Seal salt (eval is on get, not create — some UAs throw on create-time eval). */
9
9
  salt?: Uint8Array;
10
+ /** Already-bound cred ids so create does not assert an existing key. */
11
+ excludeCredentials?: readonly Uint8Array[];
10
12
  };
11
13
  /** Default salt for DIPLOMATIC PRF eval (UTF-8). */
12
14
  export declare const DEFAULT_PRF_SALT: Uint8Array<ArrayBufferLike>;
15
+ /** Default WebAuthn user.name / displayName when none is supplied. */
16
+ export declare const DEFAULT_PRF_USER_NAME = "diplomatic-prf";
13
17
  /** Bytes of PRF output we consume (first N of `results.first`). */
14
18
  export declare const PRF_OUTPUT_LEN = 32;
15
- export type PrfCreateResult = {
19
+ /** Public ceremony facts (no PRF bytes). */
20
+ export type PrfCeremony = {
16
21
  credId: Uint8Array;
22
+ userId?: Uint8Array;
23
+ attachment?: AuthenticatorAttachmentName;
24
+ transports?: string[];
25
+ aaguid?: Uint8Array;
26
+ };
27
+ export type PrfCreateResult = PrfCeremony & {
17
28
  prfEnabled: boolean;
18
29
  };
19
30
  /** Internal: PRF output + cred id. For Enclave only — do not re-export to apps. */
20
- export type PrfEvalResult = {
31
+ export type PrfEvalResult = PrfCeremony & {
21
32
  prf: Uint8Array;
22
- credId: Uint8Array;
23
33
  };
24
34
  export type PrfEvalOpts = PrfRp & {
25
- credId?: Uint8Array;
35
+ /** One id, or every bound id so a spare key can assert in one get(). */
36
+ credId?: Uint8Array | readonly Uint8Array[];
26
37
  salt?: Uint8Array;
27
38
  };
28
39
  /** Best-effort: client advertises PRF extension. */
@@ -1,34 +1,32 @@
1
1
  import { Enclave } from "../../shared/crypto/enclave";
2
2
  import type { ICrypto } from "../../shared/types";
3
3
  import type { ISeedStore, SetSeedOpts } from "../../types";
4
- import { type PrfSeedMeta, PrfSeedStore, type PrfSeedStoreOpts } from "../../passkey/prf-store";
4
+ import { type Keyring, PrfSeedStore, type PrfSeedStoreOpts } from "../../passkey/prf-store";
5
5
  export declare class IDBSeedStore implements ISeedStore {
6
6
  #private;
7
7
  db: IDBDatabase;
8
8
  constructor(db: IDBDatabase, crypto: ICrypto);
9
9
  /**
10
- * Hold enclave in memory only. Durable identity is sealed meta
11
- * ({@link openPrfStore} / {@link persistPrfMeta}), never plain seed.
10
+ * Hold enclave in memory only. Durable identity is the keyring
11
+ * ({@link openPrfStore} / {@link persistKeyring}), never plain seed.
12
12
  * Pins identity (nonce+hash) on first save; later saves must match
13
13
  * (largeBlob / PRF unlock cannot switch masters under this DB).
14
14
  * `opts.persist` is ignored (kept for API compatibility).
15
15
  *
16
16
  * TODO(passphrase): when PRF is missing, `persist: true` (or a dedicated
17
- * wrap API) should seal under a passphrase KDF and write sealed meta here —
18
- * same shape as prfMeta, not a return of raw seed.
17
+ * bind API) should seal under a passphrase KDF and write sealed meta here —
18
+ * same shape as a keyring entry, not a return of raw seed.
19
19
  */
20
20
  save(enclave: Enclave, _opts?: SetSeedOpts): Promise<Enclave>;
21
21
  load(): Promise<Enclave | undefined>;
22
22
  /**
23
- * Drop in-memory enclave. Keeps {@link K_PRF_META} and {@link K_ID_PIN}
23
+ * Drop in-memory enclave. Keeps {@link K_KEYRING} and {@link K_ID_PIN}
24
24
  * so unlock / largeBlob restore still match this device.
25
25
  */
26
26
  clearSession(): Promise<void>;
27
27
  wipe(): Promise<void>;
28
- loadPrfMeta(): Promise<PrfSeedMeta | undefined>;
29
- persistPrfMeta(meta: PrfSeedMeta | undefined): Promise<void>;
30
- hasPrfMeta(): Promise<boolean>;
31
- /** Last PRF passkey id, if this device has a sealed PRF identity. */
32
- lastPrfCredId(): Promise<Uint8Array | undefined>;
33
- openPrfStore(opts: Omit<PrfSeedStoreOpts, "persistMeta" | "meta">): Promise<PrfSeedStore>;
28
+ loadKeyring(): Promise<Keyring | undefined>;
29
+ persistKeyring(ring: Keyring | undefined): Promise<void>;
30
+ hasKeyring(): Promise<boolean>;
31
+ openPrfStore(opts: Omit<PrfSeedStoreOpts, "persistKeyring" | "keyring">): Promise<PrfSeedStore>;
34
32
  }
@@ -35,7 +35,7 @@ export type SetSeedOpts = {
35
35
  /**
36
36
  * When true, request durable storage **if the store supports a non-plaintext
37
37
  * durable form** (e.g. passkey largeBlob as IdentityBundle wire).
38
- * IDB holds the enclave in memory only — durable identity is PRF-sealed meta
38
+ * IDB holds the enclave in memory only — durable identity is the PRF keyring
39
39
  * via {@link IDBSeedStore.openPrfStore}, never plain seed.
40
40
  * Default false (session enclave only).
41
41
  *
@@ -50,7 +50,7 @@ export type SetSeedOpts = {
50
50
  * Default `client.wipe()` / `client.wipe({})` clears **everything except seed**:
51
51
  * msgs, ents, meta = true; seed = false.
52
52
  * That avoids passkey/largeBlob UV when the user only wants to clear app data.
53
- * Pass `{ seed: true }` to also clear identity (may prompt the authenticator).
53
+ * Pass `{ seed: true }` to also clear identity (may prompt the binding key).
54
54
  */
55
55
  export type WipeOpts = {
56
56
  /** Message archive. Default true. */