@1shotapi/ows-signer 0.3.1 → 0.3.2

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1shotapi/ows-signer",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "OWS Signing Layer — plain JavaScript WebAuthn PRF key custody with curve-based signing (on-chain via EIP-8244)",
5
5
  "type": "module",
6
6
  "files": [
package/src/constants.js CHANGED
@@ -21,6 +21,7 @@ export const METHODS = [
21
21
  "createCredential",
22
22
  "signDigest",
23
23
  "revealPrivateKey",
24
+ "importPrivateKey",
24
25
  "createRecoveryData",
25
26
  "recoverKey",
26
27
  "getPublicKey",
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,14 @@ import {
30
30
  withCeremony,
31
31
  zeroize,
32
32
  } from "./state.js";
33
- import { clearUi, CeremonyDeniedError, promptCeremonyConfirm, promptPassphrase, showPrivateKey } from "./ui.js";
33
+ import {
34
+ clearUi,
35
+ CeremonyDeniedError,
36
+ promptCeremonyConfirm,
37
+ promptPassphrase,
38
+ promptPrivateKey,
39
+ showPrivateKey,
40
+ } from "./ui.js";
34
41
  import {
35
42
  createPasskeyCredential,
36
43
  getAssertionSignatureBase64Url,
@@ -126,6 +133,10 @@ export async function handleRequest(
126
133
  await handleRevealPrivateKey(params, correlationId, targetOrigin);
127
134
  return;
128
135
 
136
+ case "importPrivateKey":
137
+ await handleImportPrivateKey(params, correlationId, targetOrigin);
138
+ return;
139
+
129
140
  case "createRecoveryData":
130
141
  await handleCreateRecoveryData(params, correlationId, targetOrigin);
131
142
  return;
@@ -462,6 +473,64 @@ async function handleRevealPrivateKey(params, correlationId, targetOrigin) {
462
473
  );
463
474
  }
464
475
 
476
+ /**
477
+ * Paste a secp256k1 private key hex into the Signing Layer and start a recovery
478
+ * session (same session semantics as `recoverKey` without a backup envelope).
479
+ *
480
+ * @param {Record<string, unknown>} _params
481
+ * @param {string | undefined} correlationId
482
+ * @param {string} targetOrigin
483
+ */
484
+ async function handleImportPrivateKey(_params, correlationId, targetOrigin) {
485
+ const raw = await promptPrivateKey();
486
+ let privateKey;
487
+ try {
488
+ privateKey = parseImportedPrivateKeyHex(raw);
489
+ } catch {
490
+ emitInvalid(correlationId, targetOrigin, "invalidPrivateKey");
491
+ return;
492
+ }
493
+ if (!secpUtils.isValidPrivateKey(privateKey)) {
494
+ zeroize(privateKey);
495
+ emitInvalid(correlationId, targetOrigin, "invalidPrivateKey");
496
+ return;
497
+ }
498
+
499
+ setRecoveryPrivateKey(privateKey);
500
+
501
+ const secp256k1PublicKey = secpGetPublicKey(privateKey, false);
502
+ const ed25519Seed = await ed25519SeedFromSecp256k1Scalar(privateKey);
503
+ const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
504
+ emitKeyDerived(
505
+ targetOrigin,
506
+ correlationId,
507
+ secp256k1PublicKey,
508
+ ed25519PublicKey,
509
+ );
510
+ zeroize(ed25519Seed);
511
+
512
+ emitEvent(
513
+ window.parent,
514
+ targetOrigin,
515
+ "RecoverySessionStarted",
516
+ correlationId,
517
+ { recoverySessionActive: true },
518
+ );
519
+ }
520
+
521
+ /**
522
+ * @param {string} raw
523
+ * @returns {Uint8Array}
524
+ */
525
+ function parseImportedPrivateKeyHex(raw) {
526
+ const trimmed = raw.trim();
527
+ const withPrefix =
528
+ trimmed.startsWith("0x") || trimmed.startsWith("0X")
529
+ ? trimmed
530
+ : `0x${trimmed}`;
531
+ return parse0xHex(withPrefix, 32);
532
+ }
533
+
465
534
  /**
466
535
  * @param {Record<string, unknown>} params
467
536
  * @param {string | undefined} correlationId
package/src/ui.js CHANGED
@@ -302,3 +302,68 @@ export function promptPassphrase(passwordText, buttonText, minPasswordLength) {
302
302
  root.append(input, button);
303
303
  });
304
304
  }
305
+
306
+ /**
307
+ * Prompt for a secp256k1 private key hex (with or without `0x`). Resolves with
308
+ * the trimmed hex string the user entered; rejects on Cancel.
309
+ *
310
+ * @returns {Promise<string>}
311
+ */
312
+ export function promptPrivateKey() {
313
+ if (!root) {
314
+ return Promise.reject(new Error("uiNotInitialized"));
315
+ }
316
+ cancelPendingCeremonyConfirm();
317
+ clearUi();
318
+ return new Promise((resolve, reject) => {
319
+ const header = document.createElement("h2");
320
+ header.className = "ows-ceremony-header";
321
+ header.textContent = "Import private key";
322
+ const body = document.createElement("p");
323
+ body.className = "ows-ceremony-text";
324
+ body.textContent =
325
+ "Paste your secp256k1 private key. It stays in this secure panel and is never sent to the host.";
326
+ const input = document.createElement("input");
327
+ input.type = "password";
328
+ input.className = "ows-password";
329
+ input.placeholder = "0x…";
330
+ input.autocomplete = "off";
331
+ input.spellcheck = false;
332
+ const errorEl = document.createElement("p");
333
+ errorEl.className = "ows-ceremony-text";
334
+ errorEl.style.color = "tomato";
335
+ errorEl.hidden = true;
336
+ const confirmBtn = document.createElement("button");
337
+ confirmBtn.type = "button";
338
+ confirmBtn.className = "ows-confirm";
339
+ confirmBtn.textContent = "Import";
340
+ const denyBtn = document.createElement("button");
341
+ denyBtn.type = "button";
342
+ denyBtn.className = "ows-deny";
343
+ denyBtn.textContent = "Cancel";
344
+ const actions = document.createElement("div");
345
+ actions.className = "ows-actions";
346
+ actions.append(confirmBtn, denyBtn);
347
+
348
+ const finish = (fn) => {
349
+ clearUi();
350
+ fn();
351
+ };
352
+
353
+ confirmBtn.addEventListener("click", () => {
354
+ const value = input.value.trim();
355
+ if (!value) {
356
+ errorEl.textContent = "Enter a private key.";
357
+ errorEl.hidden = false;
358
+ return;
359
+ }
360
+ finish(() => resolve(value));
361
+ });
362
+ denyBtn.addEventListener("click", () => {
363
+ finish(() => reject(new CeremonyDeniedError()));
364
+ });
365
+
366
+ root.append(header, body, input, errorEl, actions);
367
+ input.focus();
368
+ });
369
+ }