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 @@ - -
-
Key Manager
-

- If you'd like to manage your keys, you can do so here. You can export your keys to transfer to a different - device, or import keys exported from a different device. -

-

- Public Key: -


-      

-

- 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: +
    1. Send the link below to someone you want to get a secret from.
    2. They add their secret and share the URL the app generates.
    3. Only your browser can open the URL and decrypt the secret.
    4. -
    -
    +
+
-
- - -
- `; +
+ + +
+ `; -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:
    -
  1. Enter the information you want to send back to the original requester into the text input below.
  2. -
  3. Click on 'Generate Response'. This will create a new URL that contains your encrypted message.
  4. -
  5. Send the newly generated URL back to the original requester. Only their browser will be able to decrypt the message.
  6. +
  7. + Enter the information you want to send back to the original requester + into the text input below. +
  8. +
  9. + Click on 'Generate Response'. This will create a new URL that contains + your encrypted message. +
  10. +
  11. + Send the newly generated URL back to the original requester. Only their + browser will be able to decrypt the message. +
-
{ + state.message = (e.target as HTMLInputElement).value; + update(state.container!); + }} /> - +
- `; +`; 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` +
+
Key Manager
+

+ If you'd like to manage your keys, you can do so here. You can export your keys to transfer to a different + device, or import keys exported from a different device. +

+

+ Public Key: +

${ecdhPublicKey}
+

+

+ Private Key: +

${ecdhPrivateKey}
+

+

+ + + + +

+
+
`; + +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); +};