refactor with uhtml, dynamically swap out page template whenever hash changes in URL

This commit is contained in:
Silas 2024-02-01 21:33:11 -05:00
parent d2d91e6ab3
commit 01d817a206
Signed by: silentsilas
GPG Key ID: 4199EFB7DAA34349
8 changed files with 409 additions and 210 deletions

View File

@ -67,28 +67,7 @@
</article> </article>
</dialog> </dialog>
<dialog class="key-manager" id="keyManagerModal"> <div id="keyManagerContainer"></div>
<article>
<header>Key Manager</header>
<p>
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.
</p>
<p>
Public Key:
<pre id="ecdhPublicKey"></pre>
</p>
<p>
Private Key:
<pre id="ecdhPrivateKey"></pre>
</p>
<p>
<button id="exportKeypair">Export Keys</button>
<button id="importKeypair">Import Keys</button>
<button id="closeKeyManager">Close</button>
</p>
</article>
</dialog>
<main class="container"> <main class="container">
<section> <section>

154
package-lock.json generated
View File

@ -1,14 +1,15 @@
{ {
"name": "sure-ts", "name": "sure",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sure-ts", "name": "sure",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@picocss/pico": "^1.5.11" "@picocss/pico": "^1.5.11",
"uhtml": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.2.2", "typescript": "^5.2.2",
@ -389,6 +390,16 @@
"resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-1.5.11.tgz", "resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-1.5.11.tgz",
"integrity": "sha512-cDaFiSyNPtuSTwSQt/05xsw8+g2ek4/S58tgh9Nc7miJCCdUrY9PAyl4OPWRJtYgJDdEvkUA9GuGj0J4nDP4Cw==" "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": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.9.6", "version": "4.9.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", "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==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "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": { "node_modules/esbuild": {
"version": "0.19.12", "version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "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": "^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": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@ -722,6 +847,29 @@
"node": ">=14.17" "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": { "node_modules/vite": {
"version": "5.0.12", "version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",

View File

@ -14,6 +14,7 @@
"vite-plugin-make-offline": "^1.0.0" "vite-plugin-make-offline": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"@picocss/pico": "^1.5.11" "@picocss/pico": "^1.5.11",
"uhtml": "^4.4.7"
} }
} }

View File

@ -3,112 +3,64 @@ import "./style.css";
import { setupRequestPage } from "./routes/request.ts"; import { setupRequestPage } from "./routes/request.ts";
import { setupSendPage } from "./routes/send.ts"; import { setupSendPage } from "./routes/send.ts";
import { setupReceivePage } from "./routes/receive.ts"; import { setupReceivePage } from "./routes/receive.ts";
import { import { initKeyManager } from "./utils/keyManager.ts";
exportKeys,
importAndSaveKeys,
retrieveOrGenerateKeyPair,
} from "./utils/crypto.ts";
const updateKeysInModal = () => { type PageState = {
console.log("help"); page: string;
const publicKeyElement = document.getElementById("ecdhPublicKey"); params: URLSearchParams;
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;
}
}; };
const start = () => { let state: PageState = {
const fragmentData = new URLSearchParams(window.location.hash.slice(1)); page: "request",
params: new URLSearchParams(window.location.hash.slice(1)),
};
const renderPage = () => {
const appElement = document.querySelector<HTMLElement>("#app"); const appElement = document.querySelector<HTMLElement>("#app");
const keyManagerEl = document.getElementById("openKeyManager");
const keyManagerModal = document.getElementById("keyManagerModal");
const closeKeyManager = document.getElementById("closeKeyManager");
if (!appElement) { if (!appElement) {
throw new Error("No app element found"); throw new Error("No app element found");
} }
if (!keyManagerEl || !keyManagerModal || !closeKeyManager) { switch (state.page) {
throw new Error("No key manager element found"); 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 () => { // Define a function to update the state and re-render
const exportKeyEl = document.getElementById("exportKeypair"); const setState = (newState: PageState) => {
const importKeyEl = document.getElementById("importKeypair"); state = { ...state, ...newState };
renderPage();
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);
});
});
}; };
start(); const update = () => {
setupKeyManager(); 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"));

View File

@ -1,3 +1,4 @@
import { html, render } from "uhtml";
import { import {
deriveSharedSecret, deriveSharedSecret,
decrypt, decrypt,
@ -5,12 +6,26 @@ import {
} from "../utils/crypto"; } from "../utils/crypto";
const MESSAGE_OUTPUT_ID = "message"; 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`
<details open> <details open>
<summary>How To Use:</summary> <summary>How To Use:</summary>
<ul> <ul>
<li>If someone used your unique request URL to generate this response URL, you should see the decrypted message below.</li> <li>
<li>Be sure to open this from the same browser you used to generate the original request URL.</li> If someone used your unique request URL to generate this response URL,
you should see the decrypted message below.
</li>
<li>
Be sure to open this from the same browser you used to generate the
original request URL.
</li>
</ul> </ul>
</details> </details>
@ -19,20 +34,27 @@ const TEMPLATE = `
id="${MESSAGE_OUTPUT_ID}" id="${MESSAGE_OUTPUT_ID}"
readonly readonly
aria-label="Decrypted Message" aria-label="Decrypted Message"
.value=${state.decryptedData}
></textarea> ></textarea>
</form> </form>
`; `;
export async function setupReceivePage( export async function setupReceivePage(
element: HTMLElement, element: HTMLElement,
params: { p: string; iv: string; m: string } params: { p: string; iv: string; m: string }
) { ) {
element.innerHTML = TEMPLATE; decryptData(params, element);
decryptData(params);
} }
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 // Parse the 'p' parameter to get publicB
const publicBJwk = JSON.parse(atob(p)); 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); const decryptedData = await decrypt(aesKey, iv, m);
// Update the message output with the decrypted message // Update the message output with the decrypted message
const messageOutput = document.getElementById( setState({ decryptedData }, element);
MESSAGE_OUTPUT_ID render(element, Template(state));
) as HTMLTextAreaElement;
messageOutput.value = decryptedData;
} }

