'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.
82 lines
2.5 KiB
Dart
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;
|
|
}
|
|
}
|