@1shotapi/ows-signer 0.4.1 → 0.4.3
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/README.md +2 -2
- package/html/signer.css +63 -0
- package/package.json +1 -1
- package/src/constants.js +1 -0
- package/src/crypto/prf.js +23 -6
- package/src/debug.js +66 -5
- package/src/handlers.js +16 -42
- package/src/ui.js +15 -1
- package/src/webauthn.js +39 -18
package/README.md
CHANGED
|
@@ -68,8 +68,8 @@ Ceremony UI params (optional on WebAuthn-triggering methods): `explanationHeader
|
|
|
68
68
|
|
|
69
69
|
## PRF labels
|
|
70
70
|
|
|
71
|
-
- `ows-v1/secp256k1`
|
|
72
|
-
- `ows-v1/ed25519` (
|
|
71
|
+
- `ows-v1/secp256k1` (HKDF from PRF output, rejection sampling into the curve order)
|
|
72
|
+
- `ows-v1/ed25519` (HKDF from the secp256k1 scalar — same material as `signDigest` / recovery)
|
|
73
73
|
- `ows-v1/aes256-gcm` (HKDF from the secp256k1 scalar — same material as `signDigest`)
|
|
74
74
|
|
|
75
75
|
## Recovery envelope
|
package/html/signer.css
CHANGED
|
@@ -56,8 +56,71 @@ body {
|
|
|
56
56
|
text-align: center;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
body.ows-ceremony-open {
|
|
60
|
+
align-items: stretch;
|
|
61
|
+
justify-content: flex-end;
|
|
62
|
+
background: transparent;
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
.ows-signer-ui--ceremony {
|
|
60
66
|
gap: 0;
|
|
67
|
+
margin: 0;
|
|
68
|
+
width: 100%;
|
|
69
|
+
max-width: none;
|
|
70
|
+
height: 100%;
|
|
71
|
+
max-height: 100%;
|
|
72
|
+
min-height: 0;
|
|
73
|
+
padding: 12px 20px 0;
|
|
74
|
+
justify-content: flex-start;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
border: 1px solid var(--ows-border);
|
|
77
|
+
border-bottom: none;
|
|
78
|
+
border-radius: var(--ows-radius-card) var(--ows-radius-card) 0 0;
|
|
79
|
+
box-shadow: 0 -2px 16px rgb(0 0 0 / 6%);
|
|
80
|
+
animation: ows-sheet-in 280ms cubic-bezier(0.32, 0.72, 0, 1) both;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes ows-sheet-in {
|
|
84
|
+
from {
|
|
85
|
+
transform: translateY(100%);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
to {
|
|
89
|
+
transform: translateY(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ows-ceremony-grab {
|
|
94
|
+
flex: 0 0 auto;
|
|
95
|
+
width: 2.5rem;
|
|
96
|
+
height: 0.25rem;
|
|
97
|
+
border-radius: 999px;
|
|
98
|
+
background: color-mix(in srgb, var(--ows-text-muted) 35%, transparent);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ows-signer-ui--ceremony .ows-ceremony-grab {
|
|
102
|
+
margin: 0 auto 12px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ows-signer-ui--ceremony .ows-ceremony-header {
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
padding: 0;
|
|
108
|
+
margin: 0 0 8px;
|
|
109
|
+
text-align: left;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.ows-signer-ui--ceremony .ows-ceremony-body {
|
|
113
|
+
flex: 0 0 auto;
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
min-height: auto;
|
|
116
|
+
overflow: visible;
|
|
117
|
+
padding: 0 0 4px;
|
|
118
|
+
margin-bottom: 12px;
|
|
119
|
+
text-align: left;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ows-signer-ui--ceremony .ows-ceremony-actions {
|
|
123
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom, 0px));
|
|
61
124
|
}
|
|
62
125
|
|
|
63
126
|
.ows-signer-ui:empty {
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -4,6 +4,7 @@ export const API_VERSION = 1;
|
|
|
4
4
|
export const SIGNER_VERSION = "0.1.0";
|
|
5
5
|
|
|
6
6
|
export const PRF_LABEL_SECP256K1 = new TextEncoder().encode("ows-v1/secp256k1");
|
|
7
|
+
/** HKDF info for Ed25519 — IKM is the secp256k1 scalar (`signDigest` material). */
|
|
7
8
|
export const PRF_LABEL_ED25519 = new TextEncoder().encode("ows-v1/ed25519");
|
|
8
9
|
/** HKDF info for AES-256-GCM — IKM is the secp256k1 scalar (`signDigest` material). */
|
|
9
10
|
export const PRF_LABEL_AES256 = new TextEncoder().encode("ows-v1/aes256-gcm");
|
package/src/crypto/prf.js
CHANGED
|
@@ -76,16 +76,27 @@ export async function deriveScalarFromPrf(prfBuffer, infoLabel) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* HKDF info `ows-v1/ed25519` — IKM is the secp256k1 scalar (`signDigest`
|
|
80
|
+
* material), not raw PRF. Recovery / import only hold that scalar, so Ed25519
|
|
81
|
+
* must chain through it or a recovery session would emit a different pubkey.
|
|
82
|
+
*
|
|
83
|
+
* @param {Uint8Array} secp256k1Scalar
|
|
80
84
|
* @returns {Promise<Uint8Array>}
|
|
81
85
|
*/
|
|
82
|
-
export async function
|
|
83
|
-
return hkdfExpand(
|
|
86
|
+
export async function deriveEd25519SeedFromSecp256k1Scalar(secp256k1Scalar) {
|
|
87
|
+
return hkdfExpand(
|
|
88
|
+
secp256k1Scalar.buffer.slice(
|
|
89
|
+
secp256k1Scalar.byteOffset,
|
|
90
|
+
secp256k1Scalar.byteOffset + secp256k1Scalar.byteLength,
|
|
91
|
+
),
|
|
92
|
+
PRF_LABEL_ED25519,
|
|
93
|
+
32,
|
|
94
|
+
);
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
/**
|
|
87
98
|
* @param {PublicKeyCredential} credential
|
|
88
|
-
* @returns {Promise<{ secp256k1PrivateKey: Uint8Array, secp256k1PublicKey: Uint8Array, ed25519PublicKey: Uint8Array }>}
|
|
99
|
+
* @returns {Promise<{ secp256k1PrivateKey: Uint8Array, secp256k1PublicKey: Uint8Array, ed25519Seed: Uint8Array, ed25519PublicKey: Uint8Array }>}
|
|
89
100
|
*/
|
|
90
101
|
export async function deriveKeysFromCredential(credential) {
|
|
91
102
|
const rawPrf = /** @type {{ prf?: { results?: { first?: unknown } } }} */ (
|
|
@@ -100,12 +111,18 @@ export async function deriveKeysFromCredential(credential) {
|
|
|
100
111
|
prfBuffer,
|
|
101
112
|
PRF_LABEL_SECP256K1,
|
|
102
113
|
);
|
|
103
|
-
const ed25519Seed =
|
|
114
|
+
const ed25519Seed =
|
|
115
|
+
await deriveEd25519SeedFromSecp256k1Scalar(secp256k1PrivateKey);
|
|
104
116
|
// Uncompressed (0x04 ‖ X ‖ Y) — required by viem `publicKeyToAddress`.
|
|
105
117
|
const secp256k1PublicKey = secpGetPublicKey(secp256k1PrivateKey, false);
|
|
106
118
|
const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
|
|
107
119
|
|
|
108
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
secp256k1PrivateKey,
|
|
122
|
+
secp256k1PublicKey,
|
|
123
|
+
ed25519Seed,
|
|
124
|
+
ed25519PublicKey,
|
|
125
|
+
};
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
/**
|
package/src/debug.js
CHANGED
|
@@ -1,13 +1,74 @@
|
|
|
1
1
|
const DEBUG_KEY = "ows-signer:debug";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* True when a chrome-extension:// (or similar) page is an ancestor.
|
|
5
|
+
* Chromium exposes this via `location.ancestorOrigins` even when top is
|
|
6
|
+
* cross-origin — useful for side-panel embeds where WebAuthn often misbehaves.
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function isExtensionAncestor() {
|
|
10
|
+
try {
|
|
11
|
+
const origins = globalThis.location?.ancestorOrigins;
|
|
12
|
+
if (!origins || origins.length === 0) return false;
|
|
13
|
+
for (let i = 0; i < origins.length; i++) {
|
|
14
|
+
const origin = origins[i];
|
|
15
|
+
if (
|
|
16
|
+
typeof origin === "string" &&
|
|
17
|
+
(origin.startsWith("chrome-extension://") ||
|
|
18
|
+
origin.startsWith("moz-extension://") ||
|
|
19
|
+
origin.startsWith("safari-web-extension://"))
|
|
20
|
+
) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
/** @returns {boolean} */
|
|
4
31
|
export function isDebug() {
|
|
5
32
|
if (typeof globalThis === "undefined") return false;
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
33
|
+
if (globalThis.OWS_SIGNER_DEBUG === true) return true;
|
|
34
|
+
// Always log in extension side-panel embeds — GPM get() often hangs with no UI.
|
|
35
|
+
if (isExtensionAncestor()) return true;
|
|
36
|
+
try {
|
|
37
|
+
return (
|
|
38
|
+
typeof localStorage !== "undefined" &&
|
|
39
|
+
localStorage.getItem(DEBUG_KEY) === "1"
|
|
40
|
+
);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @returns {Record<string, unknown>} */
|
|
47
|
+
export function describeCeremonyEnvironment() {
|
|
48
|
+
let visibilityState = null;
|
|
49
|
+
let hasFocus = null;
|
|
50
|
+
try {
|
|
51
|
+
visibilityState = document.visibilityState;
|
|
52
|
+
hasFocus = document.hasFocus?.() ?? null;
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
let ancestorOrigins = null;
|
|
57
|
+
try {
|
|
58
|
+
ancestorOrigins = globalThis.location?.ancestorOrigins
|
|
59
|
+
? Array.from(globalThis.location.ancestorOrigins)
|
|
60
|
+
: null;
|
|
61
|
+
} catch {
|
|
62
|
+
ancestorOrigins = null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
extensionAncestor: isExtensionAncestor(),
|
|
66
|
+
ancestorOrigins,
|
|
67
|
+
visibilityState,
|
|
68
|
+
hasFocus,
|
|
69
|
+
userActivationActive: navigator.userActivation?.isActive ?? null,
|
|
70
|
+
userActivationHasBeenActive: navigator.userActivation?.hasBeenActive ?? null,
|
|
71
|
+
};
|
|
11
72
|
}
|
|
12
73
|
|
|
13
74
|
/** @param {...unknown} args */
|
package/src/handlers.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
API_VERSION,
|
|
3
3
|
METHODS,
|
|
4
|
-
PRF_LABEL_ED25519,
|
|
5
4
|
SIGN_SCHEMES,
|
|
6
5
|
SIGNER_VERSION,
|
|
7
6
|
} from "./constants.js";
|
|
@@ -10,9 +9,8 @@ import {
|
|
|
10
9
|
encryptAes256Batch,
|
|
11
10
|
} from "./crypto/aes256.js";
|
|
12
11
|
import {
|
|
13
|
-
|
|
12
|
+
deriveEd25519SeedFromSecp256k1Scalar,
|
|
14
13
|
deriveKeysFromCredential,
|
|
15
|
-
hkdfExpand,
|
|
16
14
|
normalizePrfOutputToArrayBuffer,
|
|
17
15
|
} from "./crypto/prf.js";
|
|
18
16
|
import { getPublicKeyAsync as edGetPublicKeyAsync } from "./crypto/vendor/noble-ed25519.js";
|
|
@@ -47,34 +45,6 @@ import {
|
|
|
47
45
|
getCosePublicKeyBase64Url,
|
|
48
46
|
} from "./webauthn.js";
|
|
49
47
|
|
|
50
|
-
/**
|
|
51
|
-
* @param {PublicKeyCredential} credential
|
|
52
|
-
* @returns {Promise<Uint8Array>}
|
|
53
|
-
*/
|
|
54
|
-
async function ed25519SeedFromCredential(credential) {
|
|
55
|
-
const prfRaw = /** @type {{ prf?: { results?: { first?: unknown } } }} */ (
|
|
56
|
-
credential.getClientExtensionResults()
|
|
57
|
-
).prf?.results?.first;
|
|
58
|
-
const prfBuf = normalizePrfOutputToArrayBuffer(prfRaw);
|
|
59
|
-
if (!prfBuf) throw new Error("noPrf");
|
|
60
|
-
return deriveEd25519SeedFromPrf(prfBuf);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @param {Uint8Array} secp256k1Scalar
|
|
65
|
-
* @returns {Promise<Uint8Array>}
|
|
66
|
-
*/
|
|
67
|
-
async function ed25519SeedFromSecp256k1Scalar(secp256k1Scalar) {
|
|
68
|
-
return hkdfExpand(
|
|
69
|
-
secp256k1Scalar.buffer.slice(
|
|
70
|
-
secp256k1Scalar.byteOffset,
|
|
71
|
-
secp256k1Scalar.byteOffset + secp256k1Scalar.byteLength,
|
|
72
|
-
),
|
|
73
|
-
PRF_LABEL_ED25519,
|
|
74
|
-
32,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
48
|
/**
|
|
79
49
|
* @param {string} targetOrigin
|
|
80
50
|
* @param {string | undefined} correlationId
|
|
@@ -311,6 +281,7 @@ async function handleCreateCredential(params, correlationId, targetOrigin) {
|
|
|
311
281
|
secp256k1PublicKey: to0xHex(keys.secp256k1PublicKey),
|
|
312
282
|
});
|
|
313
283
|
zeroize(keys.secp256k1PrivateKey);
|
|
284
|
+
zeroize(keys.ed25519Seed);
|
|
314
285
|
});
|
|
315
286
|
}
|
|
316
287
|
|
|
@@ -432,7 +403,7 @@ async function handleSignDigest(params, correlationId, targetOrigin) {
|
|
|
432
403
|
if (hasRecoverySession()) {
|
|
433
404
|
const cached = getRecoveryPrivateKey();
|
|
434
405
|
if (!cached) throw new Error("recoverySessionEmpty");
|
|
435
|
-
const ed25519Seed = await
|
|
406
|
+
const ed25519Seed = await deriveEd25519SeedFromSecp256k1Scalar(cached);
|
|
436
407
|
try {
|
|
437
408
|
const results = await signParsedDigests(
|
|
438
409
|
items,
|
|
@@ -455,7 +426,6 @@ async function handleSignDigest(params, correlationId, targetOrigin) {
|
|
|
455
426
|
() => getPasskeyAssertion(undefined, credentialId),
|
|
456
427
|
);
|
|
457
428
|
const keys = await deriveKeysFromCredential(credential);
|
|
458
|
-
const ed25519Seed = await ed25519SeedFromCredential(credential);
|
|
459
429
|
emitKeyDerived(
|
|
460
430
|
targetOrigin,
|
|
461
431
|
correlationId,
|
|
@@ -466,7 +436,7 @@ async function handleSignDigest(params, correlationId, targetOrigin) {
|
|
|
466
436
|
const results = await signParsedDigests(
|
|
467
437
|
items,
|
|
468
438
|
keys.secp256k1PrivateKey,
|
|
469
|
-
ed25519Seed,
|
|
439
|
+
keys.ed25519Seed,
|
|
470
440
|
credentialId ?? getCredentialId(credential),
|
|
471
441
|
);
|
|
472
442
|
emitEvent(window.parent, targetOrigin, "DigestSigned", correlationId, {
|
|
@@ -474,7 +444,7 @@ async function handleSignDigest(params, correlationId, targetOrigin) {
|
|
|
474
444
|
});
|
|
475
445
|
} finally {
|
|
476
446
|
zeroize(keys.secp256k1PrivateKey);
|
|
477
|
-
zeroize(ed25519Seed);
|
|
447
|
+
zeroize(keys.ed25519Seed);
|
|
478
448
|
}
|
|
479
449
|
});
|
|
480
450
|
}
|
|
@@ -506,6 +476,7 @@ async function handleRevealPrivateKey(params, correlationId, targetOrigin) {
|
|
|
506
476
|
await dismiss;
|
|
507
477
|
} finally {
|
|
508
478
|
zeroize(keys.secp256k1PrivateKey);
|
|
479
|
+
zeroize(keys.ed25519Seed);
|
|
509
480
|
}
|
|
510
481
|
});
|
|
511
482
|
emitEvent(
|
|
@@ -543,7 +514,7 @@ async function handleImportPrivateKey(_params, correlationId, targetOrigin) {
|
|
|
543
514
|
setRecoveryPrivateKey(privateKey);
|
|
544
515
|
|
|
545
516
|
const secp256k1PublicKey = secpGetPublicKey(privateKey, false);
|
|
546
|
-
const ed25519Seed = await
|
|
517
|
+
const ed25519Seed = await deriveEd25519SeedFromSecp256k1Scalar(privateKey);
|
|
547
518
|
const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
|
|
548
519
|
emitKeyDerived(
|
|
549
520
|
targetOrigin,
|
|
@@ -607,7 +578,7 @@ async function handleCreateRecoveryData(params, correlationId, targetOrigin) {
|
|
|
607
578
|
const cached = getRecoveryPrivateKey();
|
|
608
579
|
if (!cached) throw new Error("recoverySessionEmpty");
|
|
609
580
|
const secp256k1PublicKey = secpGetPublicKey(cached, false);
|
|
610
|
-
const ed25519Seed = await
|
|
581
|
+
const ed25519Seed = await deriveEd25519SeedFromSecp256k1Scalar(cached);
|
|
611
582
|
const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
|
|
612
583
|
emitKeyDerived(
|
|
613
584
|
targetOrigin,
|
|
@@ -645,6 +616,7 @@ async function handleCreateRecoveryData(params, correlationId, targetOrigin) {
|
|
|
645
616
|
passphrase,
|
|
646
617
|
);
|
|
647
618
|
zeroize(keys.secp256k1PrivateKey);
|
|
619
|
+
zeroize(keys.ed25519Seed);
|
|
648
620
|
emitEvent(
|
|
649
621
|
window.parent,
|
|
650
622
|
targetOrigin,
|
|
@@ -683,7 +655,7 @@ async function handleRecoverKey(params, correlationId, targetOrigin) {
|
|
|
683
655
|
// Emit public keys so OWSSigner can cache addresses (uncompressed secp256k1 —
|
|
684
656
|
// viem publicKeyToAddress requires 0x04 ‖ X ‖ Y).
|
|
685
657
|
const secp256k1PublicKey = secpGetPublicKey(privateKey, false);
|
|
686
|
-
const ed25519Seed = await
|
|
658
|
+
const ed25519Seed = await deriveEd25519SeedFromSecp256k1Scalar(privateKey);
|
|
687
659
|
const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
|
|
688
660
|
emitKeyDerived(
|
|
689
661
|
targetOrigin,
|
|
@@ -787,6 +759,7 @@ async function handleEncryptAES256(params, correlationId, targetOrigin) {
|
|
|
787
759
|
});
|
|
788
760
|
} finally {
|
|
789
761
|
zeroize(keys.secp256k1PrivateKey);
|
|
762
|
+
zeroize(keys.ed25519Seed);
|
|
790
763
|
}
|
|
791
764
|
});
|
|
792
765
|
}
|
|
@@ -847,6 +820,7 @@ async function handleDecryptAES256(params, correlationId, targetOrigin) {
|
|
|
847
820
|
});
|
|
848
821
|
} finally {
|
|
849
822
|
zeroize(keys.secp256k1PrivateKey);
|
|
823
|
+
zeroize(keys.ed25519Seed);
|
|
850
824
|
}
|
|
851
825
|
});
|
|
852
826
|
}
|
|
@@ -901,6 +875,7 @@ async function handleGetPublicKey(params, correlationId, targetOrigin) {
|
|
|
901
875
|
}
|
|
902
876
|
|
|
903
877
|
zeroize(keys.secp256k1PrivateKey);
|
|
878
|
+
zeroize(keys.ed25519Seed);
|
|
904
879
|
});
|
|
905
880
|
}
|
|
906
881
|
|
|
@@ -1031,7 +1006,6 @@ async function handleExecuteBatch(params, correlationId, targetOrigin) {
|
|
|
1031
1006
|
() => getPasskeyAssertion(challenge, credentialId),
|
|
1032
1007
|
);
|
|
1033
1008
|
const keys = await deriveKeysFromCredential(credential);
|
|
1034
|
-
const ed25519Seed = await ed25519SeedFromCredential(credential);
|
|
1035
1009
|
emitKeyDerived(
|
|
1036
1010
|
targetOrigin,
|
|
1037
1011
|
correlationId,
|
|
@@ -1041,7 +1015,7 @@ async function handleExecuteBatch(params, correlationId, targetOrigin) {
|
|
|
1041
1015
|
try {
|
|
1042
1016
|
await runOps(
|
|
1043
1017
|
keys.secp256k1PrivateKey,
|
|
1044
|
-
ed25519Seed,
|
|
1018
|
+
keys.ed25519Seed,
|
|
1045
1019
|
keys.secp256k1PublicKey,
|
|
1046
1020
|
keys.ed25519PublicKey,
|
|
1047
1021
|
credentialId ?? getCredentialId(credential),
|
|
@@ -1049,7 +1023,7 @@ async function handleExecuteBatch(params, correlationId, targetOrigin) {
|
|
|
1049
1023
|
);
|
|
1050
1024
|
} finally {
|
|
1051
1025
|
zeroize(keys.secp256k1PrivateKey);
|
|
1052
|
-
zeroize(ed25519Seed);
|
|
1026
|
+
zeroize(keys.ed25519Seed);
|
|
1053
1027
|
}
|
|
1054
1028
|
});
|
|
1055
1029
|
return;
|
|
@@ -1057,7 +1031,7 @@ async function handleExecuteBatch(params, correlationId, targetOrigin) {
|
|
|
1057
1031
|
|
|
1058
1032
|
const cached = getRecoveryPrivateKey();
|
|
1059
1033
|
if (!cached) throw new Error("recoverySessionEmpty");
|
|
1060
|
-
const ed25519Seed = await
|
|
1034
|
+
const ed25519Seed = await deriveEd25519SeedFromSecp256k1Scalar(cached);
|
|
1061
1035
|
const secp256k1PublicKey = secpGetPublicKey(cached, false);
|
|
1062
1036
|
const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
|
|
1063
1037
|
emitKeyDerived(
|
package/src/ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { to0xHex } from "./hex.js";
|
|
2
|
+
import { debugLog, describeCeremonyEnvironment } from "./debug.js";
|
|
2
3
|
|
|
3
4
|
/** @type {HTMLElement | null} */
|
|
4
5
|
let root = null;
|
|
@@ -65,7 +66,14 @@ export function initUi(container) {
|
|
|
65
66
|
root.className = "ows-signer-ui";
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
function setCeremonyOpen(open) {
|
|
70
|
+
if (typeof document !== "undefined" && document.body) {
|
|
71
|
+
document.body.classList.toggle("ows-ceremony-open", open);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
export function clearUi() {
|
|
76
|
+
setCeremonyOpen(false);
|
|
69
77
|
if (root) {
|
|
70
78
|
root.innerHTML = "";
|
|
71
79
|
root.className = "ows-signer-ui";
|
|
@@ -169,11 +177,16 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
169
177
|
cancelPendingCeremonyConfirm();
|
|
170
178
|
clearUi();
|
|
171
179
|
root.className = "ows-signer-ui ows-signer-ui--ceremony";
|
|
180
|
+
setCeremonyOpen(true);
|
|
172
181
|
const ui = resolveCeremonyUi(fields);
|
|
173
182
|
|
|
174
183
|
return new Promise((resolve, reject) => {
|
|
175
184
|
pendingConfirmReject = reject;
|
|
176
185
|
|
|
186
|
+
const grab = document.createElement("div");
|
|
187
|
+
grab.className = "ows-ceremony-grab";
|
|
188
|
+
grab.setAttribute("aria-hidden", "true");
|
|
189
|
+
|
|
177
190
|
const header = document.createElement("h2");
|
|
178
191
|
header.className = "ows-ceremony-header";
|
|
179
192
|
header.textContent = ui.header;
|
|
@@ -223,6 +236,7 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
223
236
|
try {
|
|
224
237
|
// Start WebAuthn in the same turn as the click (no prior await).
|
|
225
238
|
// Keep copy visible until the ceremony settles.
|
|
239
|
+
debugLog("ceremony Confirm click", describeCeremonyEnvironment());
|
|
226
240
|
Promise.resolve(runOnConfirm()).then(settleSuccess, settleFailure);
|
|
227
241
|
} catch (error) {
|
|
228
242
|
settleFailure(error);
|
|
@@ -236,7 +250,7 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
236
250
|
});
|
|
237
251
|
|
|
238
252
|
actions.append(confirmBtn, denyBtn);
|
|
239
|
-
root.append(header, body, actions);
|
|
253
|
+
root.append(grab, header, body, actions);
|
|
240
254
|
});
|
|
241
255
|
}
|
|
242
256
|
|
package/src/webauthn.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { getCeremonyAbortSignal, getRpId } from "./state.js";
|
|
2
2
|
import { PRF_LABEL_SECP256K1 } from "./constants.js";
|
|
3
3
|
import { bufferToBase64Url } from "./hex.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
debugLog,
|
|
6
|
+
describeCeremonyEnvironment,
|
|
7
|
+
describePrfExtensionResults,
|
|
8
|
+
} from "./debug.js";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* @param {Uint8Array} [userId]
|
|
@@ -29,9 +33,7 @@ export async function createPasskeyCredential(name, options = {}) {
|
|
|
29
33
|
const displayName = options.userDisplayName ?? name;
|
|
30
34
|
const signal = getCeremonyAbortSignal();
|
|
31
35
|
|
|
32
|
-
debugLog("createPasskeyCredential
|
|
33
|
-
isActive: navigator.userActivation?.isActive ?? false,
|
|
34
|
-
});
|
|
36
|
+
debugLog("createPasskeyCredential start", describeCeremonyEnvironment());
|
|
35
37
|
|
|
36
38
|
const credential = await navigator.credentials.create({
|
|
37
39
|
publicKey: {
|
|
@@ -88,27 +90,46 @@ export async function getPasskeyAssertion(challenge, credentialId) {
|
|
|
88
90
|
: undefined;
|
|
89
91
|
const signal = getCeremonyAbortSignal();
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
debugLog("getPasskeyAssertion start", {
|
|
94
|
+
...describeCeremonyEnvironment(),
|
|
95
|
+
rpId,
|
|
96
|
+
hasChallenge: Boolean(challenge),
|
|
97
|
+
hasAllowCredentials: Boolean(allowCredentials),
|
|
98
|
+
credentialIdPrefix:
|
|
99
|
+
typeof credentialId === "string" ? credentialId.slice(0, 8) : null,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let credential;
|
|
103
|
+
try {
|
|
104
|
+
credential = await navigator.credentials.get({
|
|
105
|
+
publicKey: {
|
|
106
|
+
challenge: challenge ?? crypto.getRandomValues(new Uint8Array(32)),
|
|
107
|
+
rpId,
|
|
108
|
+
userVerification: "required",
|
|
109
|
+
allowCredentials,
|
|
110
|
+
extensions: {
|
|
111
|
+
prf: {
|
|
112
|
+
eval: {
|
|
113
|
+
first: PRF_LABEL_SECP256K1,
|
|
114
|
+
},
|
|
101
115
|
},
|
|
102
116
|
},
|
|
103
117
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
})
|
|
118
|
+
...(signal ? { signal } : {}),
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
debugLog("getPasskeyAssertion rejected", {
|
|
122
|
+
...describeCeremonyEnvironment(),
|
|
123
|
+
name: error instanceof Error ? error.name : typeof error,
|
|
124
|
+
message: error instanceof Error ? error.message : String(error),
|
|
125
|
+
});
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
107
128
|
|
|
108
129
|
if (!credential || !(credential instanceof PublicKeyCredential)) {
|
|
109
130
|
throw new Error("credentialGetFailed");
|
|
110
131
|
}
|
|
111
|
-
debugLog("getPasskeyAssertion", describePrfExtensionResults(credential));
|
|
132
|
+
debugLog("getPasskeyAssertion ok", describePrfExtensionResults(credential));
|
|
112
133
|
return credential;
|
|
113
134
|
}
|
|
114
135
|
|