reencryptToCurrentKey method
Re-encrypts every xpub and hdpubkey row that is not under the current key version with the current key, and returns how many rows changed.
Completes a key rotation: once it returns, previousKeys are no longer
needed. Runs in one transaction; throws SecureStorageException (and
changes nothing) if any row cannot be decrypted.
Implementation
Future<int> reencryptToCurrentKey() async {
try {
return await _pool.runTx((session) async {
final rows = await session.execute(
Sql.named('''
SELECT key_name, encrypted_value, nonce, key_version
FROM secure_secrets
WHERE key_version <> @current
AND (key_name LIKE @xpub_pattern OR key_name LIKE @hdpubkey_pattern)
FOR UPDATE
'''),
parameters: {
'current': _encryptionService.keyVersion,
'xpub_pattern': '$_xpubKeyPrefix%',
'hdpubkey_pattern': '$_hdPubKeyPrefix%',
},
);
for (final row in rows) {
final keyName = row[0] as String;
final plaintext = await _decryptRow(
keyName,
row[1] as Uint8List,
row[2] as Uint8List,
row[3] as int,
);
final encrypted = await _encryptionService.encrypt(
plaintext: plaintext,
context: keyName,
);
await session.execute(
Sql.named('''
UPDATE secure_secrets
SET encrypted_value = @encrypted_value,
nonce = @nonce,
key_version = @key_version,
updated_at = NOW()
WHERE key_name = @key_name
'''),
parameters: {
'key_name': keyName,
'encrypted_value':
TypedValue(Type.byteArray, encrypted.ciphertext),
'nonce': TypedValue(Type.byteArray, encrypted.nonce),
'key_version': _encryptionService.keyVersion,
},
);
}
return rows.length;
});
} on SecureStorageException {
rethrow;
} catch (e) {
throw SecureStorageException('Failed to re-encrypt secrets: $e', e);
}
}