From 01d817a206e995953a50f371be5f18b3a199eb5a Mon Sep 17 00:00:00 2001
From: Silas
Date: Thu, 1 Feb 2024 21:33:11 -0500
Subject: [PATCH] refactor with uhtml, dynamically swap out page template
whenever hash changes in URL
---
index.html | 23 +-----
package-lock.json | 154 +++++++++++++++++++++++++++++++++++++++-
package.json | 3 +-
src/main.ts | 142 ++++++++++++------------------------
src/routes/receive.ts | 44 ++++++++----
src/routes/request.ts | 63 ++++++++--------
src/routes/send.ts | 107 ++++++++++++++++------------
src/utils/keyManager.ts | 83 ++++++++++++++++++++++
8 files changed, 409 insertions(+), 210 deletions(-)
create mode 100644 src/utils/keyManager.ts
diff --git a/index.html b/index.html
index c2110e7..7ff47bf 100644
--- a/index.html
+++ b/index.html
@@ -67,28 +67,7 @@
-
-
- Private Key:
-
-
-
-
-
-
-
-
-
+
diff --git a/package-lock.json b/package-lock.json
index 789bcdb..258f7d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,14 +1,15 @@
{
- "name": "sure-ts",
+ "name": "sure",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "sure-ts",
+ "name": "sure",
"version": "0.0.0",
"dependencies": {
- "@picocss/pico": "^1.5.11"
+ "@picocss/pico": "^1.5.11",
+ "uhtml": "^4.4.7"
},
"devDependencies": {
"typescript": "^5.2.2",
@@ -389,6 +390,16 @@
"resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-1.5.11.tgz",
"integrity": "sha512-cDaFiSyNPtuSTwSQt/05xsw8+g2ek4/S58tgh9Nc7miJCCdUrY9PAyl4OPWRJtYgJDdEvkUA9GuGj0J4nDP4Cw=="
},
+ "node_modules/@preact/signals-core": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.5.1.tgz",
+ "integrity": "sha512-dE6f+WCX5ZUDwXzUIWNMhhglmuLpqJhuy3X3xHrhZYI0Hm2LyQwOu0l9mdPiWrVNsE+Q7txOnJPgtIqHCYoBVA==",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.9.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
@@ -564,6 +575,92 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
+ "node_modules/@webreflection/signal": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@webreflection/signal/-/signal-2.0.0.tgz",
+ "integrity": "sha512-qtReCZZOK2x6d4ObGg4VfiNQOpPxcVZ2VGx0Yevlw0fTJ5PJsK3cDN/SXEI+equTfkNcDgtEIh37eIVj0oaqJw==",
+ "optional": true
+ },
+ "node_modules/@webreflection/uparser": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@webreflection/uparser/-/uparser-0.3.3.tgz",
+ "integrity": "sha512-XxGfo8jr2eVuvP5lrmwjgMAM7QjtZ0ngFD+dd9Fd3GStcEb4QhLlTiqZYF5O3l5k4sU/V6ZiPrVCzCWXWFEmCw==",
+ "dependencies": {
+ "domconstants": "^1.1.6"
+ }
+ },
+ "node_modules/custom-function": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/custom-function/-/custom-function-1.0.6.tgz",
+ "integrity": "sha512-styyvwOki/EYr+VBe7/m9xAjq6uKx87SpDKIpFRdTQnofBDSZpBEFc9qJLmaJihjjTeEpAIJ+nz+9fUXj+BPNQ=="
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domconstants": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/domconstants/-/domconstants-1.1.6.tgz",
+ "integrity": "sha512-CuaDrThJ4VM+LyZ4ax8n52k0KbLJZtffyGkuj1WhpTRRcSfcy/9DfOBa68jenhX96oNUTunblSJEUNC4baFdmQ=="
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ]
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/esbuild": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
@@ -616,6 +713,34 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/gc-hook": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz",
+ "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A=="
+ },
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
+ },
+ "node_modules/htmlparser2": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
+ "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.1.0",
+ "entities": "^4.5.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -722,6 +847,29 @@
"node": ">=14.17"
}
},
+ "node_modules/udomdiff": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/udomdiff/-/udomdiff-1.1.0.tgz",
+ "integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA=="
+ },
+ "node_modules/uhtml": {
+ "version": "4.4.7",
+ "resolved": "https://registry.npmjs.org/uhtml/-/uhtml-4.4.7.tgz",
+ "integrity": "sha512-Y4iuj8bGyP+ryk2J+9T4xhOWymnizB7QyTdXF/mA+B3rFNCC5GQq06+wI64QCMvvcNjQMBRbI2kMFppcEJZGWA==",
+ "dependencies": {
+ "@webreflection/uparser": "^0.3.3",
+ "custom-function": "^1.0.6",
+ "domconstants": "^1.1.6",
+ "gc-hook": "^0.3.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^9.1.0",
+ "udomdiff": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "@preact/signals-core": "^1.5.1",
+ "@webreflection/signal": "^2.0.0"
+ }
+ },
"node_modules/vite": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
diff --git a/package.json b/package.json
index f9370d8..c70d32e 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"vite-plugin-make-offline": "^1.0.0"
},
"dependencies": {
- "@picocss/pico": "^1.5.11"
+ "@picocss/pico": "^1.5.11",
+ "uhtml": "^4.4.7"
}
}
diff --git a/src/main.ts b/src/main.ts
index 6eb6e94..cbdbdd8 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,112 +3,64 @@ import "./style.css";
import { setupRequestPage } from "./routes/request.ts";
import { setupSendPage } from "./routes/send.ts";
import { setupReceivePage } from "./routes/receive.ts";
-import {
- exportKeys,
- importAndSaveKeys,
- retrieveOrGenerateKeyPair,
-} from "./utils/crypto.ts";
+import { initKeyManager } from "./utils/keyManager.ts";
-const updateKeysInModal = () => {
- console.log("help");
- const publicKeyElement = document.getElementById("ecdhPublicKey");
- const privateKeyElement = document.getElementById("ecdhPrivateKey");
-
- const publicKey = localStorage.getItem("ecdhPublic");
- const privateKey = localStorage.getItem("ecdhPrivate");
-
- if (!publicKeyElement || !privateKeyElement) {
- throw new Error("Key manager elements not found");
- }
-
- if (publicKey && privateKey) {
- publicKeyElement.textContent = publicKey;
- privateKeyElement.textContent = privateKey;
- }
+type PageState = {
+ page: string;
+ params: URLSearchParams;
};
-const start = () => {
- const fragmentData = new URLSearchParams(window.location.hash.slice(1));
+let state: PageState = {
+ page: "request",
+ params: new URLSearchParams(window.location.hash.slice(1)),
+};
+
+const renderPage = () => {
const appElement = document.querySelector("#app");
- const keyManagerEl = document.getElementById("openKeyManager");
- const keyManagerModal = document.getElementById("keyManagerModal");
- const closeKeyManager = document.getElementById("closeKeyManager");
if (!appElement) {
throw new Error("No app element found");
}
- if (!keyManagerEl || !keyManagerModal || !closeKeyManager) {
- throw new Error("No key manager element found");
+ switch (state.page) {
+ case "request":
+ setupRequestPage(appElement);
+ break;
+ case "send":
+ setupSendPage(appElement, state.params.get("p")!);
+ break;
+ case "receive":
+ setupReceivePage(appElement, {
+ p: decodeURIComponent(state.params.get("p")!),
+ iv: decodeURIComponent(state.params.get("iv")!),
+ m: decodeURIComponent(state.params.get("m")!),
+ });
+ break;
}
-
- retrieveOrGenerateKeyPair();
- keyManagerEl.addEventListener("click", () => {
- updateKeysInModal();
- keyManagerModal.setAttribute("open", "");
- });
-
- closeKeyManager.addEventListener("click", () => {
- keyManagerModal.removeAttribute("open");
- });
-
- // If the URL contains the 'p', 'iv', and 'm' parameters, then the receiver page is set up
- if (
- fragmentData.has("p") &&
- fragmentData.has("iv") &&
- fragmentData.has("m")
- ) {
- setupReceivePage(appElement, {
- p: decodeURIComponent(fragmentData.get("p")!),
- iv: decodeURIComponent(fragmentData.get("iv")!),
- m: decodeURIComponent(fragmentData.get("m")!),
- });
- return;
- }
-
- // If the URL contains the 'p' parameter, then the sender page is set up
- if (fragmentData.has("p")) {
- setupSendPage(appElement, fragmentData.get("p")!);
- return;
- }
-
- // Otherwise, the request page is set up
- setupRequestPage(appElement);
};
-const setupKeyManager = async () => {
- const exportKeyEl = document.getElementById("exportKeypair");
- const importKeyEl = document.getElementById("importKeypair");
-
- if (!exportKeyEl || !importKeyEl) {
- throw new Error("Key management elements not found");
- }
-
- exportKeyEl.addEventListener("click", exportKeys);
- importKeyEl.addEventListener("click", () => {
- const fileInput = document.createElement("input");
- fileInput.type = "file";
- fileInput.style.display = "none";
- document.body.appendChild(fileInput);
-
- fileInput.click();
-
- fileInput.addEventListener("change", async (e) => {
- const file = (e.target as HTMLInputElement).files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = async (e) => {
- const keys = JSON.parse(e.target?.result as string);
- await importAndSaveKeys(keys);
-
- document.body.removeChild(fileInput);
- alert("Successfully imported keypair");
- };
- reader.readAsText(file);
- });
- });
+// Define a function to update the state and re-render
+const setState = (newState: PageState) => {
+ state = { ...state, ...newState };
+ renderPage();
};
-start();
-setupKeyManager();
+const update = () => {
+ const params = new URLSearchParams(window.location.hash.slice(1));
+
+ if (params.has("p") && params.has("iv") && params.has("m")) {
+ setState({ page: "receive", params });
+ } else if (params.has("p")) {
+ setState({ page: "send", params });
+ } else {
+ setState({ page: "request", params });
+ }
+};
+
+// Listen for changes in the hash and update the state accordingly
+window.addEventListener("hashchange", update);
+
+// Initialize the app
+update();
+renderPage();
+initKeyManager(document.querySelector("#keyManagerContainer"));
diff --git a/src/routes/receive.ts b/src/routes/receive.ts
index 9601cb0..834e4b7 100644
--- a/src/routes/receive.ts
+++ b/src/routes/receive.ts
@@ -1,3 +1,4 @@
+import { html, render } from "uhtml";
import {
deriveSharedSecret,
decrypt,
@@ -5,12 +6,26 @@ import {
} from "../utils/crypto";
const MESSAGE_OUTPUT_ID = "message";
-const TEMPLATE = `
+
+type PageState = {
+ decryptedData: string;
+};
+let state: PageState = {
+ decryptedData: "",
+};
+// Move the template to a `uhtml` function
+const Template = (state: PageState) => html`
How To Use:
- - If someone used your unique request URL to generate this response URL, you should see the decrypted message below.
- - Be sure to open this from the same browser you used to generate the original request URL.
+ -
+ If someone used your unique request URL to generate this response URL,
+ you should see the decrypted message below.
+
+ -
+ Be sure to open this from the same browser you used to generate the
+ original request URL.
+
@@ -19,20 +34,27 @@ const TEMPLATE = `
id="${MESSAGE_OUTPUT_ID}"
readonly
aria-label="Decrypted Message"
+ .value=${state.decryptedData}
>
- `;
+`;
export async function setupReceivePage(
element: HTMLElement,
params: { p: string; iv: string; m: string }
) {
- element.innerHTML = TEMPLATE;
-
- decryptData(params);
+ decryptData(params, element);
}
-async function decryptData({ p, iv, m }: { p: string; iv: string; m: string }) {
+const setState = (newState: PageState, element: HTMLElement) => {
+ state = { ...state, ...newState };
+ render(element, Template(state));
+};
+
+async function decryptData(
+ { p, iv, m }: { p: string; iv: string; m: string },
+ element: HTMLElement
+) {
// Parse the 'p' parameter to get publicB
const publicBJwk = JSON.parse(atob(p));
@@ -57,8 +79,6 @@ async function decryptData({ p, iv, m }: { p: string; iv: string; m: string }) {
const decryptedData = await decrypt(aesKey, iv, m);
// Update the message output with the decrypted message
- const messageOutput = document.getElementById(
- MESSAGE_OUTPUT_ID
- ) as HTMLTextAreaElement;
- messageOutput.value = decryptedData;
+ setState({ decryptedData }, element);
+ render(element, Template(state));
}
diff --git a/src/routes/request.ts b/src/routes/request.ts
index d178df1..d011b6c 100644
--- a/src/routes/request.ts
+++ b/src/routes/request.ts
@@ -1,41 +1,42 @@
+import { html, render } from "uhtml";
import { retrieveOrGenerateKeyPair } from "../utils/crypto.ts";
const URL_INPUT_ID = "request-url";
const COPY_BUTTON_ID = "copy-button";
-const TEMPLATE = `
-
- How To Use:
-
+
+export async function setupRequestPage(element: HTMLElement) {
+ const url = await generateUrl();
+ const template = html`
+
+ How To Use:
+
- Send the link below to someone you want to get a secret from.
- They add their secret and share the URL the app generates.
- Only your browser can open the URL and decrypt the secret.
-
-
+
+
-
- `;
+
+ `;
-export async function setupRequestPage(element: HTMLElement) {
- element.innerHTML = TEMPLATE;
-
- const url = await generateUrl();
- const input = document.getElementById(URL_INPUT_ID) as HTMLInputElement;
- input.value = url.toString();
-
- const copyButton = document.getElementById(
- COPY_BUTTON_ID
- ) as HTMLButtonElement;
- copyButton.addEventListener("click", copyToClipboard);
+ render(element, template);
}
async function copyToClipboard(event: Event) {
@@ -47,13 +48,9 @@ async function copyToClipboard(event: Event) {
async function generateUrl(): Promise {
const { publicKey: ecdhPublic } = await retrieveOrGenerateKeyPair();
-
- // Generate URL with public key as the 'p' search parameter
const ecdhPublicJwk = await window.crypto.subtle.exportKey("jwk", ecdhPublic);
const url = new URL(window.location.toString());
url.search = "";
url.hash = `p=${btoa(JSON.stringify(ecdhPublicJwk))}`;
-
- // Return the generated URL
return url;
}
diff --git a/src/routes/send.ts b/src/routes/send.ts
index aed88b3..c5809e8 100644
--- a/src/routes/send.ts
+++ b/src/routes/send.ts
@@ -1,3 +1,4 @@
+import { html, render } from "uhtml";
import {
deriveSharedSecret,
encrypt,
@@ -9,64 +10,94 @@ const MESSAGE_INPUT_ID = "message";
const ENCRYPT_BUTTON_ID = "encrypt";
const REQUEST_PUBLIC_KEY = "requestPublicKey";
const COPY_BUTTON_ID = "copy-button";
-const TEMPLATE = `
+
+type PageState = {
+ message: string;
+ encryptedUrl: string;
+ isCopyButtonDisabled: boolean;
+ container: HTMLElement | null;
+};
+const state: PageState = {
+ message: "",
+ encryptedUrl: "",
+ isCopyButtonDisabled: true,
+ container: null,
+};
+
+const update = (element: HTMLElement) => {
+ // Re-render the component whenever the state changes
+ render(element, App(state));
+};
+
+const App = (state: PageState) => html`
How To Use:
- - Enter the information you want to send back to the original requester into the text input below.
- - Click on 'Generate Response'. This will create a new URL that contains your encrypted message.
- - Send the newly generated URL back to the original requester. Only their browser will be able to decrypt the message.
+ -
+ Enter the information you want to send back to the original requester
+ into the text input below.
+
+ -
+ Click on 'Generate Response'. This will create a new URL that contains
+ your encrypted message.
+
+ -
+ Send the newly generated URL back to the original requester. Only their
+ browser will be able to decrypt the message.
+
-
- `;
+`;
export async function setupSendPage(element: HTMLElement, key: string) {
- element.innerHTML = TEMPLATE;
localStorage.setItem(REQUEST_PUBLIC_KEY, key);
-
- // Add an event listener to the "Encrypt" button
- const encryptButton = document.getElementById(
- ENCRYPT_BUTTON_ID
- ) as HTMLButtonElement;
- encryptButton.addEventListener("click", encryptData);
-
- const copyButton = document.getElementById(
- COPY_BUTTON_ID
- ) as HTMLButtonElement;
- copyButton.addEventListener("click", copyToClipboard);
+ state.container = element;
+ update(element);
}
async function copyToClipboard(event: Event) {
event.preventDefault();
- const input = document.getElementById(
- ENCRYPTED_URL_INPUT_ID
- ) as HTMLInputElement;
- await navigator.clipboard.writeText(input.value);
+ await navigator.clipboard.writeText(state.encryptedUrl);
alert("Copied to clipboard! Send this URL to the requester.");
}
@@ -91,21 +122,11 @@ async function encryptData(event: Event) {
const keyPairB = await retrieveOrGenerateKeyPair();
- // Retrieve the message input
- const messageInput = document.getElementById(
- MESSAGE_INPUT_ID
- ) as HTMLInputElement;
-
// Derive the AES key from your private key and the recipient's public key
const aesKey = await deriveSharedSecret(keyPairB.privateKey, publicA);
// Encrypt the message input value using the AES key
- const { encryptedData, iv } = await encrypt(aesKey, messageInput.value);
-
- // Update the encrypted URL input with the encrypted message
- const encryptedUrlInput = document.getElementById(
- ENCRYPTED_URL_INPUT_ID
- ) as HTMLInputElement;
+ const { encryptedData, iv } = await encrypt(aesKey, state.message);
const ecdhPublicJwk = await window.crypto.subtle.exportKey(
"jwk",
@@ -123,9 +144,7 @@ async function encryptData(event: Event) {
)
)}`;
- encryptedUrlInput.value = url.toString();
- const copyButton = document.getElementById(
- COPY_BUTTON_ID
- ) as HTMLButtonElement;
- copyButton.disabled = false;
+ state.encryptedUrl = url.toString();
+ state.isCopyButtonDisabled = false;
+ update(state.container!);
}
diff --git a/src/utils/keyManager.ts b/src/utils/keyManager.ts
new file mode 100644
index 0000000..5f75c59
--- /dev/null
+++ b/src/utils/keyManager.ts
@@ -0,0 +1,83 @@
+import { html } from "uhtml";
+import {
+ exportKeys,
+ importAndSaveKeys,
+ retrieveOrGenerateKeyPair,
+} from "./crypto";
+import { reactive } from "uhtml/reactive";
+import { effect, signal } from "uhtml/preactive";
+
+const render = reactive(effect);
+const ecdhPublicKey = signal(localStorage.getItem("ecdhPublic"));
+const ecdhPrivateKey = signal(localStorage.getItem("ecdhPrivate"));
+const MODAL_ID = "keyManagerModal";
+
+const template = html` `;
+
+function importKeys(): void {
+ const el = document.querySelector("#keypairImportEl");
+ if (!el) {
+ throw new Error("No file input found");
+ }
+ (el as HTMLElement).click();
+}
+
+function handleKeypairImport(event: Event): void {
+ const fileInput = event.target as HTMLInputElement;
+ const file = fileInput.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = async (event) => {
+ const keys = JSON.parse(event.target?.result as string);
+ await importAndSaveKeys(keys);
+
+ alert("Successfully imported keypair");
+ };
+ reader.readAsText(file);
+}
+
+function close(event: Event): void {
+ const dialog = (event.target as HTMLElement).closest(`#${MODAL_ID}`);
+ dialog?.removeAttribute("open");
+}
+
+export const initKeyManager = (target: HTMLElement | null) => {
+ const openKeyManagerEl = document.querySelector("#openKeyManager");
+ if (!target || !openKeyManagerEl) {
+ throw new Error("No target element found");
+ }
+
+ retrieveOrGenerateKeyPair();
+
+ openKeyManagerEl.addEventListener("click", (event: Event) => {
+ event.preventDefault();
+ ecdhPrivateKey.value = localStorage.getItem("ecdhPrivate");
+ ecdhPublicKey.value = localStorage.getItem("ecdhPublic");
+ document.querySelector(`#${MODAL_ID}`)?.setAttribute("open", "");
+ });
+
+ render(target, template);
+};