From 779f8a93f82c09c5ba921d3cf474371210777326 Mon Sep 17 00:00:00 2001 From: b3ni15 Date: Wed, 18 Feb 2026 09:01:28 +0100 Subject: [PATCH] feat: enhance Android build process and improve error handling in chat features --- .github/workflows/android-build.yml | 29 +++++----------- app/(tabs)/_layout.tsx | 1 - app/(tabs)/index.tsx | 51 +++++++++++++++++----------- app/chat/[id].tsx | 6 ++-- app/profile.tsx | 3 ++ components/ProfileHeaderWidget.tsx | 3 ++ ios/syncre.xcodeproj/project.pbxproj | 4 +-- services/CryptoService.ts | 9 ++++- services/ReencryptionService.ts | 18 +++++++--- 9 files changed, 72 insertions(+), 52 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index abc9f94..76e6a5d 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -51,33 +51,20 @@ jobs: - name: Install dependencies run: npm ci - - name: Generate Android Keystore + - name: Setup Android Keystore run: | - # Generate new keystore with random password - KEYSTORE_PASSWORD=$(openssl rand -base64 32) - KEY_ALIAS="syncre-key" - KEY_PASSWORD=$(openssl rand -base64 32) + # Write keystore from base64 secret + echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/keystore.jks - # Create keystore - keytool -genkey -v \ - -keystore android/app/keystore.jks \ - -alias "$KEY_ALIAS" \ - -keyalg RSA \ - -keysize 2048 \ - -validity 10000 \ - -storepass "$KEYSTORE_PASSWORD" \ - -keypass "$KEY_PASSWORD" \ - -dname "CN=Syncre, OU=Development, O=Syncre, L=Unknown, ST=Unknown, C=HU" - - # Create keystore.properties + # Create keystore.properties from secrets cat > android/keystore.properties << EOF MYAPP_UPLOAD_STORE_FILE=keystore.jks - MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS - MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD - MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD + MYAPP_UPLOAD_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }} + MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }} EOF - echo "Keystore generated successfully" + echo "Keystore configured successfully" - name: Make gradlew executable run: chmod +x android/gradlew diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 38db8b1..557f7d7 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -220,7 +220,6 @@ export default function TabLayout() { console.log(`[_layout] Loaded ${chatList.length} chats:`, chatList.map((c: any) => ({ id: c.id, participantCount: c.participants?.length, - participants: c.participants?.map((p: any) => p.username), }))); setChats(chatList); if (Array.isArray(chatList)) { diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d69cd22..5194c2b 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -166,13 +166,19 @@ export default function ChatsTab() { text: 'Delete', style: 'destructive', onPress: async () => { - const response = await ChatService.deleteGroup(chat.id?.toString?.() ?? String(chat.id)); - if (response.success) { - NotificationService.show('success', 'Group deleted'); - DeviceEventEmitter.emit('chats:refresh'); - loadChats(); - } else { - NotificationService.show('error', response.error || 'Failed to delete group'); + try { + const chatId = chat.id?.toString?.() ?? String(chat.id); + const response = await ChatService.deleteGroup(chatId); + if (response.success) { + NotificationService.show('success', 'Group deleted'); + DeviceEventEmitter.emit('chats:refresh'); + loadChats(); + } else { + NotificationService.show('error', response.error || 'Failed to delete group'); + } + } catch (error: any) { + console.error('Failed to delete group:', error); + NotificationService.show('error', error?.message || 'Failed to delete group'); } }, }, @@ -190,19 +196,24 @@ export default function ChatsTab() { text: 'Leave', style: 'destructive', onPress: async () => { - const chatId = chat.id?.toString?.() ?? String(chat.id); - const memberId = user?.id?.toString?.() ?? String(user?.id || ''); - if (!memberId) { - NotificationService.show('error', 'Missing user context to leave group'); - return; - } - const response = await ChatService.removeMember(chatId, memberId); - if (response.success) { - NotificationService.show('success', 'Left group'); - DeviceEventEmitter.emit('chats:refresh'); - loadChats(); - } else { - NotificationService.show('error', response.error || 'Failed to leave group'); + try { + const chatId = chat.id?.toString?.() ?? String(chat.id); + const memberId = user?.id?.toString?.() ?? String(user?.id || ''); + if (!memberId) { + NotificationService.show('error', 'Missing user context to leave group'); + return; + } + const response = await ChatService.removeMember(chatId, memberId); + if (response.success) { + NotificationService.show('success', 'Left group'); + DeviceEventEmitter.emit('chats:refresh'); + loadChats(); + } else { + NotificationService.show('error', response.error || 'Failed to leave group'); + } + } catch (error: any) { + console.error('Failed to leave group:', error); + NotificationService.show('error', error?.message || 'Failed to leave group'); } }, }, diff --git a/app/chat/[id].tsx b/app/chat/[id].tsx index eabe5dc..22381cc 100644 --- a/app/chat/[id].tsx +++ b/app/chat/[id].tsx @@ -4999,11 +4999,10 @@ const ChatScreen: React.FC = () => { const hydratePollFromEncrypted = useCallback( async (messageId: string, poll: PollData) => { if (!poll?.encryptedPayload || !currentUserId || !chatId) return; - const attemptKey = `${messageId}:${poll.payloadVersion || 1}:${Array.isArray(poll.encryptedPayload) ? poll.encryptedPayload.length : 0}`; + const attemptKey = `${chatId}:${messageId}:${poll.payloadVersion || 1}:${Array.isArray(poll.encryptedPayload) ? poll.encryptedPayload.length : 0}`; if (pollDecryptAttemptedRef.current.has(attemptKey)) { return; } - pollDecryptAttemptedRef.current.add(attemptKey); const hasPlainText = typeof poll.question === 'string' && poll.question.trim().length > 0 && @@ -5050,6 +5049,9 @@ const ChatScreen: React.FC = () => { return; } + // Mark as attempted only after successful decrypt/parse + pollDecryptAttemptedRef.current.add(attemptKey); + setPollsData((prev) => { const existing = prev.get(messageId); if (!existing) return prev; diff --git a/app/profile.tsx b/app/profile.tsx index a8f9b0f..7feccf9 100644 --- a/app/profile.tsx +++ b/app/profile.tsx @@ -43,10 +43,13 @@ export default function ProfileScreen() { onPress: async () => { const { StorageService } = await import('../services/StorageService'); const { CryptoService } = await import('../services/CryptoService'); + const { UserCacheService } = await import('../services/UserCacheService'); // Only clear local data, don't delete server-side keys await Promise.all([ CryptoService.clearLocalIdentity(), + CryptoService.clearBackupKey(), StorageService.clear(), + UserCacheService.clear(), ]); router.replace('/' as any); }, diff --git a/components/ProfileHeaderWidget.tsx b/components/ProfileHeaderWidget.tsx index 0070d1a..d695a3f 100644 --- a/components/ProfileHeaderWidget.tsx +++ b/components/ProfileHeaderWidget.tsx @@ -66,10 +66,13 @@ export const ProfileHeaderWidget: React.FC = ({ onPress: async () => { const { StorageService } = await import('../services/StorageService'); const { CryptoService } = await import('../services/CryptoService'); + const { UserCacheService } = await import('../services/UserCacheService'); // Only clear local data, don't delete server-side keys await Promise.all([ CryptoService.clearLocalIdentity(), + CryptoService.clearBackupKey(), StorageService.clear(), + UserCacheService.clear(), ]); router.replace('/' as any); }, diff --git a/ios/syncre.xcodeproj/project.pbxproj b/ios/syncre.xcodeproj/project.pbxproj index 97837cf..78e0521 100644 --- a/ios/syncre.xcodeproj/project.pbxproj +++ b/ios/syncre.xcodeproj/project.pbxproj @@ -406,7 +406,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 2.0.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 2.0.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/services/CryptoService.ts b/services/CryptoService.ts index 6213a62..376012a 100644 --- a/services/CryptoService.ts +++ b/services/CryptoService.ts @@ -239,7 +239,14 @@ async function decryptPrivateKeyWithPassword( iterations: number, password: string ): Promise { - const derivedKey = await derivePasswordKey(password, fromBase64(salt), iterations); + // Clamp iterations to safe range to prevent abusive CPU work + const MIN_ITERATIONS = 10000; + const MAX_ITERATIONS = 200000; + const clampedIterations = Math.max(MIN_ITERATIONS, Math.min(MAX_ITERATIONS, iterations)); + if (clampedIterations !== iterations) { + console.warn(`[CryptoService] Iterations ${iterations} clamped to ${clampedIterations}`); + } + const derivedKey = await derivePasswordKey(password, fromBase64(salt), clampedIterations); const cipher = new XChaCha20Poly1305(derivedKey); const decrypted = cipher.open(fromBase64(nonce), fromBase64(encryptedPrivateKey)); diff --git a/services/ReencryptionService.ts b/services/ReencryptionService.ts index 4b41952..b232c3f 100644 --- a/services/ReencryptionService.ts +++ b/services/ReencryptionService.ts @@ -152,15 +152,23 @@ class ReencryptionServiceClass { if (envelopesToPost.length > 0) { try { - await ApiService.post( + const response = await ApiService.post( '/keys/envelopes/batch', { envelopes: envelopesToPost }, token ); - console.log('[ReencryptionService] Posted batch of envelopes', { - count: envelopesToPost.length, - chatId, - }); + if (response.success) { + console.log('[ReencryptionService] Posted batch of envelopes', { + count: envelopesToPost.length, + chatId, + }); + } else { + console.warn('[ReencryptionService] Batch post returned failure:', response); + // Fallback to sequential posting + for (const item of envelopesToPost) { + await ApiService.post('/keys/envelopes', item, token).catch(() => null); + } + } } catch (error) { console.warn('[ReencryptionService] Failed to post envelope batch, falling back to sequential...', error); // Fallback if batch endpoint doesn't exist yet