CH-J SM Mobileمدیریت سرور از طریق SSH
منو
منبع منتشرشده

CH-J SM Mobile

پوشه‌ها و فایل‌های یک نسخهٔ مشخص از برنامه را مرور کنید.

دانلود ZIP منبع
CH-J / SM Mobile 175 فایل
sm-mobile-20261016T185235Z-8e93ecc1b59a افزوده‌شده: · ساخته‌شده:
vault.dart افزوده‌شده: تاریخ ثبت نشده است ۲۰٫۵ KB · 625 خط
کد دانلود فایل
1import 'dart:async';
2import 'dart:convert';
3import 'dart:isolate';
4import 'dart:math';
5import 'dart:typed_data';
6import 'package:characters/characters.dart';
7import 'package:cryptography/cryptography.dart';
8import '../core/failure.dart';
9import '../models/server_profile.dart';
10import 'blob_store.dart';
11import 'secure_store.dart';
13const _aad = 'CHJ_SM_MOBILE_VAULT_V1';
15/// Allowed master password length, in user-perceived characters (grapheme
16/// clusters, the unit a text field counts). Spaces are part of the password.
17const minPasswordLength = 4, maxPasswordLength = 64;
19int passwordLength(String password) => password.characters.length;
21/// Rejects a new master password outside [minPasswordLength] and
22/// [maxPasswordLength]. Only applied when a password is set: an existing
23/// Vault opens with whatever password it was created with.
24void checkNewPassword(String password) {
25 final n = passwordLength(password);
26 if (n < minPasswordLength || n > maxPasswordLength) {
27 throw const Failure('passwordLength');
28 }
31/// A rough strength estimate (0 = very weak … 4 = strong) for the indicator
32/// shown while a password is typed. It is advice only: every password of
33/// allowed length is accepted, and the Vault encryption is the same for all.
34int passwordStrength(String password) {
35 final n = passwordLength(password);
36 if (n == 0) return 0;
37 var pool = 0;
38 if (RegExp(r'[a-z]').hasMatch(password)) pool += 26;
39 if (RegExp(r'[A-Z]').hasMatch(password)) pool += 26;
40 if (RegExp(r'[0-9]').hasMatch(password)) pool += 10;
41 if (RegExp(r'[^A-Za-z0-9]').hasMatch(password)) pool += 33;
42 // Repeated characters add little.
43 final distinct = password.characters.toSet().length;
44 final bits = distinct * (log(max(pool, 10)) / ln2) + (n - distinct) * 1.5;
45 if (bits < 28) return 0;
46 if (bits < 40) return 1;
47 if (bits < 60) return 2;
48 if (bits < 80) return 3;
49 return 4;
52/// Key derivation used by an envelope. Version 1 Vaults use PBKDF2; new and
53/// re-keyed Vaults use Argon2id (RFC 9106), which is memory-hard.
54class Kdf {
55 final String name;
56 final int memory, iterations, parallelism;
57 const Kdf.argon2id({
58 this.memory = 65536,
59 this.iterations = 3,
60 this.parallelism = 1,
61 }) : name = 'argon2id';
62 const Kdf.pbkdf2()
63 : name = 'pbkdf2-sha256',
64 memory = 0,
65 iterations = 600000,
66 parallelism = 1;
68 static const current = Kdf.argon2id();
70 Map<String, dynamic> toJson() => name == 'argon2id'
71 ? {
72 'name': name,
73 'memory': memory,
74 'iterations': iterations,
75 'parallelism': parallelism,
76 }
77 : {'name': name, 'iterations': iterations};
79 static Kdf of(Map<String, dynamic> envelope) {
80 if (envelope['version'] == 1) return const Kdf.pbkdf2();
81 final k = Map<String, dynamic>.from(envelope['kdf'] as Map);
82 if (k['name'] != 'argon2id') throw const Failure('storageError');
83 final memory = k['memory'], iterations = k['iterations'];
84 final parallelism = k['parallelism'];
85 // Bounds keep a damaged envelope from demanding absurd resources, and
86 // keep the parameters from ever being weaker than the default.
87 if (memory is! int ||
88 iterations is! int ||
89 parallelism is! int ||
90 memory < 65536 ||
91 memory > 1048576 ||
92 iterations < 3 ||
93 iterations > 20 ||
94 parallelism < 1 ||
95 parallelism > 8) {
96 throw const Failure('storageError');
97 }
98 return Kdf.argon2id(
99 memory: memory,
100 iterations: iterations,
101 parallelism: parallelism,
102 );
103 }
105 Future<Uint8List> derive(String password, List<int> salt) {
106 final name = this.name, memory = this.memory;
107 final iterations = this.iterations, parallelism = this.parallelism;
108 return Isolate.run(() async {
109 final KdfAlgorithm algorithm = name == 'argon2id'
110 ? Argon2id(
111 memory: memory,
112 iterations: iterations,
113 parallelism: parallelism,
114 hashLength: 32,
115 )
116 : Pbkdf2(
117 macAlgorithm: Hmac.sha256(),
118 iterations: iterations,
119 bits: 256,
120 );
121 final key = await algorithm.deriveKey(
122 secretKey: SecretKey(utf8.encode(password)),
123 nonce: salt,
124 );
125 return Uint8List.fromList(await key.extractBytes());
126 });
127 }
130/// An unsaved editor buffer kept across a lock. It is stored encrypted with
131/// the Vault data key and only readable after the next unlock.
132class Draft {
133 final Map<String, dynamic> json;
134 const Draft(this.json);
137class Vault {
138 final SecureStore store;
139 final BlobStore blobs;
140 Uint8List? _key;
141 Map<String, dynamic>? _data, _envelope;
142 List<Draft> _drafts = const [];
143 int generation = 0;
144 Future<void> _queue = Future.value();
145 Vault(this.store, {BlobStore? blobs}) : blobs = blobs ?? MemoryBlobStore();
146 bool get unlocked => _key != null && _data != null;
147 Future<bool> exists() async => await store.read('vault') != null;
149 /// The key derivation of the open Vault ('argon2id' or 'pbkdf2-sha256').
150 String? get kdfName => _envelope == null ? null : Kdf.of(_envelope!).name;
151 List<ServerProfile> get profiles {
152 _require();
153 return (_data!['profiles'] as List)
154 .map((e) => ServerProfile.fromJson(Map<String, dynamic>.from(e)))
155 .toList();
156 }
158 /// Unsaved editor buffers recovered at the last unlock.
159 List<Draft> get drafts {
160 _require();
161 return _drafts;
162 }
164 void _require() {
165 if (!unlocked) throw const Failure('locked');
166 }
168 static List<int> _salt() {
169 final random = Random.secure();
170 return List<int>.generate(16, (_) => random.nextInt(256));
171 }
173 static String _newKeyId() {
174 final random = Random.secure();
175 return List.generate(
176 16,
177 (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
178 ).join();
179 }
181 /// A version 2 envelope for [data] sealed with [key], whose key is wrapped
182 /// with a key derived from [password].
183 Future<Map<String, dynamic>> _envelopeFor(
184 String password,
185 Map<String, dynamic> data,
186 List<int> key,
187 String keyId,
188 ) async {
189 final aes = AesGcm.with256bits();
190 final salt = _salt();
191 const kdf = Kdf.current;
192 final derived = await kdf.derive(password, salt);
193 try {
194 final wrapped = await aes.encrypt(
195 key,
196 secretKey: SecretKey(derived),
197 aad: utf8.encode('$_aad:wrap'),
198 );
199 return {
200 'version': 2,
201 'kdf': kdf.toJson(),
202 'keyId': keyId,
203 'salt': base64Encode(salt),
204 'wrap': base64Encode(wrapped.concatenation()),
205 'data': await _seal(data, key),
206 };
207 } finally {
208 derived.fillRange(0, derived.length, 0);
209 }
210 }
212 Future<void> create(String password) async {
213 final epoch = generation;
214 checkNewPassword(password);
215 if (await exists()) throw const Failure('storageError');
216 final key = Uint8List.fromList(
217 await (await AesGcm.with256bits().newSecretKey()).extractBytes(),
218 );
219 try {
220 final data = <String, dynamic>{
221 'profiles': [],
222 'secrets': {},
223 'hosts': {},
224 };
225 final envelope = await _envelopeFor(password, data, key, _newKeyId());
226 if (epoch != generation) throw const Failure('locked');
227 await store.write('vault', jsonEncode(envelope));
228 if (epoch != generation) throw const Failure('locked');
229 _envelope = envelope;
230 _data = data;
231 _drafts = const [];
232 _key = Uint8List.fromList(key);
233 } finally {
234 key.fillRange(0, key.length, 0);
235 }
236 }
238 Future<Map<String, dynamic>> _readEnvelope() async {
239 final text = await store.read('vault');
240 if (text == null) throw const Failure('storageError');
241 final e = Map<String, dynamic>.from(jsonDecode(text));
242 if (e['version'] != 1 && e['version'] != 2) {
243 throw const Failure('storageError');
244 }
245 return e;
246 }
248 /// The data key wrapped in [e], unwrapped with [password]. Throws
249 /// 'unlockFailed' for a wrong password.
250 Future<Uint8List> _unwrap(Map<String, dynamic> e, String password) async {
251 Uint8List? derived;
252 try {
253 derived = await Kdf.of(e).derive(password, base64Decode(e['salt']));
254 return Uint8List.fromList(
255 await AesGcm.with256bits().decrypt(
256 SecretBox.fromConcatenation(
257 base64Decode(e['wrap']),
258 nonceLength: 12,
259 macLength: 16,
260 ),
261 secretKey: SecretKey(derived),
262 aad: utf8.encode('$_aad:wrap'),
263 ),
264 );
265 } on Failure {
266 rethrow;
267 } catch (_) {
268 throw const Failure('unlockFailed');
269 } finally {
270 derived?.fillRange(0, derived.length, 0);
271 }
272 }
274 /// Waits for writes queued before a lock (such as stashed drafts), so an
275 /// unlock never reads an envelope that is about to be replaced.
276 Future<void> _settled() => _queue.catchError((Object _) {});
278 Future<void> unlock(String password) async {
279 await _settled();
280 final epoch = generation;
281 Uint8List? key;
282 try {
283 final e = await _readEnvelope();
284 key = await _unwrap(e, password);
285 await _open(e, key, epoch);
286 if (e['version'] == 1) _upgrade(password, epoch);
287 } on Failure {
288 rethrow;
289 } catch (_) {
290 throw const Failure('unlockFailed');
291 } finally {
292 key?.fillRange(0, key.length, 0);
293 }
294 }
296 /// Re-wraps the unchanged data key of a version 1 (PBKDF2) Vault with
297 /// Argon2id. The old envelope stays in place until the new one is written,
298 /// so a failure leaves the Vault as it was.
299 void _upgrade(String password, int epoch) {
300 final result = _queue.then((_) async {
301 if (epoch != generation || !unlocked) return;
302 final current = await _readEnvelope();
303 if (current['version'] != 1) return;
304 final envelope = await _envelopeFor(password, _data!, _key!, 'v1');
305 if (epoch != generation) return;
306 await store.write('vault', jsonEncode(envelope));
307 _envelope = envelope;
308 });
309 _queue = result.catchError((Object _) {});
310 }
312 /// Completes when queued background writes (an upgrade, stashed drafts)
313 /// are done. Used by tests and before reporting a change as finished.
314 Future<void> get idle => _settled();
316 Future<void> _open(Map<String, dynamic> e, List<int> key, int epoch) async {
317 final plaintext = await AesGcm.with256bits().decrypt(
318 SecretBox.fromConcatenation(
319 base64Decode(e['data']),
320 nonceLength: 12,
321 macLength: 16,
322 ),
323 secretKey: SecretKey(key),
324 aad: utf8.encode(_aad),
325 );
326 final data = Map<String, dynamic>.from(jsonDecode(utf8.decode(plaintext)));
327 final drafts = await _readDrafts(e, key);
328 if (epoch != generation) throw const Failure('locked');
329 _envelope = e;
330 _data = data;
331 _drafts = drafts;
332 _key = Uint8List.fromList(key);
333 }
335 static String _draftName(Map<String, dynamic> e) =>
336 'drafts-${e['keyId'] ?? 'v1'}';
338 Future<List<Draft>> _readDrafts(Map<String, dynamic> e, List<int> key) async {
339 try {
340 final box = await blobs.read(_draftName(e));
341 if (box == null) return const [];
342 final plain = await AesGcm.with256bits().decrypt(
343 SecretBox.fromConcatenation(box, nonceLength: 12, macLength: 16),
344 secretKey: SecretKey(key),
345 aad: utf8.encode('$_aad:drafts'),
346 );
347 return [
348 for (final d in jsonDecode(utf8.decode(plain)) as List)
349 Draft(Map<String, dynamic>.from(d as Map)),
350 ];
351 } catch (_) {
352 // Unreadable drafts never block access to the Vault itself.
353 return const [];
354 }
355 }
357 Future<Uint8List> _sealDrafts(List<Draft> drafts, List<int> key) async =>
358 (await AesGcm.with256bits().encrypt(
359 utf8.encode(jsonEncode([for (final d in drafts) d.json])),
360 secretKey: SecretKey(key),
361 aad: utf8.encode('$_aad:drafts'),
362 )).concatenation();
364 /// Keeps only [keep] of the recovered drafts (sealed again with the data
365 /// key); the others are gone for good.
366 Future<void> keepDrafts(List<Draft> keep) {
367 if (keep.isEmpty) return clearDrafts();
368 final epoch = generation;
369 final result = _queue.then((_) async {
370 _require();
371 if (epoch != generation) throw const Failure('locked');
372 await blobs.write(_draftName(_envelope!), await _sealDrafts(keep, _key!));
373 _drafts = List.unmodifiable(keep);
374 });
375 _queue = result.catchError((Object _) {});
376 return result;
377 }
379 /// Removes recovered drafts once the user restored or discarded them.
380 Future<void> clearDrafts() {
381 final epoch = generation;
382 final result = _queue.then((_) async {
383 _require();
384 if (epoch != generation) throw const Failure('locked');
385 await blobs.delete(_draftName(_envelope!));
386 _drafts = const [];
387 });
388 _queue = result.catchError((Object _) {});
389 return result;
390 }
392 Future<String> _seal(Map<String, dynamic> data, List<int> key) async =>
393 base64Encode(
394 (await AesGcm.with256bits().encrypt(
395 utf8.encode(jsonEncode(data)),
396 secretKey: SecretKey(key),
397 aad: utf8.encode(_aad),
398 )).concatenation(),
399 );
400 Future<void> change(void Function(Map<String, dynamic>) update) {
401 final epoch = generation;
402 final result = _queue.then((_) async {
403 _require();
404 if (epoch != generation) throw const Failure('locked');
405 final data = Map<String, dynamic>.from(jsonDecode(jsonEncode(_data)));
406 update(data);
407 final envelope = {..._envelope!, 'data': await _seal(data, _key!)};
408 if (epoch != generation) throw const Failure('locked');
409 await store.write('vault', jsonEncode(envelope));
410 if (epoch != generation) throw const Failure('locked');
411 _envelope = envelope;
412 _data = data;
413 });
414 _queue = result.catchError((Object _) {});
415 return result;
416 }
418 /// Changes the master password.
419 ///
420 /// The current password is checked against the stored envelope. A new
421 /// random data key then replaces the old one: the data and any drafts are
422 /// sealed again, and the new envelope is verified with the new password
423 /// before it replaces the old envelope in a single write. Until that write
424 /// succeeds the old password and data stay valid; afterwards nothing
425 /// derived from the old password or the old data key (such as a stored
426 /// biometric key) can open the Vault.
427 Future<void> changePassword(String current, String next) {
428 final epoch = generation;
429 final result = _queue.then((_) async {
430 _require();
431 checkNewPassword(next);
432 final stored = await _readEnvelope();
433 final check = await _unwrap(stored, current);
434 try {
435 if (!_same(check, _key!)) throw const Failure('unlockFailed');
436 } finally {
437 check.fillRange(0, check.length, 0);
438 }
439 if (epoch != generation) throw const Failure('locked');
440 final key = Uint8List.fromList(
441 await (await AesGcm.with256bits().newSecretKey()).extractBytes(),
442 );
443 try {
444 final envelope = await _envelopeFor(next, _data!, key, _newKeyId());
445 // Verify before replacing anything.
446 final verify = await _unwrap(envelope, next);
447 try {
448 if (!_same(verify, key)) throw const Failure('storageError');
449 await AesGcm.with256bits().decrypt(
450 SecretBox.fromConcatenation(
451 base64Decode(envelope['data']),
452 nonceLength: 12,
453 macLength: 16,
454 ),
455 secretKey: SecretKey(verify),
456 aad: utf8.encode(_aad),
457 );
458 } finally {
459 verify.fillRange(0, verify.length, 0);
460 }
461 if (epoch != generation) throw const Failure('locked');
462 // Drafts first, under the new key id: until the envelope is written
463 // they are unused, and the old drafts stay readable with the old key.
464 final oldDrafts = _draftName(stored);
465 if (_drafts.isNotEmpty) {
466 await blobs.write(
467 _draftName(envelope),
468 await _sealDrafts(_drafts, key),
469 );
470 }
471 await store.write('vault', jsonEncode(envelope));
472 _envelope = envelope;
473 _key!.fillRange(0, _key!.length, 0);
474 _key = Uint8List.fromList(key);
475 try {
476 await blobs.delete(oldDrafts);
477 } catch (_) {
478 /* Sealed with a key that no longer exists. */
479 }
480 } finally {
481 key.fillRange(0, key.length, 0);
482 }
483 });
484 _queue = result.catchError((Object _) {});
485 return result;
486 }
488 static bool _same(List<int> a, List<int> b) {
489 if (a.length != b.length) return false;
490 var diff = 0;
491 for (var i = 0; i < a.length; i++) {
492 diff |= a[i] ^ b[i];
493 }
494 return diff == 0;
495 }
497 Future<void> save(
498 ServerProfile profile, {
499 String? password,
500 String? privateKey,
501 bool clearSecrets = false,
502 }) => change((d) {
503 final list = d['profiles'] as List;
504 final index = list.indexWhere((p) => p['id'] == profile.id);
505 if (index < 0) {
506 list.add(profile.toJson());
507 } else {
508 list[index] = profile.toJson();
509 }
510 final secrets = d['secrets'] as Map;
511 if (clearSecrets) secrets.remove(profile.id);
512 final previous = Map<String, dynamic>.from(secrets[profile.id] ?? {});
513 if (password != null) {
514 if (password.isEmpty) {
515 previous.remove('password');
516 } else {
517 previous['password'] = password;
518 }
519 }
520 if (privateKey != null) {
521 if (privateKey.isEmpty) {
522 previous.remove('privateKey');
523 } else {
524 previous['privateKey'] = privateKey;
525 }
526 }
527 secrets[profile.id] = previous;
528 });
529 Future<void> deleteProfile(String id) => change((d) {
530 (d['profiles'] as List).removeWhere((p) => p['id'] == id);
531 (d['secrets'] as Map).remove(id);
532 });
533 String? secret(String id, String field) {
534 _require();
535 return _data!['secrets'][id]?[field] as String?;
536 }
538 String? fingerprint(String endpoint) {
539 _require();
540 return _data!['hosts'][endpoint] as String?;
541 }
543 Future<void> trust(String endpoint, String fingerprint) => change((d) {
544 d['hosts'][endpoint] = fingerprint;
545 });
547 /// Small encrypted settings kept with the Vault data (recent and favorite
548 /// files). Not part of the profile export.
549 Map<String, dynamic> section(String name) {
550 _require();
551 return Map<String, dynamic>.from(
552 jsonDecode(jsonEncode(_data![name] ?? {})) as Map,
553 );
554 }
556 Future<void> setSection(String name, Map<String, dynamic> value) =>
557 change((d) => d[name] = value);
559 Future<void> enableBiometric(SecureStore gatedStore) async {
560 _require();
561 final epoch = generation;
562 await gatedStore.write('key', base64Encode(_key!));
563 if (epoch != generation) {
564 await gatedStore.delete('key');
565 throw const Failure('locked');
566 }
567 }
569 /// Opens the Vault with the data key released by the operating system
570 /// after biometric authentication. Throws 'biometricInvalid' when there is
571 /// no key (removed by the system after a biometric change) or the key no
572 /// longer opens this Vault (for example after a password change).
573 Future<void> unlockBiometric(SecureStore gatedStore) async {
574 await _settled();
575 final epoch = generation;
576 final value = await gatedStore.read('key');
577 if (value == null) throw const Failure('biometricInvalid');
578 final key = base64Decode(value);
579 try {
580 await _open(await _readEnvelope(), key, epoch);
581 } on Failure {
582 rethrow;
583 } catch (_) {
584 throw const Failure('biometricInvalid');
585 } finally {
586 key.fillRange(0, key.length, 0);
587 }
588 }
590 /// Locks the Vault. [drafts] are unsaved editor buffers; they are sealed
591 /// with the data key before it is erased, and written after any pending
592 /// change, so they survive the lock without ever being stored in clear.
593 void lock({List<Draft> drafts = const []}) {
594 final key = _key == null ? null : Uint8List.fromList(_key!);
595 final envelope = _envelope;
596 generation++;
597 _key?.fillRange(0, _key!.length, 0);
598 _key = null;
599 _data = null;
600 _envelope = null;
601 _drafts = const [];
602 if (key == null) return;
603 if (drafts.isEmpty || envelope == null) {
604 key.fillRange(0, key.length, 0);
605 return;
606 }
607 final result = _queue.then((_) async {
608 try {
609 // The envelope can only have changed through this Vault, whose
610 // pending writes are already done.
611 final current = await _readEnvelope();
612 if (current['keyId'] != envelope['keyId']) return;
613 // Drafts from an earlier lock that were never restored are kept.
614 final previous = await _readDrafts(current, key);
615 await blobs.write(
616 _draftName(current),
617 await _sealDrafts([...previous, ...drafts], key),
618 );
619 } finally {
620 key.fillRange(0, key.length, 0);
621 }
622 });
623 _queue = result.catchError((Object _) {});
624 }
SHA-2569bedd75afac47b757791978fded0e2d4d1994d1e67fa122a1fca28612039ebfa

CH-J Proprietary Software License 1.14 · کد منبع تحت CH-J Proprietary Software License 1.14 ارائه می‌شود. در دسترس بودن آن شرایط مجوز را تغییر نمی‌دهد و حق اضافه‌ای اعطا نمی‌کند.

SHA-256 بایگانی46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
فایل‌ها