mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Remove E2EE and switch to plaintext messaging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
1 parent
43fa7033dc
commit
c48936686e
46 files changed
+738
-6874
No files matched your search
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* EncryptionKeyInitializer Component
|
||||
*
|
||||
* Handles automatic initialization and upload of E2EE encryption keys.
|
||||
* Should be placed in the app layout to run after authentication.
|
||||
*
|
||||
* Uses global Zustand store for key state to ensure all components
|
||||
* see the same keys at all times.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { useEncryptionKeys } from '../hooks';
|
||||
import { useAuthStore } from '../store';
|
||||
import { reencryptPendingMessages } from '../lib/encryption/reencryptPendingMessages';
|
||||
import { triggerKeyRotation, getAdminPublicKey } from '../lib/encryption';
|
||||
import { LoadingOverlay } from './ui/LoadingOverlay';
|
||||
import { colors } from '../theme';
|
||||
|
||||
/**
|
||||
* Check if crypto is available (react-native-quick-crypto polyfill installed)
|
||||
*/
|
||||
function isCryptoAvailable(): boolean {
|
||||
return typeof crypto !== 'undefined' && crypto.subtle !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that initializes encryption keys after user authentication.
|
||||
*
|
||||
* Flow:
|
||||
* 1. When user signs in, check if they have encryption keys
|
||||
* 2. If not, generate new RSA key pair
|
||||
* 3. Upload public key to Supabase profile
|
||||
* 4. If user has a partner without keys, trigger re-encryption when partner generates keys
|
||||
*/
|
||||
export function EncryptionKeyInitializer(): JSX.Element | null {
|
||||
const { initializeAndUploadKeys, isLoading: keysLoading, hasKeys, error } = useEncryptionKeys();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const couple = useAuthStore((s) => s.couple);
|
||||
|
||||
// Track if keys were regenerated this session (new device, reinstall, etc.)
|
||||
const [wasRegenerated, setWasRegenerated] = useState(false);
|
||||
const hasTriggeredRotation = useRef(false);
|
||||
const hasLoggedCryptoWarning = useRef(false);
|
||||
const isInitializing = useRef(false);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Show loading overlay when we are initializing keys (generating new ones takes time)
|
||||
// or when rotation is in progress
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
const [loadingStatus, setLoadingStatus] = useState('Connecting...');
|
||||
|
||||
// Effect 1: Initialize encryption keys when user is authenticated
|
||||
useEffect(() => {
|
||||
// Skip on web - E2EE not supported
|
||||
if (Platform.OS === 'web') return;
|
||||
|
||||
// Check if crypto is available (native modules rebuilt)
|
||||
if (!isCryptoAvailable()) {
|
||||
if (!hasLoggedCryptoWarning.current) {
|
||||
console.warn('[E2EE Init] Crypto not available. Run: npx expo prebuild --clean');
|
||||
hasLoggedCryptoWarning.current = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if no user, already initialized, or currently initializing
|
||||
if (!user?.id || hasInitialized.current || isInitializing.current) return;
|
||||
|
||||
// Wait for initial key loading to complete
|
||||
if (keysLoading) return;
|
||||
|
||||
// Skip if keys already exist in global state
|
||||
if (hasKeys) {
|
||||
console.log('[E2EE Init] Keys already loaded in global state');
|
||||
hasInitialized.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if there was an error loading keys (avoid retry loop)
|
||||
if (error) {
|
||||
console.warn('[E2EE Init] Skipping key initialization due to previous error:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const initKeys = async () => {
|
||||
try {
|
||||
// Mark as initializing to prevent re-entry
|
||||
isInitializing.current = true;
|
||||
// Only show loading if we are generating keys (which might take a second)
|
||||
// Standard loading is fast enough to not need a full screen blocker
|
||||
|
||||
console.log('[E2EE Init] Starting key initialization...');
|
||||
|
||||
// Initialize keys and upload in one operation
|
||||
const result = await initializeAndUploadKeys();
|
||||
|
||||
console.log('[E2EE Init] Encryption keys initialized and uploaded');
|
||||
|
||||
// Pre-fetch admin key to warm the cache before user reaches chat
|
||||
// This reduces the delay before secureReady becomes true
|
||||
try {
|
||||
await getAdminPublicKey();
|
||||
console.log('[E2EE Init] Admin key pre-fetched successfully');
|
||||
} catch (adminKeyError) {
|
||||
// Non-fatal: admin key will be fetched again when needed
|
||||
console.warn('[E2EE Init] Failed to pre-fetch admin key:', adminKeyError);
|
||||
}
|
||||
|
||||
// Track if keys were regenerated (for rotation decision)
|
||||
if (result.wasRegenerated) {
|
||||
console.log('[E2EE Init] Keys were regenerated this session');
|
||||
setWasRegenerated(true);
|
||||
}
|
||||
|
||||
hasInitialized.current = true;
|
||||
} catch (err) {
|
||||
console.error('[E2EE Init] Failed to initialize encryption keys:', err);
|
||||
// Reset initialization flag so we can retry on next render
|
||||
// But only if it's a transient error, not a crypto unavailable error
|
||||
if (err instanceof Error && !err.message.includes('Crypto')) {
|
||||
isInitializing.current = false;
|
||||
}
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initKeys();
|
||||
}, [user?.id, keysLoading, hasKeys, error, initializeAndUploadKeys]);
|
||||
|
||||
// Effect 2: Trigger key rotation when keys were regenerated and couple is available
|
||||
useEffect(() => {
|
||||
// Skip on web or if crypto not available
|
||||
if (Platform.OS === 'web' || !isCryptoAvailable()) return;
|
||||
|
||||
// Need user and couple to trigger rotation
|
||||
if (!user?.id || !couple?.id) return;
|
||||
|
||||
// Only trigger once per session
|
||||
if (hasTriggeredRotation.current) return;
|
||||
|
||||
// Wait for keys to be initialized in global state
|
||||
if (!hasKeys) return;
|
||||
|
||||
// Only trigger rotation if keys were actually regenerated this session
|
||||
// (new device, app reinstall, key corruption recovery, etc.)
|
||||
if (!wasRegenerated) {
|
||||
// If we're done initializing but not rotating, hide loading
|
||||
if (showLoading) {
|
||||
setShowLoading(false);
|
||||
isInitializing.current = false;
|
||||
}
|
||||
console.log('[E2EE Init] Keys unchanged, skipping rotation');
|
||||
return;
|
||||
}
|
||||
|
||||
const runRotation = async () => {
|
||||
try {
|
||||
hasTriggeredRotation.current = true;
|
||||
// Only show loading for rotation which is a heavier operation
|
||||
setShowLoading(true);
|
||||
setLoadingStatus('Updating security...');
|
||||
|
||||
// Trigger key rotation - re-wraps message keys for the new public key
|
||||
console.log('[E2EE Init] Keys were regenerated, triggering key rotation...');
|
||||
await triggerKeyRotation();
|
||||
|
||||
// Trigger re-encryption of pending messages (for messages where
|
||||
// recipient had no key at send time)
|
||||
await reencryptPendingMessages({ force: true });
|
||||
|
||||
console.log('[E2EE Init] Key rotation complete');
|
||||
} catch (err) {
|
||||
console.error('[E2EE Init] Failed to run key rotation:', err);
|
||||
// Reset so we can retry
|
||||
hasTriggeredRotation.current = false;
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
isInitializing.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
runRotation();
|
||||
}, [user?.id, couple?.id, hasKeys, wasRegenerated, showLoading]);
|
||||
|
||||
// Reset initialization flags when user changes (sign out/sign in)
|
||||
useEffect(() => {
|
||||
if (!user?.id) {
|
||||
hasInitialized.current = false;
|
||||
setWasRegenerated(false);
|
||||
isInitializing.current = false;
|
||||
hasTriggeredRotation.current = false;
|
||||
setShowLoading(false);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
return (
|
||||
<LoadingOverlay
|
||||
visible={showLoading}
|
||||
statusText={loadingStatus}
|
||||
detailText="This helps keep your conversations private."
|
||||
fullScreen={true}
|
||||
spinnerColor={colors.primary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user