@1shotapi/ows-signer 0.3.0 → 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 +2 -1
- package/html/index.html +6 -0
- package/package.json +1 -1
- package/src/constants.js +1 -0
- package/src/handlers.js +85 -4
- package/src/ui.js +96 -11
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ Only accepts messages when `event.source === window.parent`. Replies use the par
|
|
|
40
40
|
| `createCredential` | `name`, `options?` (incl. ceremony UI) | `KeyDerived`, `CredentialCreated` |
|
|
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
|
-
| `revealPrivateKey` | `credentialId?`, ceremony UI? | `KeyDerived` (
|
|
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/html/index.html
CHANGED
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,14 @@ 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
|
+
} 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;
|
|
@@ -445,9 +456,79 @@ async function handleRevealPrivateKey(params, correlationId, targetOrigin) {
|
|
|
445
456
|
keys.secp256k1PublicKey,
|
|
446
457
|
keys.ed25519PublicKey,
|
|
447
458
|
);
|
|
448
|
-
|
|
449
|
-
|
|
459
|
+
try {
|
|
460
|
+
const dismiss = showPrivateKey(keys.secp256k1PrivateKey);
|
|
461
|
+
zeroize(keys.secp256k1PrivateKey);
|
|
462
|
+
await dismiss;
|
|
463
|
+
} finally {
|
|
464
|
+
zeroize(keys.secp256k1PrivateKey);
|
|
465
|
+
}
|
|
450
466
|
});
|
|
467
|
+
emitEvent(
|
|
468
|
+
window.parent,
|
|
469
|
+
targetOrigin,
|
|
470
|
+
"PrivateKeyRevealed",
|
|
471
|
+
correlationId,
|
|
472
|
+
{},
|
|
473
|
+
);
|
|
474
|
+
}
|
|
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);
|
|
451
532
|
}
|
|
452
533
|
|
|
453
534
|
/**
|
package/src/ui.js
CHANGED
|
@@ -225,23 +225,43 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
|
+
* Show the private key hex with Copy + Done. Resolves when the user clicks Done
|
|
229
|
+
* (or the UI is cleared), so Branding can keep the ceremony panel open.
|
|
230
|
+
*
|
|
228
231
|
* @param {Uint8Array} privateKey
|
|
232
|
+
* @returns {Promise<void>}
|
|
229
233
|
*/
|
|
230
234
|
export function showPrivateKey(privateKey) {
|
|
231
|
-
if (!root)
|
|
235
|
+
if (!root) {
|
|
236
|
+
return Promise.reject(new Error("uiNotInitialized"));
|
|
237
|
+
}
|
|
232
238
|
cancelPendingCeremonyConfirm();
|
|
233
239
|
clearUi();
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
const hex = to0xHex(privateKey);
|
|
241
|
+
return new Promise((resolve) => {
|
|
242
|
+
const pre = document.createElement("pre");
|
|
243
|
+
pre.className = "ows-key";
|
|
244
|
+
pre.textContent = hex;
|
|
245
|
+
const copyBtn = document.createElement("button");
|
|
246
|
+
copyBtn.type = "button";
|
|
247
|
+
copyBtn.className = "ows-copy";
|
|
248
|
+
copyBtn.textContent = "Copy";
|
|
249
|
+
copyBtn.addEventListener("click", async () => {
|
|
250
|
+
await navigator.clipboard.writeText(pre.textContent ?? "");
|
|
251
|
+
});
|
|
252
|
+
const doneBtn = document.createElement("button");
|
|
253
|
+
doneBtn.type = "button";
|
|
254
|
+
doneBtn.className = "ows-confirm";
|
|
255
|
+
doneBtn.textContent = "Done";
|
|
256
|
+
doneBtn.addEventListener("click", () => {
|
|
257
|
+
clearUi();
|
|
258
|
+
resolve();
|
|
259
|
+
});
|
|
260
|
+
const actions = document.createElement("div");
|
|
261
|
+
actions.className = "ows-actions";
|
|
262
|
+
actions.append(copyBtn, doneBtn);
|
|
263
|
+
root.append(pre, actions);
|
|
243
264
|
});
|
|
244
|
-
root.append(pre, copyBtn);
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
/**
|
|
@@ -282,3 +302,68 @@ export function promptPassphrase(passwordText, buttonText, minPasswordLength) {
|
|
|
282
302
|
root.append(input, button);
|
|
283
303
|
});
|
|
284
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
|
+
}
|