recoverFrom method
Regains access to the database localId on a fresh install, using a
recovery phrase minted by enableRecovery plus the database
passphrase.
The passphrase unlocks the device roster (its key derives from it); the
roster then syncs from any reachable peer, and the keypair derived from
phrase unwraps the group key from its still-valid wrap. This device is
then admitted to the roster as deviceLabel and the returned Database
is DeviceState.ready.
Needs at least one reachable peer that holds the roster (there is no
server to ask). Throws StateError when recovery is disabled, when the
phrase's roster entry has been revoked, or when no usable wrap arrives in
time; ArgumentError when phrase is not a valid mnemonic.
Implementation
Future<Database> recoverFrom(
String localId,
String phrase, {
required String passphrase,
String deviceLabel = 'recovered device',
}) async {
final derive = _requireRecovery();
if (_databases.containsKey(localId)) {
throw StateError('Database "$localId" is already open.');
}
if (!_validateMnemonic(phrase)) {
throw ArgumentError.value(
phrase,
'phrase',
'not a valid recovery mnemonic',
);
}
final recovery = await derive(phrase, _config.salt);
final entryKey = '${_config.appId}/$localId';
final key = await _deriveKey(passphrase, _config.salt);
await _keyStore.setKey(entryKey, key);
// Never auto-found here: the group exists elsewhere; founding a fresh key
// on a briefly-alone install would fork it.
final db = await _finishOpen(
localId,
entryKey,
key,
_cipherFactory(key),
allowAutoFound: false,
);
var adopted = false;
for (var i = 0; i < 100 && !adopted; i++) {
await db.forceSync();
await Future<void>.delayed(const Duration(milliseconds: 5));
adopted = await db.adoptRecovery(recovery, label: deviceLabel);
}
if (!adopted) {
throw StateError(
'Recovery timed out: no usable wrap for this phrase arrived. Is a '
'peer that holds the roster reachable?',
);
}
return db;
}