Nailathalang source
I-download ang source ZIP
CH-J SM Mobile
I-browse ang mga direktoryo at file ng isang partikular na release ng application.
CH-J / SM Mobile
175 file
sm-mobile-20261016T185235Z-8e93ecc1b59a
Idinagdag: · Binuo:
1
import 'dart:convert';2
import 'dart:math';3
import 'package:cryptography/cryptography.dart';4
import 'package:flutter/material.dart';5
import 'package:flutter/services.dart';6
import 'package:flutter_test/flutter_test.dart';7
import 'package:sm_mobile/core/app_controller.dart';8
import 'package:sm_mobile/core/failure.dart';9
import 'package:sm_mobile/l10n/strings.dart';10
import 'package:sm_mobile/models/server_profile.dart';11
import 'package:sm_mobile/services/blob_store.dart';12
import 'package:sm_mobile/services/secure_store.dart';13
import 'package:sm_mobile/services/vault.dart';14
import 'package:sm_mobile/ui/gates.dart';15
import 'support.dart';17
Matcher failure(String code) =>18
throwsA(isA<Failure>().having((f) => f.code, 'code', code));20
/// A Vault envelope exactly as SM Mobile 0.0.2 wrote it (version 1, PBKDF2).21
Future<String> legacyEnvelope(String password, Map data) async {22
const aad = 'CHJ_SM_MOBILE_VAULT_V1';23
final aes = AesGcm.with256bits();24
final random = Random.secure();25
final salt = List<int>.generate(16, (_) => random.nextInt(256));26
final derived =27
await (await Pbkdf2(28
macAlgorithm: Hmac.sha256(),29
iterations: 600000,30
bits: 256,31
).deriveKey(secretKey: SecretKey(utf8.encode(password)), nonce: salt))32
.extractBytes();33
final key = await (await aes.newSecretKey()).extractBytes();34
final wrapped = await aes.encrypt(35
key,36
secretKey: SecretKey(derived),37
aad: utf8.encode('$aad:wrap'),38
);39
final box = await aes.encrypt(40
utf8.encode(jsonEncode(data)),41
secretKey: SecretKey(key),42
aad: utf8.encode(aad),43
);44
return jsonEncode({45
'version': 1,46
'salt': base64Encode(salt),47
'wrap': base64Encode(wrapped.concatenation()),48
'data': base64Encode(box.concatenation()),49
});50
}52
/// Simulates the system biometric store: reads either release the key or53
/// fail like a cancelled prompt.54
class FakeBiometric extends MemoryStore {55
int reads = 0;56
bool cancel = false;57
@override58
Future<String?> read(String key) async {59
reads++;60
if (cancel) {61
throw PlatformException(62
code: 'Unexpected security result code',63
details: -128,64
);65
}66
return super.read(key);67
}68
}70
void main() {71
TestWidgetsFlutterBinding.ensureInitialized();73
group('master password length', () {74
test(75
'3 characters are rejected, 4, 12 and 64 accepted, 65 rejected',76
() async {77
await expectLater(78
Vault(MemoryStore()).create('abc'),79
failure('passwordLength'),80
);81
for (final length in [4, 12, 64]) {82
final v = Vault(MemoryStore());83
await v.create('x' * (length - 1) + '!');84
expect(v.unlocked, true, reason: '$length characters');85
v.lock();86
}87
await expectLater(88
Vault(MemoryStore()).create('y' * 65),89
failure('passwordLength'),90
);91
},92
);94
test(95
'a 4-digit PIN, spaces and special characters are kept as typed',96
() async {97
final store = MemoryStore();98
final v = Vault(store);99
await v.create('1234');100
v.lock();101
await v.unlock('1234');102
expect(v.unlocked, true);103
v.lock();105
final spaced = Vault(MemoryStore());106
await spaced.create(' p ä§ ');107
spaced.lock();108
// Not trimmed: the inner text alone does not open it.109
await expectLater(spaced.unlock('p ä§'), failure('unlockFailed'));110
await spaced.unlock(' p ä§ ');111
expect(spaced.unlocked, true);112
},113
);115
test('length counts user-perceived characters', () {116
expect(passwordLength('👍🏽👍🏽👍🏽👍🏽'), 4);117
expect(() => checkNewPassword('👍🏽👍🏽👍🏽'), throwsA(isA<Failure>()));118
checkNewPassword('👍🏽👍🏽👍🏽👍🏽');119
expect(passwordStrength('1234'), 0);120
expect(passwordStrength('correct horse battery staple 42!'), 4);121
});122
});124
group('key derivation', () {125
test('new Vaults use Argon2id with RFC 9106 parameters', () async {126
final store = MemoryStore();127
final v = Vault(store);128
await v.create('argon test');129
final e = jsonDecode(store.values['vault']!);130
expect(e['version'], 2);131
expect(e['kdf'], {132
'name': 'argon2id',133
'memory': 65536,134
'iterations': 3,135
'parallelism': 1,136
});137
expect(v.kdfName, 'argon2id');138
});140
test('weakened or unknown KDF parameters are refused', () async {141
final store = MemoryStore();142
final v = Vault(store);143
await v.create('argon test');144
v.lock();145
final e = jsonDecode(store.values['vault']!);146
e['kdf']['memory'] = 1024;147
store.values['vault'] = jsonEncode(e);148
await expectLater(v.unlock('argon test'), failure('storageError'));149
});151
test('a version 1 (PBKDF2) Vault opens with its old long password and is '152
'upgraded to Argon2id without data loss', () async {153
final long = 'L' * 100; // allowed before; above today's maximum154
final profile = ServerProfile(155
name: 'old',156
host: 'example.com',157
username: 'root',158
);159
final store = MemoryStore({160
'vault': await legacyEnvelope(long, {161
'profiles': [profile.toJson()],162
'secrets': {163
profile.id: {'password': 'kept'},164
},165
'hosts': {'example.com:22': 'SHA256:abc'},166
}),167
});168
final v = Vault(store);169
await v.unlock(long);170
await v.idle;171
expect(jsonDecode(store.values['vault']!)['version'], 2);172
expect(v.profiles.single.name, 'old');173
expect(v.secret(profile.id, 'password'), 'kept');174
v.lock();175
final reopened = Vault(store);176
await reopened.unlock(long);177
expect(reopened.fingerprint('example.com:22'), 'SHA256:abc');178
expect(reopened.kdfName, 'argon2id');179
}, timeout: const Timeout(Duration(minutes: 2)));180
});182
group('change password', () {183
late MemoryStore store;184
late Vault vault;185
late ServerProfile profile;186
setUp(() async {187
store = MemoryStore();188
vault = Vault(store);189
await vault.create('old password');190
profile = ServerProfile(191
name: 'web',192
host: 'web.example',193
username: 'admin',194
);195
await vault.save(profile, password: 'ssh-secret', privateKey: 'KEY');196
await vault.trust('web.example:22', 'SHA256:fp');197
await vault.setSection('documents', {198
'recent': ['a'],199
});200
});202
test('keeps all data; old password fails, new password works', () async {203
await vault.changePassword('old password', '9876');204
expect(vault.unlocked, true);205
vault.lock();206
await expectLater(vault.unlock('old password'), failure('unlockFailed'));207
await vault.unlock('9876');208
expect(vault.profiles.single.name, 'web');209
expect(vault.secret(profile.id, 'password'), 'ssh-secret');210
expect(vault.secret(profile.id, 'privateKey'), 'KEY');211
expect(vault.fingerprint('web.example:22'), 'SHA256:fp');212
expect(vault.section('documents'), {213
'recent': ['a'],214
});215
});217
test(218
'wrong current password or invalid new length change nothing',219
() async {220
final before = store.values['vault'];221
await expectLater(222
vault.changePassword('wrong', 'new password'),223
failure('unlockFailed'),224
);225
await expectLater(226
vault.changePassword('old password', 'abc'),227
failure('passwordLength'),228
);229
await expectLater(230
vault.changePassword('old password', 'z' * 65),231
failure('passwordLength'),232
);233
expect(store.values['vault'], before);234
vault.lock();235
await vault.unlock('old password');236
expect(vault.profiles, hasLength(1));237
},238
);240
test('a failed write keeps the old password and data working', () async {241
final before = store.values['vault'];242
store.failWrites = true;243
await expectLater(244
vault.changePassword('old password', 'new password'),245
throwsStateError,246
);247
store.failWrites = false;248
expect(store.values['vault'], before);249
vault.lock();250
await expectLater(vault.unlock('new password'), failure('unlockFailed'));251
await vault.unlock('old password');252
expect(vault.secret(profile.id, 'password'), 'ssh-secret');253
});255
test(256
'a biometric key stored before the change no longer opens the Vault',257
() async {258
final gated = MemoryStore();259
await vault.enableBiometric(gated);260
await vault.changePassword('old password', 'new password');261
vault.lock();262
await expectLater(263
vault.unlockBiometric(gated),264
failure('biometricInvalid'),265
);266
expect(vault.unlocked, false);267
},268
);269
});271
group('drafts', () {272
test(273
'unsaved buffers survive a lock encrypted and a password change',274
() async {275
final store = MemoryStore();276
final blobs = MemoryBlobStore();277
final vault = Vault(store, blobs: blobs);278
await vault.create('draft password');279
vault.lock(280
drafts: [281
const Draft({'name': 'nginx.conf', 'text': 'secret draft text'}),282
],283
);284
await vault.idle;285
final stored = blobs.values.values.single;286
expect(287
utf8.decode(stored, allowMalformed: true),288
isNot(contains('secret draft text')),289
);290
await vault.unlock('draft password');291
expect(vault.drafts.single.json['text'], 'secret draft text');292
await vault.changePassword('draft password', 'other password');293
vault.lock();294
await vault.unlock('other password');295
expect(vault.drafts.single.json['name'], 'nginx.conf');296
await vault.clearDrafts();297
expect(vault.drafts, isEmpty);298
vault.lock();299
await vault.unlock('other password');300
expect(vault.drafts, isEmpty);301
},302
);303
});305
group('biometric unlock on the lock screen', () {306
Future<AppController> controller(307
WidgetTester tester,308
FakeBiometric gated,309
) async {310
final store = MemoryStore();311
final c = AppController(store);312
c.biometricStore = (_) => gated;313
c.biometricSupported = () async => true;314
await tester.runAsync(() async {315
await c.initialize();316
await c.acceptLicense();317
await c.openVault('lock screen password', create: true);318
await c.setBiometric(true, 'reason');319
});320
c.lock();321
return c;322
}324
Widget app(AppController c) => MaterialApp(325
locale: const Locale('en'),326
localizationsDelegates: AppLocalizations.localizationsDelegates,327
supportedLocales: AppLocalizations.supportedLocales,328
home: ListenableBuilder(329
listenable: c,330
builder: (_, _) =>331
c.vault.unlocked ? const Text('UNLOCKED') : VaultGate(c),332
),333
);335
testWidgets('starts by itself and unlocks with the released key', (336
tester,337
) async {338
final gated = FakeBiometric();339
final c = await controller(tester, gated);340
await tester.pumpWidget(app(c));341
await tester.runAsync(() async {342
await tester.pump();343
await tester.pump();344
for (var i = 0; i < 100 && !c.vault.unlocked; i++) {345
await Future<void>.delayed(const Duration(milliseconds: 20));346
}347
});348
await tester.pump();349
expect(gated.reads, 1);350
expect(find.text('UNLOCKED'), findsOneWidget);351
c.dispose();352
});354
testWidgets(355
'a cancelled prompt is not repeated; the password still works',356
(tester) async {357
final gated = FakeBiometric()..cancel = true;358
final c = await controller(tester, gated);359
await tester.pumpWidget(app(c));360
await tester.runAsync(() async {361
for (var i = 0; i < 10; i++) {362
await tester.pump();363
await Future<void>.delayed(const Duration(milliseconds: 20));364
}365
});366
await tester.pump();367
expect(gated.reads, 1);368
expect(c.vault.unlocked, false);369
expect(370
find.textContaining('Biometric check cancelled'),371
findsOneWidget,372
);373
expect(find.text('Try biometrics again'), findsOneWidget);374
// The fallback is the master password, not the device passcode.375
await tester.enterText(376
find.byType(TextField).first,377
'lock screen password',378
);379
await tester.runAsync(() async {380
await tester.tap(find.widgetWithText(FilledButton, 'Unlock'));381
for (var i = 0; i < 200 && !c.vault.unlocked; i++) {382
await Future<void>.delayed(const Duration(milliseconds: 20));383
}384
});385
await tester.pump();386
expect(c.vault.unlocked, true);387
expect(gated.reads, 1);388
c.dispose();389
},390
);392
testWidgets('a key removed by the system turns biometric unlock off', (393
tester,394
) async {395
final gated = FakeBiometric();396
final c = await controller(tester, gated);397
gated.values.clear(); // what iOS/Android do after a biometric change398
await tester.pumpWidget(app(c));399
await tester.runAsync(() async {400
for (var i = 0; i < 10; i++) {401
await tester.pump();402
await Future<void>.delayed(const Duration(milliseconds: 20));403
}404
});405
await tester.pump();406
expect(c.biometric, false);407
expect(c.vault.unlocked, false);408
expect(find.textContaining('no longer valid'), findsOneWidget);409
c.dispose();410
});412
testWidgets('password change renews the biometric key', (tester) async {413
final gated = FakeBiometric();414
final c = await controller(tester, gated);415
await tester.runAsync(() async {416
await c.openVault('lock screen password');417
await c.changePassword('lock screen password', '4321', 'reason');418
c.lock();419
await c.vault.unlockBiometric(gated);420
});421
expect(c.vault.unlocked, true);422
expect(c.biometric, true);423
c.dispose();424
});425
});427
test('legacy biometric settings ask for the password once', () async {428
final store = MemoryStore({429
'preferences': jsonEncode({'biometric': true}),430
});431
final c = AppController(store);432
await c.initialize();433
expect(c.biometricVersion, 1);434
expect(c.biometricNotice, 'biometricUpgrade');435
c.dispose();436
});438
test('device store keeps strong-only, passcode-free biometric options', () {439
final s = DeviceStore.biometric('t');440
expect(441
s.storage.iOptions.params['accessControlFlags'],442
contains('biometryCurrentSet'),443
);444
expect(445
s.storage.iOptions.params['accessControlFlags'],446
isNot(contains('userPresence')),447
);448
expect(s.storage.aOptions.params['biometricType'], 'strongBiometricOnly');449
});450
}SHA-256
a7da0b66672c5d1c1495ead6f741ce953aacf1dc9ef9705bda67246cafb9828bCH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.
SHA-256 ng archive
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655