Rotation
rotation ¶
Key rotation utility for adk-secure-sessions.
Provides rotate_encryption_keys(), a standalone async function that re-encrypts all session data in a SQL database from one encryption backend to another. Designed for same-backend passphrase rotation — the scenario where additional_backends cannot be used because both old and new backends share the same backend_id.
There are two rotation paths:
- Path A — Lazy cross-backend migration: Already works via
EncryptedSessionService(additional_backends=[old_backend])for backends with differentbackend_idvalues. No utility required. - Path B — Batch same-backend rotation: Requires this utility when old and new backends share a
backend_id(e.g., rotating Fernet passphrases). Reads all encrypted records, re-encrypts with the new backend, and writes back with optimistic concurrency protection viaupdate_time.
Examples:
Rotate all session data from one Fernet passphrase to another:
from adk_secure_sessions import FernetBackend
from adk_secure_sessions.rotation import RotationResult, rotate_encryption_keys
old = FernetBackend("old-passphrase")
new = FernetBackend("new-passphrase")
result: RotationResult = await rotate_encryption_keys(
db_url="sqlite+aiosqlite:///sessions.db",
old_backend=old,
new_backend=new,
)
print(f"Rotated: {result.rotated}, Skipped: {result.skipped}")
if result.skipped:
print("Re-run to pick up records skipped due to concurrent writes")
See Also
adk_secure_sessions.services.encrypted_session: additional_backends parameter for cross-backend lazy migration (Path A).
RotationResult dataclass ¶
Result of a completed key rotation operation.
| ATTRIBUTE | DESCRIPTION |
|---|---|
rotated | Number of records successfully re-encrypted. TYPE: |
skipped | Number of records skipped due to concurrent writes detected via the TYPE: |
Examples:
result = await rotate_encryption_keys(db_url, old, new)
if result.skipped:
print(f"{result.skipped} records need a follow-up rotation pass")
Source code in src/adk_secure_sessions/rotation.py
rotate_encryption_keys async ¶
rotate_encryption_keys(
db_url: str,
old_backend: EncryptionBackend,
new_backend: EncryptionBackend,
) -> RotationResult
Re-encrypt all session data from one backend to another.
Reads all encrypted records from the four session tables (sessions, app_states, user_states, events), identifies records encrypted with old_backend by checking the envelope backend_id byte, and re-encrypts them using new_backend. Records already on a different backend are skipped silently.
Uses update_time and the existing ciphertext value as an optimistic concurrency guard for sessions, app_states, and user_states. If a record is modified between the rotation function's read and write (rows_affected == 0), it is counted as skipped. Run the function again to pick up remaining records.
Re-run semantics differ by rotation type:
- Cross-backend rotation (
old_backend.backend_id != new_backend.backend_id): Re-runs are safe. Already-rotated records carrynew_backend.backend_idin their envelope and are skipped silently by the backend-id filter. - Same-backend rotation (
old_backend.backend_id == new_backend.backend_id, e.g., twoFernetBackendinstances): A single pass is expected. Re-running with the originalold_backendwill attempt to decrypt already-rotated ciphertext with the old key and raiseDecryptionError. For same-backend rotation, stop or pause the service before running this function, run once, then reconfigure the service to usenew_backendand restart.
For the events table (no update_time column), rows_affected == 0 means the event was cascade-deleted between read and write and is not counted as skipped.
Cryptographic operations (sync_decrypt, sync_encrypt) run per record in a thread via asyncio.to_thread() to avoid blocking the event loop. For databases with very large numbers of records, run this utility during a low-traffic window to minimise thread-pool pressure.
| PARAMETER | DESCRIPTION |
|---|---|
db_url | SQLAlchemy connection string (e.g., TYPE: |
old_backend | Backend used to decrypt existing records. Records whose envelope TYPE: |
new_backend | Backend used to encrypt re-written records. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
RotationResult | A |
RotationResult | and |
RotationResult | four tables. |
| RAISES | DESCRIPTION |
|---|---|
DecryptionError | If a record cannot be decrypted with |
Examples:
Rotate from one Fernet passphrase to another:
from adk_secure_sessions import FernetBackend
from adk_secure_sessions.rotation import rotate_encryption_keys
old = FernetBackend("old-passphrase")
new = FernetBackend("new-passphrase")
result = await rotate_encryption_keys(
db_url="sqlite+aiosqlite:///sessions.db",
old_backend=old,
new_backend=new,
)
print(f"Rotated {result.rotated} records, skipped {result.skipped}")
See Also
adk_secure_sessions.services.encrypted_session: Use additional_backends for cross-backend lazy migration (Path A, no utility required).
Source code in src/adk_secure_sessions/rotation.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | |