tane/apps/app_seeds/lib/services/recovery_sheet_service.dart
vjrj d6781870d9 feat(backup): sealed backups + printable recovery sheet
'Save a backup' now writes a sealed .tanemaki file (AES-256-GCM under a
key HKDF-derived from the root seed; format TANEBK v1, open and stable).
Restoring on the same identity is silent; a copy sealed by another
identity asks for the printed recovery code (TANE1 base32, typo-tolerant)
and adopts that seed — recovering your bank recovers you. Legacy plain
exports still restore. Settings gains a recovery sheet (QR + code PDF).
BackupBox/RecoveryCode live in commons_core, pure Dart, TDD.
2026-07-09 22:32:13 +02:00

82 lines
2.5 KiB
Dart

import 'dart:typed_data';
import 'package:flutter/services.dart' show rootBundle;
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'file_service.dart';
/// Renders the printable recovery sheet — a QR plus the typed code and short
/// instructions ("keep two paper copies, like your best seed"). All strings
/// arrive already localized.
class RecoverySheetService {
RecoverySheetService({required FileService files}) : _files = files;
final FileService _files;
Future<Uint8List> buildPdf({
required String title,
required String intro,
required String code,
}) async {
final base = pw.Font.ttf(
await rootBundle.load('assets/fonts/DejaVuSans.ttf'),
);
final bold = pw.Font.ttf(
await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'),
);
final doc = pw.Document(
theme: pw.ThemeData.withFont(base: base, bold: bold),
);
doc.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
build: (context) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(title, style: pw.TextStyle(font: bold, fontSize: 20)),
pw.SizedBox(height: 12),
pw.Text(intro, style: const pw.TextStyle(fontSize: 11)),
pw.SizedBox(height: 24),
pw.Center(
child: pw.BarcodeWidget(
barcode: pw.Barcode.qrCode(),
data: code,
width: 220,
height: 220,
// The code is printed below with the Unicode-capable face;
// the widget's built-in caption would fall back to Courier.
drawText: false,
),
),
pw.SizedBox(height: 24),
pw.Center(
child: pw.Text(
code,
textAlign: pw.TextAlign.center,
style: pw.TextStyle(font: bold, fontSize: 12),
),
),
],
),
),
);
return doc.save();
}
/// Builds the sheet and asks the user where to keep it. Returns true when
/// saved, false when they cancelled.
Future<bool> saveSheet({
required String title,
required String intro,
required String code,
required String suggestedName,
}) async {
final bytes = await buildPdf(title: title, intro: intro, code: code);
final path = await _files.saveFile(
suggestedName: suggestedName,
bytes: bytes,
);
return path != null;
}
}