refactor with uhtml, dynamically swap out page template whenever hash changes in URL
This commit is contained in:
parent
d2d91e6ab3
commit
01d817a206
23
index.html
23
index.html
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
142
src/main.ts
142
src/main.ts
|
@ -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"));
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
Loading…
Reference in New Issue