Refactor obfuscate service and avatar widget; move to domain layer and remove unused files
- Moved ObfuscateService and ObfuscatedAvatar from services to domain/services. - Updated references to use globals for stealth mode instead of AppConfig. - Removed old obfuscated_avatar.dart file and updated imports in call and incoming call pages. - Cleaned up imports in composition, contact, and history pages to reflect new structure. - Deleted unused BlockService, CallService, ContactService, AsymmetricCryptoService, and QRScanner classes.
This commit is contained in:
parent
f89c5440fc
commit
612cccc381
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:typed_data';
|
||||
import '../../presentation/features/call/call_page.dart';
|
||||
import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page
|
||||
|
||||
class CallService {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
@ -9,20 +10,7 @@ class CallService {
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// Private constructor
|
||||
CallService._privateConstructor() {
|
||||
_initializeMethodCallHandler();
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
static final CallService _instance = CallService._privateConstructor();
|
||||
|
||||
// Factory constructor to return the same instance
|
||||
factory CallService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void _initializeMethodCallHandler() {
|
||||
CallService() {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
final context = navigatorKey.currentContext;
|
||||
print('CallService: Received method ${call.method} with args ${call.arguments}');
|
||||
@ -33,70 +21,56 @@ class CallService {
|
||||
|
||||
switch (call.method) {
|
||||
case "callAdded":
|
||||
_handleCallAdded(context, call.arguments);
|
||||
final phoneNumber = call.arguments["callId"] as String;
|
||||
final state = call.arguments["state"] as String;
|
||||
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
|
||||
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
|
||||
if (state == "ringing") {
|
||||
_navigateToIncomingCallPage(context);
|
||||
} else {
|
||||
_navigateToCallPage(context);
|
||||
}
|
||||
break;
|
||||
case "callStateChanged":
|
||||
_handleCallStateChanged(context, call.arguments);
|
||||
final state = call.arguments["state"] as String;
|
||||
print('CallService: State changed to $state');
|
||||
if (state == "disconnected" || state == "disconnecting") {
|
||||
_closeCallPage(context);
|
||||
} else if (state == "active" || state == "dialing") {
|
||||
_navigateToCallPage(context);
|
||||
} else if (state == "ringing") {
|
||||
_navigateToIncomingCallPage(context);
|
||||
}
|
||||
break;
|
||||
case "callEnded":
|
||||
case "callRemoved":
|
||||
_handleCallEnded(context);
|
||||
print('CallService: Call ended/removed');
|
||||
_closeCallPage(context);
|
||||
currentPhoneNumber = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleCallAdded(BuildContext context, dynamic arguments) {
|
||||
final phoneNumber = arguments["callId"] as String;
|
||||
final state = arguments["state"] as String;
|
||||
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
|
||||
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
|
||||
|
||||
if (state == "ringing") {
|
||||
_navigateToIncomingCallPage(context);
|
||||
} else {
|
||||
_navigateToCallPage(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleCallStateChanged(BuildContext context, dynamic arguments) {
|
||||
final state = arguments["state"] as String;
|
||||
print('CallService: State changed to $state');
|
||||
|
||||
if (state == "disconnected" || state == "disconnecting") {
|
||||
_closeCallPage(context);
|
||||
} else if (state == "active" || state == "dialing") {
|
||||
_navigateToCallPage(context);
|
||||
} else if (state == "ringing") {
|
||||
_navigateToIncomingCallPage(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleCallEnded(BuildContext context) {
|
||||
print('CallService: Call ended/removed');
|
||||
_closeCallPage(context);
|
||||
currentPhoneNumber = null;
|
||||
}
|
||||
|
||||
void _navigateToCallPage(BuildContext context) {
|
||||
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
|
||||
print('CallService: CallPage already visible, skipping navigation');
|
||||
return;
|
||||
}
|
||||
|
||||
print('CallService: Navigating to CallPage');
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
'/call',
|
||||
arguments: {
|
||||
'displayName': currentPhoneNumber!,
|
||||
'phoneNumber': currentPhoneNumber!,
|
||||
'thumbnail': null,
|
||||
}
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/call'),
|
||||
builder: (context) => CallPage(
|
||||
displayName: currentPhoneNumber!,
|
||||
phoneNumber: currentPhoneNumber!,
|
||||
thumbnail: null,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
_isCallPageVisible = false;
|
||||
});
|
||||
|
||||
_isCallPageVisible = true;
|
||||
}
|
||||
|
||||
@ -105,20 +79,20 @@ class CallService {
|
||||
print('CallService: IncomingCallPage already visible, skipping navigation');
|
||||
return;
|
||||
}
|
||||
|
||||
print('CallService: Navigating to IncomingCallPage');
|
||||
Navigator.pushNamed(
|
||||
Navigator.push(
|
||||
context,
|
||||
'/incoming_call',
|
||||
arguments: {
|
||||
'displayName': currentPhoneNumber!,
|
||||
'phoneNumber': currentPhoneNumber!,
|
||||
'thumbnail': null,
|
||||
}
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/incoming_call'),
|
||||
builder: (context) => IncomingCallPage(
|
||||
displayName: currentPhoneNumber!,
|
||||
phoneNumber: currentPhoneNumber!,
|
||||
thumbnail: null,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
_isCallPageVisible = false;
|
||||
});
|
||||
|
||||
_isCallPageVisible = true;
|
||||
}
|
||||
|
||||
@ -127,7 +101,6 @@ class CallService {
|
||||
print('CallService: CallPage not visible, skipping pop');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Navigator.canPop(context)) {
|
||||
print('CallService: Popping CallPage');
|
||||
Navigator.pop(context);
|
||||
@ -146,7 +119,6 @@ class CallService {
|
||||
print('CallService: Making GSM call to $phoneNumber');
|
||||
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
||||
print('CallService: makeGsmCall result: $result');
|
||||
|
||||
if (result["status"] != "calling") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to initiate call")),
|
||||
@ -166,7 +138,6 @@ class CallService {
|
||||
print('CallService: Hanging up call');
|
||||
final result = await _channel.invokeMethod('hangUpCall');
|
||||
print('CallService: hangUpCall result: $result');
|
||||
|
||||
if (result["status"] != "ended") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to end call")),
|
||||
@ -180,4 +151,4 @@ class CallService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,296 +1,170 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:pointycastle/asymmetric/api.dart';
|
||||
import 'package:asn1lib/asn1lib.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Service for handling asymmetric cryptography operations
|
||||
class AsymmetricCryptoService {
|
||||
static const String _privateKeyTag = 'private_key';
|
||||
static const String _publicKeyTag = 'public_key';
|
||||
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
|
||||
AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>? _currentKeyPair;
|
||||
|
||||
/// Initialize with the default key pair, creating one if it doesn't exist
|
||||
Future<void> initializeDefaultKeyPair() async {
|
||||
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 {
|
||||
// Try to load existing keys
|
||||
final privateKeyStr = await _secureStorage.read(key: _privateKeyTag);
|
||||
final publicKeyStr = await _secureStorage.read(key: _publicKeyTag);
|
||||
// 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 with alias '$alias': ${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});
|
||||
|
||||
if (privateKeyStr != null && publicKeyStr != null) {
|
||||
try {
|
||||
// Parse existing keys
|
||||
final privateKey = _parsePrivateKeyFromPem(privateKeyStr);
|
||||
final publicKey = _parsePublicKeyFromPem(publicKeyStr);
|
||||
|
||||
_currentKeyPair = AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(publicKey, privateKey);
|
||||
debugPrint('Loaded existing key pair successfully');
|
||||
return;
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing stored keys: $e');
|
||||
// Continue to generate new keys
|
||||
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||
if (existingKeys != null) {
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
keysList.removeWhere((key) => key['alias'] == alias);
|
||||
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) {
|
||||
print("No keys found");
|
||||
return [];
|
||||
}
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
return keysList.cast<Map<String, dynamic>>();
|
||||
} 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 the key exists in metadata
|
||||
final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias);
|
||||
|
||||
if (!defaultKeyExists) {
|
||||
await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new key pair
|
||||
await generateAndStoreKeyPair();
|
||||
|
||||
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) {
|
||||
debugPrint('Error initializing key pair: $e');
|
||||
// Instead of rethrowing, we'll continue without encryption capability
|
||||
// This ensures the app doesn't crash during startup
|
||||
throw Exception("Failed to update key label: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a new key pair and store it securely
|
||||
Future<void> generateAndStoreKeyPair() async {
|
||||
try {
|
||||
debugPrint('Generating new RSA key pair...');
|
||||
|
||||
// Generate key pair with a simpler approach
|
||||
final keyPair = await _generateRSAKeyPairSimple(1024); // Smaller keys for faster generation
|
||||
_currentKeyPair = keyPair;
|
||||
|
||||
// Export to PEM format
|
||||
final publicKeyPem = _encodePublicKeyToPem(keyPair.publicKey);
|
||||
final privateKeyPem = _encodePrivateKeyToPem(keyPair.privateKey);
|
||||
|
||||
// Store in secure storage
|
||||
await _secureStorage.write(key: _publicKeyTag, value: publicKeyPem);
|
||||
await _secureStorage.write(key: _privateKeyTag, value: privateKeyPem);
|
||||
|
||||
debugPrint('New key pair generated and stored successfully');
|
||||
} catch (e) {
|
||||
debugPrint('Failed to generate key pair: $e');
|
||||
// Don't throw, allow the app to continue without encryption
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt data with the public key
|
||||
Uint8List? encrypt(String plainText) {
|
||||
if (_currentKeyPair == null) {
|
||||
debugPrint('No key pair available for encryption');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final cipher = PKCS1Encoding(RSAEngine())
|
||||
..init(true, PublicKeyParameter<RSAPublicKey>(_currentKeyPair!.publicKey));
|
||||
|
||||
final input = Uint8List.fromList(utf8.encode(plainText));
|
||||
return cipher.process(input);
|
||||
} catch (e) {
|
||||
debugPrint('Encryption error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt data with the private key
|
||||
String? decrypt(Uint8List encryptedData) {
|
||||
if (_currentKeyPair == null) {
|
||||
debugPrint('No key pair available for decryption');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final cipher = PKCS1Encoding(RSAEngine())
|
||||
..init(false, PrivateKeyParameter<RSAPrivateKey>(_currentKeyPair!.privateKey));
|
||||
|
||||
final decrypted = cipher.process(encryptedData);
|
||||
return utf8.decode(decrypted);
|
||||
} catch (e) {
|
||||
debugPrint('Decryption error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current public key in PEM format
|
||||
Future<String?> getPublicKeyPem() async {
|
||||
return await _secureStorage.read(key: _publicKeyTag);
|
||||
}
|
||||
|
||||
// Simpler RSA key pair generation that doesn't use FortunaRandom
|
||||
Future<AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>> _generateRSAKeyPairSimple(int bitLength) async {
|
||||
// Use a simple secure random instead of FortunaRandom
|
||||
final secureRandom = _SecureRandom();
|
||||
|
||||
// Create RSA key generator
|
||||
final keyGen = RSAKeyGenerator()
|
||||
..init(ParametersWithRandom(
|
||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
|
||||
secureRandom,
|
||||
));
|
||||
|
||||
// Generate and return the key pair
|
||||
return keyGen.generateKeyPair() as AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>;
|
||||
}
|
||||
|
||||
// Original method (but no longer used directly)
|
||||
static AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> _generateRSAKeyPair(int bitLength) {
|
||||
try {
|
||||
final secureRandom = _SecureRandom();
|
||||
|
||||
final keyGen = RSAKeyGenerator()
|
||||
..init(ParametersWithRandom(
|
||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
|
||||
secureRandom,
|
||||
));
|
||||
|
||||
return keyGen.generateKeyPair() as AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>;
|
||||
} catch (e) {
|
||||
debugPrint('Error in _generateRSAKeyPair: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
String _encodePublicKeyToPem(RSAPublicKey publicKey) {
|
||||
final asn1Sequence = ASN1Sequence();
|
||||
|
||||
asn1Sequence.add(ASN1Integer(publicKey.modulus!));
|
||||
asn1Sequence.add(ASN1Integer(publicKey.exponent!));
|
||||
|
||||
final base64 = base64Encode(asn1Sequence.encodedBytes);
|
||||
return '-----BEGIN PUBLIC KEY-----\n$base64\n-----END PUBLIC KEY-----';
|
||||
}
|
||||
|
||||
String _encodePrivateKeyToPem(RSAPrivateKey privateKey) {
|
||||
final asn1Sequence = ASN1Sequence();
|
||||
|
||||
asn1Sequence.add(ASN1Integer(BigInt.from(0))); // version
|
||||
asn1Sequence.add(ASN1Integer(privateKey.modulus!));
|
||||
asn1Sequence.add(ASN1Integer(privateKey.publicExponent!));
|
||||
asn1Sequence.add(ASN1Integer(privateKey.privateExponent!));
|
||||
asn1Sequence.add(ASN1Integer(privateKey.p!));
|
||||
asn1Sequence.add(ASN1Integer(privateKey.q!));
|
||||
// d mod (p-1)
|
||||
asn1Sequence.add(ASN1Integer(privateKey.privateExponent! % (privateKey.p! - BigInt.from(1))));
|
||||
// d mod (q-1)
|
||||
asn1Sequence.add(ASN1Integer(privateKey.privateExponent! % (privateKey.q! - BigInt.from(1))));
|
||||
// q^-1 mod p
|
||||
asn1Sequence.add(ASN1Integer(_modInverse(privateKey.q!, privateKey.p!)));
|
||||
|
||||
final base64 = base64Encode(asn1Sequence.encodedBytes);
|
||||
return '-----BEGIN RSA PRIVATE KEY-----\n$base64\n-----END RSA PRIVATE KEY-----';
|
||||
}
|
||||
|
||||
RSAPrivateKey _parsePrivateKeyFromPem(String pemString) {
|
||||
final pemContent = pemString
|
||||
.replaceAll('-----BEGIN RSA PRIVATE KEY-----', '')
|
||||
.replaceAll('-----END RSA PRIVATE KEY-----', '')
|
||||
.replaceAll('\n', '');
|
||||
|
||||
final asn1Parser = ASN1Parser(base64Decode(pemContent));
|
||||
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||
|
||||
// Parse sequence values
|
||||
final values = topLevelSeq.elements!.map((obj) => (obj as ASN1Integer).valueAsBigInteger).toList();
|
||||
|
||||
// Create RSA private key from components
|
||||
return RSAPrivateKey(
|
||||
values[1], // modulus
|
||||
values[3], // privateExponent
|
||||
values[4], // p
|
||||
values[5], // q
|
||||
);
|
||||
}
|
||||
|
||||
RSAPublicKey _parsePublicKeyFromPem(String pemString) {
|
||||
final pemContent = pemString
|
||||
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
|
||||
.replaceAll('-----END PUBLIC KEY-----', '')
|
||||
.replaceAll('\n', '');
|
||||
|
||||
final asn1Parser = ASN1Parser(base64Decode(pemContent));
|
||||
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||
|
||||
// Extract modulus and exponent
|
||||
final modulus = (topLevelSeq.elements![0] as ASN1Integer).valueAsBigInteger;
|
||||
final exponent = (topLevelSeq.elements![1] as ASN1Integer).valueAsBigInteger;
|
||||
|
||||
return RSAPublicKey(modulus, exponent);
|
||||
}
|
||||
|
||||
// Modular multiplicative inverse
|
||||
BigInt _modInverse(BigInt a, BigInt m) {
|
||||
// Extended Euclidean Algorithm to find modular inverse
|
||||
BigInt t = BigInt.zero, newT = BigInt.one;
|
||||
BigInt r = m, newR = a;
|
||||
|
||||
while (newR != BigInt.zero) {
|
||||
final quotient = r ~/ newR;
|
||||
|
||||
final tempT = t;
|
||||
t = newT;
|
||||
newT = tempT - quotient * newT;
|
||||
|
||||
final tempR = r;
|
||||
r = newR;
|
||||
newR = tempR - quotient * newR;
|
||||
}
|
||||
|
||||
if (r > BigInt.one) throw Exception('$a is not invertible modulo $m');
|
||||
if (t < BigInt.zero) t += m;
|
||||
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple secure random implementation that doesn't use AESEngine
|
||||
class _SecureRandom implements SecureRandom {
|
||||
final Random _random = Random.secure();
|
||||
|
||||
@override
|
||||
String get algorithmName => 'Dart_SecureRandom';
|
||||
|
||||
@override
|
||||
void seed(CipherParameters params) {
|
||||
// No additional seeding required as Random.secure() is already seeded
|
||||
}
|
||||
|
||||
@override
|
||||
BigInt nextBigInteger(int bitLength) {
|
||||
final fullBytes = bitLength ~/ 8;
|
||||
final remainingBits = bitLength % 8;
|
||||
|
||||
final bytes = Uint8List(fullBytes + (remainingBits > 0 ? 1 : 0));
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = nextUint8();
|
||||
}
|
||||
|
||||
// Adjust the last byte to match the remaining bits
|
||||
if (remainingBits > 0) {
|
||||
bytes[bytes.length - 1] &= (1 << remainingBits) - 1;
|
||||
}
|
||||
|
||||
return BigInt.parse(bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''), radix: 16);
|
||||
}
|
||||
|
||||
@override
|
||||
int nextUint16() => (_random.nextInt(1 << 16) & 0xFFFF);
|
||||
|
||||
@override
|
||||
int nextUint32() => (_random.nextInt(1 << 32) & 0xFFFFFFFF);
|
||||
|
||||
@override
|
||||
int nextUint8() => (_random.nextInt(1 << 8) & 0xFF);
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
// No reset needed for Random.secure()
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Uint8List nextBytes(int count) {
|
||||
final bytes = Uint8List(count);
|
||||
for (var i = 0; i < count; i++) {
|
||||
bytes[i] = nextUint8();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,10 @@
|
||||
import '../../core/config/app_config.dart';
|
||||
// lib/services/obfuscate_service.dart
|
||||
import 'package:dialer/widgets/color_darkener.dart';
|
||||
|
||||
import '../../globals.dart' as globals;
|
||||
import 'dart:ui';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ObfuscateService {
|
||||
// Private constructor
|
||||
@ -14,7 +20,7 @@ class ObfuscateService {
|
||||
|
||||
// Public method to obfuscate data
|
||||
String obfuscateData(String data) {
|
||||
if (AppConfig.isStealthMode) {
|
||||
if (globals.isStealthMode) {
|
||||
return _obfuscateData(data);
|
||||
} else {
|
||||
return data;
|
||||
@ -34,3 +40,52 @@ class ObfuscateService {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ObfuscatedAvatar extends StatelessWidget {
|
||||
final Uint8List? imageBytes;
|
||||
final double radius;
|
||||
final Color backgroundColor;
|
||||
final String? fallbackInitial;
|
||||
|
||||
const ObfuscatedAvatar({
|
||||
Key? key,
|
||||
required this.imageBytes,
|
||||
this.radius = 25,
|
||||
this.backgroundColor = Colors.grey,
|
||||
this.fallbackInitial,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageBytes != null && imageBytes!.isNotEmpty) {
|
||||
return ClipOval(
|
||||
child: ImageFiltered(
|
||||
imageFilter: globals.isStealthMode
|
||||
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
|
||||
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
|
||||
child: Image.memory(
|
||||
imageBytes!,
|
||||
fit: BoxFit.cover,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: backgroundColor,
|
||||
child: Text(
|
||||
fallbackInitial != null && fallbackInitial!.isNotEmpty
|
||||
? fallbackInitial![0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: darken(backgroundColor),
|
||||
fontSize: radius,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/config/app_config.dart';
|
||||
import '../../../core/utils/color_utils.dart';
|
||||
|
||||
class ObfuscatedAvatar extends StatelessWidget {
|
||||
final Uint8List? imageBytes;
|
||||
final double radius;
|
||||
final Color backgroundColor;
|
||||
final String? fallbackInitial;
|
||||
|
||||
const ObfuscatedAvatar({
|
||||
Key? key,
|
||||
required this.imageBytes,
|
||||
this.radius = 25,
|
||||
this.backgroundColor = Colors.grey,
|
||||
this.fallbackInitial,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageBytes != null && imageBytes!.isNotEmpty) {
|
||||
return ClipOval(
|
||||
child: ImageFiltered(
|
||||
imageFilter: AppConfig.isStealthMode
|
||||
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
|
||||
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
|
||||
child: Image.memory(
|
||||
imageBytes!,
|
||||
fit: BoxFit.cover,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: backgroundColor,
|
||||
child: Text(
|
||||
fallbackInitial != null && fallbackInitial!.isNotEmpty
|
||||
? fallbackInitial![0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: darken(backgroundColor),
|
||||
fontSize: radius,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import 'dart:typed_data';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../core/utils/color_utils.dart';
|
||||
import '../../../presentation/common/widgets/obfuscated_avatar.dart';
|
||||
// import '../../../presentation/common/widgets/obfuscated_avatar.dart';
|
||||
|
||||
class CallPage extends StatefulWidget {
|
||||
final String displayName;
|
||||
|
@ -4,7 +4,6 @@ import 'dart:typed_data';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../core/utils/color_utils.dart';
|
||||
import '../../../presentation/common/widgets/obfuscated_avatar.dart';
|
||||
import 'call_page.dart';
|
||||
|
||||
class IncomingCallPage extends StatefulWidget {
|
||||
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../services/contact_service.dart';
|
||||
import '../../../services/obfuscate_service.dart';
|
||||
import '../../../services/call_service.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
import '../contacts/widgets/add_contact_button.dart';
|
||||
|
||||
class CompositionPage extends StatefulWidget {
|
||||
|
@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../contacts/contact_state.dart';
|
||||
import '../contacts/widgets/alphabet_scroll_page.dart';
|
||||
import '../../common/widgets/loading_indicator.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
|
||||
class ContactPage extends StatefulWidget {
|
||||
const ContactPage({super.key});
|
||||
@ -12,8 +10,6 @@ class ContactPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ContactPageState extends State<ContactPage> {
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contactState = ContactState.of(context);
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../../core/utils/color_utils.dart';
|
||||
import '../../../common/widgets/obfuscated_avatar.dart';
|
||||
import '../contact_state.dart';
|
||||
import 'add_contact_button.dart';
|
||||
import 'contact_modal.dart';
|
||||
|
@ -3,10 +3,10 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../../widgets/username_color_generator.dart';
|
||||
import '../../../../widgets/color_darkener.dart';
|
||||
import '../../../../services/obfuscate_service.dart';
|
||||
import '../../../../services/block_service.dart';
|
||||
import '../../../../services/contact_service.dart';
|
||||
import '../../../../services/call_service.dart';
|
||||
import '../../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../../domain/services/block_service.dart';
|
||||
import '../../../../domain/services/contact_service.dart';
|
||||
import '../../../../domain/services/call_service.dart';
|
||||
|
||||
class ContactModal extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/contact.dart';
|
||||
import 'package:dialer/services/contact_service.dart';
|
||||
import 'package:dialer/domain/services/contact_service.dart';
|
||||
|
||||
class QRCodeButton extends StatelessWidget {
|
||||
final List<Contact> contacts;
|
||||
|
@ -5,11 +5,11 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../services/obfuscate_service.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../widgets/color_darkener.dart';
|
||||
import '../../../widgets/username_color_generator.dart';
|
||||
import '../../../services/block_service.dart';
|
||||
import '../../../services/call_service.dart';
|
||||
import '../../../domain/services/block_service.dart';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
import '../contacts/contact_state.dart';
|
||||
import '../contacts/widgets/contact_modal.dart';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
|
||||
import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart';
|
||||
|
||||
class ManageKeysPage extends StatefulWidget {
|
||||
const ManageKeysPage({Key? key}) : super(key: key);
|
||||
|
@ -1,78 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service for managing blocked phone numbers
|
||||
class BlockService {
|
||||
static const String _blockedNumbersKey = 'blocked_numbers';
|
||||
|
||||
// Private constructor
|
||||
BlockService._privateConstructor();
|
||||
|
||||
// Singleton instance
|
||||
static final BlockService _instance = BlockService._privateConstructor();
|
||||
|
||||
// Factory constructor to return the same instance
|
||||
factory BlockService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/// Block a phone number
|
||||
Future<bool> blockNumber(String phoneNumber) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
|
||||
// Don't add if already blocked
|
||||
if (blockedNumbers.contains(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
blockedNumbers.add(phoneNumber);
|
||||
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
|
||||
} catch (e) {
|
||||
debugPrint('Error blocking number: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unblock a phone number
|
||||
Future<bool> unblockNumber(String phoneNumber) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
|
||||
if (!blockedNumbers.contains(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
blockedNumbers.remove(phoneNumber);
|
||||
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
|
||||
} catch (e) {
|
||||
debugPrint('Error unblocking number: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a number is blocked
|
||||
Future<bool> isNumberBlocked(String phoneNumber) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
return blockedNumbers.contains(phoneNumber);
|
||||
} catch (e) {
|
||||
debugPrint('Error checking if number is blocked: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all blocked numbers
|
||||
Future<List<String>> getBlockedNumbers() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
} catch (e) {
|
||||
debugPrint('Error getting blocked numbers: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../presentation/features/call/call_page.dart';
|
||||
import '../presentation/features/call/incoming_call_page.dart'; // Import the new page
|
||||
|
||||
class CallService {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
static String? currentPhoneNumber;
|
||||
static bool _isCallPageVisible = false;
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
CallService() {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
final context = navigatorKey.currentContext;
|
||||
print('CallService: Received method ${call.method} with args ${call.arguments}');
|
||||
if (context == null) {
|
||||
print('CallService: Navigator context is null, cannot navigate');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (call.method) {
|
||||
case "callAdded":
|
||||
final phoneNumber = call.arguments["callId"] as String;
|
||||
final state = call.arguments["state"] as String;
|
||||
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
|
||||
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
|
||||
if (state == "ringing") {
|
||||
_navigateToIncomingCallPage(context);
|
||||
} else {
|
||||
_navigateToCallPage(context);
|
||||
}
|
||||
break;
|
||||
case "callStateChanged":
|
||||
final state = call.arguments["state"] as String;
|
||||
print('CallService: State changed to $state');
|
||||
if (state == "disconnected" || state == "disconnecting") {
|
||||
_closeCallPage(context);
|
||||
} else if (state == "active" || state == "dialing") {
|
||||
_navigateToCallPage(context);
|
||||
} else if (state == "ringing") {
|
||||
_navigateToIncomingCallPage(context);
|
||||
}
|
||||
break;
|
||||
case "callEnded":
|
||||
case "callRemoved":
|
||||
print('CallService: Call ended/removed');
|
||||
_closeCallPage(context);
|
||||
currentPhoneNumber = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _navigateToCallPage(BuildContext context) {
|
||||
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
|
||||
print('CallService: CallPage already visible, skipping navigation');
|
||||
return;
|
||||
}
|
||||
print('CallService: Navigating to CallPage');
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/call'),
|
||||
builder: (context) => CallPage(
|
||||
displayName: currentPhoneNumber!,
|
||||
phoneNumber: currentPhoneNumber!,
|
||||
thumbnail: null,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
_isCallPageVisible = false;
|
||||
});
|
||||
_isCallPageVisible = true;
|
||||
}
|
||||
|
||||
void _navigateToIncomingCallPage(BuildContext context) {
|
||||
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') {
|
||||
print('CallService: IncomingCallPage already visible, skipping navigation');
|
||||
return;
|
||||
}
|
||||
print('CallService: Navigating to IncomingCallPage');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/incoming_call'),
|
||||
builder: (context) => IncomingCallPage(
|
||||
displayName: currentPhoneNumber!,
|
||||
phoneNumber: currentPhoneNumber!,
|
||||
thumbnail: null,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
_isCallPageVisible = false;
|
||||
});
|
||||
_isCallPageVisible = true;
|
||||
}
|
||||
|
||||
void _closeCallPage(BuildContext context) {
|
||||
if (!_isCallPageVisible) {
|
||||
print('CallService: CallPage not visible, skipping pop');
|
||||
return;
|
||||
}
|
||||
if (Navigator.canPop(context)) {
|
||||
print('CallService: Popping CallPage');
|
||||
Navigator.pop(context);
|
||||
_isCallPageVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> makeGsmCall(
|
||||
BuildContext context, {
|
||||
required String phoneNumber,
|
||||
String? displayName,
|
||||
Uint8List? thumbnail,
|
||||
}) async {
|
||||
try {
|
||||
currentPhoneNumber = phoneNumber;
|
||||
print('CallService: Making GSM call to $phoneNumber');
|
||||
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
||||
print('CallService: makeGsmCall result: $result');
|
||||
if (result["status"] != "calling") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to initiate call")),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print("CallService: Error making call: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error making call: $e")),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> hangUpCall(BuildContext context) async {
|
||||
try {
|
||||
print('CallService: Hanging up call');
|
||||
final result = await _channel.invokeMethod('hangUpCall');
|
||||
print('CallService: hangUpCall result: $result');
|
||||
if (result["status"] != "ended") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to end call")),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print("CallService: Error hanging up call: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error hanging up call: $e")),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
class ContactService {
|
||||
Future<List<Contact>> fetchContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
List<Contact> contacts = await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
withThumbnail: true,
|
||||
);
|
||||
return contacts;
|
||||
} else {
|
||||
// Permission denied
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Contact>> fetchFavoriteContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
// Get all contacts and filter for favorites
|
||||
List<Contact> allContacts = await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
withThumbnail: true,
|
||||
);
|
||||
return allContacts.where((c) => c.isStarred).toList();
|
||||
} else {
|
||||
// Permission denied
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<Contact?> addNewContact(Contact contact) async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
try {
|
||||
return await FlutterContacts.insertContact(contact);
|
||||
} catch (e) {
|
||||
debugPrint('Error adding contact: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void showContactQRCodeDialog(BuildContext context, Contact contact) {
|
||||
final String vCard = contact.toVCard();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.grey[900],
|
||||
title: Text(
|
||||
'QR Code for ${contact.displayName}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: QrImageView(
|
||||
data: vCard,
|
||||
version: QrVersions.auto,
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
const Text(
|
||||
'Scan this code to add this contact',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
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 with alias '$alias': ${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});
|
||||
|
||||
final String? existingKeys = await _secureStorage.read(key: 'keys');
|
||||
if (existingKeys != null) {
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
keysList.removeWhere((key) => key['alias'] == alias);
|
||||
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) {
|
||||
print("No keys found");
|
||||
return [];
|
||||
}
|
||||
List<dynamic> keysList = jsonDecode(existingKeys);
|
||||
return keysList.cast<Map<String, dynamic>>();
|
||||
} 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 the key exists in metadata
|
||||
final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias);
|
||||
|
||||
if (!defaultKeyExists) {
|
||||
await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias});
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
// lib/services/obfuscate_service.dart
|
||||
import 'package:dialer/widgets/color_darkener.dart';
|
||||
|
||||
import '../../globals.dart' as globals;
|
||||
import 'dart:ui';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ObfuscateService {
|
||||
// Private constructor
|
||||
ObfuscateService._privateConstructor();
|
||||
|
||||
// Singleton instance
|
||||
static final ObfuscateService _instance = ObfuscateService._privateConstructor();
|
||||
|
||||
// Factory constructor to return the same instance
|
||||
factory ObfuscateService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
// Public method to obfuscate data
|
||||
String obfuscateData(String data) {
|
||||
if (globals.isStealthMode) {
|
||||
return _obfuscateData(data);
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// Private helper method for obfuscation logic
|
||||
String _obfuscateData(String data) {
|
||||
if (data.isNotEmpty) {
|
||||
// Ensure the string has at least two characters to obfuscate
|
||||
if (data.length == 1) {
|
||||
return '${data[0]}';
|
||||
} else {
|
||||
return '${data[0]}...${data[data.length - 1]}';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ObfuscatedAvatar extends StatelessWidget {
|
||||
final Uint8List? imageBytes;
|
||||
final double radius;
|
||||
final Color backgroundColor;
|
||||
final String? fallbackInitial;
|
||||
|
||||
const ObfuscatedAvatar({
|
||||
Key? key,
|
||||
required this.imageBytes,
|
||||
this.radius = 25,
|
||||
this.backgroundColor = Colors.grey,
|
||||
this.fallbackInitial,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imageBytes != null && imageBytes!.isNotEmpty) {
|
||||
return ClipOval(
|
||||
child: ImageFiltered(
|
||||
imageFilter: globals.isStealthMode
|
||||
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
|
||||
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
|
||||
child: Image.memory(
|
||||
imageBytes!,
|
||||
fit: BoxFit.cover,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
backgroundColor: backgroundColor,
|
||||
child: Text(
|
||||
fallbackInitial != null && fallbackInitial!.isNotEmpty
|
||||
? fallbackInitial![0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: darken(backgroundColor),
|
||||
fontSize: radius,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class QRCodeScannerScreen extends StatefulWidget {
|
||||
const QRCodeScannerScreen({super.key});
|
||||
|
||||
@override
|
||||
_QRCodeScannerScreenState createState() => _QRCodeScannerScreenState();
|
||||
}
|
||||
|
||||
class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
|
||||
MobileScannerController cameraController = MobileScannerController();
|
||||
bool _flashEnabled = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cameraController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR Code'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_flashEnabled = !_flashEnabled;
|
||||
cameraController.toggleTorch();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flip_camera_ios),
|
||||
onPressed: () => cameraController.switchCamera(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MobileScanner(
|
||||
controller: cameraController,
|
||||
onDetect: (capture) {
|
||||
final List<Barcode> barcodes = capture.barcodes;
|
||||
if (barcodes.isNotEmpty) {
|
||||
// Return the first barcode value
|
||||
final String? code = barcodes.first.rawValue;
|
||||
if (code != null) {
|
||||
Navigator.pop(context, code);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user