diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 018e17a..d5eb112 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -12,6 +12,14 @@ analyzer: invalid_return_type_for_catch_error: ignore use_build_context_synchronously: ignore use_null_aware_elements: ignore + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml linter: diff --git a/mobile/lib/controllers/auth_state.dart b/mobile/lib/controllers/auth_state.dart index 54809cb..7bf2b26 100644 --- a/mobile/lib/controllers/auth_state.dart +++ b/mobile/lib/controllers/auth_state.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; +import 'package:mobile/services/signal_service.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:app_links/app_links.dart'; import 'package:mobile/models/user.dart'; @@ -67,13 +68,12 @@ class AuthState extends ChangeNotifier { final userData = data['data'] ?? data['user'] ?? data; _currentUser = UserModel.fromJson(userData); - + await _authService.saveUserProfile(jsonEncode(userData)); notifyListeners(); - } catch (e) { debugPrint("Network failed, attempting to load cached user profile: $e"); - + try { final cachedData = await _authService.getCachedUserProfile(); if (cachedData != null) { @@ -81,8 +81,8 @@ class AuthState extends ChangeNotifier { _currentUser = UserModel.fromJson(decodedData); notifyListeners(); } else { - _errorMessage = "No internet connection and no cached profile."; - notifyListeners(); + _errorMessage = "No internet connection and no cached profile."; + notifyListeners(); } } catch (cacheError) { debugPrint("Cache read failed: $cacheError"); @@ -95,6 +95,9 @@ class AuthState extends ChangeNotifier { if (_token != null) { await loadUserProfile(); + if (_currentUser != null) { + await SignalService().initializeAndUploadKeys(_currentUser!.id); + } } notifyListeners(); @@ -129,6 +132,10 @@ class AuthState extends ChangeNotifier { await loadUserProfile(); + if (_currentUser != null) { + await SignalService().initializeAndUploadKeys(_currentUser!.id); + } + _isLoading = false; notifyListeners(); return true; @@ -179,6 +186,10 @@ class AuthState extends ChangeNotifier { await loadUserProfile(); + if (_currentUser != null) { + await SignalService().initializeAndUploadKeys(_currentUser!.id); + } + _isLoading = false; notifyListeners(); return true; @@ -237,6 +248,10 @@ class AuthState extends ChangeNotifier { await _authService.saveTokens(accessToken, refreshToken ?? accessToken); await loadUserProfile(); + if (_currentUser != null) { + await SignalService().initializeAndUploadKeys(_currentUser!.id); + } + _isLoading = false; notifyListeners(); return true; diff --git a/mobile/lib/controllers/chat/active_chat_controller.dart b/mobile/lib/controllers/chat/active_chat_controller.dart index ef1e2d9..f51757c 100644 --- a/mobile/lib/controllers/chat/active_chat_controller.dart +++ b/mobile/lib/controllers/chat/active_chat_controller.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:mobile/services/signal_service.dart'; import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:uuid/uuid.dart'; import '../../models/message.dart'; @@ -40,7 +42,6 @@ class ActiveChatController extends ChangeNotifier { isPeerOnline = false; notifyListeners(); - // 1. Load Local DB Messages try { final db = await DatabaseHelper.instance.database; final localData = await db.query( @@ -78,15 +79,41 @@ class ActiveChatController extends ChangeNotifier { debugPrint("Local cache read failed: $e"); } - // 2. Fetch Network History try { final res = await _api.getChatHistory(targetUid, isGroup: isGroup); + if (currentChatUserId != targetUid) return; final targetList = _extractDataList(res.data, ['messages']); - final loadedMessages = targetList.reversed - .map((json) => Message.fromJson(json)) - .toList(); + + final db = await DatabaseHelper.instance.database; + final localData = await db.query( + 'messages', + where: 'chat_id = ?', + whereArgs: [targetUid], + ); + + final Map localDecryptedContents = {}; + for (var row in localData) { + final content = row['content'] as String; + if (!content.contains('ciphertext') && !content.contains('🔒')) { + localDecryptedContents[row['id'] as String] = content; + } + } + + List loadedMessages = []; + for (var json in targetList.reversed) { + Message parsedMsg = Message.fromJson(json); + + if (localDecryptedContents.containsKey(parsedMsg.id)) { + loadedMessages.add( + parsedMsg.copyWith(content: localDecryptedContents[parsedMsg.id]), + ); + } else { + Message decryptedMsg = await _decryptMessageIfNeeded(parsedMsg); + loadedMessages.add(decryptedMsg); + } + } if (currentChatUserId != targetUid) return; @@ -108,6 +135,10 @@ class ActiveChatController extends ChangeNotifier { final db = await DatabaseHelper.instance.database; Batch batch = db.batch(); for (var msg in loadedMessages) { + if (msg.content.contains('ciphertext') || msg.content.contains('🔒')) { + continue; + } + batch.insert('messages', { 'id': msg.id, 'chat_id': targetUid, @@ -148,9 +179,35 @@ class ActiveChatController extends ChangeNotifier { if (currentChatUserId != targetUid) return; final targetList = _extractDataList(res.data, ['messages']); - final loadedMessages = targetList.reversed - .map((json) => Message.fromJson(json)) - .toList(); + + final db = await DatabaseHelper.instance.database; + final localData = await db.query( + 'messages', + where: 'chat_id = ?', + whereArgs: [targetUid], + ); + + final Map localDecryptedContents = {}; + for (var row in localData) { + final content = row['content'] as String; + if (!content.contains('ciphertext') && !content.contains('🔒')) { + localDecryptedContents[row['id'] as String] = content; + } + } + + List loadedMessages = []; + for (var json in targetList.reversed) { + Message parsedMsg = Message.fromJson(json); + + if (localDecryptedContents.containsKey(parsedMsg.id)) { + loadedMessages.add( + parsedMsg.copyWith(content: localDecryptedContents[parsedMsg.id]), + ); + } else { + Message decryptedMsg = await _decryptMessageIfNeeded(parsedMsg); + loadedMessages.add(decryptedMsg); + } + } if (currentChatUserId != targetUid) return; @@ -196,6 +253,10 @@ class ActiveChatController extends ChangeNotifier { final db = await DatabaseHelper.instance.database; Batch batch = db.batch(); for (var msg in loadedMessages) { + if (msg.content.contains('ciphertext') || msg.content.contains('🔒')) { + continue; + } + batch.insert('messages', { 'id': msg.id, 'chat_id': targetUid, @@ -244,6 +305,15 @@ class ActiveChatController extends ChangeNotifier { if (currentChatUserId == null || cleanContent.isEmpty) return; final targetId = currentChatUserId!; + + if (!isCurrentChatGroup) { + final sessionReady = await SignalService().establishSessionIfNeeded(targetId); + if (!sessionReady) { + debugPrint("Send aborted: Target user has no E2EE keys on server."); + return; + } + } + final clientMessageId = _uuid.v4(); QuotedMessage? quoted; @@ -283,11 +353,28 @@ class ActiveChatController extends ChangeNotifier { 'sync_status': 'pending', }); + String securePayload; + if (isCurrentChatGroup) { + securePayload = jsonEncode({ + 'type': 0, + 'ciphertext': cleanContent, + }); + } else { + final encryptedData = await SignalService().encryptMessage( + targetId, + cleanContent, + ); + securePayload = jsonEncode({ + 'type': encryptedData['type'], + 'ciphertext': encryptedData['ciphertext'], + }); + } + final payload = { 'messageId': clientMessageId, 'receiverId': isCurrentChatGroup ? null : targetId, 'groupId': isCurrentChatGroup ? targetId : null, - 'content': cleanContent, + 'content': securePayload, 'replyToMessageId': replyingTo?.id, }; @@ -297,25 +384,25 @@ class ActiveChatController extends ChangeNotifier { payload, ); - isCurrentChatGroup && senderId != null - ? _ws.sendGroupChat( - messageId: clientMessageId, - groupId: targetId, - content: cleanContent, - senderId: senderId, - replyToMessageId: replyingTo?.id, - ) - : _ws.sendChat( - messageId: clientMessageId, - receiverId: targetId, - content: cleanContent, - replyToMessageId: replyingTo?.id, - ); + if (isCurrentChatGroup) { + _ws.sendGroupChat( + messageId: clientMessageId, + groupId: targetId, + content: securePayload, + replyToMessageId: replyingTo?.id, + ); + } else { + _ws.sendChat( + messageId: clientMessageId, + receiverId: targetId, + content: securePayload, + replyToMessageId: replyingTo?.id, + ); + } if (_ws.isConnected) { markMessageAsSynced(clientMessageId); } - } catch (e) { debugPrint("Immediate send failed, message queued: $e"); } @@ -416,9 +503,11 @@ class ActiveChatController extends ChangeNotifier { } } - void addRealTimeMessage(Message newMsg) { + void addRealTimeMessage(Message incomingMsg) async { if (currentChatUserId == null) return; + Message newMsg = await _decryptMessageIfNeeded(incomingMsg); + bool belongsToCurrentChat = (isCurrentChatGroup && newMsg.receiverId == currentChatUserId) || (!isCurrentChatGroup && @@ -432,6 +521,21 @@ class ActiveChatController extends ChangeNotifier { activeChat = [...activeChat, newMsg]; notifyListeners(); + try { + await DatabaseHelper.instance.insertMessage({ + 'id': newMsg.id, + 'chat_id': currentChatUserId, + 'sender_id': newMsg.senderId, + 'content': newMsg.content, + 'created_at': newMsg.createdAt.millisecondsSinceEpoch, + 'is_read': newMsg.isRead ? 1 : 0, + 'reply_to_id': newMsg.replyToMessageId, + 'sync_status': 'synced', + }); + } catch (e) { + debugPrint("Failed to save incoming WS message to DB: $e"); + } + _ws.sendReadReceipt( receiverId: isCurrentChatGroup ? null : currentChatUserId, groupId: isCurrentChatGroup ? currentChatUserId : null, @@ -443,4 +547,94 @@ class ActiveChatController extends ChangeNotifier { } } } + + Future _decryptMessageIfNeeded(Message msg) async { + final content = msg.content.trim(); + + if (content.startsWith('{') && content.contains('ciphertext')) { + + bool isSelfChat = msg.senderId == msg.receiverId; + bool isSentByMe = isSelfChat || (!isCurrentChatGroup && msg.senderId != currentChatUserId) || msg.senderId == 'me'; + + if (isSentByMe) { + try { + final db = await DatabaseHelper.instance.database; + + final exactMatch = await db.query( + 'messages', + where: 'id = ?', + whereArgs: [msg.id], + ); + + if (exactMatch.isNotEmpty) { + final exactContent = exactMatch.first['content'].toString(); + if (!exactContent.contains('ciphertext') && !exactContent.contains('🔒')) { + return msg.copyWith(content: exactContent); + } + } + + final targetChatId = currentChatUserId ?? msg.receiverId; + final fallbackRows = await db.query( + 'messages', + where: 'chat_id = ?', + whereArgs: [targetChatId], + ); + + int minDiff = -1; + String? closestPlaintext; + + for (var row in fallbackRows) { + final rSender = row['sender_id'].toString(); + if (rSender != 'me' && rSender != msg.senderId) continue; + + final rowContent = row['content'].toString(); + if (rowContent.contains('ciphertext') || rowContent.contains('🔒')) continue; + + final localTime = row['created_at'] as int; + final diff = (localTime - msg.createdAt.millisecondsSinceEpoch).abs(); + + if (minDiff == -1 || diff < minDiff) { + minDiff = diff; + closestPlaintext = rowContent; + } + } + + if (closestPlaintext != null) { + return msg.copyWith(content: closestPlaintext); + } + } catch (e) { + debugPrint("Local sent-message lookup crashed: $e"); + } + + return msg.copyWith(content: "🔒 [Sent from another device]"); + } + + try { + final Map payload = jsonDecode(content); + + if (isCurrentChatGroup && payload['type'] == 0) { + return msg.copyWith(content: payload['ciphertext']); + } + + final decryptedText = await SignalService().decryptMessage( + msg.senderId, + payload['ciphertext'], + payload['type'], + ); + return msg.copyWith(content: decryptedText); + } catch (e) { + debugPrint("LibSignal Decryption Failed: $e"); + final errStr = e.toString(); + + if (errStr.contains('DuplicateMessageException')) { + return msg.copyWith(content: "🔒 [Message already decrypted]"); + } else if (errStr.contains('NoSessionException') || errStr.contains('Bad Mac')) { + return msg.copyWith(content: "🔒 [Encrypted for past session]"); + } + + return msg.copyWith(content: "🔒 [Encrypted Message]"); + } + } + return msg; + } } \ No newline at end of file diff --git a/mobile/lib/controllers/chat/inbox_controller.dart b/mobile/lib/controllers/chat/inbox_controller.dart index 593449e..bd8ec79 100644 --- a/mobile/lib/controllers/chat/inbox_controller.dart +++ b/mobile/lib/controllers/chat/inbox_controller.dart @@ -38,19 +38,40 @@ class InboxController extends ChangeNotifier { try { final response = await _api.getConversations(); final rawData = _parseResponse(response.data, ['conversations']); + final db = await DatabaseHelper.instance.database; List combinedInbox = []; for (var json in rawData) { try { final bool isGroup = json['is_group'] == true || json['type'] == 'group'; + + InboxItem item; if (isGroup) { - combinedInbox.add(InboxItem.fromGroup(Group.fromJson(json))); + item = InboxItem.fromGroup(Group.fromJson(json)); } else { - combinedInbox.add( - InboxItem.fromConversation(Conversation.fromJson(json)), + item = InboxItem.fromConversation(Conversation.fromJson(json)); + } + + if (item.lastMessage.contains('ciphertext') || + item.lastMessage.contains('🔒')) { + final localMsg = await db.query( + 'messages', + where: + 'chat_id = ? AND content NOT LIKE ? AND content NOT LIKE ?', + whereArgs: [item.id, '%ciphertext%', '%🔒%'], + orderBy: 'created_at DESC', + limit: 1, ); + + if (localMsg.isNotEmpty) { + item.lastMessage = localMsg.first['content'].toString(); + } else { + item.lastMessage = "🔒 Encrypted Message"; + } } + + combinedInbox.add(item); } catch (e) { debugPrint("BAD JSON OBJECT: $json"); } @@ -162,6 +183,10 @@ class InboxController extends ChangeNotifier { final db = await DatabaseHelper.instance.database; Batch batch = db.batch(); for (var msg in loadedMessages) { + if (msg.content.contains('ciphertext') || msg.content.contains('🔒')) { + continue; + } + batch.insert('messages', { 'id': msg.id, 'chat_id': chatId, diff --git a/mobile/lib/models/message.dart b/mobile/lib/models/message.dart index 8f553d5..9be807d 100644 --- a/mobile/lib/models/message.dart +++ b/mobile/lib/models/message.dart @@ -108,12 +108,13 @@ class Message { bool? isRead, String? id, String? syncStatus, + String? content, }) { return Message( id: id ?? this.id, senderId: senderId, receiverId: receiverId, - content: content, + content: content ?? this.content, createdAt: createdAt, isRead: isRead ?? this.isRead, replyToMessageId: replyToMessageId, diff --git a/mobile/lib/pages/chat/chat_details_page.dart b/mobile/lib/pages/chat/chat_details_page.dart index bfaa77c..2f0392e 100644 --- a/mobile/lib/pages/chat/chat_details_page.dart +++ b/mobile/lib/pages/chat/chat_details_page.dart @@ -8,7 +8,9 @@ import 'package:mobile/controllers/chat/group_details_controller.dart'; import 'package:mobile/controllers/chat/inbox_controller.dart'; import 'package:mobile/pages/chat/chat_page.dart'; import 'package:mobile/providers/group_controller_provider.dart'; +import 'package:mobile/services/signal_service.dart'; import 'package:provider/provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; class ChatMember { final String userId; @@ -181,9 +183,88 @@ class _ChatDetailsPageState extends State { leading: const Icon(Icons.lock), title: const Text('Encryption'), subtitle: const Text( - 'Messages and calls are end-to-end encrypted.', + 'Messages and calls are end-to-end encrypted. Tap to verify.', ), - onTap: () {}, + onTap: () async { + final currentUserId = context.read().currentUser?.id; + if (currentUserId == null) return; + + final safetyNumber = await SignalService().getSafetyNumber( + currentUserId, + widget.chatId + ); + + if (!context.mounted) return; + + if (safetyNumber == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Send a message first to establish a secure session.')), + ); + return; + } + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Verify Security Code", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(16), + color: Colors.white, + child: QrImageView( + data: safetyNumber, + version: QrVersions.auto, + size: 200.0, + ), + ), + const SizedBox(height: 20), + Text( + safetyNumber, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + letterSpacing: 2, + fontWeight: FontWeight.w500 + ), + ), + const SizedBox(height: 20), + const Text( + "To verify that messages and calls are end-to-end encrypted, scan this code on your contact's phone, or compare the number above.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 40), + ], + ), + ), + ); + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.refresh, color: Colors.orange), + title: const Text('Reset Secure Session', style: TextStyle(color: Colors.orange)), + subtitle: const Text('Use this if messages are failing to decrypt.'), + onTap: () async { + await SignalService().forceResetSession(widget.chatId); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Secure session reset. Send a new message to reconnect.')), + ); + } + }, ), const Divider(height: 1), ListTile( diff --git a/mobile/lib/pages/chat/chat_page.dart b/mobile/lib/pages/chat/chat_page.dart index 112520a..5b89f9c 100644 --- a/mobile/lib/pages/chat/chat_page.dart +++ b/mobile/lib/pages/chat/chat_page.dart @@ -537,7 +537,7 @@ class _ChatPageState extends State with WidgetsBindingObserver { itemCount: activeChat.length + (chatState.isPeerTyping ? 2 : 1) + - 1, + 2, itemBuilder: (context, index) { if (index == 0) { return const SizedBox(height: 140); @@ -595,6 +595,33 @@ class _ChatPageState extends State with WidgetsBindingObserver { index - (chatState.isPeerTyping ? 3 : 2); final int realIndex = activeChat.length - 1 - msgIndex; + if (realIndex == -1) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme + .colorScheme + .surfaceContainerHighest + .withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + "🔒 Messages and calls are end-to-end encrypted. No one outside of this chat can read or listen to them.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } final msg = activeChat[realIndex]; final bool isHighlighted = diff --git a/mobile/lib/pages/settings/privacy_security.dart b/mobile/lib/pages/settings/privacy_security.dart index d55c347..9a1ffec 100644 --- a/mobile/lib/pages/settings/privacy_security.dart +++ b/mobile/lib/pages/settings/privacy_security.dart @@ -1,10 +1,94 @@ import 'package:flutter/material.dart'; +import 'package:mobile/services/signal_service.dart'; class PrivacySecuritySettingsPage extends StatelessWidget { const PrivacySecuritySettingsPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold(appBar: AppBar(title: Text('Privacy and security'))); + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar(title: const Text('Privacy and security')), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Icon( + Icons.lock_person, + size: 64, + color: theme.colorScheme.primary, + ), + const SizedBox(height: 16), + const Text( + "Your privacy is protected", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + "Elephant uses the Signal Protocol to secure your messages. Nobody, not even Elephant, can read your messages or listen to your calls.", + textAlign: TextAlign.center, + style: TextStyle(color: theme.colorScheme.onSurfaceVariant), + ), + ], + ), + ), + const Divider(), + const ListTile( + leading: Icon(Icons.timer), + title: Text('Disappearing messages'), + subtitle: Text('Off'), + trailing: Icon(Icons.chevron_right), + ), + const Divider(), + ListTile( + leading: const Icon(Icons.delete_forever, color: Colors.red), + title: const Text( + 'Reset All Secure Sessions', + style: TextStyle(color: Colors.red), + ), + subtitle: const Text('Fixes widespread decryption errors.'), + onTap: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Reset All Sessions?"), + content: const Text( + "This will delete all local cryptographic sessions. Your chat history will remain, but you will need to exchange new messages to re-establish secure connections with your contacts.", + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + await SignalService().clearAllSessions(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'All secure sessions have been reset.', + ), + ), + ); + } + }, + child: const Text( + "Reset", + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); } } diff --git a/mobile/lib/services/chat/chat_event_handler.dart b/mobile/lib/services/chat/chat_event_handler.dart index c9f0251..31b567e 100644 --- a/mobile/lib/services/chat/chat_event_handler.dart +++ b/mobile/lib/services/chat/chat_event_handler.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:mobile/services/signal_service.dart'; import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:mobile/controllers/chat/active_chat_controller.dart'; import 'package:mobile/controllers/chat/inbox_controller.dart'; @@ -35,11 +38,17 @@ class ChatEventHandler { .toLowerCase(); isCurrentChat = (eventGroupId == cleanCurrentChat); } else { - final String? eventSenderId = data['sender_id']?.toString().trim().toLowerCase() ?? - data['sender']?.toString().trim().toLowerCase(); - final String? eventReceiverId = data['receiver_id']?.toString().trim().toLowerCase(); - - isCurrentChat = (eventSenderId == cleanCurrentChat || eventReceiverId == cleanCurrentChat); + final String? eventSenderId = + data['sender_id']?.toString().trim().toLowerCase() ?? + data['sender']?.toString().trim().toLowerCase(); + final String? eventReceiverId = data['receiver_id'] + ?.toString() + .trim() + .toLowerCase(); + + isCurrentChat = + (eventSenderId == cleanCurrentChat || + eventReceiverId == cleanCurrentChat); } } @@ -61,14 +70,21 @@ class ChatEventHandler { case 'chat': case 'message': final String? incomingId = data['id']?.toString(); - final String echoId = data['message_id']?.toString() ?? + final String echoId = + data['message_id']?.toString() ?? data['messageId']?.toString() ?? data['client_message_id']?.toString() ?? incomingId ?? ''; - final String cleanSenderId = (data['sender_id'] ?? data['sender'] ?? '').toString().trim().toLowerCase(); - final String cleanReceiverId = (data['receiver_id'] ?? '').toString().trim().toLowerCase(); + final String cleanSenderId = (data['sender_id'] ?? data['sender'] ?? '') + .toString() + .trim() + .toLowerCase(); + final String cleanReceiverId = (data['receiver_id'] ?? '') + .toString() + .trim() + .toLowerCase(); final String myId = currentUserId.trim().toLowerCase(); final bool isMe = (cleanSenderId == 'me' || cleanSenderId == myId); @@ -76,6 +92,27 @@ class ChatEventHandler { ? data['group_id'].toString() : (isMe ? cleanReceiverId : cleanSenderId); + try { + final String rawContent = data['content']?.toString() ?? ''; + if (rawContent.startsWith('{') && rawContent.contains('ciphertext')) { + final Map payload = jsonDecode(rawContent); + final String remoteSender = data['sender_id']?.toString() ?? ''; + + final decryptedText = await SignalService().decryptMessage( + remoteSender, + payload['ciphertext'], + payload['type'], + ); + data['content'] = + decryptedText; + } + } catch (e) { + debugPrint( + "❌ Decryption failed for message. It may be out of sync: $e", + ); + data['content'] = "🔒 [Message Decryption Failed]"; + } + try { final db = await DatabaseHelper.instance.database; final queuedItems = await db.query( @@ -88,7 +125,7 @@ class ChatEventHandler { if (isOurMessage) { final String newMsgId = incomingId ?? echoId; - + int index = activeChatController.activeChat.indexWhere( (m) => m.id == echoId || m.id == newMsgId, ); @@ -107,7 +144,9 @@ class ChatEventHandler { if (index != -1) { final existingMsg = activeChatController.activeChat[index]; - final newList = List.from(activeChatController.activeChat); + final newList = List.from( + activeChatController.activeChat, + ); newList[index] = Message( id: newMsgId, senderId: existingMsg.senderId, @@ -123,11 +162,19 @@ class ChatEventHandler { activeChatController.activeChat = newList; activeChatController.refreshUI(); - await db.delete('action_queue', where: 'id = ?', whereArgs: [originalClientId]); + await db.delete( + 'action_queue', + where: 'id = ?', + whereArgs: [originalClientId], + ); if (originalClientId != newMsgId) { - await db.delete('messages', where: 'id = ?', whereArgs: [originalClientId]); + await db.delete( + 'messages', + where: 'id = ?', + whereArgs: [originalClientId], + ); } - + await db.insert('messages', { 'id': newMsgId, 'chat_id': dbChatId, @@ -173,7 +220,9 @@ class ChatEventHandler { }, conflictAlgorithm: ConflictAlgorithm.replace); if (isCurrentChat) { - if (!activeChatController.activeChat.any((msg) => msg.id == incomingMsg.id)) { + if (!activeChatController.activeChat.any( + (msg) => msg.id == incomingMsg.id, + )) { activeChatController.activeChat = [ ...activeChatController.activeChat, incomingMsg, @@ -199,7 +248,6 @@ class ChatEventHandler { syncStatus: 'synced', isRead: isCurrentChat, ); - } catch (e) { debugPrint("Failed to save incoming message to DB: $e"); } @@ -217,10 +265,17 @@ class ChatEventHandler { break; case 'read_receipt': - final String payloadSender = (data['sender_id'] ?? '').toString().toLowerCase(); - final String payloadReceiver = (data['receiver_id'] ?? '').toString().toLowerCase(); - final String payloadGroup = (data['group_id'] ?? '').toString().toLowerCase(); - final String safeChatId = (activeChatController.currentChatUserId ?? '').toLowerCase(); + final String payloadSender = (data['sender_id'] ?? '') + .toString() + .toLowerCase(); + final String payloadReceiver = (data['receiver_id'] ?? '') + .toString() + .toLowerCase(); + final String payloadGroup = (data['group_id'] ?? '') + .toString() + .toLowerCase(); + final String safeChatId = (activeChatController.currentChatUserId ?? '') + .toLowerCase(); bool isRelevantToThisChat = false; String dbTargetChatId = ""; @@ -283,4 +338,4 @@ class ChatEventHandler { break; } } -} \ No newline at end of file +} diff --git a/mobile/lib/services/chat/chat_sync_service.dart b/mobile/lib/services/chat/chat_sync_service.dart index 89a8d41..08c26a4 100644 --- a/mobile/lib/services/chat/chat_sync_service.dart +++ b/mobile/lib/services/chat/chat_sync_service.dart @@ -31,7 +31,7 @@ class ChatSyncService { messageId: payload['messageId'], groupId: payload['groupId'], content: payload['content'], - senderId: currentUserId.isNotEmpty ? currentUserId : 'me', + // senderId: currentUserId.isNotEmpty ? currentUserId : 'me', replyToMessageId: payload['replyToMessageId'], ); } else { diff --git a/mobile/lib/services/db_services.dart b/mobile/lib/services/db_services.dart index 85424a7..fabd74e 100644 --- a/mobile/lib/services/db_services.dart +++ b/mobile/lib/services/db_services.dart @@ -89,17 +89,73 @@ class DatabaseHelper { PRIMARY KEY (group_id, user_id) ) '''); + + await db.execute(''' + CREATE TABLE signal_local_keys ( + id INTEGER PRIMARY KEY DEFAULT 1, + registration_id INTEGER NOT NULL, + identity_key_pair TEXT NOT NULL + ) + '''); + + await db.execute(''' + CREATE TABLE signal_identities ( + address TEXT PRIMARY KEY, + identity_key TEXT NOT NULL + ) + '''); + + await db.execute(''' + CREATE TABLE signal_sessions ( + address TEXT PRIMARY KEY, + record TEXT NOT NULL + ) + '''); + + await db.execute(''' + CREATE TABLE signal_prekeys ( + key_id INTEGER PRIMARY KEY, + record TEXT NOT NULL + ) + '''); + + await db.execute(''' + CREATE TABLE signal_signed_prekeys ( + key_id INTEGER PRIMARY KEY, + record TEXT NOT NULL + ) + '''); } - Future insertMessage(Map messageData) async { - final db = await instance.database; - await db.insert( - 'messages', - messageData, + Future insertMessage(Map row) async { + Database db = await instance.database; + + if (row['content'] != null) { + String newContent = row['content'].toString(); + + if (newContent.contains('ciphertext') || newContent.contains('🔒')) { + List> existing = await db.query( + 'messages', + where: 'id = ?', + whereArgs: [row['id']], + ); + + if (existing.isNotEmpty) { + String oldContent = existing.first['content'].toString(); + if (!oldContent.contains('ciphertext') && !oldContent.contains('🔒')) { + row['content'] = oldContent; + } + } + } + } + + return await db.insert( + 'messages', + row, conflictAlgorithm: ConflictAlgorithm.replace, ); } - + Future queueAction( String id, String type, diff --git a/mobile/lib/services/signal_service.dart b/mobile/lib/services/signal_service.dart new file mode 100644 index 0000000..e29f39f --- /dev/null +++ b/mobile/lib/services/signal_service.dart @@ -0,0 +1,223 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:mobile/services/sqlite_signal_store.dart'; +import 'package:mobile/services/db_services.dart'; +import 'api_services.dart'; + +class SignalService { + static final SignalService _instance = SignalService._internal(); + factory SignalService() => _instance; + + final ApiService _api = ApiService(); + + late SQLiteSignalStore _store; + bool _isInitialized = false; + + SignalService._internal(); + + Future initStore() async { + if (_isInitialized) return; + _store = SQLiteSignalStore(); + _isInitialized = true; + } + + SignalProtocolAddress _getAddress(String userId) => + SignalProtocolAddress(userId, 1); + + Future initializeAndUploadKeys(String currentUserId) async { + await initStore(); + + final db = await DatabaseHelper.instance.database; + final existingKeys = await db.query('signal_local_keys', where: 'id = 1'); + + if (existingKeys.isNotEmpty) { + debugPrint("E2EE: Keys already exist locally. Skipping generation."); + return; + } + + debugPrint("E2EE: Generating new local identity keys..."); + + final identityKeyPair = generateIdentityKeyPair(); + final registrationId = generateRegistrationId(false); + + await _store.storeLocalData(identityKeyPair, registrationId); + + final preKeys = generatePreKeys(0, 100); + final signedPreKey = generateSignedPreKey(identityKeyPair, 0); + + for (var preKey in preKeys) { + await _store.storePreKey(preKey.id, preKey); + } + await _store.storeSignedPreKey(signedPreKey.id, signedPreKey); + + final publicPreKeys = preKeys + .map( + (k) => { + 'id': k.id, + 'content': base64Encode(k.getKeyPair().publicKey.serialize()), + }, + ) + .toList(); + + final payload = { + 'device_id': 'main', + 'identity_key': base64Encode(identityKeyPair.getPublicKey().serialize()), + 'signed_prekey': base64Encode(signedPreKey.getKeyPair().publicKey.serialize()), + 'signature': base64Encode(signedPreKey.signature), + 'one_time_prekeys': publicPreKeys, + }; + + try { + await _api.post("/e2ee/keys", data: payload); + debugPrint("E2EE Keys uploaded successfully"); + } catch (e) { + debugPrint("Failed to upload E2EE keys: $e"); + } + } + + Future establishSessionIfNeeded(String remoteUserId) async { + await initStore(); + final address = _getAddress(remoteUserId); + + if (await _store.containsSession(address)) return true; + + try { + final response = await _api.get("/e2ee/bundle/$remoteUserId?device_id=main"); + final data = response.data['data'] ?? response.data; + + final identityKeyStr = data['identity_key']; + final signedPreKeyStr = data['signed_prekey']; + final signatureStr = data['signature']; + final otpBodyStr = data['one_time_prekey_body']; + final otpId = data['one_time_prekey_id']; + + if (identityKeyStr == null || signedPreKeyStr == null || signatureStr == null || otpBodyStr == null) { + debugPrint("Receiver bundle is missing required cryptographic keys."); + return false; + } + + final identityKey = IdentityKey(Curve.decodePoint(base64Decode(identityKeyStr), 0)); + final signedPreKey = Curve.decodePoint(base64Decode(signedPreKeyStr), 0); + final signature = base64Decode(signatureStr); + final preKey = Curve.decodePoint(base64Decode(otpBodyStr), 0); + + final bundle = PreKeyBundle( + 0, + 1, + (otpId as int?) ?? 1, + preKey, + 0, + signedPreKey, + signature, + identityKey, + ); + + final sessionBuilder = SessionBuilder(_store, _store, _store, _store, address); + + await sessionBuilder.processPreKeyBundle(bundle); + debugPrint("E2EE Session established perfectly with $remoteUserId!"); + return true; + + } on DioException catch (e) { + if (e.response?.statusCode == 404) { + debugPrint("Receiver $remoteUserId has not registered E2EE keys yet."); + } else { + debugPrint("Network error fetching bundle for $remoteUserId: ${e.message}"); + } + return false; + } catch (e) { + debugPrint("Failed to establish E2EE session with $remoteUserId: $e"); + return false; + } + } + + Future> encryptMessage( + String remoteUserId, + String plaintext, + ) async { + debugPrint("SignalService: Encrypting message for $remoteUserId..."); + final address = _getAddress(remoteUserId); + final sessionCipher = SessionCipher( + _store, + _store, + _store, + _store, + address, + ); + + final plaintextBytes = Uint8List.fromList(utf8.encode(plaintext)); + final ciphertextMessage = await sessionCipher.encrypt(plaintextBytes); + + debugPrint("SignalService: Encryption successful (Type ${ciphertextMessage.getType()})."); + return { + 'type': ciphertextMessage.getType(), + 'ciphertext': base64Encode(ciphertextMessage.serialize()), + }; + } + + Future decryptMessage( + String remoteUserId, + String base64Ciphertext, + int type, + ) async { + debugPrint("SignalService: Decrypting message from $remoteUserId..."); + final address = _getAddress(remoteUserId); + final sessionCipher = SessionCipher( + _store, + _store, + _store, + _store, + address, + ); + final bytes = base64Decode(base64Ciphertext); + + Uint8List plaintextBytes; + + if (type == CiphertextMessage.prekeyType) { + plaintextBytes = await sessionCipher.decrypt(PreKeySignalMessage(bytes)); + } else { + final signalMsg = SignalMessage.fromSerialized(bytes); + plaintextBytes = await sessionCipher.decryptFromSignal(signalMsg); + } + + debugPrint("SignalService: Decryption successful!"); + return utf8.decode(plaintextBytes); + } + + Future getSafetyNumber( + String localUserId, + String remoteUserId, + ) async { + final localKeyPair = await _store.getIdentityKeyPair(); + final remoteAddress = _getAddress(remoteUserId); + final remoteIdentity = await _store.getIdentity(remoteAddress); + + if (remoteIdentity == null) return null; + + final generator = NumericFingerprintGenerator(5200); + + final fingerprint = generator.createFor( + 1, + Uint8List.fromList(utf8.encode(localUserId)), + localKeyPair.getPublicKey(), + Uint8List.fromList(utf8.encode(remoteUserId)), + remoteIdentity, + ); + + return fingerprint.displayableFingerprint.localFingerprintNumbers; + } + + Future forceResetSession(String remoteUserId) async { + final address = _getAddress(remoteUserId); + await _store.deleteSession(address); + await establishSessionIfNeeded(remoteUserId); + } + + Future clearAllSessions() async { + final db = await DatabaseHelper.instance.database; + await db.delete('signal_sessions'); + } +} \ No newline at end of file diff --git a/mobile/lib/services/sqlite_signal_store.dart b/mobile/lib/services/sqlite_signal_store.dart new file mode 100644 index 0000000..4695aed --- /dev/null +++ b/mobile/lib/services/sqlite_signal_store.dart @@ -0,0 +1,258 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; +import 'db_services.dart'; + +class SQLiteSignalStore implements SignalProtocolStore { + String _toB64(Uint8List bytes) => base64Encode(bytes); + + Uint8List _fromB64(String b64) => base64Decode(b64); + + Future storeLocalData( + IdentityKeyPair keyPair, + int registrationId, + ) async { + final db = await DatabaseHelper.instance.database; + await db.insert('signal_local_keys', { + 'id': 1, + 'registration_id': registrationId, + 'identity_key_pair': _toB64(keyPair.serialize()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + @override + Future getIdentityKeyPair() async { + final db = await DatabaseHelper.instance.database; + final res = await db.query('signal_local_keys', where: 'id = 1'); + if (res.isEmpty) throw Exception("Local Identity Key not generated yet."); + return IdentityKeyPair.fromSerialized( + _fromB64(res.first['identity_key_pair'] as String), + ); + } + + @override + Future getLocalRegistrationId() async { + final db = await DatabaseHelper.instance.database; + final res = await db.query('signal_local_keys', where: 'id = 1'); + if (res.isEmpty) throw Exception("Registration ID not generated yet."); + return res.first['registration_id'] as int; + } + + @override + Future saveIdentity( + SignalProtocolAddress address, + IdentityKey? identityKey, + ) async { + if (identityKey == null) return false; + final db = await DatabaseHelper.instance.database; + final existing = await getIdentity(address); + + if (existing != null && existing != identityKey) { + await db.update( + 'signal_identities', + {'identity_key': _toB64(identityKey.serialize())}, + where: 'address = ?', + whereArgs: [address.getName()], + ); + return true; + } + + await db.insert('signal_identities', { + 'address': address.getName(), + 'identity_key': _toB64(identityKey.serialize()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + + return false; + } + + @override + Future getIdentity(SignalProtocolAddress address) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_identities', + where: 'address = ?', + whereArgs: [address.getName()], + ); + if (res.isEmpty) return null; + + return IdentityKey.fromBytes( + _fromB64(res.first['identity_key'] as String), + 0, + ); + } + + @override + Future isTrustedIdentity( + SignalProtocolAddress address, + IdentityKey? identityKey, + Direction direction, + ) async { + if (identityKey == null) return false; + final trusted = await getIdentity(address); + return trusted == null || trusted == identityKey; + } + + @override + Future loadPreKey(int preKeyId) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_prekeys', + where: 'key_id = ?', + whereArgs: [preKeyId], + ); + if (res.isEmpty) throw InvalidKeyIdException("No prekey found"); + return PreKeyRecord.fromBuffer(_fromB64(res.first['record'] as String)); + } + + @override + Future storePreKey(int preKeyId, PreKeyRecord record) async { + final db = await DatabaseHelper.instance.database; + await db.insert('signal_prekeys', { + 'key_id': preKeyId, + 'record': _toB64(record.serialize()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + @override + Future containsPreKey(int preKeyId) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_prekeys', + where: 'key_id = ?', + whereArgs: [preKeyId], + ); + return res.isNotEmpty; + } + + @override + Future removePreKey(int preKeyId) async { + final db = await DatabaseHelper.instance.database; + await db.delete( + 'signal_prekeys', + where: 'key_id = ?', + whereArgs: [preKeyId], + ); + } + + @override + Future loadSignedPreKey(int signedPreKeyId) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_signed_prekeys', + where: 'key_id = ?', + whereArgs: [signedPreKeyId], + ); + if (res.isEmpty) throw InvalidKeyIdException("No signed prekey found"); + + return SignedPreKeyRecord.fromSerialized( + _fromB64(res.first['record'] as String), + ); + } + + @override + Future> loadSignedPreKeys() async { + final db = await DatabaseHelper.instance.database; + final res = await db.query('signal_signed_prekeys'); + return res + .map( + (row) => SignedPreKeyRecord.fromSerialized( + _fromB64(row['record'] as String), + ), + ) + .toList(); + } + + @override + Future storeSignedPreKey( + int signedPreKeyId, + SignedPreKeyRecord record, + ) async { + final db = await DatabaseHelper.instance.database; + await db.insert('signal_signed_prekeys', { + 'key_id': signedPreKeyId, + 'record': _toB64(record.serialize()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + @override + Future containsSignedPreKey(int signedPreKeyId) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_signed_prekeys', + where: 'key_id = ?', + whereArgs: [signedPreKeyId], + ); + return res.isNotEmpty; + } + + @override + Future removeSignedPreKey(int signedPreKeyId) async { + final db = await DatabaseHelper.instance.database; + await db.delete( + 'signal_signed_prekeys', + where: 'key_id = ?', + whereArgs: [signedPreKeyId], + ); + } + + @override + Future loadSession(SignalProtocolAddress address) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_sessions', + where: 'address = ?', + whereArgs: [address.getName()], + ); + if (res.isEmpty) { + return SessionRecord(); + } + return SessionRecord.fromSerialized( + _fromB64(res.first['record'] as String), + ); + } + + @override + Future> getSubDeviceSessions(String name) async { + return [1]; + } + + @override + Future storeSession( + SignalProtocolAddress address, + SessionRecord record, + ) async { + final db = await DatabaseHelper.instance.database; + await db.insert('signal_sessions', { + 'address': address.getName(), + 'record': _toB64(record.serialize()), + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + @override + Future containsSession(SignalProtocolAddress address) async { + final db = await DatabaseHelper.instance.database; + final res = await db.query( + 'signal_sessions', + where: 'address = ?', + whereArgs: [address.getName()], + ); + return res.isNotEmpty; + } + + @override + Future deleteSession(SignalProtocolAddress address) async { + final db = await DatabaseHelper.instance.database; + await db.delete( + 'signal_sessions', + where: 'address = ?', + whereArgs: [address.getName()], + ); + } + + @override + Future deleteAllSessions(String name) async { + final db = await DatabaseHelper.instance.database; + await db.delete('signal_sessions', where: 'address = ?', whereArgs: [name]); + } +} diff --git a/mobile/lib/services/ws_service.dart b/mobile/lib/services/ws_service.dart index 2b5f3fa..306cb4d 100644 --- a/mobile/lib/services/ws_service.dart +++ b/mobile/lib/services/ws_service.dart @@ -75,7 +75,6 @@ class WebSocketService { required String groupId, required String content, String? replyToMessageId, - required String senderId, }) { emit({ "type": "chat", @@ -83,7 +82,6 @@ class WebSocketService { "group_id": groupId, "content": content, "reply_to_message_id": replyToMessageId, - "sender_id": senderId, }); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 13ece19..7db0146 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" app_links: dependency: "direct main" description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" crypto: dependency: transitive description: @@ -157,10 +173,18 @@ packages: dependency: transitive description: name: dio_web_adapter - sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 + sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" fake_async: dependency: transitive description: @@ -258,18 +282,18 @@ packages: dependency: transitive description: name: flutter_secure_storage_linux - sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + sha256: "76fa9c841b3b1619fc5b5bc36efc7d158fa2356f223b6caeb1d0c80a54168546" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + sha256: "788060052712555182aba55ecb5f8b6e5cb9cfe8f776c83249a61fe3ce877db4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_secure_storage_web: dependency: transitive description: @@ -316,10 +340,10 @@ packages: dependency: transitive description: name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + sha256: "03a564c3704524ee0f7fc56fc621e8796cc2eb8c24d1f1a33b34979815b285c4" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" http_parser: dependency: transitive description: @@ -348,18 +372,26 @@ packages: dependency: transitive description: name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.3" jni_flutter: dependency: transitive description: name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" json_annotation: dependency: transitive description: @@ -400,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + libsignal_protocol_dart: + dependency: "direct main" + description: + name: libsignal_protocol_dart + sha256: "3ed4455f09c7299934a6b9ca0028daff87940d446142056e1ec25997de0e41bf" + url: "https://pub.dev" + source: hosted + version: "0.8.2" lints: dependency: transitive description: @@ -428,10 +468,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -444,10 +484,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: transitive description: @@ -476,18 +516,26 @@ packages: dependency: transitive description: name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e + url: "https://pub.dev" + source: hosted + version: "9.5.0" + optional: + dependency: transitive + description: + name: optional + sha256: f80327d7a3335a0be68418072668043c7ab291df575c21aa42e0c5633641da39 url: "https://pub.dev" source: hosted - version: "9.4.1" + version: "6.1.0+1" package_config: dependency: transitive description: name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.0" path: dependency: "direct main" description: @@ -568,14 +616,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" posix: dependency: transitive description: name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e + url: "https://pub.dev" + source: hosted + version: "6.5.2" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.0.0" provider: dependency: "direct main" description: @@ -612,10 +676,10 @@ packages: dependency: transitive description: name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + sha256: "3b4ab682aff40175afca6ff090cc5aa7ce9dafe8dfbae1537a6c678e6678baee" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "1.1.0" scrollable_positioned_list: dependency: "direct main" description: @@ -705,10 +769,10 @@ packages: dependency: "direct main" description: name: sqflite_sqlcipher - sha256: ba7733c5514cf0ccb0331997b771a890f73678bcd84cdfb5f7487a88a71f1738 + sha256: "50d05fe0495ed96a85503d9850a49c0936502f44fdba96f403c97e4c9c7043e7" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" stack_trace: dependency: transitive description: @@ -737,10 +801,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.4.1+1" term_glyph: dependency: transitive description: @@ -753,10 +817,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" typed_data: dependency: transitive description: @@ -841,10 +905,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" vm_service: dependency: transitive description: @@ -881,10 +945,18 @@ packages: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d + url: "https://pub.dev" + source: hosted + version: "6.4.0" + x25519: + dependency: transitive + description: + name: x25519 + sha256: cec3c125f0d934dccba6c4cab48f3fbf866dc78895dcc5a1584d35b0a845005b url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "0.1.1" xdg_directories: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 8c12d16..d7524da 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: shared_preferences: ^2.5.5 scrollable_positioned_list: ^0.3.8 path: ^1.9.1 + libsignal_protocol_dart: ^0.8.2 launcher_name: default: "Elephant"