Files
pyramid/lib/widgets/settings/encryption/megolm_crypto.dart
T
Bernd Steckmeister 6128953e28 refactor: settings_encryption.dart (1470 Zeilen) in 4 thematische part-Dateien unter encryption/ geteilt (M2, zeichenidentisch verifiziert)
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>
2026-07-06 17:43:53 +02:00

130 lines
4.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>;
}