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.
This commit is contained in:
parent
ba87bf2719
commit
d6781870d9
19 changed files with 1016 additions and 81 deletions
82
apps/app_seeds/lib/services/recovery_sheet_service.dart
Normal file
82
apps/app_seeds/lib/services/recovery_sheet_service.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue