import 'dart:math'; import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'dart:io'; import 'package:flutter/foundation.dart'; class WalletIdentityService { static const String _prefsKey = 'wallet_identity_phrase'; static const String _uploadEnabledKey = 'is_ai_upload_enabled'; static const String _bannedKey = 'wallet_is_banned'; static const String _banReasonKey = 'wallet_ban_reason'; static const String _serverUrlKey = 'ai_server_url'; // A standard list of 256 words (8 bits of entropy per word) static const List _wordList = [ // ... same as before 'abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'accident', 'account', 'accuse', 'achieve', 'acid', 'acoustic', 'acquire', 'across', 'act', 'action', 'actor', 'actress', 'actual', 'adapt', 'add', 'addict', 'address', 'adjust', 'admit', 'adult', 'advance', 'advice', 'aerobic', 'affair', 'afford', 'afraid', 'again', 'age', 'agent', 'agree', 'ahead', 'aim', 'air', 'airport', 'aisle', 'alarm', 'album', 'alcohol', 'alert', 'alien', 'all', 'alley', 'allow', 'almost', 'alone', 'alpha', 'already', 'also', 'alter', 'always', 'amateur', 'amazing', 'among', 'amount', 'amused', 'analyst', 'anchor', 'ancient', 'anger', 'angle', 'angry', 'animal', 'ankle', 'announce', 'annual', 'another', 'answer', 'antenna', 'antique', 'anxiety', 'any', 'apart', 'apology', 'appear', 'apple', 'approve', 'april', 'arch', 'arctic', 'area', 'arena', 'argue', 'arm', 'armed', 'armor', 'army', 'around', 'arrange', 'arrest', 'arrive', 'arrow', 'art', 'artefact', 'artist', 'artwork', 'ask', 'aspect', 'assault', 'asset', 'assist', 'assume', 'asthma', 'athlete', 'atom', 'attack', 'attend', 'attitude', 'attract', 'auction', 'audit', 'august', 'aunt', 'author', 'auto', 'autumn', 'average', 'avocado', 'avoid', 'awake', 'aware', 'away', 'awesome', 'awful', 'awkward', 'axis', 'baby', 'bachelor', 'bacon', 'badge', 'bag', 'balance', 'balcony', 'ball', 'bamboo', 'banana', 'banner', 'bar', 'barely', 'bargain', 'barrel', 'base', 'basic', 'basket', 'battle', 'beach', 'bean', 'beauty', 'because', 'become', 'beef', 'before', 'begin', 'behave', 'behind', 'believe', 'below', 'belt', 'bench', 'benefit', 'best', 'betray', 'better', 'between', 'beyond', 'bicycle', 'bid', 'bike', 'bind', 'biology', 'bird', 'birth', 'bitter', 'black', 'blade', 'blame', 'blanket', 'blast', 'bleak', 'bless', 'blind', 'blood', 'blossom', 'blouse', 'blue', 'blur', 'blush', 'board', 'boat', 'body', 'boil', 'bomb', 'bone', 'bonus', 'book', 'boost', 'border', 'boring', 'borrow', 'boss', 'bottom', 'bounce', 'box', 'boy', 'bracket', 'brain', 'brand', 'brass', 'brave', 'bread', 'breeze', 'brick', 'bridge', 'brief', 'bright', 'bring', 'brisk', 'broccoli', 'broken', 'bronze', 'broom', 'brother', 'brown', 'brush', 'bubble', 'buddy', 'budget', 'buffalo', 'build', 'bulb', 'bulk', 'bullet', 'bundle', 'bunker', 'burden', 'burger', 'burst', 'bus', 'business', 'butter' ]; /// URL par défaut du serveur de production static const String defaultServerUrl = 'https://backendia.kevlar.cloud'; /// Clé d'authentification API secrète pour le backend static const String apiKey = 'bully_secret_api_key_2026_x89'; /// Retourne l'URL de base du serveur configuré (par défaut: https://backendia.kevlar.cloud) Future getServerBaseUrl() async { final prefs = await SharedPreferences.getInstance(); final customUrl = prefs.getString(_serverUrlKey); if (customUrl != null && customUrl.trim().isNotEmpty) { return customUrl.trim().replaceAll(RegExp(r'/api/upload/?$'), ''); } return defaultServerUrl; } /// Définit une URL personnalisée pour le serveur IA Future setServerBaseUrl(String url) async { final prefs = await SharedPreferences.getInstance(); final cleanUrl = url.trim().replaceAll(RegExp(r'/api/upload/?$'), ''); await prefs.setString(_serverUrlKey, cleanUrl); } /// Vérifie si l'utilisateur a accepté l'envoi de données à l'IA (et n'est pas banni) Future isUploadEnabled() async { final prefs = await SharedPreferences.getInstance(); final isBanned = prefs.getBool(_bannedKey) ?? false; if (isBanned) return false; return prefs.getBool(_uploadEnabledKey) ?? false; } /// Active ou désactive l'envoi de données Future setUploadEnabled(bool enabled) async { final prefs = await SharedPreferences.getInstance(); final isBanned = prefs.getBool(_bannedKey) ?? false; if (isBanned) { await prefs.setBool(_uploadEnabledKey, false); return; } await prefs.setBool(_uploadEnabledKey, enabled); } /// Vérifie si ce wallet/utilisateur est banni en local Future isBanned() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool(_bannedKey) ?? false; } /// Récupère le motif de bannissement enregistré Future getBanReason() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString(_banReasonKey); } /// Enregistre l'état de bannissement et le motif Future setBanned(bool banned, {String? reason}) async { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_bannedKey, banned); if (banned) { await prefs.setString(_banReasonKey, reason ?? 'Photos non conformes aux règles de tir'); await prefs.setBool(_uploadEnabledKey, false); } else { await prefs.remove(_banReasonKey); } } /// Synchronise le statut de modération/bannissement avec le serveur backend Future syncBanStatus() async { try { final phrase = await getIdentityPhrase(); final phraseBytes = utf8.encode(phrase); final walletHash = sha256.convert(phraseBytes).toString(); final baseUrl = await getServerBaseUrl(); final res = await http.get( Uri.parse('$baseUrl/api/stats/$walletHash'), headers: {'X-API-KEY': apiKey}, ).timeout( const Duration(seconds: 4), ); if (res.statusCode == 200) { final data = jsonDecode(res.body); final isBannedOnServer = data['is_banned'] == true; if (isBannedOnServer) { await setBanned(true, reason: data['ban_reason']); } else { await setBanned(false); } return isBannedOnServer; } } catch (e) { debugPrint('Erreur synchro ban: $e'); } return await isBanned(); } /// Gets the unique 15-word identity phrase Future getIdentityPhrase() async { final prefs = await SharedPreferences.getInstance(); // Check if we already generated it final existingPhrase = prefs.getString(_prefsKey); if (existingPhrase != null && existingPhrase.isNotEmpty) { return existingPhrase; } // Generate new phrase based on device ID final phrase = await _generatePhraseFromDevice(); // Save for future use await prefs.setString(_prefsKey, phrase); return phrase; } Future _generatePhraseFromDevice() async { final deviceInfo = DeviceInfoPlugin(); String deviceId = 'unknown_device_${DateTime.now().millisecondsSinceEpoch}'; try { if (Platform.isAndroid) { final androidInfo = await deviceInfo.androidInfo; deviceId = androidInfo.id; // Unique Android ID } else if (Platform.isIOS) { final iosInfo = await deviceInfo.iosInfo; deviceId = iosInfo.identifierForVendor ?? deviceId; } else if (Platform.isWindows) { final windowsInfo = await deviceInfo.windowsInfo; deviceId = windowsInfo.deviceId; } } catch (e) { debugPrint('Erreur lecture device ID: $e'); } // Add a salt to the device ID final salt = 'bully_app_wallet_salt_v1'; final bytes = utf8.encode(deviceId + salt); final digest = sha256.convert(bytes); // Use the hash to seed a random number generator // We take the first 4 bytes of the hash as the seed final seedBytes = digest.bytes.sublist(0, 4); final seed = (seedBytes[0] << 24) | (seedBytes[1] << 16) | (seedBytes[2] << 8) | seedBytes[3]; final random = Random(seed); List phraseWords = []; for (int i = 0; i < 15; i++) { final randomIndex = random.nextInt(_wordList.length); phraseWords.add(_wordList[randomIndex]); } return phraseWords.join(' '); } }