From e8aba933d0a9399872644c0ac7f3d25d388b92ec Mon Sep 17 00:00:00 2001 From: Bartosz Date: Fri, 20 Dec 2024 09:13:05 +0000 Subject: [PATCH] Cryptographic Keys and History changes (#8) Co-authored-by: stcb <21@stcb.cc> Reviewed-on: https://git.gmoker.com/icing/G-EIP-700-TLS-7-1-eip-stephane.corbiere/pulls/8 Co-authored-by: Bartosz Co-committed-by: Bartosz --- dialer/android/.gitignore | 1 + dialer/android/gradle.properties | 2 - dialer/lib/features/history/history_page.dart | 381 +++++++++++++++--- dialer/lib/features/home/home_page.dart | 155 ++++--- .../settings/key/delete_key_pair.dart | 9 +- .../settings/key/export_private_key.dart | 88 ++-- .../settings/key/generate_new_key_pair.dart | 21 +- .../features/settings/key/key_storage.dart | 28 ++ .../settings/key/manage_keys_page.dart | 15 +- .../settings/key/show_public_key_qr.dart | 42 +- .../settings/key/show_public_key_text.dart | 46 ++- dialer/lib/features/settings/settings.dart | 2 +- dialer/pubspec.yaml | 5 +- 13 files changed, 602 insertions(+), 193 deletions(-) create mode 100644 dialer/lib/features/settings/key/key_storage.dart diff --git a/dialer/android/.gitignore b/dialer/android/.gitignore index 55afd91..e409267 100644 --- a/dialer/android/.gitignore +++ b/dialer/android/.gitignore @@ -5,6 +5,7 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +gradle.properties # Remember to never publicly share your keystore. # See https://flutter.dev/to/reference-keystore diff --git a/dialer/android/gradle.properties b/dialer/android/gradle.properties index 902cbff..8371d42 100644 --- a/dialer/android/gradle.properties +++ b/dialer/android/gradle.properties @@ -2,5 +2,3 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro android.useAndroidX=true android.enableJetifier=true dev.steenbakker.mobile_scanner.useUnbundled=true -org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 -#org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-17.0.13.0.11-3.fc41.x86_64 diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/features/history/history_page.dart index 4c38d15..589a136 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/features/history/history_page.dart @@ -1,14 +1,15 @@ -// history_page.dart - import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; -import 'package:intl/intl.dart'; // For date formatting +import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/features/contacts/contact_state.dart'; +import 'package:dialer/widgets/username_color_generator.dart'; +import 'package:dialer/widgets/color_darkener.dart'; class History { final Contact contact; final DateTime date; - final String callType; // 'incoming' or 'outgoing' + final String callType; // 'incoming' or 'outgoing' final String callStatus; // 'missed' or 'answered' final int attempts; @@ -28,9 +29,10 @@ class HistoryPage extends StatefulWidget { _HistoryPageState createState() => _HistoryPageState(); } -class _HistoryPageState extends State { +class _HistoryPageState extends State with SingleTickerProviderStateMixin { List histories = []; bool loading = true; + int? _expandedIndex; @override void didChangeDependencies() { @@ -51,7 +53,6 @@ class _HistoryPageState extends State { } List contacts = contactState.contacts; - // Ensure there are enough contacts if (contacts.isEmpty) { setState(() { loading = false; @@ -59,7 +60,6 @@ class _HistoryPageState extends State { return; } - // Build histories using the contacts setState(() { histories = List.generate( contacts.length >= 10 ? 10 : contacts.length, @@ -75,6 +75,47 @@ class _HistoryPageState extends State { }); } + List _buildGroupedList(List historyList) { + // Sort histories by date (most recent first) + historyList.sort((a, b) => b.date.compareTo(a.date)); + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = today.subtract(const Duration(days: 1)); + + List todayHistories = []; + List yesterdayHistories = []; + List olderHistories = []; + + for (var history in historyList) { + final callDate = DateTime(history.date.year, history.date.month, history.date.day); + if (callDate == today) { + todayHistories.add(history); + } else if (callDate == yesterday) { + yesterdayHistories.add(history); + } else { + olderHistories.add(history); + } + } + + // Combine them with headers + final items = []; + if (todayHistories.isNotEmpty) { + items.add('Today'); + items.addAll(todayHistories); + } + if (yesterdayHistories.isNotEmpty) { + items.add('Yesterday'); + items.addAll(yesterdayHistories); + } + if (olderHistories.isNotEmpty) { + items.add('Older'); + items.addAll(olderHistories); + } + + return items; + } + @override Widget build(BuildContext context) { final contactState = ContactState.of(context); @@ -82,9 +123,6 @@ class _HistoryPageState extends State { if (loading || contactState.loading) { return Scaffold( backgroundColor: Colors.black, - appBar: AppBar( - title: const Text('History'), - ), body: const Center( child: CircularProgressIndicator(), ), @@ -94,9 +132,6 @@ class _HistoryPageState extends State { if (histories.isEmpty) { return Scaffold( backgroundColor: Colors.black, - appBar: AppBar( - title: const Text('History'), - ), body: const Center( child: Text( 'No call history available.', @@ -106,46 +141,298 @@ class _HistoryPageState extends State { ); } + // Filter missed calls + List missedCalls = histories.where((h) => h.callStatus == 'missed').toList(); + + final allItems = _buildGroupedList(histories); + final missedItems = _buildGroupedList(missedCalls); + + return DefaultTabController( + length: 2, + child: Scaffold( + backgroundColor: Colors.black, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Container( + color: Colors.black, + child: const TabBar( + tabs: [ + Tab(text: 'All Calls'), + Tab(text: 'Missed Calls'), + ], + indicatorColor: Colors.white, + ), + ), + ), + body: TabBarView( + children: [ + // All Calls + _buildListView(allItems), + // Missed Calls + _buildListView(missedItems), + ], + ), + ), + ); + } + + Widget _buildListView(List items) { + return ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + + if (item is String) { + // This is a header item + return Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + color: Colors.grey[900], + child: Text( + item, + style: const TextStyle( + color: Colors.white70, + fontWeight: FontWeight.bold, + ), + ), + ); + } else if (item is History) { + final history = item; + final contact = history.contact; + final isExpanded = _expandedIndex == index; + + // Generate the avatar color + Color avatarColor = generateColorFromName(contact.displayName); + + return Column( + children: [ + ListTile( + leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.thumbnail!), + ) + : CircleAvatar( + backgroundColor: avatarColor, + child: Text( + contact.displayName.isNotEmpty + ? contact.displayName[0].toUpperCase() + : '?', + style: TextStyle(color: darken(avatarColor, 0.4)), + ), + ), + title: Text( + contact.displayName, + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + '${history.callType} - ${history.callStatus} - ${DateFormat('MMM dd, hh:mm a').format(history.date)}', + style: const TextStyle(color: Colors.grey), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${history.attempts}x', + style: const TextStyle(color: Colors.white), + ), + IconButton( + icon: const Icon(Icons.phone, color: Colors.green), + onPressed: () async { + if (contact.phones.isNotEmpty) { + final Uri callUri = + Uri(scheme: 'tel', path: contact.phones.first.number); + if (await canLaunchUrl(callUri)) { + await launchUrl(callUri); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not launch call')), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Contact has no phone number')), + ); + } + }, + ), + ], + ), + onTap: () { + setState(() { + _expandedIndex = isExpanded ? null : index; + }); + }, + ), + if (isExpanded) + Container( + color: Colors.grey[850], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton.icon( + onPressed: () async { + if (history.contact.phones.isNotEmpty) { + final Uri smsUri = + Uri(scheme: 'sms', path: history.contact.phones.first.number); + if (await canLaunchUrl(smsUri)) { + await launchUrl(smsUri); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not send message')), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Contact has no phone number')), + ); + } + }, + icon: const Icon(Icons.message, color: Colors.white), + label: const Text('Message', style: TextStyle(color: Colors.white)), + ), + TextButton.icon( + onPressed: () { + // Navigate to Call Details page + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CallDetailsPage(history: history), + ), + ); + }, + icon: const Icon(Icons.info, color: Colors.white), + label: const Text('Details', style: TextStyle(color: Colors.white)), + ), + TextButton.icon( + onPressed: () { + // Implement block number functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Number blocked (functionality not implemented)'), + ), + ); + }, + icon: const Icon(Icons.block, color: Colors.white), + label: const Text('Block', style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ], + ); + } + + return const SizedBox.shrink(); + }, + ); + } +} + +class CallDetailsPage extends StatelessWidget { + final History history; + + const CallDetailsPage({Key? key, required this.history}) : super(key: key); + + @override + Widget build(BuildContext context) { + final contact = history.contact; + return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - title: const Text('History'), + title: const Text('Call Details'), + backgroundColor: Colors.black, ), - body: ListView.builder( - itemCount: histories.length, - itemBuilder: (context, index) { - final history = histories[index]; - final contact = history.contact; + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Display Contact Name and Thumbnail + Row( + children: [ + (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) + ? CircleAvatar( + backgroundImage: MemoryImage(contact.thumbnail!), + radius: 30, + ) + : CircleAvatar( + backgroundColor: Colors.grey[700], + radius: 30, + child: Text( + contact.displayName.isNotEmpty + ? contact.displayName[0].toUpperCase() + : '?', + style: const TextStyle(color: Colors.white), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + contact.displayName, + style: const TextStyle(color: Colors.white, fontSize: 24), + ), + ), + ], + ), + const SizedBox(height: 24), - return ListTile( - leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) - ? CircleAvatar( - backgroundImage: MemoryImage(contact.thumbnail!), - ) - : CircleAvatar( - child: Text( - contact.displayName.isNotEmpty - ? contact.displayName[0] - : '?', + // Display call type, status, date, attempts + DetailRow( + label: 'Call Type:', + value: history.callType, + ), + DetailRow( + label: 'Call Status:', + value: history.callStatus, + ), + DetailRow( + label: 'Date:', + value: DateFormat('MMM dd, yyyy - hh:mm a').format(history.date), + ), + DetailRow( + label: 'Attempts:', + value: '${history.attempts}', + ), + + const SizedBox(height: 24), + + // If you have more details like duration, contact number, etc. + if (contact.phones.isNotEmpty) + DetailRow( + label: 'Number:', + value: contact.phones.first.number, ), - ), - title: Text( - contact.displayName, - style: const TextStyle(color: Colors.white), - ), - subtitle: Text( - '${history.callType} - ${history.callStatus} - ${DateFormat('MMM dd, hh:mm a').format(history.date)}', - style: const TextStyle(color: Colors.grey), - ), - trailing: Text( - '${history.attempts}x', - style: const TextStyle(color: Colors.white), - ), - onTap: () { - // Handle tap event if needed - }, - ); - }, + ], + ), + ), + ); + } +} + +class DetailRow extends StatelessWidget { + final String label; + final String value; + + const DetailRow({Key? key, required this.label, required this.value}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Text( + label, + style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.right, + ), + ), + ], ), ); } diff --git a/dialer/lib/features/home/home_page.dart b/dialer/lib/features/home/home_page.dart index 99328cb..dbfc84f 100644 --- a/dialer/lib/features/home/home_page.dart +++ b/dialer/lib/features/home/home_page.dart @@ -18,7 +18,8 @@ class _MyHomePageState extends State @override void initState() { super.initState(); - _tabController = TabController(length: 4, vsync: this, initialIndex: 1); + // Set the TabController length to 3 + _tabController = TabController(length: 3, vsync: this, initialIndex: 1); _tabController.addListener(_handleTabIndex); _fetchContacts(); } @@ -69,68 +70,97 @@ class _MyHomePageState extends State left: 16.0, right: 16.0, ), - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 30, 30, 30), - borderRadius: BorderRadius.circular(12.0), - border: Border( - top: BorderSide(color: Colors.grey.shade800, width: 1), - left: BorderSide(color: Colors.grey.shade800, width: 1), - right: BorderSide(color: Colors.grey.shade800, width: 1), - bottom: BorderSide(color: Colors.grey.shade800, width: 2), - ), - ), - child: SearchAnchor( - builder: (BuildContext context, SearchController controller) { - return SearchBar( - controller: controller, - padding: WidgetStateProperty.all( - const EdgeInsets.only( - top: 6.0, - bottom: 6.0, - left: 16.0, - right: 16.0, + child: Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 30, 30, 30), + borderRadius: BorderRadius.circular(12.0), + border: Border( + top: BorderSide(color: Colors.grey.shade800, width: 1), + left: BorderSide(color: Colors.grey.shade800, width: 1), + right: BorderSide(color: Colors.grey.shade800, width: 1), + bottom: + BorderSide(color: Colors.grey.shade800, width: 2), ), ), - onTap: () { - controller.openView(); - _onSearchChanged(''); - }, - backgroundColor: WidgetStateProperty.all( - const Color.fromARGB(255, 30, 30, 30)), - hintText: 'Search contacts', - hintStyle: WidgetStateProperty.all( - const TextStyle(color: Colors.grey, fontSize: 16.0), - ), - leading: const Icon( - Icons.search, - color: Colors.grey, - size: 24.0, - ), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - ), - ); - }, - viewOnChanged: (query) { - _onSearchChanged(query); - }, - suggestionsBuilder: - (BuildContext context, SearchController controller) { - return _contactSuggestions.map((contact) { - return ListTile( - key: ValueKey(contact.id), - title: Text(contact.displayName, - style: const TextStyle(color: Colors.white)), - onTap: () { - controller.closeView(contact.displayName); + child: SearchAnchor( + builder: + (BuildContext context, SearchController controller) { + return SearchBar( + controller: controller, + padding: + MaterialStateProperty.all( + const EdgeInsets.only( + top: 6.0, + bottom: 6.0, + left: 16.0, + right: 16.0, + ), + ), + onTap: () { + controller.openView(); + _onSearchChanged(''); + }, + backgroundColor: MaterialStateProperty.all( + const Color.fromARGB(255, 30, 30, 30)), + hintText: 'Search contacts', + hintStyle: MaterialStateProperty.all( + const TextStyle(color: Colors.grey, fontSize: 16.0), + ), + leading: const Icon( + Icons.search, + color: Colors.grey, + size: 24.0, + ), + shape: + MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + ); }, - ); - }).toList(); - }, - ), + viewOnChanged: (query) { + _onSearchChanged(query); + }, + suggestionsBuilder: + (BuildContext context, SearchController controller) { + return _contactSuggestions.map((contact) { + return ListTile( + key: ValueKey(contact.id), + title: Text(contact.displayName, + style: const TextStyle(color: Colors.white)), + onTap: () { + controller.closeView(contact.displayName); + }, + ); + }).toList(); + }, + ), + ), + ), + // 3-dot menu + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + itemBuilder: (BuildContext context) => [ + const PopupMenuItem( + value: 'settings', + child: Text('Settings'), + ), + ], + onSelected: (String value) { + if (value == 'settings') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsPage()), + ); + } + }, + ), + ], ), ), // Main content with TabBarView @@ -143,7 +173,6 @@ class _MyHomePageState extends State FavoritesPage(), HistoryPage(), ContactPage(), - SettingsPage(), // Add your SettingsPage here ], ), Positioned( @@ -187,10 +216,6 @@ class _MyHomePageState extends State icon: Icon(_tabController.index == 2 ? Icons.contacts : Icons.contacts_outlined)), - Tab( - icon: Icon(_tabController.index == 3 // Corrected index - ? Icons.settings - : Icons.settings_outlined)), ], labelColor: Colors.white, unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158), diff --git a/dialer/lib/features/settings/key/delete_key_pair.dart b/dialer/lib/features/settings/key/delete_key_pair.dart index 8d21198..373669c 100644 --- a/dialer/lib/features/settings/key/delete_key_pair.dart +++ b/dialer/lib/features/settings/key/delete_key_pair.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; +import 'key_storage.dart'; class DeleteKeyPairPage extends StatelessWidget { const DeleteKeyPairPage({super.key}); - void _deleteKeyPair(BuildContext context) { - // Key deletion logic (not implemented here) - // ... + Future _deleteKeyPair(BuildContext context) async { + final keyStorage = KeyStorage(); + await keyStorage.deleteKeys(); - // Show confirmation message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('The key pair has been deleted.'), ), ); - // Navigate back or update the UI as needed Navigator.pop(context); } diff --git a/dialer/lib/features/settings/key/export_private_key.dart b/dialer/lib/features/settings/key/export_private_key.dart index 694d757..ce96857 100644 --- a/dialer/lib/features/settings/key/export_private_key.dart +++ b/dialer/lib/features/settings/key/export_private_key.dart @@ -3,6 +3,8 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:pointycastle/export.dart' as crypto; import 'package:file_picker/file_picker.dart'; +import 'dart:io'; +import 'key_storage.dart'; class ExportPrivateKeyPage extends StatefulWidget { const ExportPrivateKeyPage({super.key}); @@ -15,55 +17,81 @@ class _ExportPrivateKeyPageState extends State { final TextEditingController _passwordController = TextEditingController(); Future _exportPrivateKey() async { - // Replace with your actual private key retrieval logic - final String privateKeyPem = 'Your private key here'; + final keyStorage = KeyStorage(); + final privateKeyPem = await keyStorage.getPrivateKey(); - // Get the password from the user input - final password = _passwordController.text; - if (password.isEmpty) { - // Show error message + if (privateKeyPem == null) { + // Show error message if there's no key + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No private key found to export.'), + ), + ); + return; + } + + final password = _passwordController.text; + if (password.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please enter a password.'), + ), + ); return; } - // Encrypt the private key using AES-256 final encryptedData = _encryptPrivateKey(privateKeyPem, password); - // Let the user pick a file location final outputFile = await FilePicker.platform.saveFile( dialogTitle: 'Save encrypted private key', fileName: 'private_key_encrypted.aes', ); if (outputFile != null) { - // Write the encrypted data to the file - // Use appropriate file I/O methods (not shown here) - // ... - // Show a confirmation dialog or message - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Key Exported'), - content: const Text('The encrypted private key has been exported successfully.'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('OK'), - ), - ], - ), - ); + try { + final file = File(outputFile); + await file.writeAsBytes(encryptedData); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Key Exported'), + content: const Text('The encrypted private key has been exported successfully.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to write file: $e'), + ), + ); + } } } Uint8List _encryptPrivateKey(String privateKey, String password) { - // Encryption logic using AES-256 - final key = crypto.PBKDF2KeyDerivator(crypto.HMac(crypto.SHA256Digest(), 64)) - .process(Uint8List.fromList(utf8.encode(password))); + // Derive a key from the password using PBKDF2 + final derivator = crypto.PBKDF2KeyDerivator( + crypto.HMac(crypto.SHA256Digest(), 64), + ); - final params = crypto.PaddedBlockCipherParameters( - crypto.ParametersWithIV(crypto.KeyParameter(key), Uint8List(16)), // Initialization Vector + final salt = Uint8List.fromList(utf8.encode('some_salt')); // In production, use a random salt and store it securely + derivator.init(crypto.Pbkdf2Parameters(salt, 1000, 32)); + final key = derivator.process(Uint8List.fromList(utf8.encode(password))); + + // Initialize AES-CBC cipher with PKCS7 padding + final iv = Uint8List(16); // zero IV for example, in production use random IV and store it + final params = crypto.PaddedBlockCipherParameters, Null>( + crypto.ParametersWithIV(crypto.KeyParameter(key), iv), null, ); + final cipher = crypto.PaddedBlockCipher('AES/CBC/PKCS7'); cipher.init(true, params); diff --git a/dialer/lib/features/settings/key/generate_new_key_pair.dart b/dialer/lib/features/settings/key/generate_new_key_pair.dart index 9254e45..40f9e18 100644 --- a/dialer/lib/features/settings/key/generate_new_key_pair.dart +++ b/dialer/lib/features/settings/key/generate_new_key_pair.dart @@ -4,12 +4,12 @@ import 'dart:math'; import 'dart:convert'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; +import 'key_storage.dart'; class GenerateNewKeyPairPage extends StatelessWidget { const GenerateNewKeyPairPage({super.key}); Future> _generateKeyPair() async { - // Key generation logic using pointycastle final keyParams = crypto.RSAKeyGeneratorParameters( BigInt.parse('65537'), 2048, @@ -17,8 +17,6 @@ class GenerateNewKeyPairPage extends StatelessWidget { ); final secureRandom = crypto.FortunaRandom(); - - // Seed the random number generator final random = Random.secure(); final seeds = List.generate(32, (_) => random.nextInt(256)); secureRandom.seed(crypto.KeyParameter(Uint8List.fromList(seeds))); @@ -31,11 +29,12 @@ class GenerateNewKeyPairPage extends StatelessWidget { final publicKey = pair.publicKey as crypto.RSAPublicKey; final privateKey = pair.privateKey as crypto.RSAPrivateKey; - // Convert keys to PEM format final publicKeyPem = _encodePublicKeyToPemPKCS1(publicKey); final privateKeyPem = _encodePrivateKeyToPemPKCS1(privateKey); - // Save keys securely (not implemented here) + // Save keys securely + final keyStorage = KeyStorage(); + await keyStorage.saveKeys(publicKey: publicKeyPem, privateKey: privateKeyPem); return {'publicKey': publicKeyPem, 'privateKey': privateKeyPem}; } @@ -52,7 +51,8 @@ class GenerateNewKeyPairPage extends StatelessWidget { Uint8List _encodePublicKeyToDer(crypto.RSAPublicKey publicKey) { final algorithmSeq = ASN1Sequence(); - algorithmSeq.add(ASN1ObjectIdentifier.fromName('rsaEncryption')); + // Create the OID directly with the arcs + algorithmSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1])); algorithmSeq.add(ASN1Null()); final publicKeySeq = ASN1Sequence(); @@ -84,8 +84,8 @@ class GenerateNewKeyPairPage extends StatelessWidget { } String _formatPem(Uint8List bytes, String label) { - final base64 = base64Encode(bytes); - final chunks = RegExp('.{1,64}').allMatches(base64).map((m) => m.group(0)!); + final base64Data = base64Encode(bytes); + final chunks = RegExp('.{1,64}').allMatches(base64Data).map((m) => m.group(0)!); return '-----BEGIN $label-----\n${chunks.join('\n')}\n-----END $label-----'; } @@ -99,13 +99,12 @@ class GenerateNewKeyPairPage extends StatelessWidget { body: Center( child: ElevatedButton( onPressed: () async { - final keys = await _generateKeyPair(); - // Display a confirmation dialog or message + await _generateKeyPair(); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Keys Generated'), - content: const Text('The new key pair has been generated successfully.'), + content: const Text('The new key pair has been generated and stored securely.'), actions: [ TextButton( onPressed: () => Navigator.pop(context), diff --git a/dialer/lib/features/settings/key/key_storage.dart b/dialer/lib/features/settings/key/key_storage.dart new file mode 100644 index 0000000..a258112 --- /dev/null +++ b/dialer/lib/features/settings/key/key_storage.dart @@ -0,0 +1,28 @@ +// key_storage.dart + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class KeyStorage { + static const _publicKeyKey = 'public_key'; + static const _privateKeyKey = 'private_key'; + + final FlutterSecureStorage _storage = const FlutterSecureStorage(); + + Future saveKeys({required String publicKey, required String privateKey}) async { + await _storage.write(key: _publicKeyKey, value: publicKey); + await _storage.write(key: _privateKeyKey, value: privateKey); + } + + Future getPublicKey() async { + return await _storage.read(key: _publicKeyKey); + } + + Future getPrivateKey() async { + return await _storage.read(key: _privateKeyKey); + } + + Future deleteKeys() async { + await _storage.delete(key: _publicKeyKey); + await _storage.delete(key: _privateKeyKey); + } +} diff --git a/dialer/lib/features/settings/key/manage_keys_page.dart b/dialer/lib/features/settings/key/manage_keys_page.dart index a3b7ad1..7e4a65f 100644 --- a/dialer/lib/features/settings/key/manage_keys_page.dart +++ b/dialer/lib/features/settings/key/manage_keys_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:dialer/features/settings/key/show_public_key_qr.dart'; -import 'package:dialer/features/settings/key/show_public_key_text.dart'; -import 'package:dialer/features/settings/key/generate_new_key_pair.dart'; -import 'package:dialer/features/settings/key/export_private_key.dart'; -import 'package:dialer/features/settings/key/delete_key_pair.dart'; +import 'show_public_key_qr.dart'; +import 'show_public_key_text.dart'; +import 'generate_new_key_pair.dart'; +import 'export_private_key.dart'; +import 'delete_key_pair.dart'; class KeyManagementPage extends StatelessWidget { const KeyManagementPage({super.key}); @@ -34,14 +34,13 @@ class KeyManagementPage extends StatelessWidget { MaterialPageRoute(builder: (context) => const ExportPrivateKeyPage()), ); break; - case 'Delete a key pair, warning POPUP': + case 'Delete a key pair': Navigator.push( context, MaterialPageRoute(builder: (context) => const DeleteKeyPairPage()), ); break; default: - // Handle default or unknown options break; } } @@ -53,7 +52,7 @@ class KeyManagementPage extends StatelessWidget { 'Display public key as QR code', 'Generate a new key pair', 'Export private key to password-encrypted file (AES 256)', - 'Delete a key pair, warning POPUP', + 'Delete a key pair', ]; return Scaffold( diff --git a/dialer/lib/features/settings/key/show_public_key_qr.dart b/dialer/lib/features/settings/key/show_public_key_qr.dart index ca00fbb..b3cdf92 100644 --- a/dialer/lib/features/settings/key/show_public_key_qr.dart +++ b/dialer/lib/features/settings/key/show_public_key_qr.dart @@ -1,26 +1,48 @@ import 'package:flutter/material.dart'; import 'package:pretty_qr_code/pretty_qr_code.dart'; +import 'key_storage.dart'; class DisplayPublicKeyQRCodePage extends StatelessWidget { const DisplayPublicKeyQRCodePage({super.key}); + Future _loadPublicKey() async { + final keyStorage = KeyStorage(); + return keyStorage.getPublicKey(); + } + @override Widget build(BuildContext context) { - // Replace with your actual public key retrieval logic - final String publicKey = 'Your public key here'; - return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text('Public Key in QR Code'), ), - body: Center( - child: PrettyQr( - data: publicKey, - size: 250, - roundEdges: true, - elementColor: Colors.white, - ), + body: FutureBuilder( + future: _loadPublicKey(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + final publicKey = snapshot.data; + if (publicKey == null) { + return const Center( + child: Text( + 'No public key found.', + style: TextStyle(color: Colors.white), + ), + ); + } + + return Center( + child: PrettyQr( + data: publicKey, + size: 250, + roundEdges: true, + elementColor: Colors.white, + ), + ); + }, ), ); } diff --git a/dialer/lib/features/settings/key/show_public_key_text.dart b/dialer/lib/features/settings/key/show_public_key_text.dart index 8b99d16..86d8340 100644 --- a/dialer/lib/features/settings/key/show_public_key_text.dart +++ b/dialer/lib/features/settings/key/show_public_key_text.dart @@ -1,27 +1,49 @@ import 'package:flutter/material.dart'; +import 'key_storage.dart'; class DisplayPublicKeyTextPage extends StatelessWidget { const DisplayPublicKeyTextPage({super.key}); + Future _loadPublicKey() async { + final keyStorage = KeyStorage(); + return await keyStorage.getPublicKey(); + } + @override Widget build(BuildContext context) { - // Replace with your actual public key retrieval logic - final String publicKey = 'Your public key here'; - return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text('Public Key as Text'), ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SelectableText( - publicKey, - style: const TextStyle(color: Colors.white), - textAlign: TextAlign.center, - ), - ), + body: FutureBuilder( + future: _loadPublicKey(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + final publicKey = snapshot.data; + if (publicKey == null) { + return const Center( + child: Text( + 'No public key found.', + style: TextStyle(color: Colors.white), + ), + ); + } + + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SelectableText( + publicKey, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), + ), + ); + }, ), ); } diff --git a/dialer/lib/features/settings/settings.dart b/dialer/lib/features/settings/settings.dart index 0081ca3..9735e1b 100644 --- a/dialer/lib/features/settings/settings.dart +++ b/dialer/lib/features/settings/settings.dart @@ -23,7 +23,7 @@ class SettingsPage extends StatelessWidget { MaterialPageRoute(builder: (context) => const SettingsAccountsPage()), ); break; - case 'Gestion de clés': + case 'Key management': Navigator.push( context, MaterialPageRoute(builder: (context) => const KeyManagementPage()), diff --git a/dialer/pubspec.yaml b/dialer/pubspec.yaml index 3e0ec90..6e36f8c 100644 --- a/dialer/pubspec.yaml +++ b/dialer/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - url_launcher: ^6.1.9 # To manage system dialer. Call, message... shared_preferences: ^2.3.3 # Local storage (no critical data) cupertino_icons: ^1.0.8 flutter_contacts: ^1.1.9+2 @@ -46,9 +45,11 @@ dependencies: mobile_scanner: ^6.0.2 pretty_qr_code: ^3.3.0 pointycastle: ^3.4.0 - file_picker: ^5.2.5 + file_picker: ^8.1.6 asn1lib: ^1.0.0 intl_utils: ^2.0.7 + url_launcher: ^6.3.1 + flutter_secure_storage: ^9.0.0 mobile_number: path: packages/mobile_number