rotatePassphrase method

Future<void> rotatePassphrase(
  1. String localId, {
  2. required String oldPassphrase,
  3. required String newPassphrase,
})

Re-encrypts localId under a key derived from newPassphrase.

The database must be open, and oldPassphrase must be the one it is open under. Everything the old passphrase protected — the device roster, and every collection when no Identity is configured — is re-encrypted in place; open Collection handles keep working. The group key is not derived from the passphrase and is left alone, so no user data is re-encrypted, no device loses its membership, and nothing has to sync.

await passnote.rotatePassphrase(
  'household',
  oldPassphrase: oldWords.join(' '),
  newPassphrase: newWords.join(' '),
);

This device only. The passphrase key is also this database's discovery Database.room, so a rotated device stops exchanging frames with peers that still hold the old one. Every other device has to be rotated with the same two phrases, or paired again — pairing hands over whatever key the sponsor currently holds.

Crash-safe. The new key is written to the KeyStore under a pending entry before any store is re-encrypted, so a process that dies halfway leaves enough behind to finish the job: the next open re-keys whatever is still on the old key and promotes the new one. A database is never left unopenable.

Throws WrongPassphraseException when oldPassphrase is not the one in force, WeakPassphraseException when newPassphrase fails the configured PassphrasePolicy, and StateError when localId is not open.

Implementation

Future<void> rotatePassphrase(
  String localId, {
  required String oldPassphrase,
  required String newPassphrase,
}) async {
  final db = _database(localId);
  if (_config.passphrasePolicy.strictness == PassphraseStrictness.reject &&
      !_validateMnemonic(newPassphrase)) {
    throw const WeakPassphraseException();
  }

  final entryKey = '${_config.appId}/$localId';
  final current = await _keyStore.getKey(entryKey);
  final oldKey = await _deriveKey(oldPassphrase, _config.salt);
  if (current == null || !_sameKey(current, oldKey)) {
    throw const WrongPassphraseException();
  }
  final newKey = await _deriveKey(newPassphrase, _config.salt);
  if (_sameKey(newKey, oldKey)) return;

  // Journal first. From here on the truth is "one of these two keys opens
  // each store", and finishing is always possible.
  await _keyStore.setKey(
    _rotationEntryKey(localId),
    _encodeRotation(newKey, db.passphraseKeyedCollections),
  );
  await db.adoptPassphraseKey(newKey, newRoom: _roomTag(newKey));
  await _keyStore.setKey(entryKey, newKey);
  _databaseKeys[localId] = newKey;
  await _keyStore.clear(_rotationEntryKey(localId));
}