set up service worker caching, fix perf issues with rerenders, split code into more modules, switch to a green theme, add modal for updating site via service worker
This commit is contained in:
parent
ccfe6c60cd
commit
ea4c8a3a30
|
@ -10,6 +10,7 @@ lerna-debug.log*
|
|||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dev-dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
|
|
122
index.html
122
index.html
|
@ -7,15 +7,6 @@
|
|||
|
||||
<title>SURE - Secure URL Requests</title>
|
||||
|
||||
<script type="module">
|
||||
document.documentElement.classList.remove("no-js");
|
||||
document.documentElement.classList.add("js");
|
||||
|
||||
window.onload = function () {
|
||||
document.body.style.opacity = "1";
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name="description" content="Securely request information via SURE links" />
|
||||
<meta property="og:title" content="SURE - Secure URL Requests" />
|
||||
<meta property="og:description" content="Securely request information via SURE links" />
|
||||
|
@ -30,7 +21,6 @@
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="application-name" content="SURE" />
|
||||
|
@ -39,24 +29,24 @@
|
|||
<meta name="msapplication-navbutton-color" content="#44a616" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="msapplication-starturl" content="/" />
|
||||
|
||||
<style>
|
||||
#js-warning {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<noscript>
|
||||
<style>
|
||||
#js-warning {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
</head>
|
||||
|
||||
<body style="opacity: 0;">
|
||||
<header class="container">
|
||||
<hgroup>
|
||||
<h1>
|
||||
SURE
|
||||
</h1>
|
||||
<h3>
|
||||
<span style="color: #1095C1;">S</span>ecure
|
||||
<span style="color: #1095C1;">U</span>RL
|
||||
<span style="color: #1095C1;">Re</span>quests
|
||||
</h3>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<!-- No JS Warning -->
|
||||
<dialog open class="js-warning">
|
||||
<body>
|
||||
<header class="container" id="header"></header>
|
||||
<dialog open id="js-warning">
|
||||
<article>
|
||||
<header>No Javascript Detected</header>
|
||||
<p>
|
||||
|
@ -66,85 +56,7 @@
|
|||
</p>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<div id="keyManagerContainer"></div>
|
||||
|
||||
<main class="container">
|
||||
<section>
|
||||
<div id="app"></div>
|
||||
|
||||
<details>
|
||||
<summary>How it Works:</summary>
|
||||
<p><strong>The first thing to understand is how public key cryptography works.</strong></p>
|
||||
<p>Essentially, you generate a pair of keys that can undo the operations of each other. If you encrypt a message
|
||||
with your public key, it can only be decrypted by your private key.</p>
|
||||
<div style="width: fit-content; margin: auto;">
|
||||
<img src="/img/public_key_crypto_chart.png" alt="Chart of the public key cryptography process" />
|
||||
</div>
|
||||
<p style="margin-top: 20px">This is why you're safe to share your "Request URL" out in the open. Anyone who
|
||||
accesses this URL will receive your public key, and encrypt the message with it along with their own private
|
||||
key. When you receive the URL with their encrypted message, you take their public key and your private key to
|
||||
undo the operations of their private key and your public key.
|
||||
</p>
|
||||
<p><strong>But of course it's not as simple as that</strong></p>
|
||||
<p>Traditional public-key cryptography like RSA is limited in the messages it can encrypt by the size of the
|
||||
public key. To overcome this limitation, we use ECDH for short public keys that can encrypt an arbitrary
|
||||
amount of data by establishing a shared "symmetric" key for encryption/decryption, which is beyond the scope
|
||||
of this explanation. But the following chart should give you a general idea of how the process works.</p>
|
||||
<img src="/img/flowchart.png" alt="Flowchart of the SURE process" />
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>FAQ</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>What is this?</strong>
|
||||
<p>
|
||||
SURE is a system for securely sending information via a URL request. It uses the Elliptic Curve
|
||||
Diffie-Hellman algorithm to generate a shared secret between the sender and recipient, which is then used
|
||||
to encrypt the message in a response URL. The device that generated the URL request can then decrypt the
|
||||
message in the URL sent back to them using their private key.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>What if I want all of my devices to be able to decrypt messages from the same request?</strong>
|
||||
<p>
|
||||
If you want to be able to decrypt messages from the same request on multiple devices, you'll need to
|
||||
export the keypair from the device that generated the request, and then import it into the other
|
||||
devices. You can do this by clicking the "Manage Keys" link at the bottom of the page, and then clicking
|
||||
the "Export Keys" button. This will give you a JSON file that you can import on other devices via the
|
||||
"Import Keys" button.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>What if I lose my keys?</strong>
|
||||
<p>
|
||||
If you lose your keys, either from clearing your localstorage or losing your device, you won't be able to
|
||||
decrypt any messages sent to you. You'll need to send over the new request URL from your device to anyone
|
||||
who wants to send you a message.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Why was this built?</strong>
|
||||
<p>
|
||||
This was built as a proof of concept for a secure messaging system that doesn't require a user to sign up
|
||||
for an account or download an app. It can even work entirely offline if you build the app from source and
|
||||
open the HTML file in your browser. You can also manage your keys so that all of your devices can decrypt
|
||||
messages from the same request URL.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<div style="width: fit-content; margin: auto;">
|
||||
<a href="https://git.silentsilas.com/silentsilas/sure" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
|
|
||||
<a href="https://silentsilas.com" target="_blank" rel="noopener noreferrer">whoami</a> |
|
||||
<a href="#" id="openKeyManager">Manage Keys</a>
|
||||
</div>
|
||||
</footer>
|
||||
<div id="page"></div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,17 +1,20 @@
|
|||
{
|
||||
"name": "sure",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build-standalone": "tsc && vite build --config vite.standalone.config.ts",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-make-offline": "^1.0.0"
|
||||
"vite-plugin-make-offline": "^1.0.0",
|
||||
"vite-plugin-pwa": "^0.17.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@picocss/pico": "^1.5.11",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 278 KiB |
Binary file not shown.
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 36 KiB |
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "SURE",
|
||||
"short_name": "SURE",
|
||||
"theme_color": "#44a616",
|
||||
"background_color": "#3d006e",
|
||||
"display": "minimal-ui",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
90
src/main.ts
90
src/main.ts
|
@ -1,66 +1,34 @@
|
|||
import "@picocss/pico/css/pico.min.css";
|
||||
import "./style.css";
|
||||
import { setupRequestPage } from "./routes/request.ts";
|
||||
import { setupSendPage } from "./routes/send.ts";
|
||||
import { setupReceivePage } from "./routes/receive.ts";
|
||||
import { initKeyManager } from "./utils/keyManager.ts";
|
||||
import "./style.scss";
|
||||
import { KeyManager } from "./template/keyManager.ts";
|
||||
import { Header } from "./template/header.ts";
|
||||
import { html } from "uhtml";
|
||||
import { effect } from "uhtml/preactive";
|
||||
import { reactive } from "uhtml/reactive";
|
||||
import { Footer } from "./template/footer.ts";
|
||||
import { HowItWorks } from "./template/howitworks.ts";
|
||||
import { FAQ } from "./template/faq.ts";
|
||||
import { page } from "./utils/store.ts";
|
||||
import { routeToPage, hashChange } from "./router.ts";
|
||||
import { UpdateModal, checkForUpdates } from "./template/update.ts";
|
||||
|
||||
type PageState = {
|
||||
page: string;
|
||||
params: URLSearchParams;
|
||||
};
|
||||
|
||||
let state: PageState = {
|
||||
page: "request",
|
||||
params: new URLSearchParams(window.location.hash.slice(1)),
|
||||
};
|
||||
|
||||
const renderPage = () => {
|
||||
const appElement = document.querySelector<HTMLElement>("#app");
|
||||
|
||||
if (!appElement) {
|
||||
throw new Error("No app 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to update the state and re-render
|
||||
const setState = (newState: PageState) => {
|
||||
state = { ...state, ...newState };
|
||||
renderPage();
|
||||
};
|
||||
|
||||
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 });
|
||||
}
|
||||
};
|
||||
const render = reactive(effect);
|
||||
checkForUpdates();
|
||||
const template = () => html` ${KeyManager()} ${UpdateModal()}
|
||||
<main class="container">
|
||||
<section>${routeToPage(page.value)}</section>
|
||||
<div>${HowItWorks}</div>
|
||||
<div>${FAQ}</div>
|
||||
</main>
|
||||
${Footer}`;
|
||||
|
||||
// Listen for changes in the hash and update the state accordingly
|
||||
window.addEventListener("hashchange", update);
|
||||
window.addEventListener("hashchange", hashChange);
|
||||
|
||||
// Initialize the app
|
||||
update();
|
||||
renderPage();
|
||||
initKeyManager(document.querySelector("#keyManagerContainer"));
|
||||
hashChange();
|
||||
|
||||
const headerElement = document.getElementById("header");
|
||||
render(headerElement, Header);
|
||||
|
||||
const appElement = document.getElementById("page");
|
||||
render(appElement, template);
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { Hole } from "uhtml";
|
||||
import { RequestPage, generateRequestUrl } from "./routes/request.ts";
|
||||
import { SendPage } from "./routes/send.ts";
|
||||
import { ReceivePage, decryptData } from "./routes/receive.ts";
|
||||
import { NotFoundPage } from "./routes/notfound.ts";
|
||||
import { LOCAL_STORAGE_KEYS, page, params } from "./utils/store.ts";
|
||||
|
||||
export enum Route {
|
||||
Request = "request",
|
||||
Send = "send",
|
||||
Receive = "receive",
|
||||
}
|
||||
|
||||
export const routeToPage = (toPage: Route): Hole => {
|
||||
switch (toPage) {
|
||||
case Route.Request:
|
||||
return RequestPage();
|
||||
case Route.Send:
|
||||
return SendPage();
|
||||
case Route.Receive:
|
||||
return ReceivePage();
|
||||
default:
|
||||
return NotFoundPage;
|
||||
}
|
||||
};
|
||||
|
||||
export const hashChange = () => {
|
||||
const updatedParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
params.value = updatedParams;
|
||||
const p = params.value.get("p");
|
||||
const iv = params.value.get("iv");
|
||||
const m = params.value.get("m");
|
||||
if (p && iv && m) {
|
||||
decryptData({ p, iv, m });
|
||||
page.value = Route.Receive;
|
||||
} else if (p) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.REQUEST_PUBLIC_KEY, p);
|
||||
page.value = Route.Send;
|
||||
} else {
|
||||
generateRequestUrl();
|
||||
page.value = Route.Request;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { html } from "uhtml";
|
||||
|
||||
export const NotFoundPage = html`
|
||||
<h2>You Lost, Dog?</h2>
|
||||
<p>
|
||||
I'm really not sure how you got here. But this is my spot; squatter's rights
|
||||
and all. So please, <a href="/">go take a hike</a>.
|
||||
</p>
|
||||
`;
|
|
@ -1,60 +1,50 @@
|
|||
import { html, render } from "uhtml";
|
||||
import { html } from "uhtml";
|
||||
import {
|
||||
deriveSharedSecret,
|
||||
decrypt,
|
||||
retrieveOrGenerateKeyPair,
|
||||
} from "../utils/crypto";
|
||||
} from "../utils/crypto.ts";
|
||||
import { signal } from "uhtml/preactive";
|
||||
|
||||
const MESSAGE_OUTPUT_ID = "message";
|
||||
const decryptedData = signal("");
|
||||
|
||||
type PageState = {
|
||||
decryptedData: string;
|
||||
};
|
||||
let state: PageState = {
|
||||
decryptedData: "",
|
||||
};
|
||||
// Move the template to a `uhtml` function
|
||||
const Template = (state: PageState) => html`
|
||||
<details open>
|
||||
<summary>How To Use:</summary>
|
||||
<ul>
|
||||
<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>
|
||||
</details>
|
||||
export const ReceivePage = () => {
|
||||
return html`
|
||||
<details open>
|
||||
<summary>How To Use:</summary>
|
||||
<ul>
|
||||
<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>
|
||||
</details>
|
||||
|
||||
<form>
|
||||
<textarea
|
||||
id="${MESSAGE_OUTPUT_ID}"
|
||||
readonly
|
||||
aria-label="Decrypted Message"
|
||||
.value=${state.decryptedData}
|
||||
></textarea>
|
||||
</form>
|
||||
`;
|
||||
|
||||
export async function setupReceivePage(
|
||||
element: HTMLElement,
|
||||
params: { p: string; iv: string; m: string }
|
||||
) {
|
||||
decryptData(params, element);
|
||||
}
|
||||
|
||||
const setState = (newState: PageState, element: HTMLElement) => {
|
||||
state = { ...state, ...newState };
|
||||
render(element, Template(state));
|
||||
<form>
|
||||
<textarea
|
||||
id="${MESSAGE_OUTPUT_ID}"
|
||||
readonly
|
||||
aria-label="Decrypted Message"
|
||||
.value=${decryptedData.value}
|
||||
></textarea>
|
||||
</form>
|
||||
`;
|
||||
};
|
||||
|
||||
async function decryptData(
|
||||
{ p, iv, m }: { p: string; iv: string; m: string },
|
||||
element: HTMLElement
|
||||
) {
|
||||
export async function decryptData({
|
||||
p,
|
||||
iv,
|
||||
m,
|
||||
}: {
|
||||
p: string;
|
||||
iv: string;
|
||||
m: string;
|
||||
}) {
|
||||
// Parse the 'p' parameter to get publicB
|
||||
const publicBJwk = JSON.parse(atob(p));
|
||||
|
||||
|
@ -76,9 +66,5 @@ async function decryptData(
|
|||
const aesKey = await deriveSharedSecret(keyPairA.privateKey, publicB);
|
||||
|
||||
// Decrypt the message using the AES key and IV
|
||||
const decryptedData = await decrypt(aesKey, iv, m);
|
||||
|
||||
// Update the message output with the decrypted message
|
||||
setState({ decryptedData }, element);
|
||||
render(element, Template(state));
|
||||
decryptedData.value = await decrypt(aesKey, iv, m);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { html, render } from "uhtml";
|
||||
import { html } from "uhtml";
|
||||
import { retrieveOrGenerateKeyPair } from "../utils/crypto.ts";
|
||||
import { signal } from "uhtml/preactive";
|
||||
|
||||
const URL_INPUT_ID = "request-url";
|
||||
const COPY_BUTTON_ID = "copy-button";
|
||||
const requestUrl = signal(new URL("https://sure.dog/"));
|
||||
|
||||
export async function setupRequestPage(element: HTMLElement) {
|
||||
const url = await generateUrl();
|
||||
const template = html`
|
||||
export function RequestPage() {
|
||||
return html`
|
||||
<details open>
|
||||
<summary>How To Use:</summary>
|
||||
<summary><strong>How To Use:</strong></summary>
|
||||
<ol>
|
||||
<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>
|
||||
|
@ -21,7 +22,7 @@ export async function setupRequestPage(element: HTMLElement) {
|
|||
type="text"
|
||||
name="request-url"
|
||||
.id=${URL_INPUT_ID}
|
||||
.value=${url.toString()}
|
||||
.value=${requestUrl.toString()}
|
||||
placeholder=""
|
||||
aria-label="Request URL"
|
||||
required
|
||||
|
@ -29,14 +30,13 @@ export async function setupRequestPage(element: HTMLElement) {
|
|||
<button
|
||||
aria-label="Copy to Clipboard"
|
||||
.id=${COPY_BUTTON_ID}
|
||||
@click=${(event: Event) => copyToClipboard(event, url.toString())}
|
||||
@click=${(event: Event) =>
|
||||
copyToClipboard(event, requestUrl.toString())}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
render(element, template);
|
||||
}
|
||||
|
||||
async function copyToClipboard(event: Event, url: string) {
|
||||
|
@ -45,11 +45,11 @@ async function copyToClipboard(event: Event, url: string) {
|
|||
alert("Copied to clipboard!");
|
||||
}
|
||||
|
||||
async function generateUrl(): Promise<URL> {
|
||||
export async function generateRequestUrl() {
|
||||
const { publicKey: ecdhPublic } = await retrieveOrGenerateKeyPair();
|
||||
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 url;
|
||||
const updatedUrl = new URL(window.location.toString());
|
||||
updatedUrl.search = "";
|
||||
updatedUrl.hash = `p=${btoa(JSON.stringify(ecdhPublicJwk))}`;
|
||||
requestUrl.value = updatedUrl;
|
||||
}
|
||||
|
|
|
@ -1,109 +1,82 @@
|
|||
import { html, render } from "uhtml";
|
||||
import { html } from "uhtml";
|
||||
import {
|
||||
deriveSharedSecret,
|
||||
encrypt,
|
||||
retrieveOrGenerateKeyPair,
|
||||
} from "../utils/crypto";
|
||||
} from "../utils/crypto.ts";
|
||||
import { signal } from "uhtml/preactive";
|
||||
import { LOCAL_STORAGE_KEYS } from "../utils/store.ts";
|
||||
|
||||
const ENCRYPTED_URL_INPUT_ID = "encrypted-url";
|
||||
const MESSAGE_INPUT_ID = "message";
|
||||
const ENCRYPT_BUTTON_ID = "encrypt";
|
||||
const REQUEST_PUBLIC_KEY = "requestPublicKey";
|
||||
const COPY_BUTTON_ID = "copy-button";
|
||||
const messageToEncrypt = signal("");
|
||||
const encryptedUrl = signal("");
|
||||
const isCopyButtonDisabled = signal(true);
|
||||
|
||||
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>
|
||||
<summary>How To Use:</summary>
|
||||
<ol>
|
||||
<li>
|
||||
Enter the information you want to send back to the original requester
|
||||
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>
|
||||
</details>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
name="message"
|
||||
.id=${MESSAGE_INPUT_ID}
|
||||
placeholder="Enter your message here..."
|
||||
aria-label="Messag
|
||||
export const SendPage = () => {
|
||||
return html`
|
||||
<details open>
|
||||
<summary>How To Use:</summary>
|
||||
<ol>
|
||||
<li>
|
||||
Enter the information you want to send back to the original requester
|
||||
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>
|
||||
</details>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
name="message"
|
||||
placeholder="Enter your message here..."
|
||||
aria-label="Messag
|
||||
to Encrypt"
|
||||
required
|
||||
value=${state.message}
|
||||
@input=${(e: Event) => {
|
||||
state.message = (e.target as HTMLInputElement).value;
|
||||
update(state.container!);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
.id=${ENCRYPT_BUTTON_ID}
|
||||
type="submit"
|
||||
value="Generate Response"
|
||||
aria-label="Generate Response"
|
||||
@click=${encryptData}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="encrypted-url"
|
||||
.id=${ENCRYPTED_URL_INPUT_ID}
|
||||
placeholder="URL with your encrypted response
|
||||
required
|
||||
value=${messageToEncrypt.value}
|
||||
@input=${(e: Event) => {
|
||||
messageToEncrypt.value = (e.target as HTMLInputElement).value;
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value="Generate Response"
|
||||
aria-label="Generate Response"
|
||||
@click=${encryptData}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="encrypted-url"
|
||||
placeholder="URL with your encrypted response
|
||||
will appear here..."
|
||||
aria-label="Encrypted URL"
|
||||
value=${state.encryptedUrl}
|
||||
/>
|
||||
<button
|
||||
aria-label="Copy to Clipboard"
|
||||
.id=${COPY_BUTTON_ID}
|
||||
@click=${copyToClipboard}
|
||||
disabled=${state.isCopyButtonDisabled}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
export async function setupSendPage(element: HTMLElement, key: string) {
|
||||
localStorage.setItem(REQUEST_PUBLIC_KEY, key);
|
||||
state.container = element;
|
||||
update(element);
|
||||
}
|
||||
aria-label="Encrypted URL"
|
||||
value=${encryptedUrl.value}
|
||||
/>
|
||||
<button
|
||||
aria-label="Copy to Clipboard"
|
||||
@click=${copyToClipboard}
|
||||
disabled=${isCopyButtonDisabled.value}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
};
|
||||
|
||||
async function copyToClipboard(event: Event) {
|
||||
event.preventDefault();
|
||||
await navigator.clipboard.writeText(state.encryptedUrl);
|
||||
await navigator.clipboard.writeText(encryptedUrl.value);
|
||||
alert("Copied to clipboard! Send this URL to the requester.");
|
||||
}
|
||||
|
||||
async function encryptData(event: Event) {
|
||||
event.preventDefault();
|
||||
const key = localStorage.getItem(REQUEST_PUBLIC_KEY)!;
|
||||
const key = localStorage.getItem(LOCAL_STORAGE_KEYS.REQUEST_PUBLIC_KEY)!;
|
||||
|
||||
// Parse the 'p' parameter to get publicA
|
||||
const publicAJwk = JSON.parse(atob(key));
|
||||
|
@ -126,7 +99,7 @@ async function encryptData(event: Event) {
|
|||
const aesKey = await deriveSharedSecret(keyPairB.privateKey, publicA);
|
||||
|
||||
// Encrypt the message input value using the AES key
|
||||
const { encryptedData, iv } = await encrypt(aesKey, state.message);
|
||||
const { encryptedData, iv } = await encrypt(aesKey, messageToEncrypt.value);
|
||||
|
||||
const ecdhPublicJwk = await window.crypto.subtle.exportKey(
|
||||
"jwk",
|
||||
|
@ -144,7 +117,6 @@ async function encryptData(event: Event) {
|
|||
)
|
||||
)}`;
|
||||
|
||||
state.encryptedUrl = url.toString();
|
||||
state.isCopyButtonDisabled = false;
|
||||
update(state.container!);
|
||||
encryptedUrl.value = url.toString();
|
||||
isCopyButtonDisabled.value = false;
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
.js-warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-js .js-warning {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body {
|
||||
transition: opacity 1s ease-in-out;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
@import "@picocss/pico/scss/pico";
|
||||
|
||||
.primary {
|
||||
color: #348037;
|
||||
font-weight: bold;
|
||||
&:hover {
|
||||
color: darken(#348037, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* Green Light scheme (Default) */
|
||||
/* Can be forced with data-theme="light" */
|
||||
[data-theme="light"],
|
||||
:root:not([data-theme="dark"]) {
|
||||
--primary: #348037;
|
||||
--primary-hover: #265f29;
|
||||
--primary-focus: rgba(39, 92, 42, 0.125);
|
||||
--primary-inverse: #FFF;
|
||||
}
|
||||
|
||||
/* Green Dark scheme (Auto) */
|
||||
/* Automatically enabled if user has Dark mode enabled */
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--primary: #43a047;
|
||||
--primary-hover: #4caf50;
|
||||
--primary-focus: rgba(67, 160, 71, 0.25);
|
||||
--primary-inverse: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* Green Dark scheme (Forced) */
|
||||
/* Enabled if forced with data-theme="dark" */
|
||||
[data-theme="dark"] {
|
||||
--primary: #43a047;
|
||||
--primary-hover: #4caf50;
|
||||
--primary-focus: rgba(67, 160, 71, 0.25);
|
||||
--primary-inverse: #FFF;
|
||||
}
|
||||
|
||||
/* Green (Common styles) */
|
||||
:root {
|
||||
--form-element-active-border-color: var(--primary);
|
||||
--form-element-focus-color: var(--primary-focus);
|
||||
--switch-color: var(--primary-inverse);
|
||||
--switch-checked-background-color: var(--primary);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { html } from "uhtml";
|
||||
|
||||
export const FAQ = html`<details>
|
||||
<summary><strong>FAQ</strong></summary>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>What is this?</strong>
|
||||
<p>
|
||||
SURE is a system for securely sending information via a URL request. It
|
||||
uses the Elliptic Curve Diffie-Hellman algorithm to generate a shared
|
||||
secret between the sender and recipient, which is then used to encrypt
|
||||
the message in a response URL. The device that generated the URL request
|
||||
can then decrypt the message in the URL sent back to them using their
|
||||
private key.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong
|
||||
>What if I want all of my devices to be able to decrypt messages from
|
||||
the same request?</strong
|
||||
>
|
||||
<p>
|
||||
If you want to be able to decrypt messages from the same request on
|
||||
multiple devices, you'll need to export the keypair from the device that
|
||||
generated the request, and then import it into the other devices. You
|
||||
can do this by clicking the "Manage Keys" link at the bottom of the
|
||||
page, and then clicking the "Export Keys" button. This will give you a
|
||||
JSON file that you can import on other devices via the "Import Keys"
|
||||
button.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>What if I lose my keys?</strong>
|
||||
<p>
|
||||
If you lose your keys, either from clearing your localstorage or losing
|
||||
your device, you won't be able to decrypt any messages sent to you.
|
||||
You'll need to send over the new request URL from your device to anyone
|
||||
who wants to send you a message.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Why was this built?</strong>
|
||||
<p>
|
||||
This was built as a proof of concept for a secure messaging system that
|
||||
doesn't require a user to sign up for an account or download an app. It
|
||||
can even work entirely offline if you build the app from source and open
|
||||
the HTML file in your browser. You can also manage your keys so that all
|
||||
of your devices can decrypt messages from the same request URL.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</details>`;
|
|
@ -0,0 +1,45 @@
|
|||
import { html } from "uhtml";
|
||||
import { openKeyManager } from "./keyManager.ts";
|
||||
|
||||
export const Footer = html`<footer class="container">
|
||||
<div>
|
||||
<p class="center">
|
||||
<a
|
||||
href="https://git.silentsilas.com/silentsilas/sure"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Source Code</a
|
||||
>
|
||||
|
|
||||
<a
|
||||
href="https://silentsilas.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>whoami</a
|
||||
>
|
||||
|
|
||||
<a
|
||||
href="#"
|
||||
id="openKeyManager"
|
||||
@click=${(event: Event) => {
|
||||
event.preventDefault();
|
||||
openKeyManager();
|
||||
}}
|
||||
>Manage Keys</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<p class="center">Version ${process.env.PACKAGE_VERSION}</p>
|
||||
<p class="center">
|
||||
Last updated on
|
||||
${new Date(process.env.BUILD_TIME as any).toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</footer>`;
|
|
@ -0,0 +1,9 @@
|
|||
import { html } from "uhtml";
|
||||
|
||||
export const Header = html` <hgroup>
|
||||
<h1>SURE</h1>
|
||||
<h2>
|
||||
<span class="primary">S</span>ecure <span class="primary">U</span>RL
|
||||
<span class="primary">Re</span>quests
|
||||
</h2>
|
||||
</hgroup>`;
|
|
@ -0,0 +1,43 @@
|
|||
import { html } from "uhtml";
|
||||
|
||||
export const HowItWorks = html`<details>
|
||||
<summary><strong>How it Works:</strong></summary>
|
||||
<p>The first thing to understand is how public key cryptography works.</p>
|
||||
<p>
|
||||
Essentially, you generate a pair of keys that can undo the operations of
|
||||
each other. If you encrypt a message with your public key, it can only be
|
||||
decrypted by your private key.
|
||||
</p>
|
||||
<div style="width: fit-content; margin: auto;">
|
||||
<img
|
||||
width="100%"
|
||||
height="auto"
|
||||
loading="lazy"
|
||||
src="/img/public_key_crypto_chart.png"
|
||||
alt="Chart of the public key cryptography process"
|
||||
/>
|
||||
</div>
|
||||
<p style="margin-top: 20px">
|
||||
This is why you're safe to share your "Request URL" out in the open. Anyone
|
||||
who accesses this URL will receive your public key, and encrypt the message
|
||||
with it along with their own private key. When you receive the URL with
|
||||
their encrypted message, you take their public key and your private key to
|
||||
undo the operations of their private key and your public key.
|
||||
</p>
|
||||
<p><strong>But of course it's not as simple as that</strong></p>
|
||||
<p>
|
||||
Traditional public-key cryptography like RSA is limited in the messages it
|
||||
can encrypt by the size of the public key. To overcome this limitation, we
|
||||
use ECDH for short public keys that can encrypt an arbitrary amount of data
|
||||
by establishing a shared "symmetric" key for encryption/decryption, which is
|
||||
beyond the scope of this explanation. But the following chart should give
|
||||
you a general idea of how the process works.
|
||||
</p>
|
||||
<img
|
||||
width="100%"
|
||||
height="auto"
|
||||
loading="lazy"
|
||||
src="/img/flowchart.png"
|
||||
alt="Flowchart of the SURE process"
|
||||
/>
|
||||
</details>`;
|
|
@ -0,0 +1,77 @@
|
|||
import { html } from "uhtml";
|
||||
import {
|
||||
exportKeys,
|
||||
importAndSaveKeys,
|
||||
retrieveOrGenerateKeyPair,
|
||||
} from "../utils/crypto.ts";
|
||||
import { signal } from "uhtml/preactive";
|
||||
import { LOCAL_STORAGE_KEYS } from "../utils/store.ts";
|
||||
|
||||
const ecdhPublicKey = signal(
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY)
|
||||
);
|
||||
const ecdhPrivateKey = signal(
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY)
|
||||
);
|
||||
const isDialogOpen = signal(false);
|
||||
|
||||
export const KeyManager =
|
||||
() => html`<dialog class="key-manager" ?open=${isDialogOpen.value}>
|
||||
<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(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const el = target.nextElementSibling as HTMLInputElement;
|
||||
el.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(): void {
|
||||
isDialogOpen.value = false;
|
||||
}
|
||||
|
||||
export async function openKeyManager() {
|
||||
await retrieveOrGenerateKeyPair();
|
||||
ecdhPublicKey.value = localStorage.getItem(
|
||||
LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY
|
||||
);
|
||||
ecdhPrivateKey.value = localStorage.getItem(
|
||||
LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY
|
||||
);
|
||||
isDialogOpen.value = true;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { html } from "uhtml";
|
||||
import { signal } from "uhtml/preactive";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
const open = signal(false);
|
||||
const updateSW = signal<
|
||||
((reloadPage?: boolean | undefined) => Promise<void>) | undefined
|
||||
>(undefined);
|
||||
|
||||
export const UpdateModal = () => {
|
||||
return html` <dialog ?open=${open.value}>
|
||||
<article>
|
||||
<header>An update is available!</header>
|
||||
<p>
|
||||
A new version of the app is available. Click the button below to update
|
||||
and reload.
|
||||
</p>
|
||||
<footer>
|
||||
<button aria-label="Update and reload" @click=${update}>Update</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>`;
|
||||
};
|
||||
|
||||
const update = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (updateSW.value) {
|
||||
console.log("Service Worker is updating.");
|
||||
updateSW.value(true);
|
||||
} else {
|
||||
console.error("Service Worker failed to update.");
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForUpdates = () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
const promise = registerSW({
|
||||
onNeedRefresh() {
|
||||
console.log("Service Worker is ready to update.");
|
||||
open.value = true;
|
||||
},
|
||||
onRegisteredSW() {
|
||||
console.log("Service Worker has been registered.");
|
||||
},
|
||||
onOfflineReady() {
|
||||
console.log("Service Worker is ready to handle offline usage.");
|
||||
},
|
||||
});
|
||||
|
||||
updateSW.value = promise;
|
||||
}
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import { LOCAL_STORAGE_KEYS } from "./store";
|
||||
|
||||
export async function generateKeyPair(): Promise<CryptoKeyPair> {
|
||||
return await window.crypto.subtle.generateKey(
|
||||
{
|
||||
|
@ -34,11 +36,15 @@ export async function retrieveOrGenerateKeyPair(): Promise<CryptoKeyPair> {
|
|||
let ecdhPrivate: CryptoKey;
|
||||
|
||||
if (
|
||||
localStorage.getItem("ecdhPublic") &&
|
||||
localStorage.getItem("ecdhPrivate")
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY) &&
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY)
|
||||
) {
|
||||
const ecdhPublicJwk = JSON.parse(localStorage.getItem("ecdhPublic")!);
|
||||
const ecdhPrivateJwk = JSON.parse(localStorage.getItem("ecdhPrivate")!);
|
||||
const ecdhPublicJwk = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY)!
|
||||
);
|
||||
const ecdhPrivateJwk = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY)!
|
||||
);
|
||||
|
||||
ecdhPrivate = await window.crypto.subtle.importKey(
|
||||
"jwk",
|
||||
|
@ -78,8 +84,14 @@ async function saveKeys(ecdhPublic: CryptoKey, ecdhPrivate: CryptoKey) {
|
|||
);
|
||||
|
||||
// Store ECDH key pair in local storage
|
||||
localStorage.setItem("ecdhPublic", JSON.stringify(ecdhPublicJwk));
|
||||
localStorage.setItem("ecdhPrivate", JSON.stringify(ecdhPrivateJwk));
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_KEYS.ECDH_PUBLIC_KEY,
|
||||
JSON.stringify(ecdhPublicJwk)
|
||||
);
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_KEYS.ECDH_PRIVATE_KEY,
|
||||
JSON.stringify(ecdhPrivateJwk)
|
||||
);
|
||||
|
||||
return {
|
||||
public: JSON.stringify(ecdhPublicJwk),
|
||||
|
@ -116,7 +128,6 @@ export async function decrypt(
|
|||
iv: string,
|
||||
encryptedData: string
|
||||
): Promise<string> {
|
||||
console.log(iv);
|
||||
// Decode the iv and encryptedData from base64
|
||||
const ivUint8Array = base64ToUint8Array(iv);
|
||||
const encryptedDataUint8Array = base64ToUint8Array(encryptedData);
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
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);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import { signal } from "uhtml/preactive";
|
||||
import { Route } from "../router";
|
||||
|
||||
// the current page / params for this URL
|
||||
export const page = signal(Route.Receive);
|
||||
export const params = signal(
|
||||
new URLSearchParams(window.location.hash.slice(1))
|
||||
);
|
||||
|
||||
export enum LOCAL_STORAGE_KEYS {
|
||||
REQUEST_PUBLIC_KEY = "requestPublicKey",
|
||||
ECDH_PUBLIC_KEY = "ecdhPublic",
|
||||
ECDH_PRIVATE_KEY = "ecdhPrivate",
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite-plugin-pwa/client" />
|
|
@ -1,6 +1,39 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { makeOffline } from "vite-plugin-make-offline";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import { version } from "./package.json";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [makeOffline()], // This is the plugin 😃
|
||||
define: {
|
||||
"process.env.PACKAGE_VERSION": JSON.stringify(version),
|
||||
"process.env.BUILD_TIME": JSON.stringify(new Date().toISOString()),
|
||||
},
|
||||
plugins: [
|
||||
VitePWA({
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
},
|
||||
includeAssets: ["**/*.png", "**/*.ico", "**/*.webmanifest"],
|
||||
manifest: {
|
||||
name: "SURE",
|
||||
short_name: "SURE",
|
||||
theme_color: "#44a616",
|
||||
background_color: "#3d006e",
|
||||
display: "minimal-ui",
|
||||
scope: "/",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: "/android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { makeOffline } from "vite-plugin-make-offline";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
makeOffline(),
|
||||
VitePWA({
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
},
|
||||
manifest: {
|
||||
name: "SURE",
|
||||
short_name: "SURE",
|
||||
theme_color: "#44a616",
|
||||
background_color: "#3d006e",
|
||||
display: "minimal-ui",
|
||||
scope: "/",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: "/android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
Loading…
Reference in New Issue