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
- Secure and very easy to use sending of encrypted data anywhere (browser ui)
+ 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?

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