Ein Backup soll nicht nur verschlüsselt, sondern auch auf Veränderungen geprüft werden. AES-GCM bietet beides mit einer etablierten Bibliothek. Ein Passwort wird über PBKDF2 in einen Schlüssel überführt. Zufälliger Salt und Nonce sorgen dafür, dass wiederholte Backups nicht dieselbe Ausgabe erzeugen. Unser eigenständiges Lernformat sichert nur Namen. Es ist ausdrücklich nicht kompatibel mit den Backups der veröffentlichten Apps. Die echte Fahrzeugakte sichert zusätzlich Anhänge, Metadaten und ihre eigene versionierte Struktur.
01Eine getrennte Konsolenapp aufsetzen
Erstelle dart create -t console backup_learning und ergänze dart pub add cryptography:2.9.0. Speichere backup.dart im bin-Verzeichnis. Verwende ausschließlich fiktive Namen und ein Lernpasswort. Der Code überschreibt keine Datei und importiert nichts in eine echte App.
import 'dart:convert';
import 'dart:math';
import 'package:cryptography/cryptography.dart';
final aad = utf8.encode('AFLEARN1');
final algorithm = AesGcm.with256bits();
List<int> randomBytes(int length) {
final random = Random.secure();
return List.generate(length, (_) => random.nextInt(256));
}
Future<SecretKey> keyFor(String password, List<int> salt) => Pbkdf2(
macAlgorithm: Hmac.sha256(),
iterations: 600000,
bits: 256,
).deriveKey(secretKey: SecretKey(utf8.encode(password)), nonce: salt);
Future<String> encrypt(String password, List<String> names) async {
final salt = randomBytes(16);
final nonce = randomBytes(12);
final box = await algorithm.encrypt(
utf8.encode(jsonEncode(names)),
secretKey: await keyFor(password, salt),
nonce: nonce,
aad: aad,
);
return jsonEncode({
'format': 'vehicle_learning',
'version': 1,
'salt': base64Encode(salt),
'nonce': base64Encode(nonce),
'cipher': base64Encode(box.cipherText),
'mac': base64Encode(box.mac.bytes),
});
}
Future<List<String>> decrypt(String password, String input) async {
if (utf8.encode(input).length > 1024 * 1024)
throw FormatException('Backup too large');
final value = jsonDecode(input);
if (value is! Map<String, dynamic> ||
value['format'] != 'vehicle_learning' ||
value['version'] != 1) {
throw FormatException('Unknown backup');
}
List<int> bytes(String key) {
final text = value[key];
if (text is! String) throw FormatException('Missing field');
return base64Decode(text);
}
final salt = bytes('salt');
final nonce = bytes('nonce');
final mac = bytes('mac');
if (salt.length != 16 || nonce.length != 12 || mac.length != 16)
throw FormatException('Invalid header');
final plaintext = await algorithm.decrypt(
SecretBox(bytes('cipher'), nonce: nonce, mac: Mac(mac)),
secretKey: await keyFor(password, salt),
aad: aad,
);
final names = jsonDecode(utf8.decode(plaintext));
if (names is! List ||
names.any((name) => name is! String || name.trim().isEmpty)) {
throw FormatException('Invalid records');
}
return names.cast<String>();
}
Future<void> main() async {
final backup = await encrypt('learning-password', ['Family car']);
print(await decrypt('learning-password', backup));
try {
await decrypt('wrong-password', backup);
throw StateError('Accepted wrong password');
} on SecretBoxAuthenticationError {
print('Wrong password rejected');
}
}
Starten / prüfen
dart run bin/backup.dart02Schlüssel und Zufall trennen
Salt ist kein Passwort und wird gespeichert. Der Schlüssel wird nicht gespeichert. PBKDF2 verwendet 600000 Iterationen wie die referenzierte App-Strategie. Der 12-Byte-Nonce wird für jede neue AES-GCM-Verschlüsselung frisch erzeugt. Ein gleicher Schlüssel mit wiederverwendetem Nonce wäre eine falsche Konstruktion.
03Ein versioniertes Lernformat beschreiben
format und version bestimmen, welche Struktur erlaubt ist. Die festen Zusatzdaten aad binden die Verschlüsselung an dieses Lernformat. base64 stellt Bytes als JSON-Text dar. Base64 ist keine Verschlüsselung, sondern nur eine Kodierung.
04Vor dem Entschlüsseln Grenzen prüfen
Die Eingabe ist auf 1 MiB begrenzt. Format, Version und Byte-Längen werden vor der teuren Ableitung geprüft. Eine echte Importfunktion benötigt zusätzlich genaue Manifest-, Datei- und Gesamtgrenzen, bevor sie unkontrolliert Speicher belegt.
05Erst authentifizieren, dann übernehmen
decrypt prüft den GCM-Tag. Ein falsches Passwort oder veränderte Cipherbytes lösen einen Fehler aus. Erst danach validieren wir alle Namen. In der App folgen außerdem Referenz-, Prüfsummen- und Dokumentprüfungen. Die bestehende Datenbank wird erst nach vollständig erfolgreicher Vorbereitung in einer kontrollierten Transaktion aktualisiert.
Das richtige Passwort liefert [Family car]. Das falsche Passwort wird abgewiesen.
06Migration und Merge bewusst unterscheiden
Eine Formatmigration übersetzt eine alte Backupstruktur in die aktuelle Form. Ein Merge entscheidet anschließend, welche IDs und Zeitstempel übernommen werden. Verschlüsselung löst diesen fachlichen Konflikt nicht. Vergleiche die echte EncryptedBackupService und ihre Tests vor jeder Änderung an einem bestehenden Format.
- Größen-/Formatprüfung
- Schlüsselableitung
- GCM-Authentifizierung
- vollständige Validierung
- Transaktion / Übernahme
Deine Übung
Erzeuge zwei Backups derselben Namen und prüfe, dass beide lesbar, aber verschieden sind. Verändere danach ein Cipherbyte und prüfe die Ablehnung.
Lösung mit Erklärung anzeigen
Verschiedene Salt- und Noncewerte machen die JSON-Ausgaben unterschiedlich. Für den Manipulationstest dekodiere cipher, ändere ein Byte und kodiere es erneut. decrypt muss mit einem Authentifizierungsfehler scheitern. Eine echte Wiederherstellungsprüfung kontrolliert zusätzlich, dass vorhandene Daten nach diesem Fehler unverändert geblieben sind.
Projektbezug und Quellen
Das Beispiel ist zum Lernen vereinfacht. Die verlinkte Codeversion zeigt die Anwendung im Projekt und die dortigen Prüfungen.