@le-space/orbitdb-identity-provider-webauthn-did 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changes
2
2
 
3
+ ## 0.7.0
4
+
5
+ **One breaking change, in the function 0.6.0 added.** `restoreIdentityFromAuthenticator()`
6
+ returned the credential id as bytes, under `credentialId` — the name the rest of this package
7
+ uses for the base64url text. An application that passed the bytes on as `rawCredentialId`, and
8
+ so gave the provider no text, got a third touch of the key and then an exception: `WebAuthn
9
+ signing error: Cannot read properties of undefined (reading 'substring')`. That happened on two
10
+ phones on 21 September, in funkpost's recovery page.
11
+
12
+ - `credentialId` is now the base64url **text** and `rawCredentialId` the **bytes**, as
13
+ `WebAuthnCredentialInfo` has always typed them.
14
+ - New: `credential`, the credential the provider takes as it is —
15
+ `OrbitDBWebAuthnIdentityProviderFunction({ webauthnCredential: restored.credential })`.
16
+ - The provider derives either form of the id from the other, so a credential that brings only
17
+ one of them signs instead of failing after the touch.
18
+ - `sign()` no longer builds a debug line from a field that may be missing.
19
+
20
+ **Migrating from 0.6.0:** read the bytes from `restored.rawCredentialId` instead of
21
+ `restored.credentialId`, or hand `restored.credential` to the provider.
22
+
23
+ **Tested now:** the step after the restore — giving the identity to OrbitDB and signing with it
24
+ — which no test reached before. Both new tests fail on 0.6.0, one of them with the exception
25
+ above, word for word.
26
+
3
27
  ## 0.6.0
4
28
 
5
29
  An identity can be restored on a second device with **nothing stored**: two
package/README.md CHANGED
@@ -386,18 +386,22 @@ If you change anything that touches signing, identity documents or verification,
386
386
 
387
387
  ## Identity Recovery Summary
388
388
 
389
- Current identity recovery behavior in this repo:
389
+ Since **0.6.0** an identity can be restored on a device that has stored nothing:
390
390
 
391
- - Discoverable passkeys are the default.
392
- - Discoverable authentication can recover the credential ID used for assertion.
393
- - Discoverable authentication does not reliably re-expose the credential public key.
394
- - Because of that, OrbitDB identity recovery requires metadata persistence.
395
- - The demos now try `largeBlob` first for identity metadata recovery.
396
- - If `largeBlob` is not supported or has no metadata, the demos fall back to local browser storage.
391
+ - Discoverable passkeys are the default, so a device that knows no credential ID can still authenticate.
392
+ - A single assertion does not expose the credential's public key — but **two do**. An ECDSA signature admits exactly two public keys, and only the signer's is in both sets, so `recoverPublicKey()` returns it and the DID follows. Every candidate is checked against the signature it came from, so the recovery cannot invent a key.
393
+ - The PRF input is fixed per relying party, so a second device asks the authenticator the same question and receives the same secret; the signing key is derived from it with the DID mixed in.
394
+ - `restoreIdentityFromAuthenticator()` does both touches and returns the DID, the public key and the signing key — and, since 0.7.0, the credential the provider takes as it is: `OrbitDBWebAuthnIdentityProviderFunction({ webauthnCredential: restored.credential })`.
395
+
396
+ Metadata persistence is therefore a **convenience, not a requirement**. The demos still try `largeBlob` first and fall back to browser storage, which saves a touch of the key; neither is needed to reconstruct the identity.
397
397
 
398
398
  Practical implication:
399
399
 
