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
29 changes: 8 additions & 21 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
51 changes: 31 additions & 20 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
},
},
Expand All @@ -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');
}
},
},
Expand Down
6 changes: 4 additions & 2 deletions app/chat/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions app/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
3 changes: 3 additions & 0 deletions components/ProfileHeaderWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,13 @@ export const ProfileHeaderWidget: React.FC<ProfileHeaderWidgetProps> = ({
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);
},
Expand Down
4 changes: 2 additions & 2 deletions ios/syncre.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 2.0.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down Expand Up @@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 2.0.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down
9 changes: 8 additions & 1 deletion services/CryptoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,14 @@ async function decryptPrivateKeyWithPassword(
iterations: number,
password: string
): Promise<Uint8Array> {
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));
Expand Down
18 changes: 13 additions & 5 deletions services/ReencryptionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading