add link at bottom of page to import/export keys, add copy to clipboard buttons, adjust the How It Works section, add FAQ section
This commit is contained in:
parent
141788714b
commit
d2d91e6ab3
116
index.html
116
index.html
|
@ -41,17 +41,17 @@
|
||||||
<meta name="msapplication-starturl" content="/" />
|
<meta name="msapplication-starturl" content="/" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="opacity: 0; background-color: #11191f;">
|
<body style="opacity: 0;">
|
||||||
<header class="container">
|
<header class="container">
|
||||||
<hgroup>
|
<hgroup>
|
||||||
<h1>
|
<h1>
|
||||||
SURE
|
SURE
|
||||||
</h1>
|
</h1>
|
||||||
<p>
|
<h3>
|
||||||
<span style="color: #44a616;">S</span>ecure
|
<span style="color: #1095C1;">S</span>ecure
|
||||||
<span style="color: #44a616;">U</span>RL
|
<span style="color: #1095C1;">U</span>RL
|
||||||
<span style="color: #44a616;">Re</span>quests
|
<span style="color: #1095C1;">Re</span>quests
|
||||||
</p>
|
</h3>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -67,38 +67,104 @@
|
||||||
</article>
|
</article>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="key-manager" id="keyManagerModal">
|
||||||
|
<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>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>How it Works:</summary>
|
<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>
|
<ul>
|
||||||
<li>Each client generates an ECDH keypair, consisting of a public key and a private key.</li>
|
<li>
|
||||||
<li>Your private key is kept in localStorage, and never leaves your device.</li>
|
<strong>What is this?</strong>
|
||||||
<li>Your public key is embedded in the URLs you generate. This key can be safely shared anywhere without
|
<p>
|
||||||
compromising security.</li>
|
SURE is a system for securely sending information via a URL request. It uses the Elliptic Curve
|
||||||
<li>When another client opens your generated URL, they will find your public ECDH key. They then generate a
|
Diffie-Hellman algorithm to generate a shared secret between the sender and recipient, which is then used
|
||||||
random IV for this specific message, and use it, along with their private ECDH key and your public ECDH key,
|
to encrypt the message in a response URL. The device that generated the URL request can then decrypt the
|
||||||
to derive a shared secret (AES-GCM).</li>
|
message in the URL sent back to them using their private key.
|
||||||
<li>This derived shared secret never leaves their device. It is used to encrypt their message to you.
|
</p>
|
||||||
The encrypted message, along with their public key and the IV for this message, are embedded in the URL they
|
</li>
|
||||||
generate.</li>
|
<li>
|
||||||
<li>Upon opening the response URL, your device uses your private ECDH key, along with the public key and IV
|
<strong>What if I want all of my devices to be able to decrypt messages from the same request?</strong>
|
||||||
from the URL, to recreate the shared secret. This secret is used to decrypt the message. If the message was
|
<p>
|
||||||
properly encrypted using the expected keys, it will be successfully decrypted and displayed to you.</li>
|
If you want to be able to decrypt messages from the same request on multiple devices, you'll need to
|
||||||
<li>If you clear your browser's local storage, you will not be able to decrypt any response URLs generated
|
export the keypair from the device that generated the request, and then import it into the other
|
||||||
with your previous unique URL.</li>
|
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>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="container">
|
<footer class="container">
|
||||||
<p>
|
<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://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>
|
|
|
||||||
</p>
|
<a href="https://silentsilas.com" target="_blank" rel="noopener noreferrer">whoami</a> |
|
||||||
|
<a href="#" id="openKeyManager">Manage Keys</a>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 337 KiB |
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
75
src/main.ts
75
src/main.ts
|
@ -3,15 +3,55 @@ 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 {
|
||||||
|
exportKeys,
|
||||||
|
importAndSaveKeys,
|
||||||
|
retrieveOrGenerateKeyPair,
|
||||||
|
} from "./utils/crypto.ts";
|
||||||
|
|
||||||
|
const updateKeysInModal = () => {
|
||||||
|
console.log("help");
|
||||||
|
const publicKeyElement = document.getElementById("ecdhPublicKey");
|
||||||
|
const privateKeyElement = document.getElementById("ecdhPrivateKey");
|
||||||
|
|
||||||
|
const publicKey = localStorage.getItem("ecdhPublic");
|
||||||
|
const privateKey = localStorage.getItem("ecdhPrivate");
|
||||||
|
|
||||||
|
if (!publicKeyElement || !privateKeyElement) {
|
||||||
|
throw new Error("Key manager elements not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicKey && privateKey) {
|
||||||
|
publicKeyElement.textContent = publicKey;
|
||||||
|
privateKeyElement.textContent = privateKey;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
const fragmentData = new URLSearchParams(window.location.hash.slice(1));
|
const fragmentData = new URLSearchParams(window.location.hash.slice(1));
|
||||||
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) {
|
||||||
|
throw new Error("No key manager element found");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 the URL contains the 'p', 'iv', and 'm' parameters, then the receiver page is set up
|
||||||
if (
|
if (
|
||||||
fragmentData.has("p") &&
|
fragmentData.has("p") &&
|
||||||
|
@ -36,4 +76,39 @@ const start = () => {
|
||||||
setupRequestPage(appElement);
|
setupRequestPage(appElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setupKeyManager = async () => {
|
||||||
|
const exportKeyEl = document.getElementById("exportKeypair");
|
||||||
|
const importKeyEl = document.getElementById("importKeypair");
|
||||||
|
|
||||||
|
if (!exportKeyEl || !importKeyEl) {
|
||||||
|
throw new Error("Key management elements not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKeyEl.addEventListener("click", exportKeys);
|
||||||
|
importKeyEl.addEventListener("click", () => {
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.style.display = "none";
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
fileInput.addEventListener("change", async (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const keys = JSON.parse(e.target?.result as string);
|
||||||
|
await importAndSaveKeys(keys);
|
||||||
|
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
alert("Successfully imported keypair");
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
setupKeyManager();
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
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 TEMPLATE = `
|
const TEMPLATE = `
|
||||||
<details open>
|
<details open>
|
||||||
<summary>How To Use:</summary>
|
<summary>How To Use:</summary>
|
||||||
<ol>
|
<ol>
|
||||||
<li><strong>Request:</strong> Copy and share the URL below with the person you want to receive data from.</li>
|
<li>Send the link below to someone you want to get a secret from.</li>
|
||||||
<li><strong>Send:</strong> The recipient will open the URL, enter their data, and hit 'Generate Response'. This generates a new URL with their encrypted message.</li>
|
<li>They add their secret and share the URL the app generates.</li>
|
||||||
<li><strong>Receive:</strong> They send you the newly generated URL. Open it to view the decrypted message. Only your browser can decrypt it.</li>
|
<li>Only your browser can open the URL and decrypt the secret.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ const TEMPLATE = `
|
||||||
aria-label="Request URL"
|
aria-label="Request URL"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<button aria-label="Copy to Clipboard" id="${COPY_BUTTON_ID}">Copy to Clipboard</button>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -29,6 +31,18 @@ export async function setupRequestPage(element: HTMLElement) {
|
||||||
const url = await generateUrl();
|
const url = await generateUrl();
|
||||||
const input = document.getElementById(URL_INPUT_ID) as HTMLInputElement;
|
const input = document.getElementById(URL_INPUT_ID) as HTMLInputElement;
|
||||||
input.value = url.toString();
|
input.value = url.toString();
|
||||||
|
|
||||||
|
const copyButton = document.getElementById(
|
||||||
|
COPY_BUTTON_ID
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
copyButton.addEventListener("click", copyToClipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const input = document.getElementById(URL_INPUT_ID) as HTMLInputElement;
|
||||||
|
await navigator.clipboard.writeText(input.value);
|
||||||
|
alert("Copied to clipboard!");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateUrl(): Promise<URL> {
|
async function generateUrl(): Promise<URL> {
|
||||||
|
|
|
@ -8,6 +8,7 @@ const ENCRYPTED_URL_INPUT_ID = "encrypted-url";
|
||||||
const MESSAGE_INPUT_ID = "message";
|
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 TEMPLATE = `
|
const TEMPLATE = `
|
||||||
<details open>
|
<details open>
|
||||||
<summary>How To Use:</summary>
|
<summary>How To Use:</summary>
|
||||||
|
@ -40,6 +41,7 @@ const TEMPLATE = `
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
<button aria-label="Copy to Clipboard" id="${COPY_BUTTON_ID}" disabled>Copy to Clipboard</button>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -52,6 +54,20 @@ export async function setupSendPage(element: HTMLElement, key: string) {
|
||||||
ENCRYPT_BUTTON_ID
|
ENCRYPT_BUTTON_ID
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
encryptButton.addEventListener("click", encryptData);
|
encryptButton.addEventListener("click", encryptData);
|
||||||
|
|
||||||
|
const copyButton = document.getElementById(
|
||||||
|
COPY_BUTTON_ID
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
copyButton.addEventListener("click", copyToClipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const input = document.getElementById(
|
||||||
|
ENCRYPTED_URL_INPUT_ID
|
||||||
|
) as HTMLInputElement;
|
||||||
|
await navigator.clipboard.writeText(input.value);
|
||||||
|
alert("Copied to clipboard! Send this URL to the requester.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function encryptData(event: Event) {
|
async function encryptData(event: Event) {
|
||||||
|
@ -108,4 +124,8 @@ async function encryptData(event: Event) {
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
encryptedUrlInput.value = url.toString();
|
encryptedUrlInput.value = url.toString();
|
||||||
|
const copyButton = document.getElementById(
|
||||||
|
COPY_BUTTON_ID
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
copyButton.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,11 @@ async function saveKeys(ecdhPublic: CryptoKey, ecdhPrivate: CryptoKey) {
|
||||||
// Store ECDH key pair in local storage
|
// Store ECDH key pair in local storage
|
||||||
localStorage.setItem("ecdhPublic", JSON.stringify(ecdhPublicJwk));
|
localStorage.setItem("ecdhPublic", JSON.stringify(ecdhPublicJwk));
|
||||||
localStorage.setItem("ecdhPrivate", JSON.stringify(ecdhPrivateJwk));
|
localStorage.setItem("ecdhPrivate", JSON.stringify(ecdhPrivateJwk));
|
||||||
|
|
||||||
|
return {
|
||||||
|
public: JSON.stringify(ecdhPublicJwk),
|
||||||
|
private: JSON.stringify(ecdhPrivateJwk),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encrypt(
|
export async function encrypt(
|
||||||
|
@ -141,3 +146,57 @@ function base64ToUint8Array(base64: string): Uint8Array {
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadFile(content: string, fileName: string, contentType: string) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const file = new Blob([content], { type: contentType });
|
||||||
|
|
||||||
|
a.href = URL.createObjectURL(file);
|
||||||
|
a.download = fileName;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportKeys(): Promise<void> {
|
||||||
|
const keys = await retrieveOrGenerateKeyPair();
|
||||||
|
|
||||||
|
const exportedKeys = await saveKeys(keys.publicKey, keys.privateKey);
|
||||||
|
const exportedKeysJson = JSON.stringify(exportedKeys);
|
||||||
|
|
||||||
|
downloadFile(exportedKeysJson, "keys.json", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importAndSaveKeys(keys: {
|
||||||
|
public: string;
|
||||||
|
private: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const ecdhPublicJwk = JSON.parse(keys.public);
|
||||||
|
const ecdhPrivateJwk = JSON.parse(keys.private);
|
||||||
|
|
||||||
|
// Import the keys
|
||||||
|
const ecdhPublic = await window.crypto.subtle.importKey(
|
||||||
|
"jwk",
|
||||||
|
ecdhPublicJwk,
|
||||||
|
{
|
||||||
|
name: "ECDH",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const ecdhPrivate = await window.crypto.subtle.importKey(
|
||||||
|
"jwk",
|
||||||
|
ecdhPrivateJwk,
|
||||||
|
{
|
||||||
|
name: "ECDH",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["deriveKey", "deriveBits"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save the imported keys
|
||||||
|
saveKeys(ecdhPublic, ecdhPrivate);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue