81 lines
2.2 KiB
Python
81 lines
2.2 KiB
Python
"""Small secret storage abstraction.
|
|
|
|
For now:
|
|
- macOS: uses the built-in Keychain via the `security` CLI.
|
|
- other OSes: prompts only (no persistent storage) unless CERTCTL_* env vars used.
|
|
|
|
This keeps dependencies minimal and avoids coupling to a specific enterprise vault.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
|
|
def _is_macos() -> bool:
|
|
return platform.system().lower() == "darwin"
|
|
|
|
|
|
def get_from_keychain(service: str, account: str) -> Optional[str]:
|
|
if not _is_macos():
|
|
return None
|
|
try:
|
|
# -w prints password only
|
|
out = subprocess.check_output(
|
|
["security", "find-generic-password", "-s", service, "-a", account, "-w"],
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
return out.decode().strip()
|
|
except subprocess.CalledProcessError:
|
|
return None
|
|
|
|
|
|
def set_in_keychain(service: str, account: str, secret: str) -> None:
|
|
if not _is_macos():
|
|
return
|
|
# Add or update
|
|
subprocess.check_call(
|
|
[
|
|
"security",
|
|
"add-generic-password",
|
|
"-U",
|
|
"-s",
|
|
service,
|
|
"-a",
|
|
account,
|
|
"-w",
|
|
secret,
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
|
|
def env_or_keychain(env_var: str, service: str, account: str) -> Optional[str]:
|
|
v = os.environ.get(env_var)
|
|
if v:
|
|
return v
|
|
return get_from_keychain(service, account)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backwards-compatible aliases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def is_macos_keychain_available() -> bool:
|
|
"""Return True if this host is macOS (Keychain supported via `security`)."""
|
|
return _is_macos()
|
|
|
|
|
|
def get_secret(service: str, account: str) -> Optional[str]:
|
|
"""Alias for get_from_keychain(service, account)."""
|
|
return get_from_keychain(service, account)
|
|
|
|
|
|
def set_secret(service: str, account: str, secret: str) -> None:
|
|
"""Alias for set_in_keychain(service, account, secret)."""
|
|
set_in_keychain(service, account, secret)
|