New generation / architecture / settings buttons
This commit is contained in:
parent
a3532f4d36
commit
97ce022436
dialer
android/app/src/main/kotlin/com/icingDialer
lib
features/settings
cryptography
delete_key_pair.dartgenerate_new_key_pair.dartload_certificate.dartmanage_keys_page.dartshow_public_key_qr.dartshow_public_key_text.dart
key
delete_key_pair.dartgenerate_new_key_pair.dartload_backup.dartmanage_keys_page.dartshow_public_key_qr.dartshow_public_key_text.dart
settings.dartwidgets
sim
services/cryptography
@ -0,0 +1,153 @@
|
||||
package com.example.yourapp.services.cryptography.platform.android
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.Signature
|
||||
|
||||
class KeystoreHelper(private val call: MethodCall, private val result: MethodChannel.Result) {
|
||||
|
||||
private val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
fun handleMethodCall() {
|
||||
when (call.method) {
|
||||
"generateKeyPair" -> generateECKeyPair()
|
||||
"signData" -> signData()
|
||||
"getPublicKey" -> getPublicKey()
|
||||
"deleteKeyPair" -> deleteKeyPair()
|
||||
"keyPairExists" -> keyPairExists()
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateECKeyPair() {
|
||||
val alias = call.argument<String>("alias")
|
||||
if (alias == null) {
|
||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
if (keyStore.containsAlias(alias)) {
|
||||
result.error("KEY_EXISTS", "Key with alias \"$alias\" already exists.", null)
|
||||
return
|
||||
}
|
||||
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC,
|
||||
ANDROID_KEYSTORE
|
||||
)
|
||||
|
||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||
)
|
||||
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
|
||||
.setUserAuthenticationRequired(false)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(parameterSpec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("KEY_GENERATION_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun signData() {
|
||||
val alias = call.argument<String>("alias")
|
||||
val data = call.argument<String>("data")
|
||||
if (alias == null || data == null) {
|
||||
result.error("INVALID_ARGUMENT", "Alias and data are required", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
val privateKey = keyStore.getKey(alias, null) ?: run {
|
||||
result.error("KEY_NOT_FOUND", "Private key not found for alias \"$alias\".", null)
|
||||
return
|
||||
}
|
||||
|
||||
val signature = Signature.getInstance("SHA256withECDSA")
|
||||
signature.initSign(privateKey)
|
||||
signature.update(data.toByteArray())
|
||||
val signedBytes = signature.sign()
|
||||
|
||||
val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT)
|
||||
result.success(signatureBase64)
|
||||
} catch (e: Exception) {
|
||||
result.error("SIGNING_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPublicKey() {
|
||||
val alias = call.argument<String>("alias")
|
||||
if (alias == null) {
|
||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
val certificate = keyStore.getCertificate(alias) ?: run {
|
||||
result.error("CERTIFICATE_NOT_FOUND", "Certificate not found for alias \"$alias\".", null)
|
||||
return
|
||||
}
|
||||
|
||||
val publicKey = certificate.publicKey
|
||||
val publicKeyBase64 = Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
|
||||
result.success(publicKeyBase64)
|
||||
} catch (e: Exception) {
|
||||
result.error("PUBLIC_KEY_RETRIEVAL_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteKeyPair() {
|
||||
val alias = call.argument<String>("alias")
|
||||
if (alias == null) {
|
||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
if (!keyStore.containsAlias(alias)) {
|
||||
result.error("KEY_NOT_FOUND", "No key found with alias \"$alias\" to delete.", null)
|
||||
return
|
||||
}
|
||||
|
||||
keyStore.deleteEntry(alias)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("KEY_DELETION_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun keyPairExists() {
|
||||
val alias = call.argument<String>("alias")
|
||||
if (alias == null) {
|
||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
val exists = keyStore.containsAlias(alias)
|
||||
result.success(exists)
|
||||
} catch (e: Exception) {
|
||||
result.error("KEY_CHECK_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +1,19 @@
|
||||
package com.icingDialer
|
||||
package com.example.yourapp
|
||||
|
||||
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 android.os.Bundle
|
||||
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
|
||||
import com.icingDialer.KeystoreHelper
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val CHANNEL = "com.icingDialer/keystore"
|
||||
private val CHANNEL = "com.example.keystore"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"generateKeyPair" -> {
|
||||
val alias = call.argument<String>("alias")
|
||||
if (alias != null) {
|
||||
try {
|
||||
KeyGeneratorHelper.generateECKeyPair(alias)
|
||||
result.success(null)
|
||||
} catch (e: Exception) {
|
||||
result.error("KEY_GENERATION_FAILED", e.message, null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||
}
|
||||
}
|
||||
// Handle other methods: signData, getPublicKey, deleteKeyPair
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
// Delegate method calls to KeystoreHelper
|
||||
KeystoreHelper(call, result).handleMethodCall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,64 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
//
|
||||
// class DeleteKeyPairPage extends StatelessWidget {
|
||||
// const DeleteKeyPairPage({super.key});
|
||||
//
|
||||
// Future<void> _deleteKeyPair(BuildContext context) async {
|
||||
// final keyStorage = KeyStorage();
|
||||
// await keyStorage.deleteKeys();
|
||||
//
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(
|
||||
// content: Text('The key pair has been deleted.'),
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// Navigator.pop(context);
|
||||
// }
|
||||
//
|
||||
// void _showConfirmationDialog(BuildContext context) {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
// return AlertDialog(
|
||||
// title: const Text('Confirm Deletion'),
|
||||
// content: const Text(
|
||||
// 'Are you sure you want to delete the cryptography pair? This action is irreversible.'),
|
||||
// actions: [
|
||||
// TextButton(
|
||||
// child: const Text('Cancel'),
|
||||
// onPressed: () {
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// ),
|
||||
// TextButton(
|
||||
// child: const Text('Delete'),
|
||||
// onPressed: () {
|
||||
// Navigator.of(context).pop();
|
||||
// _deleteKeyPair(context);
|
||||
// },
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// backgroundColor: Colors.black,
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Delete a Key Pair'),
|
||||
// ),
|
||||
// body: Center(
|
||||
// child: ElevatedButton(
|
||||
// onPressed: () {
|
||||
// _showConfirmationDialog(context);
|
||||
// },
|
||||
// child: const Text('Delete Key Pair'),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
@ -0,0 +1,40 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
|
||||
// import 'dart:math';
|
||||
//
|
||||
// class GenerateNewKeyPairPage extends StatelessWidget {
|
||||
// const GenerateNewKeyPairPage({super.key});
|
||||
//
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// backgroundColor: Colors.black,
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Generate a New Key Pair'),
|
||||
// ),
|
||||
// body: Center(
|
||||
// child: ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// var keyName = Random().nextInt(10000000).toString();
|
||||
// await AsymmetricCryptoService().generateKeyPair(keyName);
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (context) => AlertDialog(
|
||||
// title: const Text('Keys Generated (${keyName})'),
|
||||
// content: const Text('The new key pair has been generated and stored securely.'),
|
||||
// actions: [
|
||||
// TextButton(
|
||||
// onPressed: () => Navigator.pop(context),
|
||||
// child: const Text('OK'),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// child: const Text('Generate a New Key Pair'),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
249
dialer/lib/features/settings/cryptography/manage_keys_page.dart
Normal file
249
dialer/lib/features/settings/cryptography/manage_keys_page.dart
Normal file
@ -0,0 +1,249 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
|
||||
|
||||
class ManageKeysPage extends StatefulWidget {
|
||||
@override
|
||||
_ManageKeysPageState createState() => _ManageKeysPageState();
|
||||
}
|
||||
|
||||
class _ManageKeysPageState extends State<ManageKeysPage> {
|
||||
List<Map<String, dynamic>> _keys = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchKeys();
|
||||
}
|
||||
|
||||
Future<void> _fetchKeys() async {
|
||||
final AsymmetricCryptoService cryptoService = Provider.of<AsymmetricCryptoService>(context, listen: false);
|
||||
try {
|
||||
final keys = await cryptoService.getAllKeys();
|
||||
setState(() {
|
||||
_keys = keys;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
// Handle error, e.g., show a snackbar
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error fetching keys: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createKey() async {
|
||||
final AsymmetricCryptoService cryptoService = Provider.of<AsymmetricCryptoService>(context, listen: false);
|
||||
String? label = await _showCreateKeyDialog();
|
||||
if (label == null) return; // User cancelled
|
||||
|
||||
try {
|
||||
await cryptoService.generateKeyPair(label: label);
|
||||
await _fetchKeys();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Key "$label" created successfully.')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error creating key: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showCreateKeyDialog() async {
|
||||
String label = '';
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Create New Key'),
|
||||
content: TextField(
|
||||
onChanged: (value) {
|
||||
label = value;
|
||||
},
|
||||
decoration: InputDecoration(hintText: "Enter key label"),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(label.trim().isEmpty ? 'Key ${Uuid().v4()}' : label.trim()),
|
||||
child: Text('Create'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteKey(String alias, String label) async {
|
||||
final AsymmetricCryptoService cryptoService = Provider.of<AsymmetricCryptoService>(context, listen: false);
|
||||
|
||||
bool confirm = await _showDeleteConfirmationDialog(label);
|
||||
if (!confirm) return;
|
||||
|
||||
try {
|
||||
await cryptoService.deleteKeyPair(alias);
|
||||
await _fetchKeys();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Key "$label" deleted successfully.')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error deleting key: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteConfirmationDialog(String label) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Delete Key'),
|
||||
content: Text('Are you sure you want to delete the key "$label"? This action cannot be undone.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<void> _viewPublicKey(String alias, String label) async {
|
||||
final AsymmetricCryptoService cryptoService = Provider.of<AsymmetricCryptoService>(context, listen: false);
|
||||
try {
|
||||
final String publicKey = await cryptoService.getPublicKey(alias);
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Public Key for "$label"'),
|
||||
content: SelectableText(publicKey),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Close'),
|
||||
),
|
||||
// Optionally, add a button to copy the key to clipboard
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error retrieving public key: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Manage Keys'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.add),
|
||||
onPressed: _createKey,
|
||||
tooltip: 'Create New Key',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: _keys.isEmpty
|
||||
? Center(child: Text('No keys found.'))
|
||||
: ListView.builder(
|
||||
itemCount: _keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final key = _keys[index];
|
||||
return ListTile(
|
||||
title: Text(key['label']),
|
||||
subtitle: Text('Created at: ${key['created_at']}'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
onPressed: () => _editKeyLabel(key['alias'], key['label']),
|
||||
tooltip: 'Edit Label',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.visibility),
|
||||
onPressed: () => _viewPublicKey(key['alias'], key['label']),
|
||||
tooltip: 'View Public Key',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () => _deleteKey(key['alias'], key['label']),
|
||||
tooltip: 'Delete Key',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
/// Updates the label of a key.
|
||||
Future<void> _editKeyLabel(String alias, String currentLabel) async {
|
||||
final AsymmetricCryptoService cryptoService = Provider.of<AsymmetricCryptoService>(context, listen: false);
|
||||
String? newLabel = await _showEditLabelDialog(currentLabel);
|
||||
if (newLabel == null) return; // User cancelled
|
||||
|
||||
try {
|
||||
await cryptoService.updateKeyLabel(alias, newLabel);
|
||||
await _fetchKeys();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Key label updated to "$newLabel".')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error updating key label: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showEditLabelDialog(String currentLabel) async {
|
||||
String label = currentLabel;
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Edit Key Label'),
|
||||
content: TextField(
|
||||
onChanged: (value) {
|
||||
label = value;
|
||||
},
|
||||
decoration: InputDecoration(hintText: "Enter new key label"),
|
||||
controller: TextEditingController(text: currentLabel),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(label.trim().isEmpty ? currentLabel : label.trim()),
|
||||
child: Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:pretty_qr_code/pretty_qr_code.dart';
|
||||
// import 'widgets/key_storage.dart';
|
||||
//
|
||||
// class DisplayPublicKeyQRCodePage extends StatelessWidget {
|
||||
// const DisplayPublicKeyQRCodePage({super.key});
|
||||
//
|
||||
// Future<String?> _loadPublicKey() async {
|
||||
// final keyStorage = KeyStorage();
|
||||
// return keyStorage.getPublicKey();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// backgroundColor: Colors.black,
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Public Key in QR Code'),
|
||||
// ),
|
||||
// body: FutureBuilder<String?>(
|
||||
// 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,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
@ -0,0 +1,50 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'widgets/key_storage.dart';
|
||||
//
|
||||
// class DisplayPublicKeyTextPage extends StatelessWidget {
|
||||
// const DisplayPublicKeyTextPage({super.key});
|
||||
//
|
||||
// Future<String?> _loadPublicKey() async {
|
||||
// final keyStorage = KeyStorage();
|
||||
// return await keyStorage.getPublicKey();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// backgroundColor: Colors.black,
|
||||
// appBar: AppBar(
|
||||
// title: const Text('Public Key as Text'),
|
||||
// ),
|
||||
// body: FutureBuilder<String?>(
|
||||
// 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,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
@ -1,65 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'widgets/key_storage.dart';
|
||||
|
||||
class DeleteKeyPairPage extends StatelessWidget {
|
||||
const DeleteKeyPairPage({super.key});
|
||||
|
||||
Future<void> _deleteKeyPair(BuildContext context) async {
|
||||
final keyStorage = KeyStorage();
|
||||
await keyStorage.deleteKeys();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('The key pair has been deleted.'),
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _showConfirmationDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirm Deletion'),
|
||||
content: const Text(
|
||||
'Are you sure you want to delete the key pair? This action is irreversible.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Delete'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_deleteKeyPair(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('Delete a Key Pair'),
|
||||
),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
_showConfirmationDialog(context);
|
||||
},
|
||||
child: const Text('Delete Key Pair'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pointycastle/export.dart' as crypto;
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:asn1lib/asn1lib.dart';
|
||||
import 'widgets/key_storage.dart';
|
||||
|
||||
class GenerateNewKeyPairPage extends StatelessWidget {
|
||||
const GenerateNewKeyPairPage({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('Generate a New Key Pair'),
|
||||
),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await _generateKeyPair();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Keys Generated'),
|
||||
content: const Text('The new key pair has been generated and stored securely.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Generate a New Key Pair'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'show_public_key_qr.dart';
|
||||
import 'show_public_key_text.dart';
|
||||
import 'generate_new_key_pair.dart';
|
||||
import 'delete_key_pair.dart';
|
||||
|
||||
class KeyManagementPage extends StatelessWidget {
|
||||
const KeyManagementPage({super.key});
|
||||
|
||||
void _navigateToOption(BuildContext context, String option) {
|
||||
switch (option) {
|
||||
case 'Display public key as text':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const DisplayPublicKeyTextPage()),
|
||||
);
|
||||
break;
|
||||
case 'Display public key as QR code':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const DisplayPublicKeyQRCodePage()),
|
||||
);
|
||||
break;
|
||||
case 'Generate a new key pair':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const GenerateNewKeyPairPage()),
|
||||
);
|
||||
break;
|
||||
case 'Delete a key pair':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const DeleteKeyPairPage()),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyManagementOptions = [
|
||||
'Display public key as text',
|
||||
'Display public key as QR code',
|
||||
'Generate a new key pair',
|
||||
'Export private key to password-encrypted file (AES 256)',
|
||||
'Delete a key pair',
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('Key Management'),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: keyManagementOptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
keyManagementOptions[index],
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, color: Colors.white),
|
||||
onTap: () {
|
||||
_navigateToOption(context, keyManagementOptions[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pretty_qr_code/pretty_qr_code.dart';
|
||||
import 'widgets/key_storage.dart';
|
||||
|
||||
class DisplayPublicKeyQRCodePage extends StatelessWidget {
|
||||
const DisplayPublicKeyQRCodePage({super.key});
|
||||
|
||||
Future<String?> _loadPublicKey() async {
|
||||
final keyStorage = KeyStorage();
|
||||
return keyStorage.getPublicKey();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('Public Key in QR Code'),
|
||||
),
|
||||
body: FutureBuilder<String?>(
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'widgets/key_storage.dart';
|
||||
|
||||
class DisplayPublicKeyTextPage extends StatelessWidget {
|
||||
const DisplayPublicKeyTextPage({super.key});
|
||||
|
||||
Future<String?> _loadPublicKey() async {
|
||||
final keyStorage = KeyStorage();
|
||||
return await keyStorage.getPublicKey();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('Public Key as Text'),
|
||||
),
|
||||
body: FutureBuilder<String?>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// 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<void> saveKeys({required String publicKey, required String privateKey}) async {
|
||||
await _storage.write(key: _publicKeyKey, value: publicKey);
|
||||
await _storage.write(key: _privateKeyKey, value: privateKey);
|
||||
}
|
||||
|
||||
Future<String?> getPublicKey() async {
|
||||
return await _storage.read(key: _publicKeyKey);
|
||||
}
|
||||
|
||||
Future<String?> getPrivateKey() async {
|
||||
return await _storage.read(key: _privateKeyKey);
|
||||
}
|
||||
|
||||
Future<void> deleteKeys() async {
|
||||
await _storage.delete(key: _publicKeyKey);
|
||||
await _storage.delete(key: _privateKeyKey);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// 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<String> getSymmetricKey() async {
|
||||
final String symmetricKey = await _channel.invokeMethod('getSymmetricKey');
|
||||
return symmetricKey;
|
||||
}
|
||||
|
||||
// Optional: Implement key deletion or other key management methods
|
||||
}
|
@ -3,9 +3,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/features/settings/call/settingsCall.dart';
|
||||
import 'package:dialer/features/settings/sim/settings_accounts.dart';
|
||||
import 'package:dialer/features/settings/key/manage_keys_page.dart';
|
||||
// import 'package:dialer/features/settings/cryptography/';
|
||||
import 'package:dialer/features/settings/blocked/settings_blocked.dart';
|
||||
|
||||
import 'cryptography/manage_keys_page.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@ -23,10 +25,10 @@ class SettingsPage extends StatelessWidget {
|
||||
MaterialPageRoute(builder: (context) => const SettingsAccountsPage()),
|
||||
);
|
||||
break;
|
||||
case 'Key management':
|
||||
case 'Cryptography':
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const KeyManagementPage()),
|
||||
MaterialPageRoute(builder: (context) => ManageKeysPage()),
|
||||
);
|
||||
break;
|
||||
case 'Blocked numbers':
|
||||
|
@ -3,7 +3,7 @@ import 'choose_sim.dart';
|
||||
import 'sim_parameters.dart';
|
||||
|
||||
class SettingsAccountsPage extends StatelessWidget {
|
||||
const SettingsAccountsPage({Key? key}) : super(key: key);
|
||||
const SettingsAccountsPage({super.key});
|
||||
|
||||
void _navigateToAccountOption(BuildContext context, String option) {
|
||||
switch (option) {
|
||||
|
@ -2,11 +2,29 @@ import 'package:dialer/features/home/home_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/features/contacts/contact_state.dart';
|
||||
import 'globals.dart' as globals;
|
||||
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void main() {
|
||||
void main() async {
|
||||
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false');
|
||||
globals.isStealthMode = stealthFlag.toLowerCase() == 'true';
|
||||
runApp(const Dialer());
|
||||
|
||||
// Initialize CryptographyService
|
||||
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
|
||||
// Initialize default key pair
|
||||
await cryptoService.initializeDefaultKeyPair();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
Provider<AsymmetricCryptoService>(
|
||||
create: (_) => cryptoService,
|
||||
),
|
||||
// Add other providers here
|
||||
],
|
||||
child: Dialer(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class Dialer extends StatelessWidget {
|
||||
|
178
dialer/lib/services/cryptography/asymmetric_crypto_service.dart
Normal file
178
dialer/lib/services/cryptography/asymmetric_crypto_service.dart
Normal file
@ -0,0 +1,178 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AsymmetricCryptoService {
|
||||
static const MethodChannel _channel = MethodChannel('com.example.keystore');
|
||||
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
|
||||
final String _aliasPrefix = 'icing_';
|
||||
final Uuid _uuid = Uuid();
|
||||
|
||||
/// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata.
|
||||
Future<String> generateKeyPair({String? label}) async {
|
||||
try {
|
||||
// Generate a unique identifier for the key
|
||||
final String uuid = _uuid.v4();
|
||||
final String alias = '$_aliasPrefix$uuid';
|
||||
|
||||
// Invoke native method to generate the key pair
|
||||
await _channel.invokeMethod('generateKeyPair', {'alias': alias});
|
||||
|
||||
// Store key metadata securely
|
||||
final Map<String, dynamic> keyMetadata = {
|
||||
'alias': alias,
|
||||
'label': label ?? 'Key $uuid',
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
// Retrieve existing keys
|
||||
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||
List<dynamic> keysList = existingKeys != null ? jsonDecode(existingKeys) : [];
|
||||
|
||||
// Add the new key
|
||||
keysList.add(keyMetadata);
|
||||
|
||||
// Save updated keys list
|
||||
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
|
||||
|
||||
return alias;
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception("Failed to generate key pair: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Signs data using the specified key alias.
|
||||
Future<String> signData(String alias, String data) async {
|
||||
try {
|
||||
final String signature = await _channel.invokeMethod('signData', {
|
||||
'alias': alias,
|
||||
'data': data,
|
||||
});
|
||||
return signature;
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception("Failed to sign data: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the public key for the specified alias.
|
||||
Future<String> getPublicKey(String alias) async {
|
||||
try {
|
||||
final String publicKey = await _channel.invokeMethod('getPublicKey', {
|
||||
'alias': alias,
|
||||
});
|
||||
return publicKey;
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception("Failed to retrieve public key: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the key pair associated with the specified alias and removes its metadata.
|
||||
Future<void> deleteKeyPair(String alias) async {
|
||||
try {
|
||||
await _channel.invokeMethod('deleteKeyPair', {'alias': alias});
|
||||
|
||||
// Retrieve existing keys
|
||||
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||
if (existingKeys != null) {
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
keysList.removeWhere((key) => key['alias'] == alias);
|
||||
|
||||
// Save updated keys list
|
||||
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception("Failed to delete key pair: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves all stored key metadata.
|
||||
Future<List<Map<String, dynamic>>> getAllKeys() async {
|
||||
try {
|
||||
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||
if (existingKeys != null) {
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
return keysList.cast<Map<String, dynamic>>();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
throw Exception("Failed to retrieve keys: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a key pair exists for the given alias.
|
||||
Future<bool> keyPairExists(String alias) async {
|
||||
try {
|
||||
final bool exists = await _channel.invokeMethod('keyPairExists', {'alias': alias});
|
||||
return exists;
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception("Failed to check key pair existence: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the default key pair if it doesn't exist.
|
||||
Future<void> initializeDefaultKeyPair() async {
|
||||
const String defaultAlias = 'icing_default';
|
||||
final List<Map<String, dynamic>> keys = await getAllKeys();
|
||||
|
||||
// Check if default key exists in metadata
|
||||
final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias);
|
||||
|
||||
if (!defaultKeyExists) {
|
||||
// Check if default key exists in Keystore
|
||||
final bool exists = await keyPairExists(defaultAlias);
|
||||
if (!exists) {
|
||||
// Generate default key
|
||||
await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias});
|
||||
|
||||
// Store metadata
|
||||
final Map<String, dynamic> keyMetadata = {
|
||||
'alias': defaultAlias,
|
||||
'label': 'Default Key',
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
keys.add(keyMetadata);
|
||||
await _secureStorage.write(key: 'keys', value: jsonEncode(keys));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the label of a key with the specified alias.
|
||||
///
|
||||
/// [alias]: The unique alias of the key to update.
|
||||
/// [newLabel]: The new label to assign to the key.
|
||||
///
|
||||
/// Throws an exception if the key is not found or the update fails.
|
||||
Future<void> updateKeyLabel(String alias, String newLabel) async {
|
||||
try {
|
||||
// Retrieve existing keys
|
||||
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||
if (existingKeys == null) {
|
||||
throw Exception("No keys found to update.");
|
||||
}
|
||||
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
|
||||
// Find the key with the specified alias
|
||||
bool keyFound = false;
|
||||
for (var key in keysList) {
|
||||
if (key['alias'] == alias) {
|
||||
key['label'] = newLabel;
|
||||
keyFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyFound) {
|
||||
throw Exception("Key with alias \"$alias\" not found.");
|
||||
}
|
||||
|
||||
// Save the updated keys list
|
||||
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
|
||||
} catch (e) {
|
||||
throw Exception("Failed to update key label: $e");
|
||||
}
|
||||
}
|
||||
}
|
@ -55,6 +55,8 @@ dependencies:
|
||||
mobile_number:
|
||||
path: packages/mobile_number
|
||||
encrypt: ^5.0.3
|
||||
uuid: ^4.5.1
|
||||
provider: ^6.1.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user