ellipticc
Overview
Encrypted Comments for Encrypted Shares

Encrypted Comments for Encrypted Shares

We’re excited to announce encrypted comments on encrypted shares. When enabled, anyone with the share link can post and read comments, all encrypted end-to-end so only those with the share key can access them.


Summary (TL;DR)

Comments are encrypted client-side using XChaCha20-Poly1305; server stores only ciphertext and metadata. Only share link holders can decrypt and read.


How it works

  • Comments are encrypted client-side using a key derived from the share’s CEK via HKDF.
  • Server stores only ciphertext and metadata (timestamp, display name, signature, fingerprint); plaintext is never accessible.
  • Anyone with the share link can decrypt and read comments in their browser.
  • Owners can moderate: toggle comments on/off, lock to prevent new submissions, ban users across all shares, or revoke shares entirely.

How to use it

  1. Create or edit a share and enable comments in Share Settings.
  2. Open the share link; comment box appears if enabled.
  3. Write and submit a comment; browser encrypts it locally before upload.
  4. Viewers with the link can decrypt and read comments in their browser.
Important

Tip: treat any share link with comments enabled as you would any sensitive shared key; anyone with the link can view content and comments. Use the share settings to disable comments or revoke the link if needed.


Technical details

Share URLs follow the format https://drive.ellipticc.com/<shareId>#CEK, where the fragment after # is the base64-encoded 32-byte CEK. Example: https://drive.ellipticc.com/s/720fd0b44715d0c82eceeabb902212972867aa84b446ddac977e78cf89912b9c#iMEkmUFztJGxMY2XHZ7/ndSNXeSlSOrOTYI6KW+9f+s=.

Comments are encrypted with XChaCha20-Poly1305 using a key derived from the CEK via HKDF-SHA256 (info: share-comments-v1). Tampering with the CEK breaks decryption for both content and comments.

import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha2.js';
ed.hashes.sha512 = sha512;
/** Derive comment key from share CEK using HKDF-SHA256 */
export async function deriveCommentKey(shareCek: Uint8Array): Promise<Uint8Array> {
const info = new TextEncoder().encode('share-comments-v1');
const ikm = await crypto.subtle.importKey('raw', shareCek as any, { name: 'HKDF' }, false, ['deriveBits']);
const derivedBits = await crypto.subtle.deriveBits({
name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32).fill(0), info
}, ikm, 256);
return new Uint8Array(derivedBits);
}
/** Encrypt comment using XChaCha20-Poly1305 */
export async function encryptComment(content: string, key: Uint8Array): Promise<string> {
const nonce = crypto.getRandomValues(new Uint8Array(24));
const encodedContent = new TextEncoder().encode(content);
const cipher = xchacha20poly1305(key, nonce);
const encrypted = cipher.encrypt(encodedContent);
const combined = new Uint8Array(nonce.length + encrypted.length);
combined.set(nonce);
combined.set(encrypted, nonce.length);
return btoa(String.fromCharCode(...Array.from(combined)));
}
/** Decrypt comment */
export async function decryptComment(encryptedBase64: string, key: Uint8Array): Promise<string> {
try {
const combined = new Uint8Array(atob(encryptedBase64).split('').map(c => c.charCodeAt(0)));
const nonce = combined.slice(0, 24);
const encrypted = combined.slice(24);
const cipher = xchacha20poly1305(key, nonce);
const decryptedBytes = cipher.decrypt(encrypted);
return new TextDecoder().decode(decryptedBytes);
} catch (err) {
return '[Decryption Failed]';
}
}
/** Create HMAC-SHA512 fingerprint using userId as key */
export async function createMessageFingerprint(message: string, userId: string): Promise<Uint8Array> {
const encoder = new TextEncoder();
const keyBytes = encoder.encode(userId);
const messageBytes = encoder.encode(message);
const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']);
const hmac = await crypto.subtle.sign('HMAC', key, messageBytes);
return new Uint8Array(hmac);
}
/** Sign fingerprint with Ed25519 private key */
export async function signMessageFingerprint(fingerprint: Uint8Array, privateKey: Uint8Array): Promise<Uint8Array> {
return await ed.sign(fingerprint, privateKey);
}
/** Verify Ed25519 signature */
export async function verifyMessageSignature(fingerprint: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): Promise<boolean> {
return await ed.verify(signature, fingerprint, publicKey);
}

Each comment includes an HMAC-SHA512 fingerprint (using commenter’s userId as key) signed with their Ed25519 private key. Clients verify by checking the fingerprint matches and the signature is valid with the public key, ensuring authenticity and tamper detection.


FAQ

Q: Can Ellipticc read my comments?
A: No. Comments are encrypted client-side using XChaCha20-Poly1305 and stored as ciphertext. Only clients with the exact share key can decrypt them.

Q: What happens if someone tampers with the CEK in the URL?
A: Decryption fails completely. The CEK must be exact; any modification prevents deriving the correct comment key, making both share content and comments inaccessible.

Q: How does moderation work?
A: Owners can toggle comments on/off, lock comments to prevent new submissions, ban users across all their shares, or revoke shares. Bans and locks are enforced server-side. We can’t moderate anything due to encryption.

Q: What if I lose the share link?
A: No problem. You can always find and regenerate share links from your dashboard. The CEK is securely stored and accessible to you as the owner.


Note

Ready to try it? Create a share with comments enabled and start the conversation. For questions, reach out to [email protected] or join our Discord community.

We built this feature to let people collaborate and discuss shared content while keeping the privacy guarantees you expect from end-to-end encryption. Happy sharing!


ellipticc.
ellipticc.
ellipticc.
ellipticc.
ellipticc.
ellipticc.