feat(vault): API key storage in vault only
- vault/reader.py: get_key() reads from ~/.pyra/vault/secrets/api_keys.json - vault/writer.py: set_key(), delete_key() — only writer callable from setup - Both call assert_safe_path() as defense-in-depth - Keys file stays chmod 400; temporarily 600 during write then locked again - Config.yaml never touched by either module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pyra.security.boundaries import assert_safe_path
|
||||||
|
from pyra.utils.paths import pyra_home, safe_chmod
|
||||||
|
|
||||||
|
_KEYS_FILE = pyra_home() / "vault" / "secrets" / "api_keys.json"
|
||||||
|
|
||||||
|
|
||||||
|
def get_key(provider_id: str) -> str | None:
|
||||||
|
"""Read an API key from the vault. Never exposed to the AI."""
|
||||||
|
assert_safe_path(_KEYS_FILE) # defense-in-depth
|
||||||
|
|
||||||
|
if not _KEYS_FILE.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Ensure permissions remain tight before reading
|
||||||
|
safe_chmod(_KEYS_FILE, 0o400)
|
||||||
|
|
||||||
|
data: dict = json.loads(_KEYS_FILE.read_text())
|
||||||
|
return data.get(provider_id)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pyra.security.boundaries import assert_safe_path
|
||||||
|
from pyra.utils.paths import ensure_dir, pyra_home, safe_chmod
|
||||||
|
|
||||||
|
_KEYS_FILE = pyra_home() / "vault" / "secrets" / "api_keys.json"
|
||||||
|
|
||||||
|
|
||||||
|
def set_key(provider_id: str, api_key: str) -> None:
|
||||||
|
"""Store an API key in the vault. Called only by the setup wizard."""
|
||||||
|
assert_safe_path(_KEYS_FILE) # defense-in-depth
|
||||||
|
|
||||||
|
ensure_dir(_KEYS_FILE.parent, 0o700)
|
||||||
|
|
||||||
|
# Temporarily make writable to update
|
||||||
|
if _KEYS_FILE.exists():
|
||||||
|
safe_chmod(_KEYS_FILE, 0o600)
|
||||||
|
data: dict = json.loads(_KEYS_FILE.read_text())
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
data[provider_id] = api_key
|
||||||
|
|
||||||
|
_KEYS_FILE.write_text(json.dumps(data, indent=2))
|
||||||
|
safe_chmod(_KEYS_FILE, 0o400)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_key(provider_id: str) -> bool:
|
||||||
|
"""Remove an API key from the vault. Returns True if key existed."""
|
||||||
|
assert_safe_path(_KEYS_FILE)
|
||||||
|
|
||||||
|
if not _KEYS_FILE.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
safe_chmod(_KEYS_FILE, 0o600)
|
||||||
|
data: dict = json.loads(_KEYS_FILE.read_text())
|
||||||
|
existed = provider_id in data
|
||||||
|
data.pop(provider_id, None)
|
||||||
|
_KEYS_FILE.write_text(json.dumps(data, indent=2))
|
||||||
|
safe_chmod(_KEYS_FILE, 0o400)
|
||||||
|
return existed
|
||||||
Reference in New Issue
Block a user