diff --git a/index.html b/index.html index 9ecce41..c2110e7 100644 --- a/index.html +++ b/index.html @@ -41,17 +41,17 @@ - +

SURE

-

- Secure - URL - Requests -

+

+ Secure + URL + Requests +

@@ -67,41 +67,107 @@ + +
+
Key Manager
+

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

+

+ Public Key: +


+      

+

+ Private Key: +


+      

+

+ + + +

+
+
+
How it Works: +

The first thing to understand is how public key cryptography works.

+

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.

+
+ Chart of the public key cryptography process +
+

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. +

+

But of course it's not as simple as that

+

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.

+ Flowchart of the SURE process +
+ +
+ FAQ
    -
  • Each client generates an ECDH keypair, consisting of a public key and a private key.
  • -
  • Your private key is kept in localStorage, and never leaves your device.
  • -
  • Your public key is embedded in the URLs you generate. This key can be safely shared anywhere without - compromising security.
  • -
  • When another client opens your generated URL, they will find your public ECDH key. They then generate a - random IV for this specific message, and use it, along with their private ECDH key and your public ECDH key, - to derive a shared secret (AES-GCM).
  • -
  • This derived shared secret never leaves their device. It is used to encrypt their message to you. - The encrypted message, along with their public key and the IV for this message, are embedded in the URL they - generate.
  • -
  • Upon opening the response URL, your device uses your private ECDH key, along with the public key and IV - from the URL, to recreate the shared secret. This secret is used to decrypt the message. If the message was - properly encrypted using the expected keys, it will be successfully decrypted and displayed to you.
  • -
  • If you clear your browser's local storage, you will not be able to decrypt any response URLs generated - with your previous unique URL.
  • +
  • + What is this? +

    + 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. +

    +
  • +
  • + What if I want all of my devices to be able to decrypt messages from the same request? +

    + 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. +

    +
  • +
  • + What if I lose my keys? +

    + 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. +

    +
  • +
  • + Why was this built? +

    + 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. +

- + \ No newline at end of file diff --git a/public/img/flowchart.png b/public/img/flowchart.png new file mode 100644 index 0000000..d088cad Binary files /dev/null and b/public/img/flowchart.png differ diff --git a/public/img/public_key_crypto_chart.png b/public/img/public_key_crypto_chart.png new file mode 100644 index 0000000..0a09339 Binary files /dev/null and b/public/img/public_key_crypto_chart.png differ diff --git a/src/main.ts b/src/main.ts index da35ba6..6eb6e94 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,15 +3,55 @@ import "./style.css"; import { setupRequestPage } from "./routes/request.ts"; import { setupSendPage } from "./routes/send.ts"; import { setupReceivePage } from "./routes/receive.ts"; +import { + exportKeys, + importAndSaveKeys, + retrieveOrGenerateKeyPair, +} from "./utils/crypto.ts"; + +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 fragmentData = new URLSearchParams(window.location.hash.slice(1)); const appElement = document.querySelector("#app"); + const keyManagerEl = document.getElementById("openKeyManager"); + const keyManagerModal = document.getElementById("keyManagerModal"); + const closeKeyManager = document.getElementById("closeKeyManager"); if (!appElement) { throw new Error("No app element found"); } + if (!keyManagerEl || !keyManagerModal || !closeKeyManager) { + throw new Error("No key manager element found"); + } + + 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") && @@ -36,4 +76,39 @@ const start = () => { 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(); +setupKeyManager(); diff --git a/src/routes/request.ts b/src/routes/request.ts index 1d38bbd..d178df1 100644 --- a/src/routes/request.ts +++ b/src/routes/request.ts @@ -1,13 +1,14 @@ import { retrieveOrGenerateKeyPair } from "../utils/crypto.ts"; const URL_INPUT_ID = "request-url"; +const COPY_BUTTON_ID = "copy-button"; const TEMPLATE = `
How To Use:
    -
  1. Request: Copy and share the URL below with the person you want to receive data from.
  2. -
  3. Send: The recipient will open the URL, enter their data, and hit 'Generate Response'. This generates a new URL with their encrypted message.
  4. -
  5. Receive: They send you the newly generated URL. Open it to view the decrypted message. Only your browser can decrypt it.
  6. +
  7. Send the link below to someone you want to get a secret from.
  8. +
  9. They add their secret and share the URL the app generates.
  10. +
  11. Only your browser can open the URL and decrypt the secret.
@@ -20,6 +21,7 @@ const TEMPLATE = ` aria-label="Request URL" required /> + `; @@ -29,6 +31,18 @@ export async function setupRequestPage(element: HTMLElement) { 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) { + 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 { diff --git a/src/routes/send.ts b/src/routes/send.ts index 8e1de48..aed88b3 100644 --- a/src/routes/send.ts +++ b/src/routes/send.ts @@ -8,6 +8,7 @@ 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 TEMPLATE = `
How To Use: @@ -40,6 +41,7 @@ const TEMPLATE = ` placeholder="URL with your encrypted response will appear here..." aria-label="Encrypted URL" /> + `; @@ -52,6 +54,20 @@ export async function setupSendPage(element: HTMLElement, key: string) { 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) { + 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) { @@ -108,4 +124,8 @@ async function encryptData(event: Event) { )}`; encryptedUrlInput.value = url.toString(); + const copyButton = document.getElementById( + COPY_BUTTON_ID + ) as HTMLButtonElement; + copyButton.disabled = false; } diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index 471ca8e..b1e1543 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -80,6 +80,11 @@ 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)); + + return { + public: JSON.stringify(ecdhPublicJwk), + private: JSON.stringify(ecdhPrivateJwk), + }; } export async function encrypt( @@ -141,3 +146,57 @@ function base64ToUint8Array(base64: string): Uint8Array { } 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 { + 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 { + 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); +}