CH-J SM Mobileδιαχείριση διακομιστή πάνω από SSH
Μενού
Δημοσίευση πηγής

CH-J SM Mobile

Περιηγηθείτε σε καταλόγους και αρχεία για μια συγκεκριμένη έκδοση εφαρμογών.

Κατεβάστε την πηγή ZIP
CH-J / SM Mobile 134 αρχεία
sm-mobile-20261016T050549Z-3678f2a05fa9 Προστέθηκε: · Χτισμένο:
Αρχεία
vault.dart Προστέθηκε: Ημερομηνία που δεν έχει καταγραφεί 7,3 KB · 242 γραμμές
Λήψη αρχείου
1import 'dart:async';
2import 'dart:convert';
3import 'dart:isolate';
4import 'dart:math';
5import 'dart:typed_data';
6import 'package:cryptography/cryptography.dart';
7import '../core/failure.dart';
8import '../models/server_profile.dart';
9import 'secure_store.dart';
11const _aad = 'CHJ_SM_MOBILE_VAULT_V1';
12Future<Uint8List> _derive(String password, List<int> salt) =>
13 Isolate.run(() async {
14 final key = await Pbkdf2(
15 macAlgorithm: Hmac.sha256(),
16 iterations: 600000,
17 bits: 256,
18 ).deriveKey(secretKey: SecretKey(utf8.encode(password)), nonce: salt);
19 return Uint8List.fromList(await key.extractBytes());
20 });
22class Vault {
23 final SecureStore store;
24 Uint8List? _key;
25 Map<String, dynamic>? _data, _envelope;
26 int generation = 0;
27 Future<void> _queue = Future.value();
28 Vault(this.store);
29 bool get unlocked => _key != null && _data != null;
30 Future<bool> exists() async => await store.read('vault') != null;
31 List<ServerProfile> get profiles {
32 _require();
33 return (_data!['profiles'] as List)
34 .map((e) => ServerProfile.fromJson(Map<String, dynamic>.from(e)))
35 .toList();
36 }
38 void _require() {
39 if (!unlocked) throw const Failure('locked');
40 }
42 Future<void> create(String password) async {
43 final epoch = generation;
44 if (password.runes.length < 12 || password.length > 128) {
45 throw const Failure('weakPassword');
46 }
47 if (await exists()) throw const Failure('storageError');
48 final aes = AesGcm.with256bits();
49 final random = Random.secure();
50 final salt = List<int>.generate(16, (_) => random.nextInt(256));
51 final derived = await _derive(password, salt);
52 final key = Uint8List.fromList(
53 await (await aes.newSecretKey()).extractBytes(),
54 );
55 try {
56 final wrapped = await aes.encrypt(
57 key,
58 secretKey: SecretKey(derived),
59 aad: utf8.encode('$_aad:wrap'),
60 );
61 final data = <String, dynamic>{
62 'profiles': [],
63 'secrets': {},
64 'hosts': {},
65 };
66 final box = await _seal(data, key);
67 if (epoch != generation) throw const Failure('locked');
68 final envelope = {
69 'version': 1,
70 'salt': base64Encode(salt),
71 'wrap': base64Encode(wrapped.concatenation()),
72 'data': box,
73 };
74 await store.write('vault', jsonEncode(envelope));
75 if (epoch != generation) throw const Failure('locked');
76 _envelope = envelope;
77 _data = data;
78 _key = Uint8List.fromList(key);
79 } finally {
80 derived.fillRange(0, derived.length, 0);
81 key.fillRange(0, key.length, 0);
82 }
83 }
85 Future<Map<String, dynamic>> _readEnvelope() async {
86 final text = await store.read('vault');
87 if (text == null) throw const Failure('storageError');
88 final e = Map<String, dynamic>.from(jsonDecode(text));
89 if (e['version'] != 1) throw const Failure('storageError');
90 return e;
91 }
93 Future<void> unlock(String password) async {
94 final epoch = generation;
95 Uint8List? derived, key;
96 try {
97 final e = await _readEnvelope();
98 derived = await _derive(password, base64Decode(e['salt']));
99 key = Uint8List.fromList(
100 await AesGcm.with256bits().decrypt(
101 SecretBox.fromConcatenation(
102 base64Decode(e['wrap']),
103 nonceLength: 12,
104 macLength: 16,
105 ),
106 secretKey: SecretKey(derived),
107 aad: utf8.encode('$_aad:wrap'),
108 ),
109 );
110 await _open(e, key, epoch);
111 } on Failure {
112 rethrow;
113 } catch (_) {
114 throw const Failure('unlockFailed');
115 } finally {
116 derived?.fillRange(0, derived.length, 0);
117 key?.fillRange(0, key.length, 0);
118 }
119 }
121 Future<void> _open(Map<String, dynamic> e, List<int> key, int epoch) async {
122 final plaintext = await AesGcm.with256bits().decrypt(
123 SecretBox.fromConcatenation(
124 base64Decode(e['data']),
125 nonceLength: 12,
126 macLength: 16,
127 ),
128 secretKey: SecretKey(key),
129 aad: utf8.encode(_aad),
130 );
131 final data = Map<String, dynamic>.from(jsonDecode(utf8.decode(plaintext)));
132 if (epoch != generation) throw const Failure('locked');
133 _envelope = e;
134 _data = data;
135 _key = Uint8List.fromList(key);
136 }
138 Future<String> _seal(Map<String, dynamic> data, List<int> key) async =>
139 base64Encode(
140 (await AesGcm.with256bits().encrypt(
141 utf8.encode(jsonEncode(data)),
142 secretKey: SecretKey(key),
143 aad: utf8.encode(_aad),
144 )).concatenation(),
145 );
146 Future<void> change(void Function(Map<String, dynamic>) update) {
147 final epoch = generation;
148 final result = _queue.then((_) async {
149 _require();
150 if (epoch != generation) throw const Failure('locked');
151 final data = Map<String, dynamic>.from(jsonDecode(jsonEncode(_data)));
152 update(data);
153 final envelope = {..._envelope!, 'data': await _seal(data, _key!)};
154 if (epoch != generation) throw const Failure('locked');
155 await store.write('vault', jsonEncode(envelope));
156 if (epoch != generation) throw const Failure('locked');
157 _envelope = envelope;
158 _data = data;
159 });
160 _queue = result.catchError((Object _) {});
161 return result;
162 }
164 Future<void> save(
165 ServerProfile profile, {
166 String? password,
167 String? privateKey,
168 bool clearSecrets = false,
169 }) => change((d) {
170 final list = d['profiles'] as List;
171 final index = list.indexWhere((p) => p['id'] == profile.id);
172 if (index < 0) {
173 list.add(profile.toJson());
174 } else {
175 list[index] = profile.toJson();
176 }
177 final secrets = d['secrets'] as Map;
178 if (clearSecrets) secrets.remove(profile.id);
179 final previous = Map<String, dynamic>.from(secrets[profile.id] ?? {});
180 if (password != null) {
181 if (password.isEmpty) {
182 previous.remove('password');
183 } else {
184 previous['password'] = password;
185 }
186 }
187 if (privateKey != null) {
188 if (privateKey.isEmpty) {
189 previous.remove('privateKey');
190 } else {
191 previous['privateKey'] = privateKey;
192 }
193 }
194 secrets[profile.id] = previous;
195 });
196 Future<void> deleteProfile(String id) => change((d) {
197 (d['profiles'] as List).removeWhere((p) => p['id'] == id);
198 (d['secrets'] as Map).remove(id);
199 });
200 String? secret(String id, String field) {
201 _require();
202 return _data!['secrets'][id]?[field] as String?;
203 }
205 String? fingerprint(String endpoint) {
206 _require();
207 return _data!['hosts'][endpoint] as String?;
208 }
210 Future<void> trust(String endpoint, String fingerprint) => change((d) {
211 d['hosts'][endpoint] = fingerprint;
212 });
213 Future<void> enableBiometric(SecureStore gatedStore) async {
214 _require();
215 final epoch = generation;
216 await gatedStore.write('key', base64Encode(_key!));
217 if (epoch != generation) {
218 await gatedStore.delete('key');
219 throw const Failure('locked');
220 }
221 }
223 Future<void> unlockBiometric(SecureStore gatedStore) async {
224 final epoch = generation;
225 final value = await gatedStore.read('key');
226 if (value == null) throw const Failure('unlockFailed');
227 final key = base64Decode(value);
228 try {
229 await _open(await _readEnvelope(), key, epoch);
230 } finally {
231 key.fillRange(0, key.length, 0);
232 }
233 }
235 void lock() {
236 generation++;
237 _key?.fillRange(0, _key!.length, 0);
238 _key = null;
239 _data = null;
240 _envelope = null;
241 }
SHA-2568406e2e7bc9b18e1524bd099e343e9ccb1345df396b2ff4db6d221e050f10d70

CH-J Proprietary Software License 1.14 · Η πηγή παρέχεται στο CH-J Proprietary Software License 1.14. Η διαθεσιμότητά του δεν αλλάζει τους όρους της άδειας χρήσης ούτε χορηγεί πρόσθετα δικαιώματα.

Archive SHA-256c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65