mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Fix failing Amber restore from backup
This commit is contained in:
@@ -72,28 +72,8 @@ class DeviceBackupService {
|
||||
required Ref ref,
|
||||
required Signer amberSigner,
|
||||
}) async {
|
||||
final results = await ref
|
||||
.read(storageNotifierProvider.notifier)
|
||||
.query(
|
||||
RequestFilter<CustomData>(
|
||||
authors: {amberSigner.pubkey},
|
||||
tags: {
|
||||
'#d': {kDeviceKeyBackupIdentifier},
|
||||
},
|
||||
limit: 1,
|
||||
).toRequest(),
|
||||
source: const LocalAndRemoteSource(
|
||||
relays: 'AppCatalog',
|
||||
stream: false,
|
||||
),
|
||||
subscriptionPrefix: 'app-device-key-backup',
|
||||
);
|
||||
final backup = results.firstOrNull;
|
||||
if (backup == null ||
|
||||
backup.pubkey != amberSigner.pubkey ||
|
||||
!verifySignedEvent(ref, backup.event)) {
|
||||
return null;
|
||||
}
|
||||
final backup = await _findAmberBackup(ref: ref, amberSigner: amberSigner);
|
||||
if (backup == null) return null;
|
||||
try {
|
||||
final plaintext = await amberSigner.nip44Decrypt(
|
||||
backup.content,
|
||||
@@ -113,6 +93,57 @@ class DeviceBackupService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether Amber already holds a signed recovery record.
|
||||
///
|
||||
/// A normal sign-in must not replace an existing recovery key. In
|
||||
/// particular, a fresh install has already generated a new device key before
|
||||
/// Amber becomes available; publishing that key would orphan the user's
|
||||
/// existing private state.
|
||||
Future<bool> hasAmberBackup({
|
||||
required Ref ref,
|
||||
required Signer amberSigner,
|
||||
}) async =>
|
||||
await _findAmberBackup(ref: ref, amberSigner: amberSigner) != null;
|
||||
|
||||
static bool isValidAmberBackup({
|
||||
required Ref ref,
|
||||
required CustomData? backup,
|
||||
required Signer amberSigner,
|
||||
}) =>
|
||||
backup != null &&
|
||||
backup.pubkey == amberSigner.pubkey &&
|
||||
verifySignedEvent(ref, backup.event);
|
||||
|
||||
Future<CustomData?> _findAmberBackup({
|
||||
required Ref ref,
|
||||
required Signer amberSigner,
|
||||
}) async {
|
||||
final results = await ref
|
||||
.read(storageNotifierProvider.notifier)
|
||||
.query(
|
||||
RequestFilter<CustomData>(
|
||||
authors: {amberSigner.pubkey},
|
||||
tags: {
|
||||
'#d': {kDeviceKeyBackupIdentifier},
|
||||
},
|
||||
limit: 1,
|
||||
).toRequest(),
|
||||
source: const LocalAndRemoteSource(
|
||||
relays: 'AppCatalog',
|
||||
stream: false,
|
||||
),
|
||||
subscriptionPrefix: 'app-device-key-backup',
|
||||
);
|
||||
final backup = results.firstOrNull;
|
||||
return isValidAmberBackup(
|
||||
ref: ref,
|
||||
backup: backup,
|
||||
amberSigner: amberSigner,
|
||||
)
|
||||
? backup
|
||||
: null;
|
||||
}
|
||||
|
||||
Future<void> restoreDeviceKey({
|
||||
required Ref ref,
|
||||
required String privateKeyHex,
|
||||
@@ -189,6 +220,9 @@ Future<void> maybeOfferDeviceBackup(Ref ref) async {
|
||||
final service = ref.read(deviceBackupServiceProvider);
|
||||
if (service.isRestoringFromAmber) return;
|
||||
try {
|
||||
if (await service.hasAmberBackup(ref: ref, amberSigner: amberSigner)) {
|
||||
return;
|
||||
}
|
||||
await service.backupDeviceKey(ref: ref, amberSigner: amberSigner);
|
||||
if (!ref.read(deviceStateProvider).isReady) {
|
||||
unawaited(ref.read(deviceStateProvider.notifier).bootstrap());
|
||||
|
||||
@@ -41,6 +41,26 @@ class DeviceRestoreDialog extends HookConsumerWidget {
|
||||
'Alternatively, you can sign in with Amber and settings may be recovered that way.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
if (amberInstalled) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
const DeviceRestoreResult(DeviceRestoreAction.amber),
|
||||
);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
context.push('/search/app/$kAmberNaddr');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.login),
|
||||
label: Text(
|
||||
amberInstalled
|
||||
? 'Restore with Amber'
|
||||
: 'Install Amber to restore',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autocorrect: false,
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
test/services/device_backup_service_test.dart
|
||||
test/services/settings_service_test.dart`
|
||||
- `HOME=/tmp fvm flutter analyze`
|
||||
- [x] 5. Protect Amber recovery records
|
||||
- Files: `lib/services/device_backup_service.dart`,
|
||||
`lib/widgets/device_restore_dialog.dart`
|
||||
- Normal Amber sign-in preserves an existing verified Amber recovery record;
|
||||
explicit Device key management offers Amber recovery without scheduling a
|
||||
replacement backup.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -47,3 +53,16 @@ any time without treating Amber as a prerequisite or gating the UI.
|
||||
The obsolete persisted `restoreOnboardingComplete` value is removed. Amber
|
||||
recovery is now explicitly initiated from Device key management, which prevents
|
||||
normal Amber sign-in backup from overwriting a recovery record first.
|
||||
|
||||
### 2026-07-17 - Existing Amber recovery records are immutable on sign-in
|
||||
|
||||
**Context:** A fresh install creates a new device key before the user can sign
|
||||
in with Amber. Automatically backing up that key could replace the only record
|
||||
that points to the prior device's private state.
|
||||
|
||||
**Decision:** On a normal Amber sign-in, query for a signature-verified backup
|
||||
first and only create one when none exists. Recovery must be started explicitly
|
||||
from Device key management, which now exposes the Amber option.
|
||||
|
||||
**Rationale:** An Amber sign-in must not silently make prior private stacks and
|
||||
portable settings unrecoverable.
|
||||
|
||||
@@ -1,9 +1,44 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:zapstore/constants/app_constants.dart';
|
||||
import 'package:zapstore/services/device_backup_service.dart';
|
||||
import 'package:zapstore/utils/debug_utils.dart';
|
||||
|
||||
void main() {
|
||||
test('backup exceptions retain a user-safe message', () {
|
||||
const exception = DeviceBackupException('Amber backup was unavailable.');
|
||||
expect(exception.toString(), 'Amber backup was unavailable.');
|
||||
});
|
||||
|
||||
test(
|
||||
'recognizes a signed Amber recovery record before replacement',
|
||||
() async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
storageNotifierProvider.overrideWith(DummyStorageNotifier.new),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
await container
|
||||
.read(storageNotifierProvider.notifier)
|
||||
.initialize(StorageConfiguration());
|
||||
final ref = container.read(refProvider);
|
||||
final amber = Bip340PrivateKeySigner('2' * 64, ref);
|
||||
await amber.signIn(setAsActive: false);
|
||||
final backup = await PartialCustomData(
|
||||
identifier: kDeviceKeyBackupIdentifier,
|
||||
content: 'encrypted-device-key',
|
||||
).signWith(amber);
|
||||
|
||||
expect(
|
||||
DeviceBackupService.isValidAmberBackup(
|
||||
ref: ref,
|
||||
backup: backup,
|
||||
amberSigner: amber,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user