A lightweight library to use E2EE
npm install @amirafa/encryption-serviceyarn add @amirafa/encryption-servicepnpm add @amirafa/encryption-serviceimport { EncryptionService } from "@amirafa/encryption-service";
const enc = EncryptionService();
// generate user keys
await enc.generateAndStoreIdentityKeyPair("alice");
await enc.generateAndStoreECDHKeyPair("alice");
// example: encrypt and decrypt a message
const alicePriv = await enc.importPrivateKey(localStorage.getItem("alice-privateKey")!);
const bobPub = await enc.importPublicKey(localStorage.getItem("bob-publicKey")!);
const sharedKey = await enc.deriveSharedKey(alicePriv, bobPub);
const { encryptedMessage, iv } = await enc.encryptMessageAES("Hello Bob!", sharedKey);
const plain = await enc.decryptMessageAES(encryptedMessage, sharedKey, iv);
console.log(plain); // "Hello Bob!"This document describes how the EncryptionServiceClass implements end-to-end encryption (E2EE) for a secure chat system and how the backend should store and handle related data.
The system uses three cryptographic layers:
- ECDSA (Identity) — for signing and verifying identity keys.
- ECDH (Shared Key) — for deriving a mutual secret key between two users.
- AES-GCM (Message Encryption) — for encrypting and decrypting messages.
All encryption/decryption logic happens in the browser (client-side).
The backend only stores public keys, signatures, and encrypted data — never plaintext or private keys.
Generates an ECDSA key pair (identity keys) and stores both in localStorage.
await enc.generateAndStoreIdentityKeyPair("alice");Stored keys:
alice-id-publicKey
alice-id-privateKey
Generates an ECDH key pair for secure message key exchange.
await enc.generateAndStoreECDHKeyPair("alice");Stored keys:
alice-publicKey
alice-privateKey
Signs the ECDH public key using the identity private key (ECDSA).
const sig = await enc.signEcdhPublicKey(aliceIdPriv, aliceEcdhPub);Verifies that an ECDH public key truly belongs to a given identity.
const ok = await enc.verifyEcdhPublicKeySignature(bobIdPub, bobEcdhPub, sig);Derives a shared AES key using your private ECDH key and the other user’s public ECDH key.
const aesKey = await enc.deriveSharedKey(alicePrivEcdh, bobPubEcdh);Both sides independently derive the same AES key.
Encrypts a plaintext message using AES-GCM.
const { encryptedMessage, iv } = await enc.encryptMessageAES("Hello Bob", aesKey);iv (Initialization Vector) must be sent along with the ciphertext.
Decrypts a message using the same AES key and IV.
const plain = await enc.decryptMessageAES(encryptedMessage, aesKey, iv);Generates a SHA-256 fingerprint of an identity key for display and manual verification (TOFU).
const fpr = await enc.getIdentityFingerprint(aliceIdentityPub);
console.log("Alice fingerprint:", fpr);await enc.generateAndStoreIdentityKeyPair("alice");
await enc.generateAndStoreECDHKeyPair("alice");
await enc.generateAndStoreIdentityKeyPair("bob");
await enc.generateAndStoreECDHKeyPair("bob");const idPriv = await enc.importIdentityPrivateKey(localStorage.getItem("alice-id-privateKey")!);
const ecdhPub = await enc.importPublicKey(localStorage.getItem("alice-publicKey")!);
const signature = await enc.signEcdhPublicKey(idPriv, ecdhPub);
const alicePackage = {
identityPublicKey: localStorage.getItem("alice-id-publicKey")!,
ecdhPublicKey: localStorage.getItem("alice-publicKey")!,
signature
};Alice sends this JSON package to Bob via the server.
const aliceIdPub = await enc.importIdentityPublicKey(alicePackage.identityPublicKey);
const aliceEcdhPub = await enc.importPublicKey(alicePackage.ecdhPublicKey);
const valid = await enc.verifyEcdhPublicKeySignature(aliceIdPub, aliceEcdhPub, alicePackage.signature);
if (!valid) throw new Error("Fake Alice detected!");const bobPriv = await enc.importPrivateKey(localStorage.getItem("bob-privateKey")!);
const sharedKey = await enc.deriveSharedKey(bobPriv, aliceEcdhPub);Both parties now hold the same symmetric AES key.
const { encryptedMessage, iv } = await enc.encryptMessageAES("Hi Bob!", sharedKey);
const chatEntry = {
sender: "alice",
recipient: "bob",
message: Array.from(new Uint8Array(encryptedMessage)),
iv: Array.from(iv)
};
// send chatEntry to serverconst msgBuf = new Uint8Array(chatEntry.message).buffer;
const ivBuf = new Uint8Array(chatEntry.iv);
const plain = await enc.decryptMessageAES(msgBuf, sharedKey, ivBuf);
console.log("Decrypted:", plain); // "Hi Bob!"{
username: string,
identityPublicKey: string,
ecdhPublicKey: string,
signature: string
}{
sender: string,
recipient: string,
message: number[],
iv: number[]
}CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
identity_public_key TEXT NOT NULL,
ecdh_public_key TEXT NOT NULL,
signature TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);CREATE TABLE messages (
id SERIAL PRIMARY KEY,
sender VARCHAR(50) NOT NULL,
recipient VARCHAR(50) NOT NULL,
message BYTEA NOT NULL,
iv BYTEA NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (sender) REFERENCES users(username),
FOREIGN KEY (recipient) REFERENCES users(username)
);{
"sender": "alice",
"recipient": "bob",
"message": "BASE64_ENCRYPTED_DATA",
"iv": "BASE64_IV"
}{
"sender": "alice",
"message": "BASE64_ENCRYPTED_DATA",
"iv": "BASE64_IV"
}const msg = await enc.decryptMessageAES(
base64ToArrayBuffer(message),
aesKey,
base64ToUint8Array(iv)
);
console.log(msg); // "Hello Bob!"- The server stores only public keys and encrypted data.
- Private keys and plaintext never leave the user’s device.
- Messages must be delivered only to their intended recipient.
- The backend is zero-knowledge — even a data leak reveals nothing readable.
| Component | Responsibility | Location |
|---|---|---|
| ECDSA | Identity and signature | Client |
| ECDH | Shared key derivation | Client |
| AES-GCM | Message encryption/decryption | Client |
| Key & message storage | Persistence only | Backend |
| Plaintext visibility | Sender & recipient devices only | — |
MIT © 2025 — Encryption Service by Amirafa
Feel free to use, modify, and distribute under the terms of the MIT license.
- Add
git repository
- Update
README.md - Add
logo
- Add
type declaration - Update
README.md
- Add
README.md
- Minor
fixes
- published
@amirafa/encryption-service