CH-J SM Mobilequản lý máy chủ qua SSH
Menu
Mã nguồn đã công bố

CH-J SM Mobile

Duyệt thư mục và tệp của một bản phát hành ứng dụng cụ thể.

Tải mã nguồn dạng ZIP
CH-J / SM Mobile 175 tệp
sm-mobile-20261016T185235Z-8e93ecc1b59a Ngày thêm: · Ngày dựng:
inspect.dart Ngày thêm: Chưa ghi nhận ngày 11.2 KB · 376 dòng
Mã Tải tệp xuống
1import 'dart:convert';
2import 'dart:io';
3import 'dart:typed_data';
4import 'package:archive/archive.dart';
5import 'package:cryptography/cryptography.dart';
7/// Read-only inspection of archives, certificates, keys and notebooks.
8/// Nothing is extracted, executed or sent anywhere.
10class ArchiveEntry {
11 final String name;
12 final int size;
13 final int? compressed;
14 final DateTime? modified;
15 final bool directory;
16 const ArchiveEntry(
17 this.name,
18 this.size, {
19 this.compressed,
20 this.modified,
21 this.directory = false,
22 });
25class ArchiveListing {
26 final String format;
27 final List<ArchiveEntry> entries;
29 /// For a single compressed file (.gz): the uncompressed size.
30 final int? uncompressed;
31 const ArchiveListing(this.format, this.entries, {this.uncompressed});
34/// Archives larger than this are not unpacked in memory for listing.
35const archiveLimit = 64 * 1024 * 1024;
37class ArchiveUnsupported implements Exception {
38 final String format;
39 const ArchiveUnsupported(this.format);
42Uint8List _gunzipBounded(Uint8List bytes) {
43 final out = BytesBuilder(copy: false);
44 final sink = ByteConversionSink.withCallback((chunk) {
45 out.add(chunk);
46 if (out.length > archiveLimit) throw const ArchiveUnsupported('tooLarge');
47 });
48 final decoder = gzip.decoder.startChunkedConversion(sink);
49 decoder.add(bytes);
50 decoder.close();
51 return out.takeBytes();
54/// A TAR header: "ustar" magic, or (old formats) a valid header checksum.
55bool _isTar(Uint8List b) {
56 if (b.length < 512) return false;
57 if (String.fromCharCodes(b.sublist(257, 262)) == 'ustar') return true;
58 final field = String.fromCharCodes(
59 b.sublist(148, 156),
60 ).replaceAll(RegExp(r'[\x00 ]'), '');
61 final stored = int.tryParse(field, radix: 8);
62 if (stored == null) return false;
63 var sum = 0;
64 for (var i = 0; i < 512; i++) {
65 sum += i >= 148 && i < 156 ? 0x20 : b[i];
66 }
67 return sum == stored;
70/// ZIP stores DOS dates, TAR Unix seconds.
71List<ArchiveEntry> _entries(Archive a, {bool zip = false}) => [
72 for (final f in a.files)
73 ArchiveEntry(
74 f.name,
75 f.size,
76 modified: f.lastModTime == 0
77 ? null
78 : zip
79 ? f.lastModDateTime
80 : DateTime.fromMillisecondsSinceEpoch(f.lastModTime * 1000),
81 directory: !f.isFile,
82 ),
83];
85/// Lists the content of a ZIP (also JAR/APK/IPA/AAB), TAR, GZIP, TAR.GZ,
86/// TAR.BZ2 or TAR.XZ archive. 7z and RAR are not decoded.
87ArchiveListing listArchive(String name, Uint8List bytes) {
88 final lower = name.toLowerCase();
89 bool magic(List<int> m) =>
90 bytes.length >= m.length &&
91 [for (var i = 0; i < m.length; i++) bytes[i] == m[i]].every((b) => b);
92 if (lower.endsWith('.7z') || magic(const [0x37, 0x7a, 0xbc, 0xaf])) {
93 throw const ArchiveUnsupported('7z');
94 }
95 if (lower.endsWith('.rar') || magic(const [0x52, 0x61, 0x72, 0x21])) {
96 throw const ArchiveUnsupported('RAR');
97 }
98 if (magic(const [0x50, 0x4b])) {
99 return ArchiveListing(
100 'ZIP',
101 _entries(ZipDecoder().decodeBytes(bytes), zip: true),
102 );
103 }
104 if (magic(const [0x1f, 0x8b])) {
105 final inner = _gunzipBounded(bytes);
106 if (_isTar(inner)) {
107 return ArchiveListing(
108 'TAR.GZ',
109 _entries(TarDecoder().decodeBytes(inner)),
110 uncompressed: inner.length,
111 );
112 }
113 final base = name.split('/').last;
114 return ArchiveListing('GZIP', [
115 ArchiveEntry(
116 base.toLowerCase().endsWith('.gz')
117 ? base.substring(0, base.length - 3)
118 : base,
119 inner.length,
120 compressed: bytes.length,
121 ),
122 ], uncompressed: inner.length);
123 }
124 if (magic(const [0x42, 0x5a, 0x68]) || magic(const [0xfd, 0x37, 0x7a])) {
125 if (bytes.length > archiveLimit ~/ 4) {
126 throw const ArchiveUnsupported('tooLarge');
127 }
128 final inner = magic(const [0x42, 0x5a, 0x68])
129 ? BZip2Decoder().decodeBytes(bytes)
130 : XZDecoder().decodeBytes(bytes);
131 return ArchiveListing(
132 magic(const [0x42, 0x5a, 0x68]) ? 'TAR.BZ2' : 'TAR.XZ',
133 _entries(TarDecoder().decodeBytes(inner)),
134 uncompressed: inner.length,
135 );
136 }
137 if (_isTar(bytes)) {
138 return ArchiveListing('TAR', _entries(TarDecoder().decodeBytes(bytes)));
139 }
140 throw const ArchiveUnsupported('unknown');
143// ---------------------------------------------------------------------------
144// Certificates and keys
146class _Tlv {
147 final int tag, start, length;
148 final Uint8List data;
149 const _Tlv(this.tag, this.start, this.length, this.data);
150 Uint8List get value => Uint8List.sublistView(data, start, start + length);
151 int get end => start + length;
152 List<_Tlv> get children {
153 final out = <_Tlv>[];
154 var at = start;
155 while (at < end) {
156 final t = _read(data, at);
157 out.add(t);
158 at = t.end;
159 }
160 return out;
161 }
164_Tlv _read(Uint8List d, int at) {
165 if (at + 2 > d.length) throw const FormatException('DER');
166 final tag = d[at];
167 var length = d[at + 1];
168 var start = at + 2;
169 if (length & 0x80 != 0) {
170 final n = length & 0x7f;
171 if (n == 0 || n > 4 || start + n > d.length) {
172 throw const FormatException('DER length');
173 }
174 length = 0;
175 for (var i = 0; i < n; i++) {
176 length = (length << 8) | d[start + i];
177 }
178 start += n;
179 }
180 if (start + length > d.length) throw const FormatException('DER bounds');
181 return _Tlv(tag, start, length, d);
184String _oid(Uint8List v) {
185 if (v.isEmpty) return '';
186 final parts = <int>[v[0] ~/ 40, v[0] % 40];
187 var n = 0;
188 for (var i = 1; i < v.length; i++) {
189 n = (n << 7) | (v[i] & 0x7f);
190 if (v[i] & 0x80 == 0) {
191 parts.add(n);
192 n = 0;
193 }
194 }
195 return parts.join('.');
198const _names = {
199 '2.5.4.3': 'CN',
200 '2.5.4.6': 'C',
201 '2.5.4.7': 'L',
202 '2.5.4.8': 'ST',
203 '2.5.4.10': 'O',
204 '2.5.4.11': 'OU',
205 '1.2.840.113549.1.9.1': 'E',
206};
208const _algorithms = {
209 '1.2.840.113549.1.1.1': 'RSA',
210 '1.2.840.10045.2.1': 'EC',
211 '1.3.101.112': 'Ed25519',
212 '1.3.101.113': 'Ed448',
213 '1.2.840.10040.4.1': 'DSA',
214};
216String _name(_Tlv seq) => [
217 for (final set in seq.children)
218 for (final attr in set.children)
219 if (attr.children.length >= 2)
220 '${_names[_oid(attr.children[0].value)] ?? _oid(attr.children[0].value)}='
221 '${utf8.decode(attr.children[1].value, allowMalformed: true)}',
222].join(', ');
224DateTime? _time(_Tlv t) {
225 final s = ascii.decode(t.value, allowInvalid: true);
226 try {
227 if (t.tag == 0x17) {
228 // UTCTime YYMMDDHHMMSSZ
229 final y = int.parse(s.substring(0, 2));
230 return DateTime.utc(
231 y < 50 ? 2000 + y : 1900 + y,
232 int.parse(s.substring(2, 4)),
233 int.parse(s.substring(4, 6)),
234 int.parse(s.substring(6, 8)),
235 int.parse(s.substring(8, 10)),
236 int.parse(s.substring(10, 12)),
237 );
238 }
239 return DateTime.utc(
240 int.parse(s.substring(0, 4)),
241 int.parse(s.substring(4, 6)),
242 int.parse(s.substring(6, 8)),
243 int.parse(s.substring(8, 10)),
244 int.parse(s.substring(10, 12)),
245 int.parse(s.substring(12, 14)),
246 );
247 } catch (_) {
248 return null;
249 }
252String _hex(List<int> b, [String sep = ':']) =>
253 b.map((x) => x.toRadixString(16).padLeft(2, '0').toUpperCase()).join(sep);
255/// One item found in a certificate or key file. Private keys are reported
256/// as present; their content is never shown here.
257class CryptoItem {
258 final String type;
259 final Map<String, String> fields;
260 final bool secret;
261 const CryptoItem(this.type, this.fields, {this.secret = false});
264Future<CryptoItem> _certificate(Uint8List der) async {
265 final cert = _read(der, 0);
266 final tbs = cert.children.first;
267 var parts = tbs.children;
268 if (parts.isNotEmpty && parts.first.tag == 0xa0) parts = parts.sublist(1);
269 final serial = parts[0].value;
270 final issuer = parts[2], validity = parts[3], subject = parts[4];
271 final spki = parts[5];
272 final algorithm = _oid(spki.children.first.children.first.value);
273 final times = validity.children;
274 final from = _time(times[0]), to = _time(times[1]);
275 final sha256 = await Sha256().hash(Uint8List.sublistView(der, 0, cert.end));
276 return CryptoItem('X.509 certificate', {
277 'subject': _name(subject),
278 'issuer': _name(issuer),
279 'serial': _hex(serial),
280 'validFrom': from?.toIso8601String() ?? '?',
281 'validTo': to?.toIso8601String() ?? '?',
282 'expired': to == null ? '?' : (to.isBefore(DateTime.now()) ? 'yes' : 'no'),
283 'publicKey': _algorithms[algorithm] ?? algorithm,
284 'sha256': _hex(sha256.bytes),
285 });
288/// Inspects PEM, DER certificates, OpenSSH public keys and armored PGP.
289Future<List<CryptoItem>> inspectCrypto(Uint8List bytes) async {
290 final text = utf8.decode(bytes, allowMalformed: true);
291 final items = <CryptoItem>[];
292 final pem = RegExp(r'-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----');
293 for (final m in pem.allMatches(text)) {
294 final type = m.group(1)!;
295 if (type.contains('PRIVATE KEY') || type == 'PGP PRIVATE KEY BLOCK') {
296 items.add(CryptoItem(type, const {}, secret: true));
297 continue;
298 }
299 if (type == 'CERTIFICATE') {
300 try {
301 final body = m.group(2)!.replaceAll(RegExp(r'\s'), '');
302 items.add(await _certificate(base64Decode(body)));
303 continue;
304 } catch (_) {
305 /* Shown as a plain block below. */
306 }
307 }
308 items.add(CryptoItem(type, const {}));
309 }
310 for (final line in const LineSplitter().convert(text)) {
311 final parts = line.trim().split(RegExp(r'\s+'));
312 if (parts.length >= 2 && RegExp(r'^(ssh-|ecdsa-|sk-)').hasMatch(parts[0])) {
313 try {
314 final blob = base64Decode(parts[1]);
315 final hash = await Sha256().hash(blob);
316 items.add(
317 CryptoItem('OpenSSH public key', {
318 'type': parts[0],
319 'comment': parts.length > 2 ? parts.sublist(2).join(' ') : '',
320 'sha256': 'SHA256:${base64Encode(hash.bytes).replaceAll('=', '')}',
321 }),
322 );
323 } catch (_) {
324 /* Not a key line. */
325 }
326 }
327 }
328 if (items.isEmpty && bytes.isNotEmpty && bytes[0] == 0x30) {
329 try {
330 items.add(await _certificate(bytes));
331 } catch (_) {
332 /* Not DER. */
333 }
334 }
335 return items;
338// ---------------------------------------------------------------------------
339// Jupyter notebooks
341class NotebookCell {
342 final String type, source;
343 final List<String> outputs;
344 final List<Uint8List> images;
345 const NotebookCell(this.type, this.source, this.outputs, this.images);
348String _joined(Object? v) =>
349 v is List ? v.map((e) => '$e').join() : (v?.toString() ?? '');
351/// The cells of a Jupyter notebook; text and PNG outputs only.
352List<NotebookCell> parseNotebook(String text) {
353 final json = jsonDecode(text) as Map;
354 final cells = json['cells'] as List? ?? const [];
355 return [
356 for (final c in cells.cast<Map>())
357 NotebookCell(
358 c['cell_type']?.toString() ?? 'code',
359 _joined(c['source']),
360 [
361 for (final o in (c['outputs'] as List? ?? const []).cast<Map>())
362 if (o['text'] != null)
363 _joined(o['text'])
364 else if ((o['data'] as Map?)?['text/plain'] != null)
365 _joined((o['data'] as Map)['text/plain']),
366 ],
367 [
368 for (final o in (c['outputs'] as List? ?? const []).cast<Map>())
369 if ((o['data'] as Map?)?['image/png'] != null)
370 base64Decode(
371 _joined((o['data'] as Map)['image/png']).replaceAll('\n', ''),
372 ),
373 ],
374 ),
375 ];
SHA-256652d893e12804d648ad170b3d0b7a269f4a86ac6e601346b7f7f5c2d3f3fca49

CH-J Proprietary Software License 1.14 · Mã nguồn được cung cấp theo CH-J Proprietary Software License 1.14. Việc công khai mã nguồn không thay đổi các điều khoản giấy phép và không cấp thêm quyền nào.

SHA-256 của tệp nén46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Tệp