Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/authsome/auth/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""auth.models — re-exports all model types used by the auth layer."""

from authsome.auth.models.config import EncryptionConfig, ServerConfig
from authsome.auth.models.config import ServerConfig
from authsome.auth.models.connection import (
AccountInfo,
ConnectionRecord,
Expand Down Expand Up @@ -28,7 +28,6 @@
"AuthType",
"ConnectionRecord",
"ConnectionStatus",
"EncryptionConfig",
"ExportConfig",
"ExportFormat",
"FlowType",
Expand Down
8 changes: 0 additions & 8 deletions src/authsome/auth/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,10 @@ def current_spec_version() -> int:
return 0


# TODO: This isn't a property of auth module
class EncryptionConfig(BaseModel):
"""Vault encryption backend settings for the daemon."""

mode: str = "auto"


# TODO: This isn't a property of auth module
class ServerConfig(BaseModel):
"""Daemon-owned server configuration."""

spec_version: int = Field(default_factory=current_spec_version)
encryption: EncryptionConfig = Field(default_factory=EncryptionConfig)

model_config = {"extra": "allow"}
13 changes: 6 additions & 7 deletions src/authsome/server/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from authsome.server.urls import build_server_base_url
from authsome.store.local import LocalAppStore
from authsome.vault import Vault
from authsome.vault.crypto import AesGcmEncryptionWrapper, DekManager, MasterSecretResolver


def get_authsome_home() -> Path:
Expand Down Expand Up @@ -150,13 +151,11 @@ async def list_registered_identity_handles(home: Path | None = None) -> list[str

async def create_vault(app_store: AppStore) -> Vault:
"""Create the daemon vault from an initialized application store."""
resolved_home = app_store.home
config = load_server_config(resolved_home)
return Vault(
app_store=app_store,
crypto_mode=config.encryption.mode,
master_key_path=get_server_home(resolved_home) / "master.key",
)
raw_kv = app_store.kv
secret = MasterSecretResolver(get_server_home(app_store.home)).resolve()
dek = await DekManager().load_or_create(secret, raw_kv)
encrypted_kv = AesGcmEncryptionWrapper(raw_kv, dek=dek)
return Vault(encrypted_kv)


async def create_auth_service(
Expand Down
14 changes: 7 additions & 7 deletions src/authsome/server/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
router = APIRouter()


def _describe_vault_encryption(vault) -> tuple[str | None, str | None]:
def _describe_vault_encryption(vault) -> tuple[str, str]:
"""Return effective vault encryption details for API output."""
try:
return vault.crypto_source, vault.crypto_source_description
except Exception as exc:
return None, f"Unavailable ({exc})"
return "unavailable", f"Unavailable ({exc})"


@router.get("/health", response_model=HealthResponse)
Expand All @@ -33,7 +33,7 @@ def health(request: Request) -> HealthResponse:
status="ok",
version=__version__,
mode=response_mode,
configured_encryption_mode=request.app.state.server_config.encryption.mode,
configured_encryption_mode=effective_source,
effective_encryption_source=effective_source,
encryption_backend=backend_description,
)
Expand All @@ -51,7 +51,7 @@ async def ready(
checks["spec_version"] = "ok"

vault = request.app.state.vault
configured_mode = vault.crypto_mode
configured_mode = vault.crypto_source

# 1. Active Identity Check — scoped to the authenticated caller
checks["identity"] = "ok"
Expand Down Expand Up @@ -127,7 +127,7 @@ async def whoami(
"did": getattr(request.state, "did", ""),
"registration_status": getattr(request.state, "registration_status", "registered"),
"daemon_url": server_base_url,
"configured_encryption_mode": request.app.state.server_config.encryption.mode,
"effective_encryption_source": effective_source or "unavailable",
"encryption_backend": backend_description or "Unavailable",
"configured_encryption_mode": effective_source,
"effective_encryption_source": effective_source,
"encryption_backend": backend_description,
}
90 changes: 26 additions & 64 deletions src/authsome/vault/__init__.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,58 @@
"""Vault — encrypted key-value layer over AppStore."""
"""Vault — encrypted key-value layer over AsyncKeyValue."""

from __future__ import annotations

import builtins
import json
from pathlib import Path
from typing import TYPE_CHECKING

from authsome.store.interfaces import AppStore
from authsome.vault.crypto import VaultCrypto, create_crypto

if TYPE_CHECKING:
from authsome.vault.crypto import VaultCrypto


# TODO: Vault should be very thin wrapper on key_value store
# TODO: AES-GCM should be a key_value store wrapper ( see FernetWrapper)
# TODO: Add a config for vault ( master.key and crypto mode is property of this wrapper), it should load
from key_value.aio.protocols.key_value import AsyncKeyValue


class Vault:
"""Encrypted key-value store backed by an AppStore.
"""Thin domain wrapper over an already-encrypted AsyncKeyValue store.

All values are encrypted at rest using AES-256-GCM. The master key is
managed by the configured VaultCrypto backend (local file or OS keyring).
Encryption is handled by AesGcmEncryptionWrapper at construction time.
This class owns the index records used for prefix listing.
"""

