@1shotapi/ows-signer 0.3.1 → 0.3.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 +1 -0
- package/package.json +1 -1
- package/src/constants.js +1 -0
- package/src/handlers.js +109 -13
- package/src/ui.js +81 -0
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Only accepts messages when `event.source === window.parent`. Replies use the par
|
|
|
41
41
|
| `signDigest` | `digests[]`, `credentialId?`, ceremony UI? | `KeyDerived` (PRF path), `DigestSigned` (`results[]`) |
|
|
42
42
|
| `executeBatch` | batch fields + ceremony UI? | `KeyDerived` (when keys derived), `BatchExecuted` |
|
|
43
43
|
| `revealPrivateKey` | `credentialId?`, ceremony UI? | `KeyDerived`, then `PrivateKeyRevealed` (after user dismisses DOM key UI) |
|
|
44
|
+
| `importPrivateKey` | — | DOM hex input; `KeyDerived`, `RecoverySessionStarted` |
|
|
44
45
|
| `createRecoveryData` | passphrase fields + ceremony UI? | `KeyDerived`, `RecoveryDataCreated` |
|
|
45
46
|
| `recoverKey` | envelope + passphrase fields + ceremony UI? | DOM display; `RecoverySessionStarted` or re-bind → `RecoverySessionCleared` |
|
|
46
47
|
| `getPublicKey` | `credentialId?`, `challenge?`, ceremony UI? | `KeyDerived`, `PublicKey`, `ChallengeSigned?` |
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
package/src/handlers.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
normalizePrfOutputToArrayBuffer,
|
|
17
17
|
} from "./crypto/prf.js";
|
|
18
18
|
import { getPublicKeyAsync as edGetPublicKeyAsync } from "./crypto/vendor/noble-ed25519.js";
|
|
19
|
-
import { getPublicKey as secpGetPublicKey } from "./crypto/vendor/noble-secp256k1.js";
|
|
19
|
+
import { getPublicKey as secpGetPublicKey, utils as secpUtils } from "./crypto/vendor/noble-secp256k1.js";
|
|
20
20
|
import { debugLog, describePrfExtensionResults } from "./debug.js";
|
|
21
21
|
import { decryptPrivateKey, encryptPrivateKey } from "./crypto/recovery.js";
|
|
22
22
|
import { signWithScheme, validateSignPayload } from "./crypto/sign.js";
|
|
@@ -30,7 +30,15 @@ import {
|
|
|
30
30
|
withCeremony,
|
|
31
31
|
zeroize,
|
|
32
32
|
} from "./state.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
clearUi,
|
|
35
|
+
CeremonyDeniedError,
|
|
36
|
+
promptCeremonyConfirm,
|
|
37
|
+
promptPassphrase,
|
|
38
|
+
promptPrivateKey,
|
|
39
|
+
showPrivateKey,
|
|
40
|
+
updateCeremonyCopy,
|
|
41
|
+
} from "./ui.js";
|
|
34
42
|
import {
|
|
35
43
|
createPasskeyCredential,
|
|
36
44
|
getAssertionSignatureBase64Url,
|
|
@@ -126,6 +134,10 @@ export async function handleRequest(
|
|
|
126
134
|
await handleRevealPrivateKey(params, correlationId, targetOrigin);
|
|
127
135
|
return;
|
|
128
136
|
|
|
137
|
+
case "importPrivateKey":
|
|
138
|
+
await handleImportPrivateKey(params, correlationId, targetOrigin);
|
|
139
|
+
return;
|
|
140
|
+
|
|
129
141
|
case "createRecoveryData":
|
|
130
142
|
await handleCreateRecoveryData(params, correlationId, targetOrigin);
|
|
131
143
|
return;
|
|
@@ -191,9 +203,14 @@ function ceremonyFieldsFromParams(params) {
|
|
|
191
203
|
*
|
|
192
204
|
* @param {PublicKeyCredential} credential
|
|
193
205
|
* @param {string} [credentialId]
|
|
206
|
+
* @param {(() => void) | undefined} [onBeforeFollowUpGet]
|
|
194
207
|
* @returns {Promise<PublicKeyCredential>}
|
|
195
208
|
*/
|
|
196
|
-
async function credentialForKeyDerivation(
|
|
209
|
+
async function credentialForKeyDerivation(
|
|
210
|
+
credential,
|
|
211
|
+
credentialId,
|
|
212
|
+
onBeforeFollowUpGet,
|
|
213
|
+
) {
|
|
197
214
|
debugLog("ceremony extension results", describePrfExtensionResults(credential));
|
|
198
215
|
|
|
199
216
|
const rawPrf = credential.getClientExtensionResults()?.prf?.results?.first;
|
|
@@ -206,7 +223,11 @@ async function credentialForKeyDerivation(credential, credentialId) {
|
|
|
206
223
|
debugLog("no PRF results on registration; running assertion with prf.eval", {
|
|
207
224
|
credentialId: id,
|
|
208
225
|
});
|
|
209
|
-
|
|
226
|
+
if (typeof onBeforeFollowUpGet === "function") {
|
|
227
|
+
onBeforeFollowUpGet();
|
|
228
|
+
}
|
|
229
|
+
// Prefer keeping the parent Confirm panel open (call from runOnConfirm) so
|
|
230
|
+
// the user still sees why this second WebAuthn prompt appears.
|
|
210
231
|
const assertion = await getPasskeyAssertion(undefined, id);
|
|
211
232
|
debugLog(
|
|
212
233
|
"assertion extension results",
|
|
@@ -234,14 +255,31 @@ async function handleCreateCredential(params, correlationId, targetOrigin) {
|
|
|
234
255
|
: {};
|
|
235
256
|
|
|
236
257
|
await withCeremony(async () => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const prfCredential = await
|
|
242
|
-
|
|
243
|
-
credentialId
|
|
244
|
-
|
|
258
|
+
// Create + optional PRF follow-up get stay under one Confirm panel so the
|
|
259
|
+
// explanation remains visible for every WebAuthn prompt in this RPC.
|
|
260
|
+
/** @type {PublicKeyCredential | undefined} */
|
|
261
|
+
let registrationCredential;
|
|
262
|
+
const prfCredential = await promptCeremonyConfirm(options, async () => {
|
|
263
|
+
registrationCredential = await createPasskeyCredential(name, options);
|
|
264
|
+
const credentialId = getCredentialId(registrationCredential);
|
|
265
|
+
return credentialForKeyDerivation(
|
|
266
|
+
registrationCredential,
|
|
267
|
+
credentialId,
|
|
268
|
+
() => {
|
|
269
|
+
updateCeremonyCopy({
|
|
270
|
+
explanationHeader: "Unlock your new passkey",
|
|
271
|
+
explanationText:
|
|
272
|
+
"Confirm with your passkey again to finish setting up this wallet.",
|
|
273
|
+
confirmButtonText: options.confirmButtonText,
|
|
274
|
+
denyButtonText: options.denyButtonText,
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
if (!registrationCredential) {
|
|
280
|
+
throw new Error("createCredential: registration credential missing");
|
|
281
|
+
}
|
|
282
|
+
const credentialId = getCredentialId(registrationCredential);
|
|
245
283
|
const keys = await deriveKeysFromCredential(prfCredential);
|
|
246
284
|
emitKeyDerived(
|
|
247
285
|
targetOrigin,
|
|
@@ -252,7 +290,7 @@ async function handleCreateCredential(params, correlationId, targetOrigin) {
|
|
|
252
290
|
|
|
253
291
|
emitEvent(window.parent, targetOrigin, "CredentialCreated", correlationId, {
|
|
254
292
|
credentialId,
|
|
255
|
-
cosePublicKey: getCosePublicKeyBase64Url(
|
|
293
|
+
cosePublicKey: getCosePublicKeyBase64Url(registrationCredential),
|
|
256
294
|
secp256k1PublicKey: to0xHex(keys.secp256k1PublicKey),
|
|
257
295
|
});
|
|
258
296
|
zeroize(keys.secp256k1PrivateKey);
|
|
@@ -462,6 +500,64 @@ async function handleRevealPrivateKey(params, correlationId, targetOrigin) {
|
|
|
462
500
|
);
|
|
463
501
|
}
|
|
464
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Paste a secp256k1 private key hex into the Signing Layer and start a recovery
|
|
505
|
+
* session (same session semantics as `recoverKey` without a backup envelope).
|
|
506
|
+
*
|
|
507
|
+
* @param {Record<string, unknown>} _params
|
|
508
|
+
* @param {string | undefined} correlationId
|
|
509
|
+
* @param {string} targetOrigin
|
|
510
|
+
*/
|
|
511
|
+
async function handleImportPrivateKey(_params, correlationId, targetOrigin) {
|
|
512
|
+
const raw = await promptPrivateKey();
|
|
513
|
+
let privateKey;
|
|
514
|
+
try {
|
|
515
|
+
privateKey = parseImportedPrivateKeyHex(raw);
|
|
516
|
+
} catch {
|
|
517
|
+
emitInvalid(correlationId, targetOrigin, "invalidPrivateKey");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!secpUtils.isValidPrivateKey(privateKey)) {
|
|
521
|
+
zeroize(privateKey);
|
|
522
|
+
emitInvalid(correlationId, targetOrigin, "invalidPrivateKey");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
setRecoveryPrivateKey(privateKey);
|
|
527
|
+
|
|
528
|
+
const secp256k1PublicKey = secpGetPublicKey(privateKey, false);
|
|
529
|
+
const ed25519Seed = await ed25519SeedFromSecp256k1Scalar(privateKey);
|
|
530
|
+
const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
|
|
531
|
+
emitKeyDerived(
|
|
532
|
+
targetOrigin,
|
|
533
|
+
correlationId,
|
|
534
|
+
secp256k1PublicKey,
|
|
535
|
+
ed25519PublicKey,
|
|
536
|
+
);
|
|
537
|
+
zeroize(ed25519Seed);
|
|
538
|
+
|
|
539
|
+
emitEvent(
|
|
540
|
+
window.parent,
|
|
541
|
+
targetOrigin,
|
|
542
|
+
"RecoverySessionStarted",
|
|
543
|
+
correlationId,
|
|
544
|
+
{ recoverySessionActive: true },
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* @param {string} raw
|
|
550
|
+
* @returns {Uint8Array}
|
|
551
|
+
*/
|
|
552
|
+
function parseImportedPrivateKeyHex(raw) {
|
|
553
|
+
const trimmed = raw.trim();
|
|
554
|
+
const withPrefix =
|
|
555
|
+
trimmed.startsWith("0x") || trimmed.startsWith("0X")
|
|
556
|
+
? trimmed
|
|
557
|
+
: `0x${trimmed}`;
|
|
558
|
+
return parse0xHex(withPrefix, 32);
|
|
559
|
+
}
|
|
560
|
+
|
|
465
561
|
/**
|
|
466
562
|
* @param {Record<string, unknown>} params
|
|
467
563
|
* @param {string | undefined} correlationId
|
package/src/ui.js
CHANGED
|
@@ -131,6 +131,22 @@ export function resolveCeremonyUi(fields) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Update header/body on an open ceremony Confirm panel (e.g. before a follow-up
|
|
136
|
+
* WebAuthn get). No-op if the ceremony UI is not showing.
|
|
137
|
+
*
|
|
138
|
+
* @param {CeremonyUiFields | Record<string, unknown> | undefined} fields
|
|
139
|
+
*/
|
|
140
|
+
export function updateCeremonyCopy(fields) {
|
|
141
|
+
if (!root) return;
|
|
142
|
+
const header = root.querySelector(".ows-ceremony-header");
|
|
143
|
+
const text = root.querySelector(".ows-ceremony-text");
|
|
144
|
+
if (!header && !text) return;
|
|
145
|
+
const ui = resolveCeremonyUi(fields);
|
|
146
|
+
if (header) header.textContent = ui.header;
|
|
147
|
+
if (text) text.textContent = ui.explanation;
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
/**
|
|
135
151
|
* Show Confirm/Cancel. On Confirm, runs `runOnConfirm` **synchronously from the
|
|
136
152
|
* click handler** so WebAuthn keeps user activation / document focus on mobile.
|
|
@@ -302,3 +318,68 @@ export function promptPassphrase(passwordText, buttonText, minPasswordLength) {
|
|
|
302
318
|
root.append(input, button);
|
|
303
319
|
});
|
|
304
320
|
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Prompt for a secp256k1 private key hex (with or without `0x`). Resolves with
|
|
324
|
+
* the trimmed hex string the user entered; rejects on Cancel.
|
|
325
|
+
*
|
|
326
|
+
* @returns {Promise<string>}
|
|
327
|
+
*/
|
|
328
|
+
export function promptPrivateKey() {
|
|
329
|
+
if (!root) {
|
|
330
|
+
return Promise.reject(new Error("uiNotInitialized"));
|
|
331
|
+
}
|
|
332
|
+
cancelPendingCeremonyConfirm();
|
|
333
|
+
clearUi();
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
const header = document.createElement("h2");
|
|
336
|
+
header.className = "ows-ceremony-header";
|
|
337
|
+
header.textContent = "Import private key";
|
|
338
|
+
const body = document.createElement("p");
|
|
339
|
+
body.className = "ows-ceremony-text";
|
|
340
|
+
body.textContent =
|
|
341
|
+
"Paste your secp256k1 private key. It stays in this secure panel and is never sent to the host.";
|
|
342
|
+
const input = document.createElement("input");
|
|
343
|
+
input.type = "password";
|
|
344
|
+
input.className = "ows-password";
|
|
345
|
+
input.placeholder = "0x…";
|
|
346
|
+
input.autocomplete = "off";
|
|
347
|
+
input.spellcheck = false;
|
|
348
|
+
const errorEl = document.createElement("p");
|
|
349
|
+
errorEl.className = "ows-ceremony-text";
|
|
350
|
+
errorEl.style.color = "tomato";
|
|
351
|
+
errorEl.hidden = true;
|
|
352
|
+
const confirmBtn = document.createElement("button");
|
|
353
|
+
confirmBtn.type = "button";
|
|
354
|
+
confirmBtn.className = "ows-confirm";
|
|
355
|
+
confirmBtn.textContent = "Import";
|
|
356
|
+
const denyBtn = document.createElement("button");
|
|
357
|
+
denyBtn.type = "button";
|
|
358
|
+
denyBtn.className = "ows-deny";
|
|
359
|
+
denyBtn.textContent = "Cancel";
|
|
360
|
+
const actions = document.createElement("div");
|
|
361
|
+
actions.className = "ows-actions";
|
|
362
|
+
actions.append(confirmBtn, denyBtn);
|
|
363
|
+
|
|
364
|
+
const finish = (fn) => {
|
|
365
|
+
clearUi();
|
|
366
|
+
fn();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
confirmBtn.addEventListener("click", () => {
|
|
370
|
+
const value = input.value.trim();
|
|
371
|
+
if (!value) {
|
|
372
|
+
errorEl.textContent = "Enter a private key.";
|
|
373
|
+
errorEl.hidden = false;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
finish(() => resolve(value));
|
|
377
|
+
});
|
|
378
|
+
denyBtn.addEventListener("click", () => {
|
|
379
|
+
finish(() => reject(new CeremonyDeniedError()));
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
root.append(header, body, input, errorEl, actions);
|
|
383
|
+
input.focus();
|
|
384
|
+
});
|
|
385
|
+
}
|