import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../state/app/modules/account/bloc/account_bloc.dart'; import '../state/app/modules/account/bloc/account_state.dart'; class AccountData { static const _usernameField = 'username'; static const _passwordField = 'password'; static const FlutterSecureStorage _secureStorage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), ); static final AccountData _instance = AccountData._construct(); Completer _populated = Completer(); factory AccountData() => _instance; AccountData._construct() { _migrateAndLoad(); } String? _username; String? _password; String getUsername() { if (_username == null) throw Exception('Username not initialized'); return _username!; } String getPassword() { if (_password == null) throw Exception('Password not initialized'); return _password!; } String getUserSecret() => sha512.convert(utf8.encode('${getUsername()}:${getPassword()}')).toString(); Future getDeviceId() async => sha512 .convert(utf8.encode('${getUserSecret()}@${await FirebaseMessaging.instance.getToken()}')) .toString(); Future setData(String username, String password) async { await _secureStorage.write(key: _usernameField, value: username); await _secureStorage.write(key: _passwordField, value: password); _username = username; _password = password; if (!_populated.isCompleted) _populated.complete(); } Future removeData({BuildContext? context}) async { _populated = Completer(); if (context != null) { context.read().setStatus(AccountStatus.loggedOut); } _username = null; _password = null; await _secureStorage.delete(key: _usernameField); await _secureStorage.delete(key: _passwordField); } Future _migrateAndLoad() async { await _migrateFromLegacyStorage(); _username = await _secureStorage.read(key: _usernameField); _password = await _secureStorage.read(key: _passwordField); if (!_populated.isCompleted) _populated.complete(); } // Move credentials from the old SharedPreferences plain-text storage into the // platform's secure keystore. Run once per install and clear the legacy keys. Future _migrateFromLegacyStorage() async { final prefs = await SharedPreferences.getInstance(); final legacyUsername = prefs.getString(_usernameField); final legacyPassword = prefs.getString(_passwordField); if (legacyUsername == null || legacyPassword == null) return; final hasSecure = (await _secureStorage.read(key: _usernameField)) != null; if (!hasSecure) { await _secureStorage.write(key: _usernameField, value: legacyUsername); await _secureStorage.write(key: _passwordField, value: legacyPassword); } await prefs.remove(_usernameField); await prefs.remove(_passwordField); } Future waitForPopulation() async { await _populated.future; return isPopulated(); } bool isPopulated() => _username != null && _password != null; String buildHttpAuthString() { if (!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!'); return '$_username:$_password'; } }