Source code for kstlib.db.aiosqlcipher
"""Async SQLCipher wrapper built on top of aiosqlite.
Provides async database connections with SQLCipher AES-256 encryption.
This module wraps aiosqlite to use sqlcipher3 instead of standard sqlite3.
Requirements:
pip install kstlib[db-crypto] # Installs sqlcipher3
Examples:
Basic encrypted connection::
import asyncio
from kstlib.db.aiosqlcipher import connect
async def main():
async with connect(":memory:", cipher_key="secret") as db:
await db.execute("CREATE TABLE test (id INTEGER)")
asyncio.run(main())
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any
from kstlib.db.exceptions import EncryptionError
if TYPE_CHECKING:
from aiosqlite import Connection
__all__ = ["connect", "is_sqlcipher_available"]
log = logging.getLogger(__name__)
[docs]
def is_sqlcipher_available() -> bool:
"""Check if sqlcipher3 is installed and available.
Returns:
True if sqlcipher3 can be imported.
Examples:
>>> is_sqlcipher_available() # doctest: +SKIP
True
"""
try:
import sqlcipher3
return True
except ImportError:
return False
[docs]
def connect(
database: str | Path,
*,
cipher_key: str,
iter_chunk_size: int = 64,
**kwargs: Any,
) -> Connection:
"""Create an async connection to an encrypted SQLite database.
This function is a drop-in replacement for aiosqlite.connect() that
uses SQLCipher for AES-256 encryption. The cipher key is applied
immediately after connection using PRAGMA key.
Args:
database: Path to database file or ":memory:" for in-memory.
cipher_key: Encryption key for SQLCipher (required).
iter_chunk_size: Rows to fetch per iteration (default: 64).
**kwargs: Additional arguments passed to sqlcipher3.connect().
Returns:
Async Connection object (same interface as aiosqlite.Connection).
Raises:
EncryptionError: If sqlcipher3 is not installed or key is empty.
sqlite3.DatabaseError: If database exists but key is wrong.
Examples:
>>> async with connect("app.db", cipher_key="secret") as db: # doctest: +SKIP
... await db.execute("CREATE TABLE users (id INTEGER)")
>>> # With SOPS key resolution:
>>> from kstlib.db.cipher import resolve_cipher_key
>>> key = resolve_cipher_key(sops_path="secrets.yml") # doctest: +SKIP
>>> async with connect("app.db", cipher_key=key) as db: # doctest: +SKIP
... pass
"""
if not cipher_key:
raise EncryptionError("cipher_key is required for encrypted connections")
if "\x00" in cipher_key:
raise EncryptionError("Null bytes are not allowed in cipher key")
# Import sqlcipher3 (fail early if not installed)
try:
import sqlcipher3
except ImportError as e:
raise EncryptionError("sqlcipher3 is not installed. Install with: pip install kstlib[db-crypto]") from e
# Import aiosqlite Connection class (we reuse its async machinery)
from aiosqlite.core import Connection
# Resolve path
db_path = str(database) if isinstance(database, Path) else database
def connector() -> sqlcipher3.Connection:
"""Create encrypted connection with key already applied.
This runs in a worker thread (via aiosqlite).
"""
# Enable autocommit by default (isolation_level=None)
# This ensures data is persisted immediately without explicit commit
# User can override by passing isolation_level in kwargs
connect_kwargs = {"isolation_level": None, **kwargs}
# Connect using sqlcipher3 (NOT standard sqlite3)
conn = sqlcipher3.connect(db_path, **connect_kwargs)
# Apply encryption key (MUST be first operation)
# Escape single quotes to prevent SQL injection
escaped_key = cipher_key.replace("'", "''")
conn.execute(f"PRAGMA key = '{escaped_key}'")
# Verify key works by reading schema
# This will raise DatabaseError if key is wrong
try:
conn.execute("SELECT count(*) FROM sqlite_master")
except Exception as e:
conn.close()
raise EncryptionError(f"Invalid cipher key or corrupted database: {e}") from e
log.debug("SQLCipher connection established: %s", db_path)
return conn
# Return aiosqlite Connection with our custom connector
return Connection(connector, iter_chunk_size)