密钥系统级存放: SZ_KEYRING env -> XDG 数据目录 (~/.local/share/7z-encrypt/), 旧位置自动迁移; config keyring_path 空=系统默认; 测试 71 项
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"default_key": "67e7c6baa066d932fc76fa59a76b8d3d7d0c782b8881cd9d0c4d93251565f8b2"
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user