400
- - If you create a passkey on one browser profile and later open the app in a fresh profile, the passkey may still exist in the platform passkey manager, but the app can only reconstruct the OrbitDB identity if it can recover metadata from `largeBlob` or some other persisted mapping.
400
+ - A passkey created in one browser profile reconstructs the same OrbitDB identity in a fresh profile — or on another phone — with nothing carried over. Measured on one YubiKey across a Galaxy Fold 5 and a Galaxy A57 (2026-09-19): same PRF value, same recovered DID, same derived signing key.
401
+ - An authenticator without PRF gets a **refusal**, not a substitute. An identity derived from something else is a different identity that looks like success.
402
+ - Credentials registered before 0.6.0 keep the random PRF input stored with them, so their identities do not move.
403
+
404
+ Getting the _database_ back is a separate problem, and the two halves together are written up in [Getting a database back on a device that has nothing](https://github.com/NiKrause/orbitdb-storage-bridge/blob/main/docs/RECOVERY-ON-A-SECOND-DEVICE.md).
401
405
 
402
406
  ## Upstream Packages and Temporary Forks
403
407
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/orbitdb-identity-provider-webauthn-did",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "WebAuthn-based DID identity provider for OrbitDB for hardware-secured wallets and biometric Passkey authentication",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -46,9 +46,28 @@ export class WebAuthnDIDProvider {
46
46
  * @param {Uint8Array} credentialInfo.rawCredentialId - Raw credential ID bytes.
47
47
  */
48
48
  constructor(credentialInfo) {
49
- this.credentialId = credentialInfo.credentialId;
49
+ // The id twice, as WebAuthnCredentialInfo types it: text for the signature
50
+ // envelope, bytes for the authenticator. Either gives the other, so a
51
+ // credential that brings only its bytes (as 0.6.0's restore handed them
52
+ // out, under the text's name) still signs, instead of getting through the
53
+ // touch and then failing.
54
+ const { credentialId, rawCredentialId } = credentialInfo;
55
+ this.rawCredentialId =
56
+ rawCredentialId ??
57
+ (typeof credentialId === 'string'
58
+ ? new Uint8Array(
59
+ WebAuthnDIDProvider.base64urlToArrayBuffer(credentialId)
60
+ )
61
+ : credentialId
62
+ ? new Uint8Array(credentialId)
63
+ : undefined);
64
+ this.credentialId =
65
+ typeof credentialId === 'string'
66
+ ? credentialId
67
+ : this.rawCredentialId
68
+ ? WebAuthnDIDProvider.arrayBufferToBase64url(this.rawCredentialId)
69
+ : undefined;
50
70
  this.publicKey = credentialInfo.publicKey;
51
- this.rawCredentialId = credentialInfo.rawCredentialId;
52
71
  this.type = IDENTITY_TYPES.WEBAUTHN;
53
72
  }
54
73
 
@@ -712,8 +731,9 @@ export class WebAuthnDIDProvider {
712
731
  };
713
732
 
714
733
  webauthnLog('Proof created successfully: %o', {
715
- credentialId: webauthnProof.credentialId.substring(0, 16) + '...',
716
- dataHash: webauthnProof.dataHash.substring(0, 16) + '...',
734
+ // A debug line must not be what fails a signature.
735
+ credentialId: `${String(webauthnProof.credentialId).slice(0, 16)}...`,
736
+ dataHash: `${String(webauthnProof.dataHash).slice(0, 16)}...`,
717
737
  });
718
738
 
719
739
  // Return the proof as a base64url encoded string for OrbitDB
@@ -45,7 +45,11 @@ const log = logger('orbitdb-identity-provider-webauthn-did:restore');
45
45
  * @param {(step: {touch: number, of: number}) => void} [options.onTouch] -
46
46
  * Called before each ceremony, so an application can say which touch this is.
47
47
  * @returns {Promise<{did: string, publicKey: {x: Uint8Array, y: Uint8Array},
48
- * credentialId: Uint8Array, signingKey: Uint8Array, prfInput: Uint8Array}>}
48
+ * credentialId: string, rawCredentialId: Uint8Array, signingKey: Uint8Array,
49
+ * prfInput: Uint8Array, credential: Object}>} `credential` is what
50
+ * `OrbitDBWebAuthnIdentityProviderFunction({ webauthnCredential })` takes.
51
+ * `credentialId` is the base64url text, as everywhere else in this package,
52
+ * and `rawCredentialId` the bytes (before 0.7.0, `credentialId` was the bytes).
49
53
  */
50
54
  export async function restoreIdentityFromAuthenticator({
51
55
  rpId,
@@ -111,11 +115,18 @@ export async function restoreIdentityFromAuthenticator({
111
115
  );
112
116
  log('restored %s from the authenticator alone', did);
113
117
 
118
+ const rawCredentialId = new Uint8Array(first.rawId);
119
+ const credentialId =
120
+ WebAuthnDIDProvider.arrayBufferToBase64url(rawCredentialId);
114
121
  return {
115
122
  did,
116
123
  publicKey,
117
- credentialId: new Uint8Array(first.rawId),
124
+ credentialId,
125
+ rawCredentialId,
118
126
  signingKey,
119
127
  prfInput,
128
+ // Ready for the provider as it is, so nobody has to know which of the two
129
+ // ids it wants under which name.
130
+ credential: { credentialId, rawCredentialId, publicKey, prfInput },
120
131
  };
121
132
  }
package/types/index.d.ts CHANGED
@@ -436,9 +436,14 @@ export { VerificationUtils };
436
436
  export interface RestoredIdentity {
437
437
  did: string;
438
438
  publicKey: { x: Uint8Array; y: Uint8Array };
439
- credentialId: Uint8Array;
439
+ /** The credential id as base64url text. Before 0.7.0 this was the bytes. */
440
+ credentialId: string;
441
+ /** The credential id as bytes. */
442
+ rawCredentialId: Uint8Array;
440
443
  signingKey: Uint8Array;
441
444
  prfInput: Uint8Array;
445
+ /** Ready for `OrbitDBWebAuthnIdentityProviderFunction({ webauthnCredential })`. */
446
+ credential: WebAuthnCredentialInfo;
442
447
  }
443
448
 
444
449
  export interface RestoreIdentityOptions {