Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions ENCRYPTION_FEATURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# E2E Encryption Features

This document describes the End-to-End Encryption features implemented in ADHD Chat.

## UI Preview

![E2E Encryption Demo](https://github.com/user-attachments/assets/9466d1c8-ea35-4a50-ad5a-3c7ffd3a8066)

The image above shows all the encryption UI components implemented in this feature.

## Features Implemented

### 1. Encryption Setup Modal
A multi-step modal flow that guides users through setting up E2E encryption:

**Step 1: Generate Key**
- Button to initiate key generation
- Calls `crypto.createRecoveryKeyFromPassphrase()` from matrix-js-sdk
- Sets up secret storage and cross-signing

**Step 2: Display Key**
- Shows the generated recovery key in a monospaced format
- Warning message about saving the key securely
- Copy to clipboard button for convenience
- User must acknowledge they've saved the key

**Step 3: Verify Key**
- User must re-enter the recovery key
- Validates that the entered key matches the generated one
- Prevents proceeding if keys don't match

**Step 4: Completion**
- Success message confirming encryption is set up
- Auto-closes after confirmation

### 2. Home Page Integration
Added a new "Encryption" section on the Home page with:
- Explanation of E2E encryption
- "Generate Encryption Key" button
- Opens the EncryptionSetupModal when clicked

### 3. Room-Level Encryption Controls
Updated the Room page to show:
- Encryption status indicator (Encrypted ✓ or Not encrypted)
- "Enable Encryption" button for non-encrypted rooms
- Warning that encryption cannot be disabled once enabled
- Sends `m.room.encryption` state event with `m.megolm.v1.aes-sha2` algorithm

## Technical Implementation

### Components Created

#### Modal.tsx
Reusable modal component with:
- Backdrop overlay
- Keyboard support (ESC to close)
- Size variants (sm, md, lg)
- Proper accessibility attributes

#### EncryptionSetupModal.tsx
Specialized modal for encryption setup:
- Multi-step wizard interface
- Form validation
- Error handling
- Loading states

### Hooks Updated

#### useMatrixClient.ts
- Modified `handleSetupEncryption()` to return the generated key
- Proper error handling with throw statements
- Sets up:
- Secret storage via `bootstrapSecretStorage()`
- Cross-signing via `bootstrapCrossSigning()`
- Key backup via `resetKeyBackup()` if needed

### Pages Updated

#### Home.tsx
- Added encryption section with call-to-action
- Integrated EncryptionSetupModal
- Added state management for modal visibility

#### Room.tsx
- Check encryption status via `m.room.encryption` state event
- Display encryption status indicator
- Enable encryption button with loading state
- Send state event to enable encryption in room

## Usage Flow

### For Users Setting Up Encryption

1. Navigate to Home page after logging in
2. Click "Generate Encryption Key" button
3. Modal opens - click "Generate Encryption Key"
4. Copy and save the displayed recovery key securely
5. Click "I've Saved My Key"
6. Re-enter the recovery key to verify
7. Click "Verify Key"
8. See success message - encryption is now set up!

### For Users Enabling Room Encryption

1. Navigate to a room
2. Check the Encryption section
3. If not encrypted, click "Enable Encryption"
4. Room is now encrypted - all future messages will be encrypted

## Matrix SDK Integration

This implementation follows the matrix-js-sdk documentation for E2E encryption:

1. **Initialization**: `initRustCrypto()` is called during client setup
2. **Secret Storage**: Uses `bootstrapSecretStorage()` with recovery key
3. **Cross-Signing**: Uses `bootstrapCrossSigning()` for device verification
4. **Key Backup**: Automatically sets up key backup for recovery
5. **Room Encryption**: Sends `m.room.encryption` state event

## Security Considerations

- Recovery keys are generated using `createRecoveryKeyFromPassphrase()`
- Users are required to save and verify their recovery key
- Encryption cannot be disabled once enabled in a room
- Keys are stored in IndexedDB via the Rust crypto implementation
- Recovery keys are needed to decrypt messages on new devices

## Future Enhancements

Potential improvements could include:
- Device verification flow for new devices
- Key backup status indicator
- Encryption status in room list
- Cross-signing verification UI
- Session management and device list
175 changes: 175 additions & 0 deletions apps/web/src/components/EncryptionSetupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { useState } from 'react';
import Modal from './Modal';
import Button from './Button';
import Input from './Input';

interface EncryptionSetupModalProps {
isOpen: boolean;
onClose: () => void;
onSetupComplete: () => void;
onGenerateKey: () => Promise<string>;
}

type SetupStep = 'generate' | 'display' | 'verify' | 'complete';

function EncryptionSetupModal({
isOpen,
onClose,
onSetupComplete,
onGenerateKey,
}: EncryptionSetupModalProps) {
const [step, setStep] = useState<SetupStep>('generate');
const [generatedKey, setGeneratedKey] = useState<string>('');
const [verificationKey, setVerificationKey] = useState<string>('');
const [error, setError] = useState<string>('');
const [loading, setLoading] = useState(false);

const handleGenerate = async () => {
setLoading(true);
setError('');
try {
const key = await onGenerateKey();
setGeneratedKey(key);
setStep('display');
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
};

const handleContinueToVerification = () => {
setStep('verify');
};

const handleVerify = () => {
if (verificationKey.trim() !== generatedKey) {
setError('Keys do not match. Please try again.');
return;
}
setStep('complete');
setTimeout(() => {
onSetupComplete();
handleClose();
}, 1500);
};

const handleClose = () => {
setStep('generate');
setGeneratedKey('');
setVerificationKey('');
setError('');
onClose();
};

const handleCopyKey = () => {
navigator.clipboard.writeText(generatedKey);
};

return (
<Modal isOpen={isOpen} onClose={handleClose} title="Setup End-to-End Encryption">
{step === 'generate' && (
<div className="space-y-4">
<p className="text-gray-600">
Generate a recovery key to enable end-to-end encryption. This key will allow you to
decrypt your messages on new devices.
</p>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={handleClose} disabled={loading}>
Cancel
</Button>
<Button onClick={handleGenerate} disabled={loading}>
{loading ? 'Generating...' : 'Generate Encryption Key'}
</Button>
</div>
</div>
)}

{step === 'display' && (
<div className="space-y-4">
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800 font-semibold mb-2">
⚠️ Important: Save this key securely
</p>
<p className="text-sm text-yellow-700">
You will need this key to decrypt your messages if you lose access to your device.
Store it in a safe place like a password manager.
</p>
</div>

<div className="bg-gray-50 p-4 rounded-lg">
<label className="block text-sm font-medium text-gray-700 mb-2">
Your Recovery Key
</label>
<div className="flex items-center space-x-2">
<code className="flex-1 block p-3 bg-white border border-gray-300 rounded font-mono text-sm break-all">
{generatedKey}
</code>
<Button size="sm" variant="outline" onClick={handleCopyKey}>
Copy
</Button>
</div>
</div>

<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleContinueToVerification}>
I've Saved My Key
</Button>
</div>
</div>
)}

{step === 'verify' && (
<div className="space-y-4">
<p className="text-gray-600">
Please re-enter your recovery key to confirm you've saved it correctly.
</p>

<Input
label="Enter Recovery Key"
type="text"
value={verificationKey}
onChange={(e) => setVerificationKey(e.target.value)}
placeholder="Paste or type your recovery key"
error={error}
/>

<div className="flex justify-end space-x-3">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleVerify}
disabled={!verificationKey.trim()}
>
Verify Key
</Button>
</div>
</div>
)}

{step === 'complete' && (
<div className="space-y-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-sm text-green-800 font-semibold">
✓ Encryption setup complete!
</p>
<p className="text-sm text-green-700 mt-1">
Your device is now set up for end-to-end encryption.
</p>
</div>
</div>
)}
</Modal>
);
}

export default EncryptionSetupModal;
85 changes: 85 additions & 0 deletions apps/web/src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';

interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
size?: 'sm' | 'md' | 'lg';
}

function Modal({ isOpen, onClose, title, children, size = 'md' }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};

if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}

return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);

if (!isOpen) return null;

const sizeStyles = {
sm: 'max-w-md',
md: 'max-w-2xl',
lg: 'max-w-4xl',
};

return (
<div
className="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"
onClick={onClose}
/>

<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>

<div
ref={modalRef}
className={`inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle w-full ${sizeStyles[size]}`}
>
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left w-full">
<h3
className="text-lg leading-6 font-medium text-gray-900 mb-4"
id="modal-title"
>
{title}
</h3>
<div className="mt-2">{children}</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

export default Modal;
Loading