View File

@ -1,41 +1,42 @@
import { html, render } from "uhtml";
import { retrieveOrGenerateKeyPair } from "../utils/crypto.ts"; import { retrieveOrGenerateKeyPair } from "../utils/crypto.ts";
const URL_INPUT_ID = "request-url"; const URL_INPUT_ID = "request-url";
const COPY_BUTTON_ID = "copy-button"; const COPY_BUTTON_ID = "copy-button";
const TEMPLATE = `
<details open> export async function setupRequestPage(element: HTMLElement) {
<summary>How To Use:</summary> const url = await generateUrl();
<ol> const template = html`
<details open>
<summary>How To Use:</summary>
<ol>
<li>Send the link below to someone you want to get a secret from.</li> <li>Send the link below to someone you want to get a secret from.</li>
<li>They add their secret and share the URL the app generates.</li> <li>They add their secret and share the URL the app generates.</li>
<li>Only your browser can open the URL and decrypt the secret.</li> <li>Only your browser can open the URL and decrypt the secret.</li>
</ol> </ol>
</details> </details>
<form> <form>
<input <input
type="text" type="text"
name="request-url" name="request-url"
id="${URL_INPUT_ID}" .id=${URL_INPUT_ID}
placeholder="" .value=${url.toString()}
aria-label="Request URL" placeholder=""
required aria-label="Request URL"
/> required
<button aria-label="Copy to Clipboard" id="${COPY_BUTTON_ID}">Copy to Clipboard</button> />
</form> <button
`; aria-label="Copy to Clipboard"
.id=${COPY_BUTTON_ID}
@click=${copyToClipboard}
>
Copy to Clipboard
</button>
</form>
`;
export async function setupRequestPage(element: HTMLElement) { render(element, template);
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);
} }
async function copyToClipboard(event: Event) { async function copyToClipboard(event: Event) {
@ -47,13 +48,9 @@ async function copyToClipboard(event: Event) {
async function generateUrl(): Promise<URL> { async function generateUrl(): Promise<URL> {
const { publicKey: ecdhPublic } = await retrieveOrGenerateKeyPair(); 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 ecdhPublicJwk = await window.crypto.subtle.exportKey("jwk", ecdhPublic);
const url = new URL(window.location.toString()); const url = new URL(window.location.toString());
url.search = ""; url.search = "";
url.hash = `p=${btoa(JSON.stringify(ecdhPublicJwk))}`; url.hash = `p=${btoa(JSON.stringify(ecdhPublicJwk))}`;
// Return the generated URL
return url; return url;
} }

View File

@ -1,3 +1,4 @@
import { html, render } from "uhtml";
import { import {
deriveSharedSecret, deriveSharedSecret,
encrypt, encrypt,
@ -9,64 +10,94 @@ const MESSAGE_INPUT_ID = "message";
const ENCRYPT_BUTTON_ID = "encrypt"; const ENCRYPT_BUTTON_ID = "encrypt";
const REQUEST_PUBLIC_KEY = "requestPublicKey"; const REQUEST_PUBLIC_KEY = "requestPublicKey";
const COPY_BUTTON_ID = "copy-button"; 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`
<details open> <details open>
<summary>How To Use:</summary> <summary>How To Use:</summary>
<ol> <ol>
<li>Enter the information you want to send back to the original requester into the text input below.</li> <li>
<li>Click on 'Generate Response'. This will create a new URL that contains your encrypted message.</li> Enter the information you want to send back to the original requester
<li>Send the newly generated URL back to the original requester. Only their browser will be able to decrypt the message.</li> into the text input below.
</li>
<li>
Click on 'Generate Response'. This will create a new URL that contains
your encrypted message.
</li>
<li>
Send the newly generated URL back to the original requester. Only their
browser will be able to decrypt the message.
</li>
</ol> </ol>
</details> </details>
<form> <form>
<input <input
type="text" type="text"
name="message" name="message"
id="${MESSAGE_INPUT_ID}" .id=${MESSAGE_INPUT_ID}
placeholder="Enter your message here..." placeholder="Enter your message here..."
aria-label="Message to Encrypt" aria-label="Messag
to Encrypt"
required required
value=${state.message}
@input=${(e: Event) => {
state.message = (e.target as HTMLInputElement).value;
update(state.container!);
}}
/> />
<input <input
id="${ENCRYPT_BUTTON_ID}" .id=${ENCRYPT_BUTTON_ID}
type="submit" type="submit"
value="Generate Response" value="Generate Response"
aria-label="Generate Response" aria-label="Generate Response"
@click=${encryptData}
/> />
<input <input
type="text" type="text"
name="encrypted-url" name="encrypted-url"
id="${ENCRYPTED_URL_INPUT_ID}" .id=${ENCRYPTED_URL_INPUT_ID}
placeholder="URL with your encrypted response will appear here..." placeholder="URL with your encrypted response
will appear here..."
aria-label="Encrypted URL" aria-label="Encrypted URL"
value=${state.encryptedUrl}
/> />
<button aria-label="Copy to Clipboard" id="${COPY_BUTTON_ID}" disabled>Copy to Clipboard</button> <button
aria-label="Copy to Clipboard"
.id=${COPY_BUTTON_ID}
@click=${copyToClipboard}
disabled=${state.isCopyButtonDisabled}
>
Copy to Clipboard
</button>
</form> </form>
`; `;
export async function setupSendPage(element: HTMLElement, key: string) { export async function setupSendPage(element: HTMLElement, key: string) {
element.innerHTML = TEMPLATE;
localStorage.setItem(REQUEST_PUBLIC_KEY, key); localStorage.setItem(REQUEST_PUBLIC_KEY, key);
state.container = element;
// Add an event listener to the "Encrypt" button update(element);
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);
} }
async function copyToClipboard(event: Event) { async function copyToClipboard(event: Event) {
event.preventDefault(); event.preventDefault();
const input = document.getElementById( await navigator.clipboard.writeText(state.encryptedUrl);
ENCRYPTED_URL_INPUT_ID
) as HTMLInputElement;
await navigator.clipboard.writeText(input.value);
alert("Copied to clipboard! Send this URL to the requester."); alert("Copied to clipboard! Send this URL to the requester.");
} }
@ -91,21 +122,11 @@ async function encryptData(event: Event) {
const keyPairB = await retrieveOrGenerateKeyPair(); 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 // Derive the AES key from your private key and the recipient's public key
const aesKey = await deriveSharedSecret(keyPairB.privateKey, publicA); const aesKey = await deriveSharedSecret(keyPairB.privateKey, publicA);
// Encrypt the message input value using the AES key // Encrypt the message input value using the AES key
const { encryptedData, iv } = await encrypt(aesKey, messageInput.value); const { encryptedData, iv } = await encrypt(aesKey, state.message);
// Update the encrypted URL input with the encrypted message
const encryptedUrlInput = document.getElementById(
ENCRYPTED_URL_INPUT_ID
) as HTMLInputElement;
const ecdhPublicJwk = await window.crypto.subtle.exportKey( const ecdhPublicJwk = await window.crypto.subtle.exportKey(
"jwk", "jwk",
@ -123,9 +144,7 @@ async function encryptData(event: Event) {
) )
)}`; )}`;
encryptedUrlInput.value = url.toString(); state.encryptedUrl = url.toString();
const copyButton = document.getElementById( state.isCopyButtonDisabled = false;
COPY_BUTTON_ID update(state.container!);
) as HTMLButtonElement;
copyButton.disabled = false;
} }

83
src/utils/keyManager.ts Normal file
View File

@ -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` <dialog class="key-manager" .id=${MODAL_ID}>
<article>
<header>Key Manager</header>
<p>
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.
</p>
<p>
Public Key:
<pre>${ecdhPublicKey}</pre>
</p>
<p>
Private Key:
<pre>${ecdhPrivateKey}</pre>
</p>
<p>
<button @click=${exportKeys}>Export Keys</button>
<button @click=${importKeys}>Import Keys</button>
<input type="file" accept=".json" style="display: none" id="keypairImportEl" @change=${handleKeypairImport} />
<button @click=${close}>Close</button>
</p>
</article>
</dialog>`;
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);
};