From 823710d2b3c071292e649c158214a2046acf981f Mon Sep 17 00:00:00 2001 From: stcb <21@stcb.cc> Date: Wed, 15 Jan 2025 16:09:41 +0200 Subject: [PATCH] Refactor + edition of key component --- .../kotlin/com/icingDialer/MainActivity.kt | 115 ++++++++++++++ .../settings/key/delete_key_pair.dart | 2 +- .../settings/key/export_private_key.dart | 2 +- .../settings/key/generate_keypair_new.dart | 93 ++++++++++++ .../settings/key/generate_new_key_pair.dart | 2 +- .../features/settings/key/load_backup.dart | 115 ++++++++++++++ .../settings/key/show_public_key_qr.dart | 2 +- .../settings/key/show_public_key_text.dart | 2 +- .../settings/key/widgets/key_management.dart | 141 ++++++++++++++++++ .../key/{ => widgets}/key_storage.dart | 0 .../key/widgets/keystore_service.dart | 15 ++ .../example/android/app/build.gradle | 2 +- dialer/pubspec.yaml | 3 + 13 files changed, 488 insertions(+), 6 deletions(-) create mode 100644 dialer/android/app/src/main/kotlin/com/icingDialer/MainActivity.kt create mode 100644 dialer/lib/features/settings/key/generate_keypair_new.dart create mode 100644 dialer/lib/features/settings/key/load_backup.dart create mode 100644 dialer/lib/features/settings/key/widgets/key_management.dart rename dialer/lib/features/settings/key/{ => widgets}/key_storage.dart (100%) create mode 100644 dialer/lib/features/settings/key/widgets/keystore_service.dart diff --git a/dialer/android/app/src/main/kotlin/com/icingDialer/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icingDialer/MainActivity.kt new file mode 100644 index 0000000..a8a39f9 --- /dev/null +++ b/dialer/android/app/src/main/kotlin/com/icingDialer/MainActivity.kt @@ -0,0 +1,115 @@ +package com.icingDialer + +import io.flutter.embedding.android.FlutterActivity +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.annotation.NonNull +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.security.KeyStore +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import android.util.Base64 + +class MainActivity: FlutterActivity() { + private val CHANNEL = "com.yourapp/keystore" + + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "getSymmetricKey" -> { + try { + getOrCreateSymmetricKey() + result.success("symmetric_key_alias") + } catch (e: Exception) { + result.error("KEY_ERROR", e.message, null) + } + } + "encryptSeed" -> { + val args = call.arguments as Map + val seed = args["seed"]?.let { Base64.decode(it, Base64.DEFAULT) } + if (seed == null) { + result.error("INVALID_ARGUMENT", "Seed is null", null) + return@setMethodCallHandler + } + try { + val encrypted = encryptSeed(seed) + result.success(Base64.encodeToString(encrypted, Base64.DEFAULT)) + } catch (e: Exception) { + result.error("ENCRYPTION_ERROR", e.message, null) + } + } + "decryptSeed" -> { + val args = call.arguments as Map + val encryptedSeed = args["encryptedSeed"]?.let { Base64.decode(it, Base64.DEFAULT) } + if (encryptedSeed == null) { + result.error("INVALID_ARGUMENT", "Encrypted seed is null", null) + return@setMethodCallHandler + } + try { + val decrypted = decryptSeed(encryptedSeed) + result.success(Base64.encodeToString(decrypted, Base64.DEFAULT)) + } catch (e: Exception) { + result.error("DECRYPTION_ERROR", e.message, null) + } + } + else -> { + result.notImplemented() + } + } + } + } + + private fun getOrCreateSymmetricKey(): SecretKey { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val alias = "symmetric_key_alias" + + // Check if the key already exists + if (!keyStore.containsAlias(alias)) { + // Create the key if it doesn't exist + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + val keyGenParameterSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + keyGenerator.init(keyGenParameterSpec) + return keyGenerator.generateKey() + } + + // Retrieve the existing key + return keyStore.getKey(alias, null) as SecretKey + } + + private fun encryptSeed(seed: ByteArray): ByteArray { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val key = keyStore.getKey("symmetric_key_alias", null) as SecretKey + + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, key) + val encryptionIv = cipher.iv + val encryptedBytes = cipher.doFinal(seed) + + // Prepend IV to encrypted bytes for storage + return encryptionIv + encryptedBytes + } + + private fun decryptSeed(encryptedSeed: ByteArray): ByteArray { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val key = keyStore.getKey("symmetric_key_alias", null) as SecretKey + + // Extract IV and encrypted bytes + val iv = encryptedSeed.copyOfRange(0, 12) // GCM IV is 12 bytes + val ciphertext = encryptedSeed.copyOfRange(12, encryptedSeed.size) + + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, key, javax.crypto.spec.GCMParameterSpec(128, iv)) + return cipher.doFinal(ciphertext) + } +} diff --git a/dialer/lib/features/settings/key/delete_key_pair.dart b/dialer/lib/features/settings/key/delete_key_pair.dart index 373669c..3674e9f 100644 --- a/dialer/lib/features/settings/key/delete_key_pair.dart +++ b/dialer/lib/features/settings/key/delete_key_pair.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'key_storage.dart'; +import 'widgets/key_storage.dart'; class DeleteKeyPairPage extends StatelessWidget { const DeleteKeyPairPage({super.key}); diff --git a/dialer/lib/features/settings/key/export_private_key.dart b/dialer/lib/features/settings/key/export_private_key.dart index ce96857..74fc6d5 100644 --- a/dialer/lib/features/settings/key/export_private_key.dart +++ b/dialer/lib/features/settings/key/export_private_key.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:pointycastle/export.dart' as crypto; import 'package:file_picker/file_picker.dart'; import 'dart:io'; -import 'key_storage.dart'; +import 'widgets/key_storage.dart'; class ExportPrivateKeyPage extends StatefulWidget { const ExportPrivateKeyPage({super.key}); diff --git a/dialer/lib/features/settings/key/generate_keypair_new.dart b/dialer/lib/features/settings/key/generate_keypair_new.dart new file mode 100644 index 0000000..c59b32d --- /dev/null +++ b/dialer/lib/features/settings/key/generate_keypair_new.dart @@ -0,0 +1,93 @@ +// generate_with_recovery_phrase.dart + +import 'package:bip39_mnemonic/bip39_mnemonic.dart'; +import 'package:flutter/material.dart'; +import 'widgets/key_management.dart'; + +class GenerateWithRecoveryPhrasePage extends StatefulWidget { + const GenerateWithRecoveryPhrasePage({super.key}); + + @override + _GenerateWithRecoveryPhrasePageState createState() => _GenerateWithRecoveryPhrasePageState(); +} + +class _GenerateWithRecoveryPhrasePageState extends State { + final KeyManagement _keyManagement = KeyManagement(); + bool _isGenerating = false; + String? _recoveryPhrase; + + Future _generateKeyPair() async { + setState(() { + _isGenerating = true; + }); + + try { + Mnemonic mnemonic = await _keyManagement.generateKeyPairWithRecovery(); + setState(() { + _recoveryPhrase = mnemonic.toString(); + }); + _showRecoveryPhraseDialog(mnemonic.toString()); + } catch (e) { + _showErrorDialog('Failed to generate key pair: $e'); + } finally { + setState(() { + _isGenerating = false; + }); + } + } + + void _showRecoveryPhraseDialog(String phrase) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Recovery Phrase'), + content: SingleChildScrollView( + child: Text( + 'Please write down your recovery phrase and keep it in a safe place. This phrase can be used to restore your key pair.\n\n$phrase', + style: const TextStyle(fontSize: 16), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + void _showErrorDialog(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Generate Key Pair with Recovery'), + ), + body: Center( + child: _isGenerating + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: _generateKeyPair, + child: const Text('Generate Key Pair'), + ), + ), + ); + } +} 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 40f9e18..8c038f5 100644 --- a/dialer/lib/features/settings/key/generate_new_key_pair.dart +++ b/dialer/lib/features/settings/key/generate_new_key_pair.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'dart:convert'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; -import 'key_storage.dart'; +import 'widgets/key_storage.dart'; class GenerateNewKeyPairPage extends StatelessWidget { const GenerateNewKeyPairPage({super.key}); diff --git a/dialer/lib/features/settings/key/load_backup.dart b/dialer/lib/features/settings/key/load_backup.dart new file mode 100644 index 0000000..1208a6c --- /dev/null +++ b/dialer/lib/features/settings/key/load_backup.dart @@ -0,0 +1,115 @@ +// restore_from_recovery_phrase.dart + +import 'package:flutter/material.dart'; +import './widgets/key_management.dart'; + +class RestoreFromRecoveryPhrasePage extends StatefulWidget { + const RestoreFromRecoveryPhrasePage({super.key}); + + @override + _RestoreFromRecoveryPhrasePageState createState() => _RestoreFromRecoveryPhrasePageState(); +} + +class _RestoreFromRecoveryPhrasePageState extends State { + final KeyManagement _keyManagement = KeyManagement(); + final TextEditingController _controller = TextEditingController(); + bool _isRestoring = false; + + Future _restoreKeyPair() async { + String mnemonic = _controller.text.trim(); + + if (mnemonic.isEmpty) { + _showErrorDialog('Please enter your recovery phrase.'); + return; + } + + setState(() { + _isRestoring = true; + }); + + try { + EcdsaKeyPair keyPair = await _keyManagement.restoreKeyPair(mnemonic); + _showSuccessDialog('Key pair restored successfully.'); + } catch (e) { + _showErrorDialog('Failed to restore key pair: $e'); + } finally { + setState(() { + _isRestoring = false; + }); + } + } + + void _showSuccessDialog(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Success'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + void _showErrorDialog(String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Restore Key Pair'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: _isRestoring + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + const Text( + 'Enter your 12-word recovery phrase to restore your key pair.', + style: TextStyle(color: Colors.white), + ), + const SizedBox(height: 20), + TextField( + controller: _controller, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Recovery Phrase', + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _restoreKeyPair, + child: const Text('Restore Key Pair'), + ), + ], + ), + ), + ); + } +} 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 b3cdf92..70bb63c 100644 --- a/dialer/lib/features/settings/key/show_public_key_qr.dart +++ b/dialer/lib/features/settings/key/show_public_key_qr.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:pretty_qr_code/pretty_qr_code.dart'; -import 'key_storage.dart'; +import 'widgets/key_storage.dart'; class DisplayPublicKeyQRCodePage extends StatelessWidget { const DisplayPublicKeyQRCodePage({super.key}); 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 86d8340..09b0261 100644 --- a/dialer/lib/features/settings/key/show_public_key_text.dart +++ b/dialer/lib/features/settings/key/show_public_key_text.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'key_storage.dart'; +import 'widgets/key_storage.dart'; class DisplayPublicKeyTextPage extends StatelessWidget { const DisplayPublicKeyTextPage({super.key}); diff --git a/dialer/lib/features/settings/key/widgets/key_management.dart b/dialer/lib/features/settings/key/widgets/key_management.dart new file mode 100644 index 0000000..0452e74 --- /dev/null +++ b/dialer/lib/features/settings/key/widgets/key_management.dart @@ -0,0 +1,141 @@ +// key_management.dart + +import 'package:cryptography/cryptography.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:encrypt/encrypt.dart' as encrypt; // For symmetric encryption +import 'keystore_service.dart'; + +class KeyManagement { + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); + final String _encryptedSeedKey = 'encrypted_seed'; + final KeystoreService _keystoreService = KeystoreService(); + + // Generate a new key pair with a recovery phrase + Future generateKeyPairWithRecovery() async { + // 1. Generate a 12-word recovery phrase + bip39.Mnemonic mnemonic = bip39.Mnemonic.generate(bip39.Language.english); + + // 2. Derive seed from mnemonic + List seed = mnemonic.seed; + + // 3. Generate ECC key pair from seed + final algorithm = Ecdsa.p256(Sha256()); + final keyPair = await algorithm.newKeyPairFromSeed(seed); + + // 4. Serialize public key for storage or transmission + final publicKey = await keyPair.extractPublicKey(); + String publicKeyBase64 = base64Encode(publicKey); + + // 5. Encrypt the seed using a symmetric key stored in Keystore + Uint8List encryptedSeed = await _encryptSeed(Uint8List.fromList(seed)); + + // 6. Store the encrypted seed securely + await _secureStorage.write( + key: _encryptedSeedKey, + value: base64Encode(encryptedSeed), + ); + + // 7. Return the mnemonic for user to backup + return mnemonic; + } + +// Encrypt the seed using a symmetric key from Keystore + Future _encryptSeed(Uint8List seed) async { + // Retrieve the symmetric key alias + String symmetricKeyAlias = await _keystoreService.getSymmetricKey(); + + // Since the symmetric key is non-extractable, we use a cryptographic library + // that can interface with the Keystore for encryption. + // However, Dart's cryptography package doesn't directly support this. + // Alternative Approach: + // Use the cryptography package's AES-GCM to encrypt the seed with a key derived from the symmetric key. + + // For demonstration, we'll use a placeholder symmetric key. + // In reality, you would need to perform encryption operations on the native side + // where the symmetric key resides. + + // Placeholder: Generate a random key (Not secure) + // Replace this with actual encryption using the Keystore-managed key. + final algorithm = AesGcm.with256bits(); + final secretKey = await algorithm.newSecretKey(); + final nonce = algorithm.newNonce(); + final encrypted = await algorithm.encrypt( + seed, + secretKey: secretKey, + nonce: nonce, + ); + + // Combine nonce and ciphertext for storage + return Uint8List.fromList([...nonce, ...encrypted.cipherText]); + } + + // Decrypt the seed using the symmetric key + Future _decryptSeed() async { + String? encryptedSeedBase64 = await _secureStorage.read(key: _encryptedSeedKey); + if (encryptedSeedBase64 == null) { + throw Exception('No seed found'); + } + + Uint8List encryptedSeed = base64Decode(encryptedSeedBase64); + + // Split nonce and ciphertext + final nonce = encryptedSeed.sublist(0, 12); // AesGcm nonce is typically 12 bytes + final ciphertext = encryptedSeed.sublist(12); + + // Retrieve the symmetric key alias + String symmetricKeyAlias = await _keystoreService.getSymmetricKey(); + + // Perform decryption + // As with encryption, perform decryption on the native side + // where the symmetric key is securely stored. + + // Placeholder: Generate a random key (Not secure) + // Replace this with actual decryption using the Keystore-managed key. + final algorithm = AesGcm.with256bits(); + final secretKey = await algorithm.newSecretKey(); + final decrypted = await algorithm.decrypt( + SecretBox(ciphertext, nonce: nonce, mac: Mac.empty), + secretKey: secretKey, + ); + + return decrypted; + } + + // Restore key pair from recovery phrase + Future restoreKeyPair(String mnemonic) async { + if (!bip39.validateMnemonic(mnemonic)) { + throw Exception('Invalid mnemonic'); + } + + // Derive seed from mnemonic + Uint8List seed = bip39.mnemonicToSeed(mnemonic); + + // Generate key pair from seed + final algorithm = Ecdsa.p256(Sha256()); + final keyPair = await algorithm.newKeyPairFromSeed(seed); + + // Encrypt and store the seed + Uint8List encryptedSeed = await _encryptSeed(seed); + await _secureStorage.write( + key: _encryptedSeedKey, + value: base64Encode(encryptedSeed), + ); + + return keyPair; + } + + // Retrieve public key + Future getPublicKey() async { + // Implement a method to retrieve the public key from stored key pair + // This requires storing the public key during key generation + // For simplicity, assuming it's stored separately + // Example: + // return await _secureStorage.read(key: 'public_key'); + throw UnimplementedError('Public key retrieval not implemented'); + } + +// Additional methods like signing, verifying can be added here +} diff --git a/dialer/lib/features/settings/key/key_storage.dart b/dialer/lib/features/settings/key/widgets/key_storage.dart similarity index 100% rename from dialer/lib/features/settings/key/key_storage.dart rename to dialer/lib/features/settings/key/widgets/key_storage.dart diff --git a/dialer/lib/features/settings/key/widgets/keystore_service.dart b/dialer/lib/features/settings/key/widgets/keystore_service.dart new file mode 100644 index 0000000..21e385a --- /dev/null +++ b/dialer/lib/features/settings/key/widgets/keystore_service.dart @@ -0,0 +1,15 @@ +// keystore_service.dart + +import 'package:flutter/services.dart'; + +class KeystoreService { + static const MethodChannel _channel = MethodChannel('com.icingDialer/keystore'); + + // Generate or retrieve the symmetric key from Keystore + Future getSymmetricKey() async { + final String symmetricKey = await _channel.invokeMethod('getSymmetricKey'); + return symmetricKey; + } + +// Optional: Implement key deletion or other key management methods +} diff --git a/dialer/packages/mobile_number/example/android/app/build.gradle b/dialer/packages/mobile_number/example/android/app/build.gradle index cf077d8..c4ef2e0 100644 --- a/dialer/packages/mobile_number/example/android/app/build.gradle +++ b/dialer/packages/mobile_number/example/android/app/build.gradle @@ -29,7 +29,7 @@ android { defaultConfig { - applicationId "com.example.mobile_number_example" + applicationId "com.icingDialer.mobile_number_example" minSdkVersion 21 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() diff --git a/dialer/pubspec.yaml b/dialer/pubspec.yaml index 87aa442..025d60f 100644 --- a/dialer/pubspec.yaml +++ b/dialer/pubspec.yaml @@ -50,8 +50,11 @@ dependencies: url_launcher: ^6.3.1 flutter_secure_storage: ^9.0.0 audioplayers: ^6.1.0 + cryptography: ^2.0.0 + convert: ^3.0.1 mobile_number: path: packages/mobile_number + encrypt: ^5.0.3 dev_dependencies: flutter_test: