diff --git a/README.md b/README.md index e685316..7b178d8 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,9 @@ uv pip install --python .venv/bin/python tqdm python config.py --init python config.py --set-server http://服务器IP:8000 python config.py --set-token 你的token +# 密钥库位置 (系统级): 默认 ~/.local/share/7z-encrypt/keyring.json +# 环境变量 SZ_KEYRING 可指定; 旧版 config/keyring.json 自动迁移 +# 密钥导出/导入: TUI 菜单 7/8 (红色警告+二次确认) # 3. TUI (推荐) python3 ../tui/scripts/tui.py diff --git a/config.py b/config.py index 7007826..558569c 100644 --- a/config.py +++ b/config.py @@ -19,7 +19,7 @@ DEFAULT_CONFIG = Path(__file__).resolve().parent / "config" / "config.json" # 默认值 (用户 config.json 逐层覆盖) DEFAULTS: dict[str, Any] = { "server": {"url": "", "token": "", "timeout": 30, "max_retries": 3, "retry_delay": 1.0}, - "crypto": {"key_id": "default_key", "keyring_path": "config/keyring.json"}, + "crypto": {"key_id": "default_key", "keyring_path": ""}, "splitter": {"chunk_size_mb": 10}, "state": {"db_path": "config/tasks.db", "cleanup_days": 7}, } @@ -105,8 +105,10 @@ class AppConfig: return self.data["crypto"]["key_id"] @property - def keyring_path(self) -> Path: - return Path(self.data["crypto"]["keyring_path"]) + def keyring_path(self) -> Path | None: + """密钥库路径: 空 = 系统默认 (SZ_KEYRING env / XDG 数据目录)""" + raw = self.data["crypto"]["keyring_path"] + return Path(raw) if raw else None @property def chunk_size(self) -> int: @@ -124,7 +126,7 @@ class AppConfig: # ---------- 分发: 构造各模块实例 ---------- def build_crypto(self): - """crypto 拿密钥 (keyring 路径)""" + """crypto 拿密钥 (keyring 路径, None = 系统默认)""" from crypto import CryptoEngine return CryptoEngine(self.keyring_path) diff --git a/config/keyring.json.bak b/config/keyring.json.bak new file mode 100644 index 0000000..05efec6 --- /dev/null +++ b/config/keyring.json.bak @@ -0,0 +1,3 @@ +{ + "default_key": "67e7c6baa066d932fc76fa59a76b8d3d7d0c782b8881cd9d0c4d93251565f8b2" +} \ No newline at end of file diff --git a/crypto.py b/crypto.py index 7e3f0c8..38cd471 100644 --- a/crypto.py +++ b/crypto.py @@ -17,6 +17,7 @@ import base64 import io import json import os +import shutil from pathlib import Path from typing import IO, Any, Callable @@ -29,8 +30,20 @@ CONTEXT = b"7z-encrypt:v1" # 文件名加密独立 context (与文件内容加密隔离, 防交叉) NAME_CTX = CONTEXT + b":name" -# 默认密钥库位置 (配置外置原则, 权限 600) -DEFAULT_KEYRING = Path(__file__).resolve().parent / "config" / "keyring.json" +# 默认密钥库位置 (系统级: 环境变量 SZ_KEYRING 优先, 否则 XDG 数据目录) +# 配置外置原则, 权限 600 +def _default_keyring() -> Path: + env = os.environ.get("SZ_KEYRING") + if env: + return Path(env).expanduser() + xdg = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share") + return Path(xdg) / "7z-encrypt" / "keyring.json" + + +DEFAULT_KEYRING = _default_keyring() + +# 旧位置 (v1: 项目目录 config/keyring.json) — 自动迁移到系统位置 +LEGACY_KEYRING = Path(__file__).resolve().parent / "config" / "keyring.json" class CryptoError(RuntimeError): @@ -50,8 +63,20 @@ class CryptoEngine: def __init__(self, keyring_path: str | Path | None = None) -> None: self.keyring_path = Path(keyring_path) if keyring_path else DEFAULT_KEYRING + self._migrate_legacy_keyring() self._keyring = self._load_keyring() + def _migrate_legacy_keyring(self) -> None: + """旧位置密钥自动迁移到系统位置 (仅默认路径时, 避免密钥"丢失")""" + if self.keyring_path != DEFAULT_KEYRING: + return + if self.keyring_path.exists() or not LEGACY_KEYRING.exists(): + return + self.keyring_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(LEGACY_KEYRING, self.keyring_path) + os.chmod(self.keyring_path, 0o600) + print(f"[密钥库] 旧密钥已迁移 -> {self.keyring_path}") + # ---------- 密钥库 ---------- def _load_keyring(self) -> dict[str, bytes]: diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 1baffa3..6a79c49 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -7,6 +7,7 @@ import os import sys import tempfile import unittest +from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -40,7 +41,35 @@ class TestKeyring(unittest.TestCase): with self.assertRaises(IntegrityError): engine2.decrypt_name(token) - def test_generate_key(self): + def test_default_keyring_env_override(self): + # SZ_KEYRING 环境变量指定密钥路径 (系统级密钥位置); XDG 默认兜底 + import crypto as crypto_mod + old = os.environ.get("SZ_KEYRING") + old_xdg = os.environ.get("XDG_DATA_HOME") + try: + os.environ["SZ_KEYRING"] = "/tmp/sz-kr-test/kr.json" + self.assertEqual(crypto_mod._default_keyring(), Path("/tmp/sz-kr-test/kr.json")) + # 未设 env 时用 XDG 数据目录 + os.environ.pop("SZ_KEYRING", None) + os.environ["XDG_DATA_HOME"] = "/tmp/sz-xdg-test" + self.assertEqual(crypto_mod._default_keyring(), Path("/tmp/sz-xdg-test/7z-encrypt/keyring.json")) + # 都没有 -> ~/.local/share + os.environ.pop("XDG_DATA_HOME", None) + self.assertEqual( + crypto_mod._default_keyring(), + Path.home() / ".local" / "share" / "7z-encrypt" / "keyring.json", + ) + finally: + if old: + os.environ["SZ_KEYRING"] = old + else: + os.environ.pop("SZ_KEYRING", None) + if old_xdg: + os.environ["XDG_DATA_HOME"] = old_xdg + else: + os.environ.pop("XDG_DATA_HOME", None) + + def test_generate_and_persist(self): k1 = self.eng.generate_key('k') self.assertEqual(len(k1), 32) self.assertTrue(os.path.exists(self.kr)) diff --git a/transfer.py b/transfer.py index 00f673d..fc8f4ff 100644 --- a/transfer.py +++ b/transfer.py @@ -303,11 +303,16 @@ def _load_token() -> str: def _load_keyring_path() -> Path: + """密钥库路径: config.json 显式指定用指定, 否则系统默认 (SZ_KEYRING env/XDG)""" try: cfg = Path("config/config.json") - return Path(str(json.loads(cfg.read_text(encoding="utf-8")).get("crypto", {}).get("keyring_path", "config/keyring.json"))) + raw = str(json.loads(cfg.read_text(encoding="utf-8")).get("crypto", {}).get("keyring_path", "")) + if raw: + return Path(raw) except (OSError, json.JSONDecodeError): - return Path("config/keyring.json") + pass + from crypto import DEFAULT_KEYRING + return DEFAULT_KEYRING def main() -> int: