Opublikowane źródło
Pobierz źródło ZIP
CH-J SM Mobile
Przeglądaj katalogi i pliki dla konkretnego wydania aplikacji.
CH-J / SM Mobile
134 pliki
sm-mobile-20261016T050549Z-3678f2a05fa9
Dodano: · Built:
1
import 'dart:async';2
import 'dart:convert';3
import 'dart:isolate';4
import 'dart:math';5
import 'dart:typed_data';6
import 'package:cryptography/cryptography.dart';7
import '../core/failure.dart';8
import '../models/server_profile.dart';9
import 'secure_store.dart';11
const _aad = 'CHJ_SM_MOBILE_VAULT_V1';12
Future<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
});22
class 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
}242
}SHA-256
8406e2e7bc9b18e1524bd099e343e9ccb1345df396b2ff4db6d221e050f10d70CH-J Proprietary Software License 1.14 · Źródło jest dostarczane pod CH-J Proprietary Software License 1.14. Jego dostępność nie zmienia warunków licencji ani nie przyznaje dodatkowych praw.
SHA-256
c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65