المصدر المنشور
تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J SM Mobile
تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.
CH-J / SM Mobile
175 ملفات
sm-mobile-20261016T185235Z-8e93ecc1b59a
تاريخ الإضافة: · تاريخ البناء:
1
import 'dart:async';2
import 'dart:convert';3
import 'package:flutter/material.dart';4
import 'package:flutter/services.dart';5
import 'package:local_auth/local_auth.dart';6
import '../services/blob_store.dart';7
import '../services/documents.dart';8
import '../services/legal.dart';9
import '../services/secure_store.dart';10
import '../services/sftp_service.dart';11
import '../services/ssh_service.dart';12
import '../services/vault.dart';13
import 'failure.dart';15
const idleChoices = [1, 2, 5, 10, 15, 30, 60];17
/// Maps a platform error from the biometric key store to a message key.18
String biometricFailure(PlatformException e) {19
final text = '${e.code} ${e.message} ${e.details}'.toLowerCase();20
if (text.contains('-128') ||21
text.contains('cancel') ||22
text.contains('negative')) {23
return 'biometricCancelled';24
}25
if (text.contains('lockout') || text.contains('-8')) {26
return 'biometricLockout';27
}28
if (text.contains('not enrolled') ||29
text.contains('none_enrolled') ||30
text.contains('no_hardware') ||31
text.contains('-7') ||32
text.contains('-6')) {33
return 'biometricUnavailable';34
}35
if (text.contains('invalidated') || text.contains('permanently')) {36
return 'biometricInvalid';37
}38
return 'biometricFailed';39
}41
class AppController extends ChangeNotifier with WidgetsBindingObserver {42
final SecureStore store;43
final BlobStore blobs;44
late final Vault vault = Vault(store, blobs: blobs);45
late final Legal legal = Legal(store);46
late final SshService ssh = SshService(vault);47
bool ready = false, accepted = false, initialized = false, foreground = true;48
bool biometric = false, lockBackground = true, authenticating = false;49
bool systemDialog = false;50
String? startupError;51
Locale? locale;52
ThemeMode theme = ThemeMode.system;53
int idleMinutes = 5, monitorSeconds = 10;55
/// Editor preferences: font size, word wrap, tabs or spaces, indent width.56
Map<String, dynamic> editorPrefs = {};57
DateTime _lastActivity = DateTime.now();58
Timer? _timer;59
void Function()? onLocked;61
/// Version of the biometric key storage. Version 2 accepts only strong62
/// biometrics (no device passcode fallback) and is invalidated by the63
/// system when the enrolled biometrics change.64
int biometricVersion = 2;66
/// Whether the automatic biometric prompt was already shown for the67
/// current locked period; a failed or cancelled prompt is not repeated68
/// until the user asks for it or the app returns from the background.69
bool biometricAttempted = false;71
/// A localization key explaining why biometric unlock is not available72
/// right now (shown on the lock screen), or null.73
String? biometricNotice;75
SftpService? _sftp;76
Object? _sftpClient;77
Future<SftpService>? _opening;79
/// One SFTP session per SSH connection, shared by the file browser and80
/// the editor so closing one page never interrupts the other.81
Future<SftpService> sftp() async {82
final client = ssh.client; // throws 'disconnected'83
if (_sftp != null && identical(_sftpClient, client)) return _sftp!;84
if (_opening != null && identical(_sftpClient, client)) return _opening!;85
_sftpClient = client;86
final opening = _opening = client.sftp().then((s) {87
if (!identical(_sftpClient, client)) {88
s.close();89
throw const Failure('disconnected');90
}91
return _sftp = SftpService(s);92
});93
try {94
return await opening;95
} finally {96
if (identical(_opening, opening)) _opening = null;97
}98
}100
/// An SFTP session to the server of [profileId], or 'connectToSave' when101
/// the app is connected elsewhere (or not at all).102
Future<SftpService> sftpFor(String profileId) async {103
if (!ssh.connected || ssh.profile?.id != profileId) {104
throw const Failure('connectToSave');105
}106
return await sftp();107
}109
/// Files handed over by other apps, opened once the Vault is unlocked.110
final pendingIncoming = <DeviceDocument>[];111
StreamSubscription<List<DeviceDocument>>? _incoming;113
/// The directory shown in the SFTP browser, offered for "save as".114
String? remoteDirectory;116
/// Files on the device (system document picker); replaceable in tests.117
DocumentPlatform documents = MethodChannelDocuments();119
/// Open editors register here; on lock their unsaved buffers are kept as120
/// encrypted drafts.121
final draftSources = <Draft? Function()>{};123
/// The store holding the biometric-gated copy of the Vault data key;124
/// replaceable in tests.125
late SecureStore Function(String reason) biometricStore = (reason) =>126
DeviceStore.biometric(reason, negative: biometricNegativeText);128
/// Localized prompt texts, kept current by the lock screen.129
String biometricReasonText = 'SM Mobile';130
String biometricNegativeText = 'Cancel';131
Future<bool> Function() biometricSupported = () =>132
LocalAuthentication().isDeviceSupported();133
AppController(this.store, {BlobStore? blobs})134
: blobs = blobs ?? MemoryBlobStore();135
Future<void> initialize() async {136
WidgetsBinding.instance.addObserver(this);137
try {138
final prefs = jsonDecode(await store.read('preferences') ?? '{}');139
if (['cs', 'de', 'en'].contains(prefs['language'])) {140
locale = Locale(prefs['language']);141
}142
theme = ThemeMode.values.firstWhere(143
(t) => t.name == prefs['theme'],144
orElse: () => ThemeMode.system,145
);146
biometric = prefs['biometric'] == true;147
biometricVersion = prefs['biometricVersion'] is int148
? prefs['biometricVersion']149
: 1;150
if (biometric && biometricVersion < 2) {151
// Version 1 keys also opened with the device passcode. They are not152
// used any more; biometric unlock is set up again after the next153
// unlock with the master password.154
biometricNotice = 'biometricUpgrade';155
}156
lockBackground = prefs['lockBackground'] != false;157
idleMinutes = idleChoices.contains(prefs['idleMinutes'])158
? prefs['idleMinutes']159
: 5;160
monitorSeconds = [5, 10, 30, 60].contains(prefs['monitorSeconds'])161
? prefs['monitorSeconds']162
: 10;163
if (prefs['editor'] is Map) {164
editorPrefs = Map<String, dynamic>.from(prefs['editor']);165
}166
accepted = await legal.load();167
initialized = await vault.exists();168
} catch (e) {169
startupError = e is Failure ? e.code : 'storageError';170
}171
try {172
_incoming = documents.incoming.listen((docs) {173
pendingIncoming.addAll(docs);174
notifyListeners();175
});176
} catch (_) {177
/* No document support on this platform. */178
}179
ready = true;180
_timer = Timer.periodic(const Duration(seconds: 1), (_) {181
if (vault.unlocked &&182
DateTime.now().difference(_lastActivity) >=183
Duration(minutes: idleMinutes)) {184
lock();185
}186
});187
notifyListeners();188
}190
void activity() {191
_lastActivity = DateTime.now();192
}194
void changed() {195
notifyListeners();196
}198
Future<T> withSystemDialog<T>(Future<T> Function() action) async {199
systemDialog = true;200
try {201
return await action();202
} finally {203
systemDialog = false;204
activity();205
}206
}208
Future<void> saveSettings() async {209
await store.write(210
'preferences',211
jsonEncode({212
'language': locale?.languageCode,213
'theme': theme.name,214
'biometric': biometric,215
'biometricVersion': biometricVersion,216
'lockBackground': lockBackground,217
'idleMinutes': idleMinutes,218
'monitorSeconds': monitorSeconds,219
'editor': editorPrefs,220
}),221
);222
notifyListeners();223
}225
Future<void> acceptLicense() async {226
await legal.accept(guidanceDisplayed: true);227
accepted = true;228
notifyListeners();229
}231
Future<void> openVault(String password, {bool create = false}) async {232
if (!accepted) throw const Failure('licenseError');233
if (create) {234
try {235
await vault.create(password);236
} finally {237
initialized = await vault.exists();238
notifyListeners();239
}240
} else {241
await vault.unlock(password);242
}243
activity();244
biometricAttempted = false;245
if (biometric && biometricVersion < 2) await _upgradeBiometric();246
notifyListeners();247
}249
/// Moves a version 1 biometric key (which the device passcode could also250
/// release) to version 2 storage. Writing does not prompt on iOS; Android251
/// asks for a biometric confirmation, which the user may decline.252
Future<void> _upgradeBiometric() async {253
try {254
await vault.enableBiometric(biometricStore(biometricReasonText));255
biometricVersion = 2;256
biometricNotice = null;257
} catch (_) {258
biometric = false;259
biometricVersion = 2;260
biometricNotice = 'biometricReenable';261
}262
await _deleteLegacyBiometric();263
try {264
await saveSettings();265
} catch (_) {266
/* Retried with the next settings change. */267
}268
}270
Future<void> _deleteLegacyBiometric() async {271
try {272
await DeviceStore.legacyBiometric().delete('key');273
} catch (_) {274
/* Nothing stored or not accessible; it is never read again. */275
}276
}278
/// Unlocks with the data key released by biometric authentication.279
///280
/// The key is stored by the system so that only a successful strong281
/// biometric check releases it; a `true` from a dialog alone opens282
/// nothing. On failure the lock screen offers the master password.283
Future<void> biometricUnlock(String reason) async {284
if (!accepted) throw const Failure('licenseError');285
if (!biometric || biometricVersion < 2) {286
throw const Failure('biometricUnavailable');287
}288
authenticating = true;289
biometricAttempted = true;290
notifyListeners();291
try {292
await withSystemDialog(293
() => vault.unlockBiometric(biometricStore(reason)),294
);295
biometricNotice = null;296
activity();297
} on Failure catch (e) {298
if (e.code == 'biometricInvalid') {299
// Removed by the system (biometrics changed) or stale. It can never300
// open the Vault again; delete it and ask for the password.301
biometric = false;302
biometricNotice = 'biometricInvalid';303
try {304
await biometricStore(reason).delete('key');305
} catch (_) {306
/* Already gone. */307
}308
try {309
await saveSettings();310
} catch (_) {311
/* Biometric stays off in memory. */312
}313
}314
rethrow;315
} on PlatformException catch (e) {316
throw Failure(biometricFailure(e));317
} finally {318
authenticating = false;319
notifyListeners();320
}321
}323
Future<void> setBiometric(bool enable, String reason) async {324
authenticating = true;325
notifyListeners();326
try {327
if (enable) {328
if (!await biometricSupported()) {329
throw const Failure('biometricUnavailable');330
}331
// Writing the key needs no separate yes/no dialog: on Android the332
// write itself is confirmed with a strong biometric, and on iOS the333
// first read is. A failed write leaves biometric unlock off.334
await withSystemDialog(335
() => vault.enableBiometric(biometricStore(reason)),336
);337
biometricVersion = 2;338
biometricNotice = null;339
} else {340
await biometricStore(reason).delete('key');341
}342
biometric = enable;343
await saveSettings();344
} on PlatformException catch (e) {345
throw Failure(biometricFailure(e));346
} finally {347
authenticating = false;348
notifyListeners();349
}350
}352
/// Changes the master password. The Vault gets a new data key, so a353
/// biometric key stored before the change can no longer open it; when354
/// biometric unlock is on, the new key is stored in its place, or355
/// biometric unlock is turned off if that is not possible.356
Future<void> changePassword(357
String current,358
String next,359
String reason,360
) async {361
await vault.changePassword(current, next);362
activity();363
if (!biometric) return;364
try {365
await withSystemDialog(366
() => vault.enableBiometric(biometricStore(reason)),367
);368
biometricVersion = 2;369
} catch (_) {370
biometric = false;371
biometricNotice = 'biometricReenable';372
try {373
await biometricStore(reason).delete('key');374
} catch (_) {375
/* The stored key no longer opens the Vault. */376
}377
await saveSettings();378
throw const Failure('passwordChangedBiometricOff');379
}380
await saveSettings();381
}383
void lock() {384
final drafts = <Draft>[];385
for (final source in draftSources.toList()) {386
try {387
final draft = source();388
if (draft != null) drafts.add(draft);389
} catch (_) {390
/* A broken editor must not prevent locking. */391
}392
}393
ssh.disconnect(clear: true);394
vault.lock(drafts: drafts);395
biometricAttempted = false;396
onLocked?.call();397
notifyListeners();398
}400
@override401
void didChangeAppLifecycleState(AppLifecycleState state) {402
foreground = state == AppLifecycleState.resumed;403
if ((state == AppLifecycleState.paused ||404
state == AppLifecycleState.hidden) &&405
!systemDialog) {406
// Returning from the background offers the biometric prompt again.407
biometricAttempted = false;408
}409
if (state == AppLifecycleState.resumed) {410
if (vault.unlocked &&411
DateTime.now().difference(_lastActivity) >=412
Duration(minutes: idleMinutes)) {413
lock();414
}415
} else if ((state == AppLifecycleState.hidden ||416
state == AppLifecycleState.paused ||417
state == AppLifecycleState.detached) &&418
!systemDialog &&419
lockBackground) {420
lock();421
}422
notifyListeners();423
}425
@override426
void dispose() {427
_timer?.cancel();428
_incoming?.cancel();429
WidgetsBinding.instance.removeObserver(this);430
ssh.dispose();431
vault.lock();432
super.dispose();433
}434
}SHA-256
c41b2596df8aa3f9c08d3e9b3f5062af518cf44417188b516cf69638353198afCH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.
SHA-256 للأرشيف
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655