fix(aibox/skills): Batch M 波次1 凭据收口(S1/S4/S5/S7/S8)

凭据不再进命令行 argv/会话日志,对齐 SKILLS_FORMAT.md J12 条例:
- gitclient: token 改经 GIT_CONFIG_* 环境变量传 http.extraheader(git>=2.31),不再嵌入 clone URL(不进 .git/config);新增 _scrub_token 输出统一脱敏
- ssh-exec: 移除 -p/--password,密码仅经环境变量 AIBOX_SSH_PASS 提供
- sftp-transfer: 移除 -p/--password,AIBOX_SFTP_PASS 优先、getpass 兜底,密钥认证不再强制索密码
- aes-crypto: 移除 --key,密钥仅经 --key-file/--load-key 文件途径;生成密钥不再打印明文;save_key_iv 写后 chmod 600

验证:py_compile 通过;aes 生成/加载/加解密闭环实测正常,--key 被拒绝;全量测试 493 passed / 4 failed(平台环境项)/ 1 skipped,与基线一致。审计报告 §17 结论 + §18 波次1修复记录同步。
This commit is contained in:
qing committed 2026-08-20 12:29:34 +08:00
1 parent e43aeeaf44
commit 5dc5ec9af2
5 files changed
+238 -54

No files matched your search

