From 2f17150e7920d8d70a5e43e26c97a679fa4a474f Mon Sep 17 00:00:00 2001 From: Simon Neumann <29802170+simaosimao@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:56:37 +0200 Subject: [PATCH 1/3] add option to intentionally throw away your keys --- build/build-iconify.ts | 1 + src/css/mdi-icons.css | 4 ++++ src/css/styles.css | 10 ++++++++++ src/index.html | 31 ++++++++++++++++++++++++++++++- src/ts/whisper-crypto.ts | 22 ++++++++++++++++++---- src/ts/whisper.ts | 14 +++++++++++++- 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/build/build-iconify.ts b/build/build-iconify.ts index 3abd13f..d67f623 100644 --- a/build/build-iconify.ts +++ b/build/build-iconify.ts @@ -23,6 +23,7 @@ const chosenIcons: Array = [ 'lock-open-check-outline', 'robot-dead', 'security', + 'trash-can-outline', 'tray-arrow-down', ]; diff --git a/src/css/mdi-icons.css b/src/css/mdi-icons.css index 6ddb0f8..53cf736 100644 --- a/src/css/mdi-icons.css +++ b/src/css/mdi-icons.css @@ -71,6 +71,10 @@ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 12h7c-.53 4.11-3.28 7.78-7 8.92zH5V6.3l7-3.11M12 1L3 5v6c0 5.55 3.84 10.73 9 12c5.16-1.27 9-6.45 9-12V5z'/%3E%3C/svg%3E"); } +.icon--mdi--trash-can-outline { + --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3zM7 6h10v13H7zm2 2v9h2V8zm4 0v9h2V8z'/%3E%3C/svg%3E"); +} + .icon--mdi--tray-arrow-down { --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M2 12h2v5h16v-5h2v5c0 1.11-.89 2-2 2H4a2 2 0 0 1-2-2zm10 3l5.55-5.46l-1.42-1.41L13 11.25V2h-2v9.25L7.88 8.13L6.46 9.55z'/%3E%3C/svg%3E"); } diff --git a/src/css/styles.css b/src/css/styles.css index d83bc72..a164b84 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -6,6 +6,16 @@ /* Syncorix Lime Greem */ } +/* for foreground elements like dialogs */ +::backdrop { + background-color: white; + opacity: 0.75; +} + +dialog { + border: 1px solid gray; +} + /* create a box w/ margins to put content into */ .content-box { margin-top: 2rem; diff --git a/src/index.html b/src/index.html index ba7435c..7d00aa7 100644 --- a/src/index.html +++ b/src/index.html @@ -354,7 +354,7 @@

Your keys

- + @@ -365,12 +365,41 @@

Your keys

+
+ +
Throw keys away
+ + +
+ +
+

+ Data encrypted with this key pair cannot be decrypted again and once deleted the + keys cannot be recovered. +

+ Are you sure? +
+
Yes
+
No
+
+
+
diff --git a/src/ts/whisper-crypto.ts b/src/ts/whisper-crypto.ts index 7858e77..4c1dc3b 100644 --- a/src/ts/whisper-crypto.ts +++ b/src/ts/whisper-crypto.ts @@ -23,7 +23,9 @@ enum DBModes { RW = 'readwrite' } type KeysAvailableListener = (personalKeyPair: KeyPair) => void; -export default function (...listeners: KeysAvailableListener[]) { + + +export function start(listeners: KeysAvailableListener[]) { // Check for IndexedDB support // TODO: Consider using https://modernizr.com/ for IndexedDB and WebCryptoAPI checks if (!window.indexedDB) { @@ -33,7 +35,19 @@ export default function (...listeners: KeysAvailableListener[]) { } else { console.warn("No listeners provided, skipping key retrieval.") } -} // makes this a module +} + +export async function deleteAndRegenerateKeys(listeners: KeysAvailableListener[]) { + const request = indexedDB.open(keyDatabaseName, dbSchemaVersion); + request.onsuccess = e => { + (e.target as IDBOpenDBRequest).result + .transaction([keyPairsObjectStoreName], DBModes.RW) + .objectStore(keyPairsObjectStoreName) + .clear(); + console.log("%s deleted from IndexedDB as requested by user", keyPairsObjectStoreName); + start(listeners); + } +} export async function validateAndParseJWKFile(jwkFile: File): Promise { // Parse the JWK string into a JSON object @@ -162,7 +176,7 @@ async function storeKeyPairlocally(keyPair: KeyPair, targetDatabase: IDBDatabase const transaction = targetDatabase.transaction([keyPairsObjectStoreName], DBModes.RW); const objectStore = transaction.objectStore(keyPairsObjectStoreName); objectStore.put(keyPair, 0); // For now we only support a single pair - console.log("Successfully persisted keypair for kid %s", keyPair.publicKey.kid) + console.log("Successfully persisted keypair with label %s and kid %s", keyPair.publicKey.xKidHint, keyPair.publicKey.kid) return keyPair; } @@ -207,4 +221,4 @@ async function hashWithSHA256(input: string): Promise { const hashArray = Array.from(new Uint8Array(hashBuffer)); // Convert buffer to byte array const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); // Convert bytes to hex return hashHex; -} \ No newline at end of file +} diff --git a/src/ts/whisper.ts b/src/ts/whisper.ts index 1ddcd16..3eb788a 100644 --- a/src/ts/whisper.ts +++ b/src/ts/whisper.ts @@ -14,7 +14,7 @@ import * as dropzone from './whisper-dropzone' import { softwareVersion } from './build-info' // Call default module exports, provide listeners -crypto.default(keysAvailable) +crypto.start([keysAvailable]) dropzone.default([whisperFilesAdded], [recipientKeysAdded], [receivedWhisperFilesAdded]) // Screens selectable per menu @@ -112,6 +112,7 @@ Alpine.magic('reset', reset); Alpine.magic('submitPersonalKeyHint', submitPersonalKeyHint); Alpine.magic('executeEncryption', executeEncryption); Alpine.magic('executeDecryption', executeDecryption); +Alpine.magic('deleteAndRegenerateKeys', deleteAndRegenerateKeys); // Start the show Alpine.start(); @@ -212,6 +213,17 @@ function executeDecryption() { } } +function deleteAndRegenerateKeys() { + // Completely reset UI + reset(); + const store = (Alpine.store(whisperStateStoreName) as Store) + store.accountScreen.publicKeyUiModel = null; + store.personalKeyPair = null; + store.engineState = CryptoEngineState.Unknown; + // Delete and regenerate personal key pair + crypto.deleteAndRegenerateKeys([keysAvailable]); +} + function toUiModel(publicKey: JWKWithKeyHint): PublicKeyUiModel { if (!publicKey.kid) { throw "kid is missing on public jwk!" From f9a8f527f5b0cba6276201bc683c0a62e7e29668 Mon Sep 17 00:00:00 2001 From: Simon Neumann <29802170+simaosimao@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:59:01 +0200 Subject: [PATCH 2/3] move function alongside non exports --- src/ts/whisper-crypto.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ts/whisper-crypto.ts b/src/ts/whisper-crypto.ts index 4c1dc3b..56ed403 100644 --- a/src/ts/whisper-crypto.ts +++ b/src/ts/whisper-crypto.ts @@ -112,14 +112,6 @@ export async function encryptFileForMultipleRecipients(file: File, recipients: j ; } -async function addRecipient(jwk: jose.JWK, encryptor: jose.GeneralEncrypt): Promise { - // Use ECDH-ES+A256KW for key wrapping, will generate an epk (ephemeral public key) and cek (Content Encryption Key) - return jose.importJWK(jwk, jweAlg) - .then(pk => encryptor.addRecipient(pk)) - // Add the kid to recipient header in JWE in order to select key when decrypting - .then(r => r.setUnprotectedHeader({ kid: jwk.kid })); -} - export async function decryptFile(jwe: jose.GeneralJWE, personalPrivateKey: CryptoKey): Promise { return jose.generalDecrypt(jwe, personalPrivateKey) .then(res => res.plaintext) @@ -203,6 +195,14 @@ function addAlgorithm(jwk: jose.JWK): jose.JWK { return jwk; } +async function addRecipient(jwk: jose.JWK, encryptor: jose.GeneralEncrypt): Promise { + // Use ECDH-ES+A256KW for key wrapping, will generate an epk (ephemeral public key) and cek (Content Encryption Key) + return jose.importJWK(jwk, jweAlg) + .then(pk => encryptor.addRecipient(pk)) + // Add the kid to recipient header in JWE in order to select key when decrypting + .then(r => r.setUnprotectedHeader({ kid: jwk.kid })); +} + async function addFingerprint(jwk: jose.JWK): Promise { return fingerprint(jwk) .then(fp => { jwk.kid = fp; return jwk }); From dc71b4133d6a82cfa79e201eb4cceca52af1d680 Mon Sep 17 00:00:00 2001 From: Simon Neumann <29802170+simaosimao@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:14:43 +0200 Subject: [PATCH 3/3] update doc --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0ec999b..7fbb596 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # whisper-ui -![](img-doc/whisper.png) Secure and very easy to use sending of encrypted data anywhere (browser ui) +![](img-doc/whisper.png) Secure and very easy to use local encryption to send data anywhere (browser ui) ## Motivation Make it super easy to locally encrypt sensible data for designated recipients ensuring privacy, integrity and compliance on whatever way the data is transported. Keys should be considered throw away material and not be reused often in order to render the transported cryptograms useless, even if they are retained in e.g. mailboxes. +## Demo +See it in action and test it at [whisper.syncorix.com](https://whisper.syncorix.com/). Use at your own risk according to [LICENSE](LICENSE). + ## How does it work? ![](img-doc/how-does-it-work.png) This is quite simple: Whisper! uses [asymmetric cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) and @@ -31,12 +34,15 @@ Drop encrypted stuff you received, then decrypt locally and download result: ## Features (free) +* Easy to use and portable across major browsers +* Completely local, you can disconnect internet after the page is loaded * A keypair is automatically generated locally and stored in the browser's IndexedDB * The private key is created with [```{ extractable: false }```](src/ts/whisper-crypto.ts) and cannot be [exported by the application](https://www.w3.org/TR/WebCryptoAPI/#cryptokey-interface-members). * The public part of the keypair can be copied and distributed to those who want to send you sensitive data * The private part remains local and is the only way to decrypt data addressed to you +* Pretty secure algorithms like ECDH-ES, P-256 for key pair, ECDH-ES+A256KW for key wrapping and A256GCM for content encryption * Use the hosted version (the good stuff happens locally anyway) or self host and modify it -* TODO: List security and compliance features +* TODO: continue ## Advanced and convenience features (non-free) * Get a branded, maintained and supported installation for your organization and its partners