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
5 changes: 2 additions & 3 deletions credential_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ def encrypt_secret(value: str | None, db_path: Path) -> str:
if not value or is_encrypted(value):
return value or ""
raw = value.encode("utf-8")
if os.name == "nt" and not os.environ.get("CB_GATEWAY_MASTER_KEY"):
protected = _dpapi_encrypt(raw)
return _DPAPI_PREFIX + base64.urlsafe_b64encode(protected).decode("ascii")
# Always Fernet (MASTER_KEY or sidecar key file). Linux Docker cannot open
# Windows DPAPI rows; existing enc:v1:dpapi: values still decrypt on Windows.
token = _get_fernet(db_path).encrypt(raw).decode("ascii")
return _FERNET_PREFIX + token

Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ services:
- CB_AUTH_DIR=/auth
- CB_CONTAINER_AUTH_DIR=/auth
- CB_DOCKER=1
# Topology: Windows host + Linux container cannot decrypt DPAPI blobs.
# Set CB_GATEWAY_MASTER_KEY when the SQLite file is shared across OS.
# Official QClaw/QwenWork login files are Windows DPAPI; import them on
# the host. SQLite tokens use the sidecar key file (or MASTER_KEY) so
# Linux can use already-imported accounts. WorkBuddy still mounts /auth.
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8787/health', timeout=3)"]
interval: 30s
Expand Down
15 changes: 15 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,21 @@ def isolated_db(tmp_path, monkeypatch):
credential_crypto.reset_cache()


def test_encrypt_without_master_key_uses_fernet_key_file(tmp_path, monkeypatch):
path = tmp_path / "gateway.db"
monkeypatch.setattr(db, "DB_PATH", path)
monkeypatch.delenv("CB_GATEWAY_MASTER_KEY", raising=False)
credential_crypto.reset_cache()
db.init_db()
account_id = db.add_account({"name": "portable", "access_token": "access-secret"})
with sqlite3.connect(path) as conn:
raw = conn.execute("SELECT access_token FROM accounts WHERE id=?", (account_id,)).fetchone()[0]
assert raw.startswith("enc:v1:fernet:")
assert "access-secret" not in raw
assert db.get_account(account_id)["access_token"] == "access-secret"
credential_crypto.reset_cache()


def test_account_credentials_are_encrypted_at_rest(isolated_db):
account_id = db.add_account(
{
Expand Down