@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 +24 -0
- package/README.md +12 -8
- package/package.json +1 -1
- package/src/webauthn/provider.js +24 -4
- package/src/webauthn/restore.js +13 -2
- package/types/index.d.ts +6 -1
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
|
-
|
|
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
|
-
-
|
|
393
|
-
-
|
|
394
|
-
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
-
|
|
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.
|
|
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",
|
package/src/webauthn/provider.js
CHANGED
|
@@ -46,9 +46,28 @@ export class WebAuthnDIDProvider {
|
|
|
46
46
|
* @param {Uint8Array} credentialInfo.rawCredentialId - Raw credential ID bytes.
|
|
47
47
|
*/
|
|
48
48
|
constructor(credentialInfo) {
|
|
49
|
-
|
|
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
|
-
|
|
716
|
-
|
|
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
|
package/src/webauthn/restore.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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 {
|