@1shotapi/ows-signer 0.2.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/src/hex.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @param {string} hex - with or without `0x` prefix
3
+ * @returns {Uint8Array}
4
+ */
5
+ export function hexToBytes(hex) {
6
+ if (hex.startsWith("0x") || hex.startsWith("0X")) {
7
+ hex = hex.slice(2);
8
+ }
9
+ const out = new Uint8Array(hex.length / 2);
10
+ for (let i = 0; i < out.length; i++) {
11
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
12
+ }
13
+ return out;
14
+ }
15
+
16
+ /**
17
+ * @param {unknown} value
18
+ * @param {number} [expectedBytes]
19
+ * @returns {Uint8Array}
20
+ */
21
+ export function parse0xHex(value, expectedBytes) {
22
+ if (typeof value !== "string" || !value.startsWith("0x")) {
23
+ throw new Error("Expected 0x-prefixed hex string");
24
+ }
25
+ const hex = value.slice(2);
26
+ if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) {
27
+ throw new Error("Invalid hex string");
28
+ }
29
+ const bytes = hexToBytes(hex);
30
+ if (expectedBytes !== undefined && bytes.length !== expectedBytes) {
31
+ throw new Error(`Expected ${expectedBytes} bytes, got ${bytes.length}`);
32
+ }
33
+ return bytes;
34
+ }
35
+
36
+ /**
37
+ * @param {Uint8Array} bytes
38
+ * @returns {`0x${string}`}
39
+ */
40
+ export function to0xHex(bytes) {
41
+ return `0x${Array.from(bytes)
42
+ .map((b) => b.toString(16).padStart(2, "0"))
43
+ .join("")}`;
44
+ }
45
+
46
+ /**
47
+ * @param {ArrayBuffer | Uint8Array} buffer
48
+ * @returns {string}
49
+ */
50
+ export function bufferToBase64Url(buffer) {
51
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
52
+ let binary = "";
53
+ for (const b of bytes) binary += String.fromCharCode(b);
54
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
55
+ }
56
+
57
+ /**
58
+ * @param {string} value
59
+ * @returns {Uint8Array}
60
+ */
61
+ export function base64UrlToBytes(value) {
62
+ const pad = "=".repeat((4 - (value.length % 4)) % 4);
63
+ const b64 = value.replace(/-/g, "+").replace(/_/g, "/") + pad;
64
+ const binary = atob(b64);
65
+ const out = new Uint8Array(binary.length);
66
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
67
+ return out;
68
+ }
package/src/main.js ADDED
@@ -0,0 +1,40 @@
1
+ import { handleRequest } from "./handlers.js";
2
+ import {
3
+ isValidNesting,
4
+ setTrustedParentOrigin,
5
+ } from "./state.js";
6
+ import { initUi } from "./ui.js";
7
+ import { isValidParentMessage, parseRequest } from "./rpc.js";
8
+
9
+ function bootstrap() {
10
+ const root = document.getElementById("ows-signer-root");
11
+ if (!root) {
12
+ throw new Error("OWS signer root element missing");
13
+ }
14
+ initUi(root);
15
+
16
+ if (!isValidNesting()) {
17
+ console.error("[ows-signer] Must be embedded in a wallet iframe (double iframe).");
18
+ return;
19
+ }
20
+
21
+ window.addEventListener("message", async (event) => {
22
+ if (!isValidParentMessage(event)) return;
23
+ if (!parseRequest(event.data)) return;
24
+
25
+ setTrustedParentOrigin(event.origin);
26
+ const targetOrigin = event.origin;
27
+ const { method, correlationId, params = {} } = /** @type {{ method: string, correlationId?: string, params?: Record<string, unknown> }} */ (
28
+ event.data
29
+ );
30
+
31
+ await handleRequest(
32
+ method,
33
+ /** @type {Record<string, unknown>} */ (params),
34
+ correlationId,
35
+ targetOrigin,
36
+ );
37
+ });
38
+ }
39
+
40
+ bootstrap();
package/src/rpc.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @typedef {(event: string, correlationId: string | undefined, data: Record<string, unknown>) => void} EmitFn
3
+ */
4
+
5
+ /**
6
+ * @param {Window} target
7
+ * @param {string} targetOrigin
8
+ * @param {string} event
9
+ * @param {string | undefined} correlationId
10
+ * @param {Record<string, unknown>} data
11
+ */
12
+ export function emitEvent(target, targetOrigin, event, correlationId, data) {
13
+ target.postMessage(
14
+ {
15
+ v: 1,
16
+ kind: "event",
17
+ event,
18
+ correlationId,
19
+ data,
20
+ },
21
+ targetOrigin,
22
+ );
23
+ }
24
+
25
+ /**
26
+ * @param {MessageEvent} event
27
+ * @returns {boolean}
28
+ */
29
+ export function isValidParentMessage(event) {
30
+ if (event.source !== window.parent) return false;
31
+ if (window.parent === window.top) return false;
32
+ if (event.source === window) return false;
33
+ return true;
34
+ }
35
+
36
+ /**
37
+ * @param {unknown} data
38
+ * @returns {data is { v: number, kind: 'request', method: string, correlationId?: string, params?: Record<string, unknown> }}
39
+ */
40
+ export function parseRequest(data) {
41
+ if (!data || typeof data !== "object") return false;
42
+ const msg = /** @type {Record<string, unknown>} */ (data);
43
+ return (
44
+ msg.v === 1 &&
45
+ msg.kind === "request" &&
46
+ typeof msg.method === "string"
47
+ );
48
+ }
package/src/state.js ADDED
@@ -0,0 +1,96 @@
1
+ /** @type {Uint8Array | null} */
2
+ let recoveryPrivateKey = null;
3
+
4
+ /** @type {Promise<unknown> | null} */
5
+ let ceremonyPromise = null;
6
+
7
+ /** @type {string | null} */
8
+ let trustedParentOrigin = null;
9
+
10
+ /**
11
+ * @returns {boolean}
12
+ */
13
+ export function isValidNesting() {
14
+ try {
15
+ return window.parent !== window && window.parent !== window.top;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * @returns {string}
23
+ */
24
+ export function getRpId() {
25
+ return window.location.hostname;
26
+ }
27
+
28
+ /**
29
+ * @param {string} origin
30
+ */
31
+ export function setTrustedParentOrigin(origin) {
32
+ if (trustedParentOrigin === null) {
33
+ trustedParentOrigin = origin;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @returns {string | null}
39
+ */
40
+ export function getTrustedParentOrigin() {
41
+ return trustedParentOrigin;
42
+ }
43
+
44
+ /**
45
+ * @returns {boolean}
46
+ */
47
+ export function hasRecoverySession() {
48
+ return recoveryPrivateKey !== null;
49
+ }
50
+
51
+ /**
52
+ * @returns {Uint8Array | null}
53
+ */
54
+ export function getRecoveryPrivateKey() {
55
+ return recoveryPrivateKey;
56
+ }
57
+
58
+ /**
59
+ * @param {Uint8Array} key
60
+ */
61
+ export function setRecoveryPrivateKey(key) {
62
+ clearRecoveryPrivateKey();
63
+ recoveryPrivateKey = key;
64
+ }
65
+
66
+ export function clearRecoveryPrivateKey() {
67
+ if (recoveryPrivateKey) {
68
+ recoveryPrivateKey.fill(0);
69
+ recoveryPrivateKey = null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {() => Promise<T>} fn
75
+ * @returns {Promise<T>}
76
+ * @template T
77
+ */
78
+ export async function withCeremony(fn) {
79
+ if (ceremonyPromise) {
80
+ throw new Error("ceremonyInProgress");
81
+ }
82
+ const promise = fn();
83
+ ceremonyPromise = promise;
84
+ try {
85
+ return await promise;
86
+ } finally {
87
+ ceremonyPromise = null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @param {Uint8Array} bytes
93
+ */
94
+ export function zeroize(bytes) {
95
+ if (bytes) bytes.fill(0);
96
+ }
package/src/ui.js ADDED
@@ -0,0 +1,68 @@
1
+ import { to0xHex } from "./hex.js";
2
+
3
+ /** @type {HTMLElement | null} */
4
+ let root = null;
5
+
6
+ /**
7
+ * @param {HTMLElement} container
8
+ */
9
+ export function initUi(container) {
10
+ root = container;
11
+ root.innerHTML = "";
12
+ root.className = "ows-signer-ui";
13
+ }
14
+
15
+ export function clearUi() {
16
+ if (root) root.innerHTML = "";
17
+ }
18
+
19
+ /**
20
+ * @param {Uint8Array} privateKey
21
+ */
22
+ export function showPrivateKey(privateKey) {
23
+ if (!root) return;
24
+ clearUi();
25
+ const pre = document.createElement("pre");
26
+ pre.className = "ows-key";
27
+ pre.textContent = to0xHex(privateKey);
28
+ const copyBtn = document.createElement("button");
29
+ copyBtn.type = "button";
30
+ copyBtn.className = "ows-copy";
31
+ copyBtn.textContent = "Copy";
32
+ copyBtn.addEventListener("click", async () => {
33
+ await navigator.clipboard.writeText(pre.textContent ?? "");
34
+ });
35
+ root.append(pre, copyBtn);
36
+ }
37
+
38
+ /**
39
+ * @param {string} passwordText
40
+ * @param {string} buttonText
41
+ * @param {number} minPasswordLength
42
+ * @returns {Promise<string>}
43
+ */
44
+ export function promptPassphrase(passwordText, buttonText, minPasswordLength) {
45
+ if (!root) {
46
+ return Promise.reject(new Error("uiNotInitialized"));
47
+ }
48
+ clearUi();
49
+ return new Promise((resolve, reject) => {
50
+ const input = document.createElement("input");
51
+ input.type = "password";
52
+ input.className = "ows-password";
53
+ input.placeholder = passwordText;
54
+ const button = document.createElement("button");
55
+ button.type = "button";
56
+ button.className = "ows-confirm";
57
+ button.textContent = buttonText;
58
+ button.addEventListener("click", () => {
59
+ if (input.value.length < minPasswordLength) {
60
+ reject(new Error("passwordTooShort"));
61
+ return;
62
+ }
63
+ resolve(input.value);
64
+ clearUi();
65
+ });
66
+ root.append(input, button);
67
+ });
68
+ }
@@ -0,0 +1,150 @@
1
+ import { getRpId } from "./state.js";
2
+ import { PRF_LABEL_SECP256K1 } from "./constants.js";
3
+ import { bufferToBase64Url } from "./hex.js";
4
+ import { debugLog, describePrfExtensionResults } from "./debug.js";
5
+
6
+ /**
7
+ * @param {Uint8Array} [userId]
8
+ * @returns {Uint8Array}
9
+ */
10
+ function randomUserId(userId) {
11
+ if (userId) return userId;
12
+ return crypto.getRandomValues(new Uint8Array(16));
13
+ }
14
+
15
+ /**
16
+ * @param {string} name
17
+ * @param {{ rpName?: string, userDisplayName?: string, userId?: string }} [options]
18
+ * @returns {Promise<PublicKeyCredential>}
19
+ */
20
+ export async function createPasskeyCredential(name, options = {}) {
21
+ const rpId = getRpId();
22
+ const rpName =
23
+ typeof options.rpName === "string" && options.rpName
24
+ ? options.rpName
25
+ : "OWS";
26
+ const userId = options.userId
27
+ ? Uint8Array.from(atob(options.userId), (c) => c.charCodeAt(0))
28
+ : randomUserId();
29
+ const displayName = options.userDisplayName ?? name;
30
+
31
+ debugLog("createPasskeyCredential userActivation.isActive", {
32
+ isActive: navigator.userActivation?.isActive ?? false,
33
+ });
34
+
35
+ const credential = await navigator.credentials.create({
36
+ publicKey: {
37
+ rp: { name: rpName, id: rpId },
38
+ user: {
39
+ id: userId,
40
+ name,
41
+ displayName,
42
+ },
43
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
44
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
45
+ authenticatorSelection: {
46
+ residentKey: "required",
47
+ requireResidentKey: true,
48
+ userVerification: "required",
49
+ },
50
+ extensions: {
51
+ prf: {
52
+ enable: {
53
+ first: PRF_LABEL_SECP256K1,
54
+ },
55
+ },
56
+ },
57
+ },
58
+ });
59
+
60
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
61
+ throw new Error("credentialCreationFailed");
62
+ }
63
+ debugLog("createPasskeyCredential", describePrfExtensionResults(credential));
64
+ return credential;
65
+ }
66
+
67
+ /**
68
+ * @param {Uint8Array} [challenge]
69
+ * @param {string} [credentialId]
70
+ * @returns {Promise<PublicKeyCredential>}
71
+ */
72
+ export async function getPasskeyAssertion(challenge, credentialId) {
73
+ const rpId = getRpId();
74
+ const allowCredentials = credentialId
75
+ ? [
76
+ {
77
+ type: "public-key",
78
+ id: base64UrlToBytes(credentialId),
79
+ },
80
+ ]
81
+ : undefined;
82
+
83
+ const credential = await navigator.credentials.get({
84
+ publicKey: {
85
+ challenge: challenge ?? crypto.getRandomValues(new Uint8Array(32)),
86
+ rpId,
87
+ userVerification: "required",
88
+ allowCredentials,
89
+ extensions: {
90
+ prf: {
91
+ eval: {
92
+ first: PRF_LABEL_SECP256K1,
93
+ },
94
+ },
95
+ },
96
+ },
97
+ });
98
+
99
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
100
+ throw new Error("credentialGetFailed");
101
+ }
102
+ debugLog("getPasskeyAssertion", describePrfExtensionResults(credential));
103
+ return credential;
104
+ }
105
+
106
+ /**
107
+ * @param {string} value
108
+ * @returns {Uint8Array}
109
+ */
110
+ function base64UrlToBytes(value) {
111
+ const pad = "=".repeat((4 - (value.length % 4)) % 4);
112
+ const b64 = value.replace(/-/g, "+").replace(/_/g, "/") + pad;
113
+ const binary = atob(b64);
114
+ const out = new Uint8Array(binary.length);
115
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * @param {PublicKeyCredential} credential
121
+ * @returns {string}
122
+ */
123
+ export function getCredentialId(credential) {
124
+ return bufferToBase64Url(credential.rawId);
125
+ }
126
+
127
+ /**
128
+ * @param {PublicKeyCredential} credential
129
+ * @returns {string | null}
130
+ */
131
+ export function getPasskeyPublicKeyBase64Url(credential) {
132
+ const response = credential.response;
133
+ if ("getPublicKey" in response && typeof response.getPublicKey === "function") {
134
+ const key = response.getPublicKey();
135
+ return key ? bufferToBase64Url(key) : null;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * @param {PublicKeyCredential} credential
142
+ * @returns {string | null}
143
+ */
144
+ export function getAssertionSignatureBase64Url(credential) {
145
+ const response = credential.response;
146
+ if ("signature" in response && response.signature) {
147
+ return bufferToBase64Url(response.signature);
148
+ }
149
+ return null;
150
+ }