CH-J SM Mobilegestione server su SSH
Menu
Sorgente pubblicata

CH-J SM Mobile

Sfoglia directory e file per una specifica release di applicazioni.

Scarica sorgente ZIP
CH-J / SM Mobile 134 file
sm-mobile-20261016T050549Z-3678f2a05fa9 Aggiunto: · Costruito:
File
security_test.dart Aggiunto: Data non registrata 8,3 KB · 270 righe
Scarica file
1import 'dart:convert';
2import 'dart:io';
3import 'package:flutter_test/flutter_test.dart';
4import 'package:sm_mobile/core/failure.dart';
5import 'package:sm_mobile/models/server_profile.dart';
6import 'package:sm_mobile/services/host_verifier.dart';
7import 'package:sm_mobile/services/diagnostics.dart';
8import 'package:sm_mobile/services/profile_transfer.dart';
9import 'package:sm_mobile/services/sftp_service.dart';
10import 'package:sm_mobile/services/vault.dart';
11import 'support.dart';
13void 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 });
SHA-25640b2c742edbb69c9354b93bfa3f0c1dc031b0de5fb54258b6bab1d57573c1a2f

CH-J Proprietary Software License 1.14 · La sorgente è fornita sotto CH-J Proprietary Software License 1.14. La sua disponibilità non modifica i termini di licenza o concede diritti aggiuntivi.

Archivio SHA-256c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65