Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J SM Mobile
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / SM Mobile
175 Dateien
sm-mobile-20261016T185235Z-8e93ecc1b59a
Hinzugefügt: · Erstellt:
1
import 'dart:convert';2
import 'dart:io';3
import 'dart:typed_data';4
import 'package:archive/archive.dart';5
import 'package:cryptography/cryptography.dart';7
/// Read-only inspection of archives, certificates, keys and notebooks.8
/// Nothing is extracted, executed or sent anywhere.10
class 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
});23
}25
class 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});32
}34
/// Archives larger than this are not unpacked in memory for listing.35
const archiveLimit = 64 * 1024 * 1024;37
class ArchiveUnsupported implements Exception {38
final String format;39
const ArchiveUnsupported(this.format);40
}42
Uint8List _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();52
}54
/// A TAR header: "ustar" magic, or (old formats) a valid header checksum.55
bool _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;68
}70
/// ZIP stores DOS dates, TAR Unix seconds.71
List<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 == 077
? null78
: zip79
? f.lastModDateTime80
: 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.87
ArchiveListing 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');141
}143
// ---------------------------------------------------------------------------144
// Certificates and keys146
class _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
}162
}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);182
}184
String _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('.');196
}198
const _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
};208
const _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
};216
String _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(', ');224
DateTime? _time(_Tlv t) {225
final s = ascii.decode(t.value, allowInvalid: true);226
try {227
if (t.tag == 0x17) {228
// UTCTime YYMMDDHHMMSSZ229
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
}250
}252
String _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 reported256
/// as present; their content is never shown here.257
class 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});262
}264
Future<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
});286
}288
/// Inspects PEM, DER certificates, OpenSSH public keys and armored PGP.289
Future<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;336
}338
// ---------------------------------------------------------------------------339
// Jupyter notebooks341
class 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);346
}348
String _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.352
List<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
];376
}SHA-256
652d893e12804d648ad170b3d0b7a269f4a86ac6e601346b7f7f5c2d3f3fca49CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655