6128953e28
Vorbereitung fuer Schnitt-Doc Schritt 3 (verification-Modul), ohne den Verschluesselungs-heiligen Code anzufassen: megolm_crypto (Export/Import- Krypto-Helfer), encryption_section (Enums + _EncryptionSection), recovery_key_dialogs (_ChangeRecoveryKeyDialog/_ShowKeyBody/_ConfirmKeyBody), fingerprint_passphrase (_FingerprintGroup/_PassphraseDialog). Rekonstruktion zeichenidentisch (1468 Code-Zeilen, ordinaler Vergleich), analyze sauber, 29 Tests gruen, App-Start ok (auth_log 17:43:14 loggedIn). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
130 lines
4.6 KiB
Dart
130 lines
4.6 KiB
Dart
part of '../../settings_modal.dart';
|
||
|
||
// ── Megolm key export / import – top-level so compute() can use them ─────────
|
||
|
||
const _kMegolmHeader = '-----BEGIN MEGOLM SESSION DATA-----';
|
||
const _kMegolmFooter = '-----END MEGOLM SESSION DATA-----';
|
||
|
||
// PBKDF2-HMAC-SHA512. args: {pass: Uint8List, salt: Uint8List, iter: int}
|
||
Uint8List _runPbkdf2(Map<String, dynamic> args) {
|
||
final pass = args['pass'] as Uint8List;
|
||
final salt = args['salt'] as Uint8List;
|
||
final iterations = args['iter'] as int;
|
||
const blockLen = 64; // SHA-512 digest size
|
||
const dkLen = 64;
|
||
|
||
final hmac = mcrypto.Hmac(mcrypto.sha512, pass);
|
||
final blockCount = (dkLen / blockLen).ceil();
|
||
final dk = Uint8List(blockCount * blockLen);
|
||
|
||
for (var i = 1; i <= blockCount; i++) {
|
||
final saltI = Uint8List(salt.length + 4);
|
||
saltI.setRange(0, salt.length, salt);
|
||
ByteData.sublistView(saltI, salt.length).setUint32(0, i);
|
||
|
||
var u = Uint8List.fromList(hmac.convert(saltI).bytes);
|
||
final block = Uint8List.fromList(u);
|
||
|
||
for (var j = 1; j < iterations; j++) {
|
||
u = Uint8List.fromList(hmac.convert(u).bytes);
|
||
for (var k = 0; k < blockLen; k++) { block[k] ^= u[k]; }
|
||
}
|
||
dk.setRange((i - 1) * blockLen, i * blockLen, block);
|
||
}
|
||
return dk.sublist(0, dkLen);
|
||
}
|
||
|
||
Uint8List _aesCtr(Uint8List key, Uint8List iv, Uint8List data) {
|
||
final cipher = pc.SICStreamCipher(pc.AESEngine())
|
||
..init(true, pc.ParametersWithIV(pc.KeyParameter(key), iv));
|
||
final out = Uint8List(data.length);
|
||
cipher.processBytes(data, 0, data.length, out, 0);
|
||
return out;
|
||
}
|
||
|
||
// args: {passphrase: String, sessions: List<dynamic>} → encrypted String
|
||
String _encryptMegolmKeys(Map<String, dynamic> args) {
|
||
final pass = args['passphrase'] as String;
|
||
final sessions = args['sessions'] as List<dynamic>;
|
||
|
||
final rng = Random.secure();
|
||
final salt = Uint8List.fromList(List.generate(16, (_) => rng.nextInt(256)));
|
||
// IV: 8 random bytes as nonce, last 8 bytes 0x00 (counter starts at 0)
|
||
final iv = Uint8List(16);
|
||
for (var i = 0; i < 8; i++) { iv[i] = rng.nextInt(256); }
|
||
|
||
const iterations = 100000;
|
||
final dk = _runPbkdf2({
|
||
'pass': Uint8List.fromList(utf8.encode(pass)),
|
||
'salt': salt,
|
||
'iter': iterations,
|
||
});
|
||
final aesKey = dk.sublist(0, 32);
|
||
final hmacKey = dk.sublist(32, 64);
|
||
|
||
final plaintext = Uint8List.fromList(utf8.encode(jsonEncode(sessions)));
|
||
final encrypted = _aesCtr(aesKey, iv, plaintext);
|
||
|
||
final iterBe = Uint8List(4)..buffer.asByteData().setUint32(0, iterations);
|
||
|
||
// header bytes: 0x01 | salt(16) | iv(16) | iterBe(4) | encrypted(n)
|
||
final header = Uint8List(1 + 16 + 16 + 4 + encrypted.length);
|
||
var off = 0;
|
||
header[off++] = 0x01;
|
||
header.setRange(off, off += 16, salt);
|
||
header.setRange(off, off += 16, iv);
|
||
header.setRange(off, off += 4, iterBe);
|
||
header.setRange(off, off + encrypted.length, encrypted);
|
||
|
||
final mac = Uint8List.fromList(
|
||
mcrypto.Hmac(mcrypto.sha256, hmacKey).convert(header).bytes,
|
||
);
|
||
|
||
final full = Uint8List(header.length + 32);
|
||
full.setRange(0, header.length, header);
|
||
full.setRange(header.length, full.length, mac);
|
||
|
||
return '$_kMegolmHeader\n${base64.encode(full)}\n$_kMegolmFooter';
|
||
}
|
||
|
||
// args: {passphrase: String, data: List<int>} → List<dynamic> sessions
|
||
List<dynamic> _decryptMegolmKeys(Map<String, dynamic> args) {
|
||
final pass = args['passphrase'] as String;
|
||
final raw = Uint8List.fromList(args['data'] as List<int>);
|
||
|
||
if (raw.length < 1 + 16 + 16 + 4 + 32 || raw[0] != 0x01) {
|
||
throw Exception('Ungültiges Dateiformat (Version ${raw.isEmpty ? "?" : raw[0]}).');
|
||
}
|
||
|
||
final salt = raw.sublist(1, 17);
|
||
final iv = raw.sublist(17, 33);
|
||
final iterations = raw.buffer.asByteData().getUint32(33);
|
||
final encrypted = raw.sublist(37, raw.length - 32);
|
||
final storedMac = raw.sublist(raw.length - 32);
|
||
|
||
if (iterations < 1 || iterations > 10000000) {
|
||
throw Exception('Iterationsanzahl außerhalb des erlaubten Bereichs.');
|
||
}
|
||
|
||
final dk = _runPbkdf2({
|
||
'pass': Uint8List.fromList(utf8.encode(pass)),
|
||
'salt': Uint8List.fromList(salt),
|
||
'iter': iterations,
|
||
});
|
||
final aesKey = dk.sublist(0, 32);
|
||
final hmacKey = dk.sublist(32, 64);
|
||
|
||
// Verify MAC over all bytes except the appended MAC itself
|
||
final toMac = raw.sublist(0, raw.length - 32);
|
||
final expectedMac = mcrypto.Hmac(mcrypto.sha256, hmacKey).convert(toMac).bytes;
|
||
var macOk = true;
|
||
for (var i = 0; i < 32; i++) {
|
||
if (expectedMac[i] != storedMac[i]) { macOk = false; break; }
|
||
}
|
||
if (!macOk) throw Exception('Falsches Passwort oder beschädigte Datei.');
|
||
|
||
final decrypted = _aesCtr(aesKey, Uint8List.fromList(iv), Uint8List.fromList(encrypted));
|
||
return jsonDecode(utf8.decode(decrypted)) as List<dynamic>;
|
||
}
|
||
|