+25 -26
View File
@@ -59,14 +59,14 @@ class AESCli:
return False
def generate_random_key(self, key_size=16):
"""生成随机密钥"""
"""生成随机密钥(S8:密钥内容不打印,避免进入会话日志;经 --save-key 持久化)"""
if key_size not in [16, 24, 32]:
print(f"错误: 密钥大小必须为16、24或32,当前为{key_size}")
return None
self.key = get_random_bytes(key_size)
key_hex = self.key.hex()
print(f"已生成 {key_size} 字节随机密钥: {key_hex}")
print(f"已生成 {key_size} 字节随机密钥(内容不显示,请用 --save-key 保存到文件)")
return key_hex
def generate_random_iv(self):
@@ -330,6 +330,10 @@ class AESCli:
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
try:
os.chmod(filepath, 0o600) # S8:密钥文件仅属主可读写
except OSError:
pass # 不支持 POSIX 权限的平台(如 Windows)跳过
print(f"密钥和IV已保存到: {filepath}")
return True
@@ -395,38 +399,33 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
# 生成随机密钥和IV
aes-crypto --gen-key
# 生成随机密钥和IV(密钥不打印,经 --save-key 保存到文件)
aes-crypto --gen-key --gen-iv --save-key keys.json
aes-crypto --gen-iv
# 文本加解密
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --encrypt-text "Hello World"
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --decrypt-text "<密文base64>"
# 文本加解密(密钥经文件提供,不走命令行)
aes-crypto --key-file key.txt --iv <IV/Nonce hex> --encrypt-text "Hello World"
aes-crypto --key-file key.txt --iv <IV/Nonce hex> --decrypt-text "<密文base64>"
# 文件加解密
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --encrypt-file input.txt
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --decrypt-file input.txt.encrypted
aes-crypto --load-key keys.json --encrypt-file input.txt
aes-crypto --load-key keys.json --decrypt-file input.txt.encrypted
# 批量加解密
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --batch-encrypt file1.txt file2.txt
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --batch-decrypt *.encrypted
# 保存和加载密钥
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --save-key keys.json
aes-crypto --load-key keys.json --encrypt-file input.txt
aes-crypto --load-key keys.json --batch-encrypt file1.txt file2.txt
aes-crypto --load-key keys.json --batch-decrypt *.encrypted
# 使用GCM模式
aes-crypto --mode GCM --gen-key --key-size 32
aes-crypto --mode GCM --key <密钥hex> --iv <Nonce hex> --encrypt-file input.txt
aes-crypto --mode GCM --gen-key --key-size 32 --save-key keys.json
aes-crypto --mode GCM --load-key keys.json --encrypt-file input.txt
# 从文件读取密钥
aes-crypto --key-file key.txt --iv <IV/Nonce hex> --encrypt-file input.txt
参数说明:
--mode CBC|GCM 加密模式 (默认: CBC)
--key KEY 十六进制密钥 (16/24/32字节)
--iv IV 十六进制IV/Nonce (CBC:16字节, GCM:12字节)
--key-file FILE 从文件读取密钥
--key-file FILE 从文件读取密钥(密钥仅经文件提供,避免进入进程列表与会话日志)
--load-key FILE 从JSON文件加载密钥和IV
--save-key FILE 保存密钥和IV到JSON文件
--gen-key 生成随机密钥
@@ -448,8 +447,7 @@ def main():
parser.add_argument('--mode', choices=['CBC', 'GCM'], default='CBC',
help='加密模式 (默认: CBC)')
# 密钥和IV设置
parser.add_argument('--key', help='十六进制密钥 (16/24/32字节)')
# 密钥和IV设置(S7:密钥仅经 --key-file/--load-key 文件途径提供,不进命令行)
parser.add_argument('--iv', help='十六进制IV/Nonce (CBC:16字节, GCM:12字节)')
parser.add_argument('--key-file', help='从文件读取密钥')
parser.add_argument('--load-key', help='从JSON文件加载密钥和IV')
@@ -491,27 +489,28 @@ def main():
sys.exit(1)
# 处理密钥文件
key_input = None
if args.key_file:
try:
with open(args.key_file, 'r') as f:
args.key = f.read().strip()
key_input = f.read().strip()
except Exception as e:
print(f"错误: 读取密钥文件失败: {e}")
sys.exit(1)
# 设置密钥和IV
if args.key:
if key_input:
if args.iv is None and not aes.iv:
print("错误: 需要同时提供密钥和IV/Nonce")
sys.exit(1)
if args.iv:
if not aes.set_key_iv(args.key, args.iv):
if not aes.set_key_iv(key_input, args.iv):
sys.exit(1)
elif aes.iv:
# 从加载的密钥文件中获取IV
try:
aes.key = bytes.fromhex(args.key)
aes.key = bytes.fromhex(key_input)
if len(aes.key) not in [16, 24, 32]:
print(f"错误: 密钥长度必须为16、24或32字节,当前为{len(aes.key)}字节")
sys.exit(1)
@@ -548,7 +547,7 @@ def main():
if args.encrypt_text or args.decrypt_text or \
args.encrypt_file or args.decrypt_file or \
args.batch_encrypt or args.batch_decrypt:
print("错误: 请先设置密钥和IV/Nonce (使用 --key 和 --iv 或 --load-key)")
print("错误: 请先设置密钥和IV/Nonce (使用 --key-file 和 --iv 或 --load-key)")
sys.exit(1)
try:
+19 -8
View File
@@ -79,19 +79,30 @@ class GitHubTool:
return date_str
def _run_git(self, repo_path: str, cmd: List[str]) -> Dict:
env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
if GITHUB_TOKEN:
# 凭据仅经环境变量传递(git>=2.31 的 GIT_CONFIG_* 机制),
# 不进命令行 argv / 远端 URL / .git/config(S1,对齐 J12 条例)
env["GIT_CONFIG_COUNT"] = "1"
env["GIT_CONFIG_KEY_0"] = "http.extraheader"
env["GIT_CONFIG_VALUE_0"] = f"Authorization: token {GITHUB_TOKEN}"
try:
result = subprocess.run(
["git"] + cmd, cwd=repo_path, capture_output=True,
text=True, timeout=60, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}
text=True, timeout=60, env=env
)
return {"success": result.returncode == 0, "stdout": result.stdout.strip(), "stderr": result.stderr.strip()}
return {"success": result.returncode == 0,
"stdout": self._scrub_token(result.stdout.strip()),
"stderr": self._scrub_token(result.stderr.strip())}
except Exception as e:
return {"success": False, "error": str(e)}
return {"success": False, "error": self._scrub_token(str(e))}
def _auth_url(self, repo_url: str) -> str:
if GITHUB_TOKEN and repo_url.startswith("https://"):
return repo_url.replace("https://", f"https://{GITHUB_TOKEN}@")
return repo_url
@staticmethod
def _scrub_token(text: str) -> str:
"""输出脱敏:任何路径回显的 token 一律替换(S1)"""
if GITHUB_TOKEN and text:
return text.replace(GITHUB_TOKEN, "***")
return text
# ==================== 搜索功能 ====================
@@ -154,7 +165,7 @@ class GitHubTool:
if os.path.exists(target_dir):
return f"⚠️ 仓库已存在: {target_dir}\n💡 使用 pull 命令拉取更新"
clone_url = self._auth_url(f"https://github.com/{repo}.git")
clone_url = f"https://github.com/{repo}.git"
cmd = ["clone"]
if depth > 0:
cmd.extend(["--depth", str(depth)])
+15 -9
View File
@@ -27,9 +27,12 @@ def build_parser():
--host SSH服务器主机名或IP地址 (必填)
--port SSH服务器端口号,默认 22
-u, --username SSH登录用户名 (必填)
-p, --password SSH登录密码 (如不提供则交互式输入)
--key SSH私钥文件路径 (用于密钥认证)
密码认证:
密码经环境变量 AIBOX_SFTP_PASS 提供;未设置且未用 --key 时
交互式输入。不走命令行参数,避免凭据进入进程列表与会话日志(J12 条例)。
操作参数:
--upload LOCAL REMOTE 上传:本地文件/目录 -> 远程服务器路径
--download REMOTE LOCAL 下载:远程服务器文件/目录 -> 本地路径
@@ -37,19 +40,19 @@ def build_parser():
-v, --verbose 显示详细信息
使用示例:
1. 上传文件(密码认证):
sftp-transfer --host 192.168.1.100 -u root -p mypass --upload /tmp/test.txt /root/
1. 上传文件(密码认证,环境变量提供密码):
AIBOX_SFTP_PASS=mypass sftp-transfer --host 192.168.1.100 -u root --upload /tmp/test.txt /root/
2. 下载文件(密钥认证):
sftp-transfer --host 192.168.1.100 -u root --key ~/.ssh/id_rsa \\
--download /root/test.txt /tmp/
3. 递归上传目录:
sftp-transfer --host 192.168.1.100 -u user -p pass -r \\
AIBOX_SFTP_PASS=pass sftp-transfer --host 192.168.1.100 -u user -r \\
--upload ./mydir /home/user/mydir
4. 下载目录:
sftp-transfer --host 192.168.1.100 -u user -p pass -r \\
AIBOX_SFTP_PASS=pass sftp-transfer --host 192.168.1.100 -u user -r \\
--download /home/user/backup ./backup
5. 交互式输入密码:
@@ -61,7 +64,6 @@ def build_parser():
parser.add_argument('--host', required=True, help='SSH服务器主机名或IP地址')
parser.add_argument('--port', type=int, default=22, help='SSH服务器端口号,默认22')
parser.add_argument('-u', '--username', required=True, help='SSH登录用户名')
parser.add_argument('-p', '--password', help='SSH登录密码(如不提供则交互式输入)')
parser.add_argument('--key', help='SSH私钥文件路径(用于密钥认证)')
# 操作参数
@@ -82,9 +84,13 @@ def log(verbose, msg):
def get_password(args):
"""获取密码:优先从命令行参数获取,否则交互式输入"""
if args.password:
return args.password
"""获取密码:环境变量 AIBOX_SFTP_PASS 优先,否则交互式输入(S5:凭据不走命令行)"""
env_pass = os.environ.get("AIBOX_SFTP_PASS")
if env_pass:
return env_pass
if args.key:
# 密钥认证时不强制索要密码(仅在私钥有口令时经交互输入)
return None
return getpass.getpass(f"请输入 {args.username}@{args.host} 的密码: ")
+15 -6
View File
@@ -2,6 +2,7 @@
"""ssh-exec - 通过SSH远程执行单条命令的工具(支持密码认证)"""
import argparse
import os
import sys
import paramiko
@@ -14,24 +15,32 @@ def main():
-H, --host SSH主机地址(默认: 127.0.0.1)
-P, --port SSH端口号(默认: 22)
-u, --user SSH用户名(必填)
-p, --password SSH密码(必填)
-c, --command 要执行的远程命令(必填)
密码认证:
密码经环境变量 AIBOX_SSH_PASS 提供(必填),不走命令行参数,
避免凭据进入进程列表与会话日志(J12 条例)。
使用示例:
%(prog)s -u qing -p 12345678 -c "ls -la"
%(prog)s -u root -H 192.168.1.100 -P 22 -p mypass -c "df -h"
%(prog)s -u qing -H 127.0.0.1 -p 12345678 -c "uname -a"
AIBOX_SSH_PASS=12345678 %(prog)s -u qing -c "ls -la"
AIBOX_SSH_PASS=mypass %(prog)s -u root -H 192.168.1.100 -P 22 -c "df -h"
AIBOX_SSH_PASS=12345678 %(prog)s -u qing -H 127.0.0.1 -c "uname -a"
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("-H", "--host", default="127.0.0.1", help="SSH主机地址(默认: 127.0.0.1)")
parser.add_argument("-P", "--port", type=int, default=22, help="SSH端口号(默认: 22)")
parser.add_argument("-u", "--user", required=True, help="SSH用户名(必填)")
parser.add_argument("-p", "--password", required=True, help="SSH密码(必填)")
parser.add_argument("-c", "--command", required=True, help="要执行的远程命令(必填)")
args = parser.parse_args()
# 密码仅从环境变量读取(S4:凭据不进命令行)
password = os.environ.get("AIBOX_SSH_PASS")
if not password:
print("错误: 请通过环境变量 AIBOX_SSH_PASS 提供SSH密码", file=sys.stderr)
sys.exit(2)
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -40,7 +49,7 @@ def main():
hostname=args.host,
port=args.port,
username=args.user,
password=args.password,
password=password,
timeout=10,
)