Published source
Download source ZIP
CH-J SM Mobile
Browse directories and files for a specific application release.
CH-J / SM Mobile
134 files
sm-mobile-20261016T050549Z-3678f2a05fa9
Added: · Built:
1
import 'dart:convert';2
import 'dart:io';3
import 'package:flutter_test/flutter_test.dart';4
import 'package:sm_mobile/core/failure.dart';5
import 'package:sm_mobile/models/server_profile.dart';6
import 'package:sm_mobile/services/host_verifier.dart';7
import 'package:sm_mobile/services/diagnostics.dart';8
import 'package:sm_mobile/services/profile_transfer.dart';9
import 'package:sm_mobile/services/sftp_service.dart';10
import 'package:sm_mobile/services/vault.dart';11
import 'support.dart';13
void main() {14
TestWidgetsFlutterBinding.ensureInitialized();15
const master = 'a sufficiently long master password';16
late MemoryStore store;17
late Vault vault;18
setUpAll(() async {19
store = MemoryStore();20
vault = Vault(store);21
await vault.create(master);22
});23
tearDownAll(() => vault.lock());24
test('profiles preserve original DNS without persisting resolved IP', () {25
final p = ServerProfile(26
name: 'DNS',27
host: 'Example.COM',28
username: 'Admin',29
);30
expect(p.host, 'Example.COM');31
expect(p.portable().keys, ['name', 'host', 'username', 'port']);32
final a = ServerProfile(33
name: 'v6',34
host: '[2001:db8::1]',35
username: 'root',36
);37
final b = ServerProfile(38
name: 'v6',39
host: '2001:0db8:0:0:0:0:0:1',40
username: 'root',41
);42
expect(a.identity, b.identity);43
});44
test('invalid hosts, ports and control characters fail closed', () {45
for (final host in [46
'host:22',47
'ssh://host',48
'bad host',49
'[invalid]',50
'host/path',51
'host\n',52
]) {53
// Leading/trailing whitespace is intentionally trimmed.54
if (host == 'host\n') continue;55
expect(56
() => ServerProfile(name: 'n', host: host, username: 'u'),57
throwsA(isA<Failure>()),58
);59
}60
expect(61
() => ServerProfile(name: 'n', host: '::1', username: 'u', port: 65536),62
throwsA(isA<Failure>()),63
);64
expect(65
() => ServerProfile(name: 'n', host: '::1', username: 'a\u0000b'),66
throwsA(isA<Failure>()),67
);68
});69
test('portable JSON matches desktop v1 and drops arbitrary secrets', () {70
final bytes = utf8.encode(71
jsonEncode({72
'format': 'chj-server-profiles',73
'version': 1,74
'servers': [75
{76
'name': 'N',77
'host': 'example.com',78
'username': 'admin',79
'password': 'DO_NOT_EXPORT',80
'privateKey': 'PRIVATE',81
'fingerprint': 'UNTRUSTED',82
},83
],84
}),85
);86
final decoded = ProfileTransfer.decode(bytes);87
final text = utf8.decode(ProfileTransfer.encode(decoded));88
expect(text, isNot(contains('DO_NOT_EXPORT')));89
expect(text, isNot(contains('PRIVATE')));90
expect(text, isNot(contains('fingerprint')));91
expect(jsonDecode(text)['servers'][0], {92
'name': 'N',93
'host': 'example.com',94
'username': 'admin',95
'port': 22,96
});97
expect(decoded.single.auth, AuthMethod.password);98
});99
test(100
'invalid import rejects whole document, unknown versions and oversized input',101
() {102
for (final text in [103
'{}',104
'{"format":"chj-server-profiles","version":2,"servers":[]}',105
'{"format":"chj-server-profiles","version":1,"servers":[{"name":"x","host":"x","username":"u"},{}]}',106
]) {107
expect(108
() => ProfileTransfer.decode(utf8.encode(text)),109
throwsA(isA<Failure>()),110
);111
}112
expect(113
() => ProfileTransfer.decode(114
List.filled(ProfileTransfer.maxBytes + 1, 32),115
),116
throwsA(isA<Failure>()),117
);118
},119
);120
test(121
'Vault encrypts profiles, passwords and keys; failed writes do not mutate live data',122
() async {123
final p = ServerProfile(124
name: 'secret server label',125
host: 'private.example',126
username: 'u',127
);128
await vault.save(129
p,130
password: 'never-cleartext',131
privateKey: 'PRIVATE_TEST_KEY',132
);133
final text = store.values['vault']!;134
for (final secret in [135
'never-cleartext',136
'PRIVATE_TEST_KEY',137
'private.example',138
'secret server label',139
master,140
]) {141
expect(text, isNot(contains(secret)));142
}143
store.failWrites = true;144
await expectLater(vault.deleteProfile(p.id), throwsStateError);145
expect(vault.secret(p.id, 'password'), 'never-cleartext');146
store.failWrites = false;147
},148
);149
test(150
'first host requires consent; known host never prompts; changed key blocks',151
() async {152
final verifier = HostVerifier(vault);153
final fp = 'SHA256:${List.filled(43, 'a').join()}';154
final changed = 'SHA256:${List.filled(43, 'b').join()}';155
var prompts = 0;156
Future<bool> prompt(String a, String b, String c) async {157
prompts++;158
return true;159
}161
expect(await verifier.verify('test:22', 'ssh-ed25519', fp, prompt), true);162
expect(await verifier.verify('test:22', 'ssh-ed25519', fp, prompt), true);163
expect(prompts, 1);164
await expectLater(165
verifier.verify('test:22', 'ssh-ed25519', changed, prompt),166
throwsA(isA<Failure>().having((e) => e.code, 'code', 'hostChanged')),167
);168
expect(prompts, 1);169
await expectLater(170
verifier.verify(171
'rejected:22',172
'ssh-ed25519',173
fp,174
(a, b, c) async => false,175
),176
throwsA(isA<Failure>()),177
);178
expect(vault.fingerprint('rejected:22'), null);179
},180
);181
test(182
'locking blocks access and invalidates pending trust decision',183
() async {184
final copy = Vault(MemoryStore(store.values));185
await copy.unlock(master);186
final fp = 'SHA256:${List.filled(43, 'c').join()}';187
await expectLater(188
HostVerifier(copy).verify('pending:22', 'ssh-ed25519', fp, (189
a,190
b,191
c,192
) async {193
copy.lock();194
return true;195
}),196
throwsA(isA<Failure>()),197
);198
expect(() => copy.profiles, throwsA(isA<Failure>()));199
expect(() => copy.secret('x', 'password'), throwsA(isA<Failure>()));200
},201
);202
test(203
'wrong password and altered authentication tag fail; original can still unlock',204
() async {205
final local = MemoryStore(store.values);206
final copy = Vault(local);207
await expectLater(208
copy.unlock('incorrect-password'),209
throwsA(isA<Failure>()),210
);211
expect(copy.unlocked, false);212
final envelope = jsonDecode(local.values['vault']!);213
final data = base64Decode(envelope['data']);214
data[data.length - 1] ^= 1;215
envelope['data'] = base64Encode(data);216
local.values['vault'] = jsonEncode(envelope);217
await expectLater(copy.unlock(master), throwsA(isA<Failure>()));218
expect(copy.unlocked, false);219
local.values['vault'] = store.values['vault']!;220
await copy.unlock(master);221
expect(copy.unlocked, true);222
copy.lock();223
},224
);225
test(226
'biometric key unavailable does not bypass encryption; fallback works',227
() async {228
final gated = MemoryStore();229
await vault.enableBiometric(gated);230
final copy = Vault(MemoryStore(store.values));231
await expectLater(232
copy.unlockBiometric(MemoryStore()),233
throwsA(isA<Failure>()),234
);235
expect(copy.unlocked, false);236
await copy.unlockBiometric(gated);237
expect(copy.unlocked, true);238
copy.lock();239
},240
);241
test('unsafe path names and traversal are rejected', () {242
for (final name in ['.', '..', '../x', 'x/y', 'x\\y', 'x\u0000']) {243
expect(() => safeName(name), throwsA(isA<Failure>()));244
}245
for (final path in [246
'/../etc',247
'/home/../etc',248
'relative',249
'/a/./b',250
'/a\\b',251
]) {252
expect(() => safePath(path), throwsA(isA<Failure>()));253
}254
expect(childPath('/', 'file'), '/file');255
expect(parentPath('/a'), '/');256
});257
test('DNS/TCP probe reports TCP timing without changing profiles', () async {258
final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);259
final accepted = socket.listen((s) => s.destroy());260
final result = await Diagnostics().probe(261
'127.0.0.1',262
socket.port,263
type: InternetAddressType.IPv4,264
);265
expect(result.addresses, ['127.0.0.1']);266
expect(result.tcpMs, greaterThanOrEqualTo(0));267
await accepted.cancel();268
await socket.close();269
});270
}SHA-256
40b2c742edbb69c9354b93bfa3f0c1dc031b0de5fb54258b6bab1d57573c1a2fCH-J Proprietary Software License 1.14 · Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
Archive SHA-256
c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65