mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
57646300a4
* fix(security): encrypt CardDAV password at rest in settings.json CardDAV password was stored in plaintext in data/settings.json, while other secrets (email, CalDAV) are encrypted using src.secret_storage. On read (_get_carddav_config): decrypt the password via decrypt(). On write (update_config): encrypt the password via encrypt() before saving to settings.json. decrypt() is a no-op on plaintext, so existing deployments upgrade transparently on the first read after the next config save. * test: add coverage for CardDAV password encryption Nine tests covering: - encrypt-on-save and decrypt-on-read round-trip - encrypted value is stored with enc: prefix (plaintext absent from file) - legacy plaintext passthrough - CARDDAV_PASSWORD env var passthrough (not decrypted) - empty password / no settings file - double-save does not corrupt - encrypt() idempotent on already-encrypted value
171 lines
5.2 KiB
Python
171 lines
5.2 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
|
|
def _import_contacts(tmp_path, monkeypatch):
|
|
sys.modules.setdefault("core.database", MagicMock())
|
|
|
|
monkeypatch.setattr(
|
|
"routes.contacts_routes.SETTINGS_FILE",
|
|
tmp_path / "settings.json",
|
|
)
|
|
monkeypatch.setattr(
|
|
"routes.contacts_routes.DATA_DIR",
|
|
tmp_path,
|
|
)
|
|
monkeypatch.setattr(
|
|
"routes.contacts_routes.LOCAL_CONTACTS_FILE",
|
|
tmp_path / "contacts.json",
|
|
)
|
|
|
|
sys.modules.pop("src.secret_storage", None)
|
|
from src import secret_storage
|
|
monkeypatch.setattr(secret_storage, "_KEY_PATH", tmp_path / ".app_key")
|
|
monkeypatch.setattr(secret_storage, "_fernet", None)
|
|
|
|
sys.modules.pop("routes.contacts_routes", None)
|
|
from routes import contacts_routes
|
|
return contacts_routes
|
|
|
|
|
|
def test_carddav_password_encrypted_at_rest(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
settings = contacts._load_settings()
|
|
password = "my-carddav-secret"
|
|
from src.secret_storage import encrypt
|
|
settings["carddav_password"] = encrypt(password)
|
|
contacts._save_settings(settings)
|
|
|
|
raw_text = (tmp_path / "settings.json").read_text(encoding="utf-8")
|
|
assert password not in raw_text
|
|
raw = json.loads(raw_text)
|
|
assert raw["carddav_password"].startswith("enc:")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == password
|
|
|
|
|
|
def test_get_carddav_config_decrypts_encrypted_value(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
from src.secret_storage import encrypt
|
|
encrypted = encrypt("super-secret")
|
|
settings = {
|
|
"carddav_url": "https://carddav.example",
|
|
"carddav_username": "u",
|
|
"carddav_password": encrypted,
|
|
}
|
|
(tmp_path / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["url"] == "https://carddav.example"
|
|
assert cfg["username"] == "u"
|
|
assert cfg["password"] == "super-secret"
|
|
|
|
|
|
def test_get_carddav_config_plaintext_legacy_passthrough(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
settings = {
|
|
"carddav_url": "https://carddav.example",
|
|
"carddav_username": "u",
|
|
"carddav_password": "legacy-plaintext",
|
|
}
|
|
(tmp_path / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == "legacy-plaintext"
|
|
|
|
|
|
def test_get_carddav_config_env_var_passthrough(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
monkeypatch.setenv("CARDDAV_PASSWORD", "env-pass")
|
|
|
|
settings = {
|
|
"carddav_url": "https://carddav.example",
|
|
"carddav_username": "u",
|
|
}
|
|
(tmp_path / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == "env-pass"
|
|
|
|
|
|
def test_get_carddav_config_env_var_not_decrypted(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
monkeypatch.setenv("CARDDAV_PASSWORD", "env:plain-value-not-encrypted")
|
|
settings = {
|
|
"carddav_url": "https://carddav.example",
|
|
"carddav_username": "u",
|
|
}
|
|
(tmp_path / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == "env:plain-value-not-encrypted"
|
|
|
|
|
|
def test_get_carddav_config_empty_password(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
settings = {
|
|
"carddav_url": "https://carddav.example",
|
|
"carddav_username": "u",
|
|
}
|
|
(tmp_path / "settings.json").write_text(json.dumps(settings), encoding="utf-8")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == ""
|
|
|
|
|
|
def test_get_carddav_config_no_settings_file(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == ""
|
|
assert cfg["url"] == ""
|
|
|
|
|
|
def test_double_save_encrypted_value_not_corrupted(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
from src.secret_storage import encrypt
|
|
password = "persistent-secret"
|
|
encrypted = encrypt(password)
|
|
|
|
settings = {"carddav_password": encrypted}
|
|
contacts._save_settings(settings)
|
|
|
|
settings2 = contacts._load_settings()
|
|
contacts._save_settings(settings2)
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == password
|
|
|
|
|
|
def test_double_save_re_encrypts_already_encrypted_is_noop(tmp_path, monkeypatch):
|
|
contacts = _import_contacts(tmp_path, monkeypatch)
|
|
|
|
from src.secret_storage import encrypt
|
|
password = "another-secret"
|
|
|
|
settings = contacts._load_settings()
|
|
settings["carddav_password"] = encrypt(password)
|
|
contacts._save_settings(settings)
|
|
|
|
settings2 = contacts._load_settings()
|
|
settings2["carddav_password"] = encrypt(settings2["carddav_password"])
|
|
contacts._save_settings(settings2)
|
|
|
|
raw = json.loads((tmp_path / "settings.json").read_text(encoding="utf-8"))
|
|
assert raw["carddav_password"].startswith("enc:")
|
|
|
|
cfg = contacts._get_carddav_config()
|
|
assert cfg["password"] == password
|