密钥系统级存放: SZ_KEYRING env -> XDG 数据目录 (~/.local/share/7z-encrypt/), 旧位置自动迁移; config keyring_path 空=系统默认; 测试 71 项

This commit is contained in:
lou
2026-08-10 15:06:46 +08:00
parent 9bd979c0f1
commit 6ca5f5a827
6 changed files with 76 additions and 9 deletions
+3
View File
@@ -120,6 +120,9 @@ uv pip install --python .venv/bin/python tqdm
python config.py --init python config.py --init
python config.py --set-server http://服务器IP:8000 python config.py --set-server http://服务器IP:8000
python config.py --set-token 你的token python config.py --set-token 你的token
# 密钥库位置 (系统级): 默认 ~/.local/share/7z-encrypt/keyring.json
# 环境变量 SZ_KEYRING 可指定; 旧版 config/keyring.json 自动迁移
# 密钥导出/导入: TUI 菜单 7/8 (红色警告+二次确认)
# 3. TUI (推荐) # 3. TUI (推荐)
python3 ../tui/scripts/tui.py python3 ../tui/scripts/tui.py
+6 -4
View File
@@ -19,7 +19,7 @@ DEFAULT_CONFIG = Path(__file__).resolve().parent / "config" / "config.json"
# 默认值 (用户 config.json 逐层覆盖) # 默认值 (用户 config.json 逐层覆盖)
DEFAULTS: dict[str, Any] = { DEFAULTS: dict[str, Any] = {
"server": {"url": "", "token": "", "timeout": 30, "max_retries": 3, "retry_delay": 1.0}, "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}, "splitter": {"chunk_size_mb": 10},
"state": {"db_path": "config/tasks.db", "cleanup_days": 7}, "state": {"db_path": "config/tasks.db", "cleanup_days": 7},
} }
@@ -105,8 +105,10 @@ class AppConfig:
return self.data["crypto"]["key_id"] return self.data["crypto"]["key_id"]
@property @property
def keyring_path(self) -> Path: def keyring_path(self) -> Path | None:
return Path(self.data["crypto"]["keyring_path"]) """密钥库路径: 空 = 系统默认 (SZ_KEYRING env / XDG 数据目录)"""
raw = self.data["crypto"]["keyring_path"]
return Path(raw) if raw else None
@property @property
def chunk_size(self) -> int: def chunk_size(self) -> int:
@@ -124,7 +126,7 @@ class AppConfig:
# ---------- 分发: 构造各模块实例 ---------- # ---------- 分发: 构造各模块实例 ----------
def build_crypto(self): def build_crypto(self):
"""crypto 拿密钥 (keyring 路径)""" """crypto 拿密钥 (keyring 路径, None = 系统默认)"""
from crypto import CryptoEngine from crypto import CryptoEngine
return CryptoEngine(self.keyring_path) return CryptoEngine(self.keyring_path)
+3
View File
@@ -0,0 +1,3 @@
{
"default_key": "67e7c6baa066d932fc76fa59a76b8d3d7d0c782b8881cd9d0c4d93251565f8b2"
}
+27 -2
View File
@@ -17,6 +17,7 @@ import base64
import io import io
import json import json
import os import os
import shutil
from pathlib import Path from pathlib import Path
from typing import IO, Any, Callable from typing import IO, Any, Callable
@@ -29,8 +30,20 @@ CONTEXT = b"7z-encrypt:v1"
# 文件名加密独立 context (与文件内容加密隔离, 防交叉) # 文件名加密独立 context (与文件内容加密隔离, 防交叉)
NAME_CTX = CONTEXT + b":name" NAME_CTX = CONTEXT + b":name"
# 默认密钥库位置 (配置外置原则, 权限 600) # 默认密钥库位置 (系统级: 环境变量 SZ_KEYRING 优先, 否则 XDG 数据目录)
DEFAULT_KEYRING = Path(__file__).resolve().parent / "config" / "keyring.json" # 配置外置原则, 权限 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): class CryptoError(RuntimeError):
@@ -50,8 +63,20 @@ class CryptoEngine:
def __init__(self, keyring_path: str | Path | None = None) -> None: def __init__(self, keyring_path: str | Path | None = None) -> None:
self.keyring_path = Path(keyring_path) if keyring_path else DEFAULT_KEYRING self.keyring_path = Path(keyring_path) if keyring_path else DEFAULT_KEYRING
self._migrate_legacy_keyring()
self._keyring = self._load_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]: def _load_keyring(self) -> dict[str, bytes]:
+30 -1
View File
@@ -7,6 +7,7 @@ import os
import sys import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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): with self.assertRaises(IntegrityError):
engine2.decrypt_name(token) 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') k1 = self.eng.generate_key('k')
self.assertEqual(len(k1), 32) self.assertEqual(len(k1), 32)
self.assertTrue(os.path.exists(self.kr)) self.assertTrue(os.path.exists(self.kr))
+7 -2
View File
@@ -303,11 +303,16 @@ def _load_token() -> str:
def _load_keyring_path() -> Path: def _load_keyring_path() -> Path:
"""密钥库路径: config.json 显式指定用指定, 否则系统默认 (SZ_KEYRING env/XDG)"""
try: try:
cfg = Path("config/config.json") 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): except (OSError, json.JSONDecodeError):
return Path("config/keyring.json") pass
from crypto import DEFAULT_KEYRING
return DEFAULT_KEYRING
def main() -> int: def main() -> int: