Files
LoveACE-EndF/encrypt_cli.py
Sibuxiangx bbc86b8330 ⚒️ 重大重构 LoveACE V2
引入了 mongodb
对数据库进行了一定程度的数据加密
性能改善
代码简化
统一错误模型和响应
使用 apifox 作为文档
2025-11-20 20:44:25 +08:00

475 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
RSA 密钥文件管理工具
支持:
1. 将 .pem 格式的密钥文件加密为 .hex 格式(使用 AES-GCM-SIV 加密)
2. 修改已加密密钥的密码
"""
import os
import shutil
from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.ciphers.aead import AESGCMSIV
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table
console = Console()
def derive_key_from_password(
password: str, salt: bytes | None = None
) -> tuple[bytes, bytes]:
"""从密码派生 AES 密钥
Args:
password (str): 用户输入的密码
salt (bytes): 盐值,如果为 None 则生成新的
Returns:
tuple[bytes, bytes]: (派生密钥, 盐值)
"""
if salt is None:
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=16, # AES-128 需要 16 字节密钥
salt=salt,
iterations=100000,
)
key = kdf.derive(password.encode("utf-8"))
return key, salt
def encrypt_pem_file(pem_file_path: str, password: str) -> str:
"""加密 PEM 文件并保存为 .hex 格式
Args:
pem_file_path (str): PEM 文件路径
password (str): 密码
Returns:
str: 保存的 .hex 文件路径
"""
pem_path = Path(pem_file_path)
# 读取 PEM 文件
if not pem_path.exists():
console.print(f"[red]✗ 文件不存在: {pem_file_path}[/red]")
return ""
with open(pem_path, "rb") as f:
plaintext = f.read()
# 派生密钥并加密
key, salt = derive_key_from_password(password)
aesgcmsiv = AESGCMSIV(key)
nonce = os.urandom(12)
ciphertext = aesgcmsiv.encrypt(nonce, plaintext, None)
# 生成 .hex 文件路径
hex_path = str(pem_path).replace(".pem", ".hex")
# 保存加密数据salt + nonce + ciphertext
with open(hex_path, "wb") as f:
f.write(salt + nonce + ciphertext)
return hex_path
def find_all_key_files(search_dir: str = ".") -> tuple[list[Path], list[Path]]:
"""检索项目中的所有密钥文件
Args:
search_dir (str): 搜索目录,默认为当前目录
Returns:
tuple[list[Path], list[Path]]: (.pem 文件列表, .hex 文件列表)
"""
search_path = Path(search_dir)
pem_files = []
hex_files = []
for pem_file in search_path.rglob("*.pem"):
# 排除备份文件
if not pem_file.name.endswith(".backup"):
pem_files.append(pem_file)
for hex_file in search_path.rglob("*.hex"):
hex_files.append(hex_file)
return pem_files, hex_files
def change_key_password(hex_file_path: str):
"""修改已加密密钥的密码
Args:
hex_file_path (str): .hex 密钥文件路径
"""
hex_path = Path(hex_file_path)
if not hex_path.exists():
console.print(
Panel(
f"[bold red]✗ 文件不存在: {hex_file_path}[/bold red]",
title="[bold red]错误[/bold red]",
expand=False,
)
)
return
# 读取加密的文件
with open(hex_path, "rb") as f:
encrypted_data = f.read()
# 解析加密数据salt(16) + nonce(12) + ciphertext
salt = encrypted_data[:16]
nonce = encrypted_data[16:28]
ciphertext = encrypted_data[28:]
# 请求旧密码
console.print(
Panel(
"[bold cyan]请输入当前密码以验证[/bold cyan]",
title="[bold blue]验证密钥[/bold blue]",
expand=False,
)
)
old_password = Prompt.ask(
"[bold]请输入当前密码[/bold]", password=True, console=console
)
# 验证旧密码
try:
old_key, _ = derive_key_from_password(old_password, salt)
aesgcmsiv = AESGCMSIV(old_key)
plaintext = aesgcmsiv.decrypt(nonce, ciphertext, None)
console.print("[bold green]✓ 密码验证成功[/bold green]")
except Exception:
console.print(
Panel(
"[bold red]✗ 密码错误或密钥文件已损坏[/bold red]",
title="[bold red]错误[/bold red]",
expand=False,
)
)
return
# 设置新密码
console.print(
Panel(
"[bold cyan]请设置新密码[/bold cyan]",
title="[bold blue]设置新密码[/bold blue]",
expand=False,
)
)
new_password = Prompt.ask(
"[bold]请输入新密码[/bold]", password=True, console=console
)
new_password_confirm = Prompt.ask(
"[bold]请确认新密码[/bold]", password=True, console=console
)
if new_password != new_password_confirm:
console.print(
Panel(
"[bold red]✗ 两次输入的密码不一致[/bold red]",
title="[bold red]错误[/bold red]",
expand=False,
)
)
return
if new_password == old_password:
console.print(
Panel(
"[bold yellow]⚠ 新密码与旧密码相同,无需修改[/bold yellow]",
expand=False,
)
)
return
# 使用新密码重新加密
console.print("[bold cyan]正在重新加密文件...[/bold cyan]")
new_key, new_salt = derive_key_from_password(new_password)
new_aesgcmsiv = AESGCMSIV(new_key)
new_nonce = os.urandom(12)
new_ciphertext = new_aesgcmsiv.encrypt(new_nonce, plaintext, None)
# 保存新的加密数据
with open(hex_path, "wb") as f:
f.write(new_salt + new_nonce + new_ciphertext)
console.print(
Panel(
"[bold green]✓ 密钥密码修改成功[/bold green]",
title="[bold blue]完成[/bold blue]",
expand=False,
)
)
def main_menu():
"""主菜单"""
while True:
console.clear()
console.print(
Panel(
"[bold cyan]RSA 密钥文件管理工具[/bold cyan]",
title="[bold blue]主菜单[/bold blue]",
expand=False,
)
)
console.print()
menu_options = [
"1. 加密 PEM 密钥文件",
"2. 修改密钥密码",
"3. 退出",
]
for option in menu_options:
console.print(f" {option}")
console.print()
choice = Prompt.ask(
"[bold]请选择操作[/bold]",
choices=["1", "2", "3"],
console=console,
)
if choice == "1":
encrypt_key_operation()
elif choice == "2":
change_password_operation()
elif choice == "3":
console.print("[bold cyan]再见![/bold cyan]")
break
def encrypt_key_operation():
"""加密密钥文件的交互操作"""
console.clear()
console.print(
Panel(
"[bold cyan]加密 PEM 密钥文件[/bold cyan]",
title="[bold blue]加密操作[/bold blue]",
expand=False,
)
)
# 获取密钥文件路径
default_path = "data/keys/private_key.pem"
private_key_path = Prompt.ask(
"[bold]请输入 RSA 私钥文件路径[/bold]",
default=default_path,
console=console,
)
console.print(
Panel(
f"[bold cyan]正在操作密钥文件[/bold cyan]\n"
f"[cyan]文件路径:{private_key_path}[/cyan]",
expand=False,
)
)
pem_path = Path(private_key_path)
if not pem_path.exists():
console.print(
Panel(
f"[bold red]✗ 文件不存在: {private_key_path}[/bold red]",
title="[bold red]错误[/bold red]",
expand=False,
)
)
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
return
# 验证是否是有效的 RSA 私钥
try:
with open(pem_path, "rb") as f:
serialization.load_pem_private_key(
f.read(), password=None, backend=default_backend()
)
console.print("[bold green]✓ RSA 私钥验证成功[/bold green]")
except Exception as e:
console.print(
Panel(
f"[bold red]✗ 无效的 RSA 私钥文件: {str(e)}[/bold red]",
title="[bold red]错误[/bold red]",
expand=False,
)
)
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
return
# 设置密码
console.print(
Panel(
"[bold cyan]请为该密钥文件设置密码[/bold cyan]",
title="[bold blue]设置密码[/bold blue]",
expand=False,
)
)
password = Prompt.ask("[bold]请输入密码[/bold]", password=True, console=console)
password_confirm = Prompt.ask(
"[bold]请确认密码[/bold]", password=True, console=console
)
if password != password_confirm:
console.print(
Panel(
"[bold red]✗ 两次输入的密码不一致[/bold red]",
title="[bold red]错误[/bold red]",
expand=False,
)
)
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
return
# 加密文件
console.print("[bold cyan]正在加密文件...[/bold cyan]")
hex_path = encrypt_pem_file(private_key_path, password)
if not hex_path:
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
return
# 备份原文件
backup_path = str(pem_path) + ".backup"
shutil.copy(pem_path, backup_path)
# 删除原文件
pem_path.unlink()
# 如果存在公钥文件,也转换为 .hex
public_key_path = str(pem_path).replace("private_key.pem", "public_key.pem")
if Path(public_key_path).exists():
public_hex_path = public_key_path.replace(".pem", ".hex")
shutil.copy(public_key_path, public_hex_path)
Path(public_key_path).unlink()
console.print(f"[cyan]公钥文件已转换: {public_hex_path}[/cyan]")
# 显示结果
console.print(
Panel(
"[bold green]✓ 密钥文件加密成功[/bold green]",
title="[bold blue]完成[/bold blue]",
expand=False,
)
)
table = Table(title="加密结果")
table.add_column("项目", style="cyan")
table.add_column("路径", style="green")
table.add_row("原文件备份", backup_path)
table.add_row("加密后的文件", hex_path)
console.print(table)
console.print(
Panel(
"[bold yellow]提示:原 .pem 文件已删除,请妥善保管上述路径中的文件[/bold yellow]",
expand=False,
)
)
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
def change_password_operation():
"""修改密码的交互操作"""
console.clear()
console.print(
Panel(
"[bold cyan]修改密钥密码[/bold cyan]",
title="[bold blue]密码修改[/bold blue]",
expand=False,
)
)
# 扫描所有 .hex 文件
console.print("[bold cyan]扫描密钥文件中...[/bold cyan]")
_, hex_files = find_all_key_files()
if not hex_files:
console.print(
Panel(
"[bold yellow]未找到任何 .hex 密钥文件[/bold yellow]",
expand=False,
)
)
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
return
# 显示所有可用的 .hex 文件
console.print()
console.print("[bold cyan]可用的密钥文件:[/bold cyan]")
table = Table()
table.add_column("序号", style="yellow")
table.add_column("文件路径", style="green")
table.add_column("大小", style="cyan")
for idx, file_path in enumerate(hex_files, 1):
file_size = file_path.stat().st_size
table.add_row(str(idx), str(file_path), f"{file_size} bytes")
console.print(table)
# 让用户选择要修改的文件
console.print()
valid_choices = [str(i) for i in range(1, len(hex_files) + 1)]
choice = Prompt.ask(
"[bold]请选择要修改的密钥文件序号[/bold]",
choices=valid_choices,
console=console,
)
selected_hex_file = hex_files[int(choice) - 1]
console.print()
console.print(
Panel(
f"[bold cyan]正在操作密钥文件[/bold cyan]\n"
f"[cyan]文件路径:{selected_hex_file}[/cyan]",
expand=False,
)
)
change_key_password(str(selected_hex_file))
Prompt.ask(
"\n[bold]按 Enter 返回主菜单[/bold]", console=console, show_default=False
)
def main():
"""主函数"""
main_menu()
if __name__ == "__main__":
main()