Source code for edri.protocol.keys
"""Pre-shared keys, from the environment or a file.
A file is supported because environment variables are readable through
``/proc/<pid>/environ``, ``docker inspect`` and pod specs, and because a file is
what Kubernetes and Docker secrets mount. It holds one key per line so a
rotation can have both the old and the new key present at once.
"""
import os
from collections.abc import Iterator
from pathlib import Path
__all__ = ["MINIMUM_SECRET_LENGTH", "KeySet"]
MINIMUM_SECRET_LENGTH = 32
[docs]
class KeySet:
"""One or more accepted keys. ``primary`` is what this process sends with."""
def __init__(self, keys: list[bytes]) -> None:
if not keys:
raise ValueError("at least one key is required")
for key in keys:
if len(key) < MINIMUM_SECRET_LENGTH:
raise ValueError(
f"secrets must be at least {MINIMUM_SECRET_LENGTH} bytes; "
"generate one with "
"python -c \"import secrets; print(secrets.token_urlsafe(32))\""
)
self.keys = keys
@property
def primary(self) -> bytes:
return self.keys[0]
def __len__(self) -> int:
return len(self.keys)
def __iter__(self) -> Iterator[bytes]:
return iter(self.keys)
[docs]
@classmethod
def from_file(cls, path: str) -> "KeySet":
try:
content = Path(path).read_text()
except OSError as e:
raise ValueError(f"cannot read the secret file {path}: {e}") from e
keys = [
line.strip().encode()
for line in content.splitlines()
if line.strip() and not line.lstrip().startswith("#")
]
if not keys:
raise ValueError(f"the secret file {path} contains no keys")
return cls(keys)
[docs]
@classmethod
def from_settings(cls) -> "KeySet | None":
"""Build from configuration, or ``None`` when no secret is configured."""
secret_file = os.getenv("EDRI_SWITCH_SECRET_FILE")
if secret_file:
return cls.from_file(secret_file)
secret = os.getenv("EDRI_SWITCH_SECRET")
if secret:
return cls([secret.encode()])
return None