← Flutter / Dart

Flutter / Dart · 17 / 18

Verschlüsselte Backups sicher wiederherstellen

Du leitest einen Schlüssel ab, prüfst Authentizität und validierst alle Daten vor einer Übernahme.

  • Groß
  • Anspruchsvoll
  • 120 min

Voraussetzungen: Eine kleine Fahrzeugakte von der UI bis zur Datenbank

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.

Beispieldateien

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.

bin/backup.dart · dartDatei öffnen
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.dart

02Schlü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.

Erwartetes Ergebnis

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.

Datenfluss
  1. Größen-/Formatprüfung
  2. Schlüsselableitung
  3. GCM-Authentifizierung
  4. vollständige Validierung
  5. 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.