import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart' as dart_crypto; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; import 'package:pointycastle/export.dart' as pc; class EccIdentityMaterial { final String privateKey; final String publicKey; final String fingerprint; const EccIdentityMaterial({ required this.privateKey, required this.publicKey, required this.fingerprint, }); } class SecureCryptoHelper { final _aesGcm = AesGcm.with256bits(); final _hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32); final _random = Random.secure(); final pc.ECDomainParameters _ecDomain = pc.ECDomainParameters('secp256r1'); static const _variantMap = { "ك": "ک", "ي": "ی", "ى": "ی", "ة": "ه", "ە": "ه", "٠": "0", "١": "1", "٢": "2", "٣": "3", "٤": "4", "٥": "5", "٦": "6", "٧": "7", "٨": "8", "٩": "9", "۰": "0", "۱": "1", "۲": "2", "۳": "3", "۴": "4", "۵": "5", "۶": "6", "۷": "7", "۸": "8", "۹": "9", }; String _visualSafe(String text) { String res = text.trim(); // 1. Remove invisible/zero-width characters (matches Python's _LEGACY_INVISIBLE_CHARS) res = res.replaceAll(RegExp(r'[\u200c\u200d\u200e\u200f\ufeff]'), ''); // 2. Normalize common decomposed (NFD) Persian characters to composite (NFC) legacy fallback res = res.replaceAll('\u0627\u0653', '\u0622'); // Alif + Madda -> آ res = res.replaceAll('\u0627\u0654', '\u0623'); // Alif + Hamza -> أ res = res.replaceAll('\u0627\u0655', '\u0625'); // Alif + Lower Hamza -> إ res = res.replaceAll('\u0648\u0654', '\u0624'); // Waw + Hamza -> ؤ res = res.replaceAll('\u064a\u0654', '\u0626'); // Yeh + Hamza (Arabic) -> ئ res = res.replaceAll('\u06cc\u0654', '\u0626'); // Yeh + Hamza (Persian) -> ئ res = res.replaceAll( '\u0627\u0644\u0644\u0647', 'الله'); // Allah ligature (Standard) // 3. Map Arabic variants and digits to Persian/Western standards (matches Python's dict) _variantMap.forEach((key, value) { res = res.replaceAll(key, value); }); // 4. Aggressive cleanup of invisible characters (Matches Python's _LEGACY_INVISIBLE_CHARS) res = res.replaceAll(RegExp(r'[\u200c\u200d\u200e\u200f\ufeff]'), ''); // NOTE: We DO NOT remove Harakats (diacritics) here because Python's NFC normalization // preserves them. We only map letter/digit variants. return res; } String b64uEncode(List bytes) { return base64UrlEncode(bytes).replaceAll('=', ''); } List b64uDecode(String value) { // 1. Strip transport prefixes if present String input = value.trim(); if (input.startsWith('b1:')) { input = input.substring(3); } // 2. Log illegal characters for diagnostics final illegalChars = input.replaceAll(RegExp(r'[A-Za-z0-9+/=\s\-_]'), ''); if (illegalChars.isNotEmpty) { debugPrint('[CRYPTO] Found illegal chars in Base64: "$illegalChars"'); } // 3. Aggressive cleaning - ALLOW | ; ! as they might be mangled separators/chars String clean = input.replaceAll(RegExp(r'[^A-Za-z0-9+/=\s\-_|;!]'), ''); // 4. Map standard mangles (Parity with Python's _repair_base64) clean = clean.replaceAll(' ', '+'); clean = clean.replaceAll('|', '/').replaceAll(';', '/').replaceAll('!', '/'); // 4. Parity with Python: translate URL-safe to Standard before decoding clean = clean.replaceAll('-', '+').replaceAll('_', '/'); // 5. Handle padding: discard all '=' to recalculate from scratch based on data bits clean = clean.split('=')[0]; // 6. Critical: Final cleanup from non-alphabet symbols after mapping/split clean = clean.replaceAll(RegExp(r'[^A-Za-z0-9+/]'), ''); // 7. Base64 length validation logic if (clean.length % 4 == 1) { debugPrint( '[CRYPTO] Base64 Critical Failure: len ${clean.length} % 4 == 1. Prefix: ${clean.substring(0, min(10, clean.length))}'); return []; } final missingPadding = (4 - (clean.length % 4)) % 4; final padded = clean + ('=' * missingPadding); try { return base64.decode(padded); } catch (e) { debugPrint('[CRYPTO] Base64 Final Decode Error: $e'); return []; } } String hexEncode(List bytes) { return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); } List hexDecode(String value) { String input = value.trim(); if (input.startsWith('h1:')) { input = input.substring(3); } final clean = input.replaceAll(RegExp(r'[^0-9A-Fa-f]'), ''); final evenLength = clean.length - (clean.length % 2); final normalized = clean.substring(0, evenLength); return List.generate( normalized.length ~/ 2, (i) => int.parse(normalized.substring(i * 2, i * 2 + 2), radix: 16), ); } List decodeTransportPayload(String value) { if (value.startsWith('h1:')) { return hexDecode(value.substring(3)); } if (value.startsWith('b1:')) { return b64uDecode(value.substring(3)); } return b64uDecode(value); } Future encryptSymmetric(String message, String password) async { final normKeyText = _visualSafe(password); final digest = dart_crypto.sha256.convert(utf8.encode(normKeyText)); final keyBytes = Uint8List.fromList(digest.bytes); final nonce = _aesGcm.newNonce(); final secretBox = await _aesGcm.encrypt( utf8.encode(message), secretKey: SecretKey(keyBytes), nonce: nonce, ); final combined = Uint8List.fromList([ ...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes, ]); return 'b1:${b64uEncode(combined)}'; } Future decryptSymmetric(String payload, String password) async { try { final raw = decodeTransportPayload(payload); final keyVariants = [ {'label': 'visual_safe', 'text': _visualSafe(password)}, {'label': 'raw', 'text': password.trim()}, ]; if (raw.length < 28) { debugPrint('[CRYPTO] Symmetric decryption failed: payload too short.'); return null; } final nonce = raw.sublist(0, 12); final ciphertextWithTag = raw.sublist(12); final ciphertext = ciphertextWithTag.sublist(0, ciphertextWithTag.length - 16); final mac = ciphertextWithTag.sublist(ciphertextWithTag.length - 16); for (final variant in keyVariants) { final label = variant['label']!; final keyText = variant['text']!; final digest = dart_crypto.sha256.convert(utf8.encode(keyText)); final keyBytes = Uint8List.fromList(digest.bytes); debugPrint('[CRYPTO] Trying symmetric decryption variant "$label".'); try { final secretBox = SecretBox( ciphertext, nonce: nonce, mac: Mac(mac), ); final decryptedBytes = await _aesGcm.decrypt( secretBox, secretKey: SecretKey(keyBytes), ); debugPrint( '[CRYPTO] Symmetric decryption succeeded via variant "$label".'); try { return utf8.decode(decryptedBytes); } catch (e) { debugPrint('[CRYPTO] UTF-8 decode failed after decryption.'); return '[UTF-8 Decode Error]'; } } catch (e) { debugPrint('[CRYPTO] Symmetric decryption variant "$label" failed.'); } } debugPrint('[CRYPTO] All symmetric decryption variants failed.'); return null; } catch (e) { debugPrint('[CRYPTO] Symmetric decryption error: $e'); return null; } } String fingerprintFromPublicBytes(List publicBytes) { final digest = dart_crypto.sha256.convert(publicBytes).toString().toUpperCase(); final parts = []; for (var i = 0; i < digest.length; i += 4) { parts.add(digest.substring(i, i + 4)); } return parts.join(' '); } List _normalizeP256Coordinate(List value) { if (value.length == 32) return List.from(value); if (value.length == 33 && value.first == 0) { return value.sublist(1); } if (value.length < 32) { return List.filled(32 - value.length, 0) + value; } return value.sublist(value.length - 32); } pc.SecureRandom _newSecureRandom() { final seed = Uint8List.fromList( List.generate(32, (_) => _random.nextInt(256)), ); return pc.FortunaRandom()..seed(pc.KeyParameter(seed)); } BigInt _bytesToBigInt(List bytes) { var result = BigInt.zero; for (final byte in bytes) { result = (result << 8) | BigInt.from(byte); } return result; } List _bigIntToBytes(BigInt value) { if (value == BigInt.zero) { return [0]; } final result = []; var current = value; while (current > BigInt.zero) { result.insert(0, (current & BigInt.from(0xff)).toInt()); current = current >> 8; } return result; } String publicKeyTransport(String value) { final publicBytes = decodePublicKey(value); return 'b1:${b64uEncode(publicBytes)}'; } Future generateIdentity() async { final params = pc.ParametersWithRandom( pc.ECKeyGeneratorParameters(_ecDomain), _newSecureRandom(), ); final generator = pc.KeyGenerator('EC')..init(params); final keyPair = generator.generateKeyPair(); final privateKey = keyPair.privateKey as pc.ECPrivateKey; final publicKey = keyPair.publicKey as pc.ECPublicKey; final privateBytes = Uint8List.fromList( _normalizeP256Coordinate(_bigIntToBytes(privateKey.d!)), ); final publicBytes = Uint8List.fromList([ ..._normalizeP256Coordinate( _bigIntToBytes(publicKey.Q!.x!.toBigInteger()!), ), ..._normalizeP256Coordinate( _bigIntToBytes(publicKey.Q!.y!.toBigInteger()!), ), ]); return EccIdentityMaterial( privateKey: b64uEncode(privateBytes), publicKey: b64uEncode(publicBytes), fingerprint: fingerprintFromPublicBytes(publicBytes), ); } List decodePublicKey(String value) { try { final String input = value.startsWith('h1:') || value.startsWith('b1:') ? value.substring(3) : value; final raw = b64uDecode(input); if (raw.isNotEmpty) { if (raw.length == 65 && raw.first == 4) { return raw.sublist(1); } if (raw.length == 64) { return raw; } } throw StateError( 'P-256 public key length must be 64 bytes (got ${raw.length}).'); } catch (e) { debugPrint('Public key decode error for value "$value": $e'); rethrow; } } Future> deriveSharedKey({ required String privateKey, required String localPublicKey, required String publicKey, }) async { final privateBytes = b64uDecode(privateKey); final publicBytes = decodePublicKey(publicKey); if (privateBytes.length != 32) { throw StateError('P-256 private key must contain 32 bytes.'); } final peerX = publicBytes.sublist(0, 32); final peerY = publicBytes.sublist(32, 64); final localPublicBytes = decodePublicKey(localPublicKey); if (localPublicBytes.length != 64) { throw StateError('Local P-256 public key must contain 64 bytes.'); } final agreement = pc.ECDHBasicAgreement() ..init( pc.ECPrivateKey( _bytesToBigInt(privateBytes), _ecDomain, ), ); final sharedPoint = pc.ECPublicKey( _ecDomain.curve.createPoint( _bytesToBigInt(peerX), _bytesToBigInt(peerY), ), _ecDomain, ); final sharedValue = agreement.calculateAgreement(sharedPoint); final sharedBytes = Uint8List.fromList( _normalizeP256Coordinate(_bigIntToBytes(sharedValue)), ); final derived = await _hkdf.deriveKey( secretKey: SecretKey(sharedBytes), nonce: utf8.encode('SABA:ECDH:P256:HKDF:v1'), info: utf8.encode('SABA:ECDH:P256:AES-256-GCM'), ); final finalKey = await derived.extractBytes(); return finalKey; } Future encryptWithSharedKey( String message, List sharedKey) async { final nonce = _aesGcm.newNonce(); final secretBox = await _aesGcm.encrypt( utf8.encode(message), secretKey: SecretKey(sharedKey), nonce: nonce, ); final combined = [ ...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes, ]; return b64uEncode(combined); } Future decryptWithSharedKey( String payload, List sharedKey) async { try { final raw = b64uDecode(payload); if (raw.length < 28) return null; final nonce = raw.sublist(0, 12); final ciphertextWithTag = raw.sublist(12); final ciphertext = ciphertextWithTag.sublist(0, ciphertextWithTag.length - 16); final mac = ciphertextWithTag.sublist(ciphertextWithTag.length - 16); try { final secretBox = SecretBox( ciphertext, nonce: nonce, mac: Mac(mac), ); final plain = await _aesGcm.decrypt(secretBox, secretKey: SecretKey(sharedKey)); return utf8.decode(plain); } catch (e) { debugPrint('[CRYPTO] Asymmetric decryption failed: $e'); return null; } } catch (e) { debugPrint('Asymmetric payload decryption error: $e'); return null; } } List randomBytes(int length) { return List.generate(length, (_) => _random.nextInt(256)); } Future encryptStorageText(String value, List keyBytes) async { final nonce = _aesGcm.newNonce(); final secretBox = await _aesGcm.encrypt( utf8.encode(value), secretKey: SecretKey(keyBytes), nonce: nonce, ); return 'enc1:${b64uEncode([ ...secretBox.nonce, ...secretBox.cipherText, ...secretBox.mac.bytes, ])}'; } Future decryptStorageText(String? value, List keyBytes) async { if (value == null || value.isEmpty) return value; if (!value.startsWith('enc1:')) return value; try { final raw = b64uDecode(value.substring(5)); if (raw.length < 28) { return null; } final nonce = raw.sublist(0, 12); final ciphertextWithTag = raw.sublist(12); final ciphertext = ciphertextWithTag.sublist(0, ciphertextWithTag.length - 16); final mac = ciphertextWithTag.sublist(ciphertextWithTag.length - 16); final secretBox = SecretBox( ciphertext, nonce: nonce, mac: Mac(mac), ); final plain = await _aesGcm.decrypt( secretBox, secretKey: SecretKey(keyBytes), ); return utf8.decode(plain); } catch (e) { debugPrint('Storage payload decryption error: $e'); return null; } } }