Skip to content
Merged
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
70 changes: 63 additions & 7 deletions src/lib/components/EncryptionWarning.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { keyManager } from '@/lib/crypto/key-manager.js';

export default function EncryptionWarning() {
const router = useRouter();
const [hasKeys, setHasKeys] = useState(true);
const [checked, setChecked] = useState(false);
const [generating, setGenerating] = useState(false);
const [error, setError] = useState('');

useEffect(() => {
async function check() {
try {
const has = await keyManager.hasUserKeys();
setHasKeys(has);
await keyManager.initialize();
setHasKeys(await keyManager.hasUserKeys());
} catch {
setHasKeys(false);
}
Expand All @@ -20,20 +24,72 @@ export default function EncryptionWarning() {
check();
}, []);

async function handleGenerate() {
// New keys give you a fresh identity but make every message that was
// encrypted to your previous key permanently unreadable. Restoring/uploading
// existing keys is almost always what the user actually wants.
const ok = window.confirm(
'Generate brand-new keys?\n\nYou will NOT be able to read any past messages or files that were encrypted to your previous keys. If you have a backup or an exported key file, use "Upload existing keys" instead.'
);
if (!ok) return;
setGenerating(true);
setError('');
try {
await keyManager.generateUserKeys();
setHasKeys(true);
} catch (err) {
setError(err?.message || 'Failed to generate keys');
} finally {
setGenerating(false);
}
}

if (!checked || hasKeys) return null;

return (
<div className="encryption-warning">
<div className="warning-icon">⚠️</div>
<div className="warning-content">
<strong>Encryption keys not found</strong>
<p>Your messages cannot be encrypted. Please go to <a href="/settings">Settings</a> to generate or restore your encryption keys.</p>
<strong>No encryption keys on this device</strong>
<p>
This browser doesn&apos;t have your encryption keys, so messages can&apos;t be decrypted or sent
securely. Upload your existing keys to keep your history, or generate new ones to start fresh.
</p>
{error && <p className="warning-error">{error}</p>}
<div className="warning-actions">
<button
type="button"
className="warn-btn warn-btn-primary"
onClick={() => router.push('/settings')}
disabled={generating}
>
Upload existing keys
</button>
<button
type="button"
className="warn-btn warn-btn-secondary"
onClick={handleGenerate}
disabled={generating}
>
{generating ? 'Generating…' : 'Generate new keys'}
</button>
</div>
<p className="warning-hint">
Switched browsers or devices? Choose <strong>Upload existing keys</strong> — generating new keys
loses access to all past messages.
</p>
</div>
<style>{`
.encryption-warning { display: flex; align-items: center; gap: 1rem; padding: .75rem 1rem; background: rgba(245,158,11,.1); border-bottom: 1px solid #f59e0b; color: #92400e; }
.warning-icon { font-size: 1.25rem; flex-shrink: 0; }
.encryption-warning { display: flex; align-items: flex-start; gap: 1rem; padding: .75rem 1rem; background: rgba(245,158,11,.1); border-bottom: 1px solid #f59e0b; color: #92400e; }
.warning-icon { font-size: 1.25rem; flex-shrink: 0; line-height: 1.4; }
.warning-content p { font-size: .875rem; margin-top: .25rem; }
.warning-content a { color: #92400e; font-weight: 600; }
.warning-error { color: #991b1b; font-weight: 600; }
.warning-actions { display: flex; gap: .5rem; margin-top: .625rem; flex-wrap: wrap; }
.warn-btn { padding: .375rem .75rem; font-size: .875rem; border-radius: .375rem; cursor: pointer; border: 1px solid transparent; font-weight: 600; }
.warn-btn:disabled { opacity: .6; cursor: default; }
.warn-btn-primary { background: #92400e; color: #fff; }
.warn-btn-secondary { background: transparent; color: #92400e; border-color: #92400e; }
.warning-hint { font-size: .75rem; margin-top: .5rem; opacity: .85; }
`}</style>
</div>
);
Expand Down
15 changes: 14 additions & 1 deletion src/lib/components/chat/MessageAttachments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ export default function MessageAttachments({ messageId }) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// Bumped to force a re-fetch when an upload for this message completes after
// the component already mounted (the carrier 'file' message renders — and
// fetches — before its attachments finish uploading).
const [reloadKey, setReloadKey] = useState(0);

useEffect(() => {
if (!messageId) return undefined;
const onUpdated = (e) => {
if (e.detail?.messageId === messageId) setReloadKey((k) => k + 1);
};
window.addEventListener('attachments:updated', onUpdated);
return () => window.removeEventListener('attachments:updated', onUpdated);
}, [messageId]);

useEffect(() => {
if (!messageId) return undefined;
Expand Down Expand Up @@ -71,7 +84,7 @@ export default function MessageAttachments({ messageId }) {
cancelled = true;
createdUrls.forEach((u) => URL.revokeObjectURL(u));
};
}, [messageId]);
}, [messageId, reloadKey]);

if (loading) {
return <div className="att-loading"><span className="att-spinner" /> Decrypting attachment…</div>;
Expand Down
7 changes: 7 additions & 0 deletions src/lib/components/chat/MessageInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ export default function MessageInput({ conversationId, disabled = false }) {
} catch {
// non-fatal: realtime will reconcile
}

// The carrier 'file' message mounted (and fetched its attachments)
// before these uploads finished, so tell it to re-fetch now that the
// files exist — otherwise it stays empty until a full page refresh.
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('attachments:updated', { detail: { messageId } }));
}
trackMessageSent({ conversationId, type: 'file', hasAttachments: true });
setSelectedFiles([]);
setMessageText('');
Expand Down
16 changes: 12 additions & 4 deletions src/lib/crypto/key-sync-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,22 @@ export class KeySyncService {
return false;
}

// Check if our key exists in the database
const hasKeyInDb = data.public_keys && data.public_keys[currentUserId];

if (!hasKeyInDb) {
// Check if our key exists in the database AND matches the local key.
// Presence alone isn't enough: after switching browsers / rotating keys
// the DB still holds the OLD public key, so a presence-only check would
// skip the sync and leave everyone encrypting to a dead key.
const dbKey = data.public_keys && data.public_keys[currentUserId];

if (!dbKey) {
console.log('🔑 Public key not found in database, sync needed');
return true;
}

if (dbKey !== publicKey) {
console.log('🔑 Database public key differs from local key (rotated/new device), sync needed');
return true;
}

console.log('🔑 Public key already synced to database');
return false;

Expand Down
Loading