def __init__(
self,
app_store: AppStore,
crypto: VaultCrypto | None = None,
crypto_mode: str = "auto",
master_key_path: Path | None = None,
) -> None:
self._app_store = app_store
self._crypto = crypto
self._crypto_mode = crypto_mode
self._master_key_path = master_key_path

@property
def crypto(self) -> VaultCrypto:
if self._crypto is None:
self._crypto = create_crypto(self._master_key_path, self._crypto_mode)
return self._crypto

@property
def crypto_mode(self) -> str:
"""Configured crypto resolution mode."""
return self._crypto_mode
def __init__(self, kv: AsyncKeyValue) -> None:
self._kv = kv

@property
def crypto_source(self) -> str:
"""Effective crypto source identifier."""
return self.crypto.source_id
return "aes-256-gcm"

@property
def crypto_source_description(self) -> str:
"""Human-readable description of the effective crypto source."""
return self.crypto.source_description
return "AES-256-GCM with Argon2id-derived DEK"

@property
def crypto_mode(self) -> str:
return "aes-256-gcm"

# ── Index helpers ─────────────────────────────────────────────────────

async def _get_index(self, collection: str) -> builtins.list[str]:
val = await self._app_store.kv.get("__index__", collection=collection)
val = await self._kv.get("__index__", collection=collection)
if not val:
return []
return json.loads(val["data"])

async def _save_index(self, collection: str, keys: builtins.list[str]) -> None:
await self._app_store.kv.put("__index__", {"data": json.dumps(sorted(keys))}, collection=collection)
await self._kv.put("__index__", {"data": json.dumps(sorted(keys))}, collection=collection)

# ── Encrypted KV interface ────────────────────────────────────────────

async def get(self, key: str, *, collection: str) -> str | None:
"""Retrieve and decrypt a value. Returns None if key not found."""
val = await self._app_store.kv.get(key, collection=collection)
val = await self._kv.get(key, collection=collection)
if val is None:
return None
return self.crypto.decrypt(val["data"])
return val["data"]

async def put(self, key: str, value: str, *, collection: str) -> None:
"""Encrypt and store a value."""
encrypted = self.crypto.encrypt(value)
await self._app_store.kv.put(key, {"data": encrypted}, collection=collection)
await self._kv.put(key, {"data": value}, collection=collection)
if key != "__index__":
idx = set(await self._get_index(collection))
if key not in idx:
Expand All @@ -91,7 +61,7 @@ async def put(self, key: str, value: str, *, collection: str) -> None:

async def delete(self, key: str, *, collection: str) -> bool:
"""Delete a key. Returns True if the key existed."""
existed = await self._app_store.kv.delete(key, collection=collection)
existed = await self._kv.delete(key, collection=collection)
if existed and key != "__index__":
idx = set(await self._get_index(collection))
idx.discard(key)
Expand All @@ -103,21 +73,13 @@ async def list(self, prefix: str = "", *, collection: str) -> builtins.list[str]
idx = await self._get_index(collection)
if prefix:
return [k for k in idx if k.startswith(prefix)]
return list(idx)

# ── Lifecycle ─────────────────────────────────────────────────────────
return builtins.list(idx)

async def check_integrity(self, *, identity: str | None = None) -> bool:
"""Perform health check on underlying store."""
"""Perform a lightweight health check on the underlying store."""
_ = identity
return await self._app_store.check_integrity()

async def close(self) -> None:
"""Release resources."""
await self._app_store.close()

async def __aenter__(self) -> Vault:
return self

async def __aexit__(self, *args: object) -> None:
await self.close()
try:
await self._kv.get("__integrity_probe__", collection="__vault_meta__")
return True
except Exception:
return False
Loading
Loading