mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Add E2EE key pair verification before send
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
1 parent
69f43ffb7d
commit
0ffa7fa952
6 files changed
+148
-29
No files matched your search
@@ -184,7 +184,11 @@ export const ChatScreen: React.FC = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error sending message:", err);
|
||||
Alert.alert("Error", "Failed to send message");
|
||||
// Show the actual error message if it's user-friendly (from encryption)
|
||||
const errorMessage = err instanceof Error && err.message
|
||||
? err.message
|
||||
: "Failed to send message";
|
||||
Alert.alert("Error", errorMessage);
|
||||
setInputText(content);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* consistency with all other encryption operations.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
encryptTextMessage,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getAdminKeyId,
|
||||
isValidPublicKeyJwk,
|
||||
sanitizePublicKeyJwk,
|
||||
verifyKeyPair,
|
||||
} from '../lib/encryption';
|
||||
import type { RSAPublicKeyJWK, EncryptedMessagePayload } from '../lib/encryption';
|
||||
import { useEncryptionKeys } from './useEncryptionKeys';
|
||||
@@ -42,6 +43,9 @@ export function useEncryptedSend(): UseEncryptedSendReturn {
|
||||
const [isEncrypting, setIsEncrypting] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Track whether we've verified the key pair this session
|
||||
const keyPairVerifiedRef = useRef(false);
|
||||
|
||||
const { publicKeyJwk, hasKeys, isLoading } = useEncryptionKeys();
|
||||
const refreshPartner = useAuthStore((s) => s.refreshPartner);
|
||||
|
||||
@@ -63,12 +67,35 @@ export function useEncryptedSend(): UseEncryptedSendReturn {
|
||||
// This ensures we always use the most current keys, not potentially stale hook state
|
||||
const currentKeys = useAuthStore.getState().encryptionKeys;
|
||||
|
||||
if (!currentKeys.hasKeys || !currentKeys.publicKeyJwk) {
|
||||
console.warn('[E2EE Send] No keys available in global store at encryption time');
|
||||
return null;
|
||||
if (!currentKeys.hasKeys || !currentKeys.publicKeyJwk || !currentKeys.privateKeyJwk) {
|
||||
const keysError = new Error('Encryption keys not ready. Please wait a moment and try again.');
|
||||
console.warn('[E2EE Send] Keys not fully available in global store');
|
||||
setError(keysError);
|
||||
throw keysError;
|
||||
}
|
||||
|
||||
const senderPublicKey = currentKeys.publicKeyJwk;
|
||||
const senderPrivateKey = currentKeys.privateKeyJwk;
|
||||
|
||||
// SAFETY CHECK: Verify the key pair works together before encrypting
|
||||
// This prevents creating unrecoverable messages if keys are mismatched
|
||||
if (!keyPairVerifiedRef.current) {
|
||||
console.log('[E2EE Send] Verifying sender key pair...');
|
||||
const isValidKeyPair = await verifyKeyPair(
|
||||
senderPublicKey as RSAPublicKeyJWK,
|
||||
senderPrivateKey
|
||||
);
|
||||
|
||||
if (!isValidKeyPair) {
|
||||
const keyError = new Error('Encryption key verification failed. Please restart the app and try again.');
|
||||
console.error('[E2EE Send] CRITICAL: Sender key pair verification failed!');
|
||||
setError(keyError);
|
||||
throw keyError;
|
||||
}
|
||||
|
||||
keyPairVerifiedRef.current = true;
|
||||
console.log('[E2EE Send] Sender key pair verified successfully');
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[E2EE Send] Using sender public key from global store, version:',
|
||||
@@ -85,8 +112,33 @@ export function useEncryptedSend(): UseEncryptedSendReturn {
|
||||
const freshPartner = await refreshPartner();
|
||||
|
||||
// Get admin key for moderation access
|
||||
const adminPublicKey = await getAdminPublicKey();
|
||||
const adminKeyId = await getAdminKeyId();
|
||||
// SAFETY: Validate the admin key before using it
|
||||
let adminPublicKey: RSAPublicKeyJWK;
|
||||
let adminKeyId: string;
|
||||
|
||||
try {
|
||||
adminPublicKey = await getAdminPublicKey();
|
||||
adminKeyId = await getAdminKeyId();
|
||||
|
||||
// Validate admin key structure
|
||||
if (!isValidPublicKeyJwk(adminPublicKey)) {
|
||||
const adminError = new Error('Unable to secure message. Please check your connection and try again.');
|
||||
console.error('[E2EE Send] Admin public key is invalid');
|
||||
setIsEncrypting(false);
|
||||
setError(adminError);
|
||||
throw adminError;
|
||||
}
|
||||
} catch (adminKeyError) {
|
||||
// Re-throw if already a user-facing error
|
||||
if (adminKeyError instanceof Error && adminKeyError.message.includes('Unable to secure')) {
|
||||
throw adminKeyError;
|
||||
}
|
||||
const fetchError = new Error('Unable to secure message. Please check your connection and try again.');
|
||||
console.error('[E2EE Send] Failed to fetch admin key:', adminKeyError);
|
||||
setIsEncrypting(false);
|
||||
setError(fetchError);
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
// Get partner's public key from the freshly fetched profile
|
||||
let partnerPublicKey = freshPartner?.public_key_jwk as
|
||||
@@ -124,7 +176,8 @@ export function useEncryptedSend(): UseEncryptedSendReturn {
|
||||
console.error('[E2EE Send] Failed to encrypt message:', err);
|
||||
setError(err as Error);
|
||||
setIsEncrypting(false);
|
||||
return null;
|
||||
// Re-throw to block sending - never fall back to plaintext
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[isE2EEAvailable, refreshPartner]
|
||||
|
||||
@@ -150,7 +150,7 @@ export async function wrapAESKey(
|
||||
|
||||
/**
|
||||
* Unwrap (decrypt) an AES key with an RSA private key
|
||||
*
|
||||
*
|
||||
* @param wrappedKeyBase64 - The wrapped key as base64 string
|
||||
* @param privateKeyJwk - The user's RSA private key
|
||||
* @returns The unwrapped AES key as ArrayBuffer
|
||||
@@ -162,10 +162,65 @@ export async function unwrapAESKey(
|
||||
const subtle = getSubtle();
|
||||
const rsaKey = await importPrivateKey(privateKeyJwk);
|
||||
const wrappedKey = base64ToArrayBuffer(wrappedKeyBase64);
|
||||
|
||||
|
||||
return await subtle.decrypt(
|
||||
{ name: KEY_WRAP_ALGORITHM },
|
||||
rsaKey,
|
||||
wrappedKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a public/private key pair actually work together.
|
||||
* This catches key mismatches that could cause unrecoverable encrypted messages.
|
||||
*
|
||||
* @param publicKeyJwk - The RSA public key
|
||||
* @param privateKeyJwk - The RSA private key
|
||||
* @returns true if the keys work together, false otherwise
|
||||
*/
|
||||
export async function verifyKeyPair(
|
||||
publicKeyJwk: RSAPublicKeyJWK,
|
||||
privateKeyJwk: RSAPrivateKeyJWK
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const subtle = getSubtle();
|
||||
|
||||
// Create test data (32 bytes - same as AES-256 key)
|
||||
const testData = getRandomValues(new Uint8Array(32));
|
||||
|
||||
// Import keys
|
||||
const publicKey = await importPublicKey(publicKeyJwk);
|
||||
const privateKey = await importPrivateKey(privateKeyJwk);
|
||||
|
||||
// Encrypt with public key
|
||||
const encrypted = await subtle.encrypt(
|
||||
{ name: KEY_WRAP_ALGORITHM },
|
||||
publicKey,
|
||||
testData as BufferSource
|
||||
);
|
||||
|
||||
// Decrypt with private key
|
||||
const decrypted = await subtle.decrypt(
|
||||
{ name: KEY_WRAP_ALGORITHM },
|
||||
privateKey,
|
||||
encrypted
|
||||
);
|
||||
|
||||
// Verify the data matches
|
||||
const decryptedArray = new Uint8Array(decrypted);
|
||||
if (decryptedArray.length !== testData.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < testData.length; i++) {
|
||||
if (testData[i] !== decryptedArray[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[E2EE] Key pair verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -65,3 +65,6 @@ export { triggerKeyRotation } from './triggerKeyRotation';
|
||||
|
||||
// Stale Key Repair
|
||||
export { repairStaleKey } from './repairStaleKey';
|
||||
|
||||
// Crypto Operations
|
||||
export { verifyKeyPair } from './crypto';
|
||||
Reference in new issue
Block a user