【免费下载链接】Cybersecurity-Projects
Building 70 Projects ranging from beginner to advanced so anyone can — learn from, build upon, use as a reference, or even copy directly. Gamified Cybersecurity learning 👇
这是一篇以pv加密命令行密码管理器(Argon2id 密钥派生 + AES-256-GCM 认证加密 + 原子持久化写入)为实验场的实战进阶指南。全文围绕 04-CHALLENGES.md 中按难度分层的 15 个扩展挑战展开:每个挑战都给出「做什么、为什么有趣、从哪改、要防什么坑」四段式说明,并配合仓库源码印证其底层调用链。读完本文,你将具备直接动手为pv增加search、export、TOTP、透明 KDF 升级等真实功能的完整能力,并能用「威胁建模」的视角审视每一次改动对整体安全模型的影响。
写在前面:为什么「构建」是消化这份代码的最好方式
pv是一个典型的「读了全项目才真正理解每一行为什么存在」的代码库。学习路径按顺序阅读 00-OVERVIEW.md、01-CONCEPTS.md、02-ARCHITECTURE.md 与 03-IMPLEMENTATION.md 之后,最诚实也最有效的下一步,就是在现有代码之上构建新的东西。
一个关于「范围」的重要提醒:不要试图全部完成这 15 个挑战,甚至不要做五个。挑一个,把它做好,然后停下来。目标不是堆砌功能,而是通过与现有代码交互、改动真实路径来内化它。每个挑战都会给出四项信息:
- What(做什么)——一两句话描述这个功能;
- Why it's interesting(为什么有趣)——完成它你会学到什么;
- Where to start(从哪开始)——你会碰到的文件;
- Watch out for(当心什么)——新手最容易踩的安全与正确性陷阱。
在开始之前,先熟悉三份关键源码,它们是几乎所有挑战的改动落点:
- main.py —— CLI 入口,所有
pv子命令的注册与胶水逻辑都在这里。使用 Typer 框架,@app.command()装饰器把普通函数变成 CLI 命令; - vault.py ——
Entry数据类、UnlockedVault生命周期、原子写入_atomic_write与文件锁_file_lock; - constants.py —— 所有「魔法数字」与提示文案的唯一出处:Argon2 参数、文件格式键名、
0o600文件模式、默认路径~/.password-vault/vault.json等。
Tier 1 — 小型功能(每个约 30 分钟)
这一层级适合作为「第一个 PR」的热身:改动量小、边界清晰,但足以强迫你把main.py与vault.py通读一遍,找到「添加一个新命令」的正确接缝。
1. 增加search命令
做什么:pv search <substring>列出所有名称中包含该子串的条目(大小写不敏感),类似pv list但带过滤。
为什么有趣:上手容易,但要求你仔细阅读main.py与vault.py,找到新增命令的合适位置,是很好的「第一个 PR」热身练习。
从哪开始:复制main.py中的list_entries命令实现。在渲染表格前,先用过滤条件筛选unlocked.names()的返回结果。
当心什么:
- 大小写不敏感比较:统一使用
name.lower()与query.lower(),不要依赖某个字母恰好同大小写; - 空搜索词应当被拒绝,而不是「返回所有条目」。参考
_validate_entry_name(vault.py)对空字符串与空白串的防御思路。
源码佐证:现有list_entries(main.py)展示了完整的命令骨架:with _unlock_or_exit(vault, master) as unlocked:解锁上下文 →unlocked.names()取名称列表 → 空库时打印MSG_VAULT_EMPTY→ 用 rich 的Table渲染。names()在 vault.py 返回sorted(self.entries.keys()),你只需在排序前后应用过滤即可。
2. 增加count命令
做什么:pv count只打印条目数量,便于在 shell 脚本中调用。
为什么有趣:这是体积最小的新命令,能强迫你思考输出格式(只有数字加换行、没有任何装饰),使其可被管道消费:if [ "$(pv count)" -eq 0 ]; then ...。
从哪开始:复制gen—— 它是最简单的现有命令。读取条目,打印len(unlocked.entries)。
当心什么:
- 使用
print()而不是console.print(),保证输出可被管道直接消费。这一点在gen命令的注释中有明确示范:「Plainprint()so the output is pipe-friendly:PASSWORD=$(pv gen 32)」(main.py); - 空保险库仍然打印
0,而不是「vault is empty」——脚本消费者期望的是稳定的数字输出。
3. 展示「最后使用时间」(last used)时间戳
做什么:为Entry增加last_used_at字段,get命令在读取时更新它并保存保险库。
为什么有趣:你会触达每一层——Entry数据类、它的from_dict/to_dict、保存路径以及main.py中的get,是观察整个架构「动起来」的好机会。
从哪开始:用field(default_factory=lambda: "")添加该字段,使旧保险库无需该字段也能正常打开;同步更新from_dict中的字段读取逻辑。
当心什么:
- 旧保险库没有这个字段——缺失键的兜底方式要与
created_at、updated_at保持一致。参考 vault.py 中data.get("created_at", "")的默认值写法; get现在会改变保险库状态 → 必须在with块结束前调用save()。参考 main.py 中delete命令「变更后显式unlocked.save()」的既有模式;- 这会改变威胁模型:攻击者窃取保险库后,现在能知道「你最近用了哪一条」。请在文档中写明这一权衡——这正是 01-CONCEPTS.md §12 强调的「每个功能都在扩展攻击面」的思维练习。
源码佐证:Entry是@dataclass(slots=True, frozen=True)的不可变记录(vault.py),updated_at的「覆写时保留原创建时间、刷新更新时间」逻辑在add_entry(vault.py),可复刻这一模式实现last_used_at。
4.get默认隐藏密码,除非传入--show
做什么:pv get github展示除密码外的所有字段(密码显示为••••••••),pv get github --show才显示真实密码。
为什么有趣:很小的 UX 改动,但正是真实密码管理器为「会议中投屏」场景会做的功能。
从哪开始:为get命令添加--show / -s标志(参照add命令中--force / -f与--generate / -g的Annotated[bool, typer.Option(...)]写法,见 main.py)。在_render_entry中按标志分支输出。
当心什么:
- 默认隐藏、显式开启可见——安全默认。这也是全项目「默认拒绝」设计哲学的延续,例如主密码从不作为 CLI 标志传入(见 README.md 的 IMPORTANT 提示);
- 圆点字符
•并非在所有终端都能渲染——需要提供回退显示(如*或<hidden>)。
Tier 2 — 中型功能(每个约数小时)
这一层级开始引入真实的安全权衡:明文落盘、剪贴板残留、密钥派生成本、升级路径。每一项都值得在完成后回头重读一遍威胁模型。
5. 实现pv export与pv import
做什么:pv export <path>将每条条目写入纯文本 JSON 文件(写入前提示输入主密码并给出强烈的「你确定吗」警告);pv import <path>执行反向操作。
为什么有趣:真实、有用、而且危险。真实——每个密码管理器都需要迁移进出的能力;有用——用户会换设备、换工具、遭遇丢失;危险——把明文凭据写到磁盘上,恰恰是本项目构建时想要避免的事。
从哪开始:在main.py中新增两个命令。导出时序列化unlocked.entries并以 0600 模式写 JSON 文件;导入时读取 JSON、校验结构、逐条调用add_entry。
当心什么:
- 导出文件是明文。写入前打印一条巨大的红色警告;
- 导出路径默认设为
./pv-export.json,而不是用户主目录——让用户想清楚文件落在了哪里; - 导入应优雅处理「条目已存在」:询问用户、或提供
--force标志、或直接跳过; - 校验导入的 JSON 的方式要与
Entry.from_dict一致——不要信任文件结构。参考 vault.py:必填字段缺失会抛出VaultFormatError,非字符串类型同样拒绝; - 导出文件若权限过弱是真正的隐患。用
os.open显式设置 0600 模式——正是_atomic_write使用的同一技巧(vault.py):「Open with mode 0600 from the very first syscall」,避免Path.write_bytes先以 umask(常为 0644)建文件再 chmod 的竞态窗口。
源码佐证:现有add命令已经提供了「逐字段收集 →Entry(...)→add_entry(name, entry, force=force)→save()」的完整链路(main.py),import可以逐行复用它。
6. 增加密码强度评分
做什么:用户运行pv add时,在保存前显示强度评分(弱/中等/强/极强)。可用zxcvbn-python这类库。
为什么有趣:实用的 UX 功能,会强迫你新增依赖(修改pyproject.toml与uv.lock),并理解「对人类的观感随机」与「能扛住离线猜测攻击」之间的区别。
从哪开始:将zxcvbn加入pyproject.toml的dependencies列表(现有依赖见 pyproject.toml:argon2-cffi、cryptography、typer、rich)。对用户输入的密码调用评分函数,把 0-4 的分数映射为颜色与标签,并允许用户继续保存。
当心什么:
- 不要阻止用户保存弱密码——他们可能有充分理由。给出警告并询问即可;
- 评分必须在密码「定型」之前展示——如果等到
save()之后才评分,用户想重试就得先删除再重新添加。
7. 增加pv copy <name>命令
做什么:将密码复制到系统剪贴板而不打印出来。可用pyperclip库。
为什么有趣:真实密码管理器都这么做。这会强迫你思考平台差异(Linux/macOS/Windows 的剪贴板 API 各不相同)以及「凭据留在剪贴板中」的安全权衡。
从哪开始:把pyperclip加入依赖。新增命令:解锁保险库 → 获取条目 → 复制密码 → 打印确认信息(绝不打印密码本身)。
当心什么:
- 在 Linux 上需要系统级安装
xclip或wl-clipboard——请在文档中写明这一前置条件; - 剪贴板内容会一直保留到被其他内容覆盖。加分挑战:启动一个后台线程,30 秒后清除剪贴板;
- pyperclip 在远程 SSH 会话中不可用——导入失败时要给出清晰的错误提示,而不是裸抛异常。
8. 为init增加--verify标志
做什么:创建保险库后,立即用同一密码尝试解锁。如果解锁失败,说明出了严重问题。
为什么有趣:纵深防御。保存 → 重新解锁的循环能在开发期捕获多类 bug,并养成「每次写入都要测试读取路径」的习惯——这对文件格式类项目尤其有价值。
从哪开始:为init增加--verify / -V标志。UnlockedVault.create()返回后,用同一密码调用UnlockedVault.unlock()。打印绿色对勾或红色警报。
当心什么:
- verify 调用意味着又一次完整的 Argon2 派生(约 0.5 秒)。必须做成 opt-in,而不是默认行为;
- 如果 verify失败,说明出了严重问题——拒绝让新保险库留在原地,删除它。
源码佐证:init命令当前流程为「存在性预检 → 两次主密码确认(_prompt_master_password_with_confirmation)→UnlockedVault.create()→ 打印成功」(main.py)。create在 vault.py 生成新盐、派生密钥、写入空条目库;unlock则走完整「解析信封 → 用文件内参数派生密钥 → GCM 解密」路径,两者配对正是写读闭环。
Tier 3 — 大型功能(每个约一个周末)
这一层级触及「真实密码管理器每天都在做」的事:算法升级、备份策略、2FA、本地 Web 界面。每个挑战都会深刻改变你对「一个功能有多难」的认知。
9. 增加 TOTP(基于时间的一次性密码)
做什么:部分网站用 TOTP 做双因素认证。目前你只能把种子存到别处(Google Authenticator、Authy)。为Entry增加 TOTP 种子字段,并提供pv totp <name>命令打印当前 6 位验证码。
为什么有趣:你会真正理解 TOTP 是什么(RFC 6238——本质上就是对当前 30 秒窗口做 HMAC-SHA1,出奇地简单)。pyotp库两行就能实现,但自己手写核心逻辑也只是一个约 30 行的练习。
从哪开始:把pyotp加入依赖。为Entry增加可选的totp_secret字段(复用挑战 3 的field(default_factory=...)模式保证旧库兼容)。在main.py中添加命令。
当心什么:
- TOTP 种子与密码同等敏感。它必须存放在加密保险库内部,而不是 sidecar 文件里——否则就绕过了本项目全部防护;
- 时钟偏移很重要。打印「此验证码还有 X 秒有效」,避免用户输入一个即将翻转的码;
- 从二维码导入 TOTP 种子是另一个独立项目——这里不要试图去做。
10. 让 KDF 成本升级变得透明
做什么:当用户解锁一个 Argon2 参数低于当前代码默认值的保险库时,自动用新默认值重新派生密钥并保存——同一密码、更强的派生。打印一条消息:「Vault parameters upgraded to current defaults.」
为什么有趣:这是真实密码管理器的做法,也正是我们在文件里存储 KDF 参数的原因。它会强迫你彻底理解change_master_password流程,并思考「用户没要求的长时间操作」这一 UX 问题。
从哪开始:在UnlockedVault.unlock内,成功解密后,将kdf_parameters与KdfParameters.defaults()比较。若不同,就地执行一次变更(调用类似change_master_password(same_password, new_kdf_parameters=...))并保存。
当心什么:
- 新 Argon2 成本只支付一次,不要付两次。重构
change_master_password,使其能在不轮换密码的情况下「就地升级」; - 用户会看到两次约 0.5 秒的连续停顿。第二次停顿前要打印消息说明原因;
- 为不想要该行为的用户提供
--no-auto-upgrade标志。
源码佐证:change_master_password(vault.py)已经支持kdf_parameters参数——注释明确写道「None 表示使用当前生产默认值——适合在轮换密码的同时升级参数」。这正是透明升级所需的全部基础设施:unlock解密成功后只需比较参数、调用change_master_password(master_password, kdf_parameters=KdfParameters.defaults())再save()。KdfParameters.defaults()在 crypto.py 返回time_cost=3, memory_cost=65536, parallelism=4(64 MiB / 3 遍 / 4 通道,见 constants.py)。
11. 实现带版本快照的pv backup命令
做什么:pv backup将当前保险库复制到~/.password-vault/backups/vault-YYYY-MM-DD-HHMMSS.json,并只保留最近 N 份。增加pv restore <timestamp>命令,用备份覆盖当前保险库。
为什么有趣:真实系统需要备份,但备份同样是攻击面——它们是攻击者可以偷走的更多文件。你需要思考:保留多少份、存在哪里、是否要加密备份索引、恢复时如何保证原子语义。
从哪开始:新增两个命令。使用现有的原子写入模式(不要用Path.write_bytes写备份,要用_atomic_write);使用现有的文件锁模式。
当心什么:
- 备份是加密文件的完整副本——它们是加密的,所以与活动保险库「同样程度」地不怕磁盘取证,但仅此而已,不会更强;
- 清理旧备份要小心——不要删除另一个
pv进程正在读取的文件。使用同一把 advisory 锁; - 「从备份 N 恢复」在覆盖活动文件之前,必须验证备份 N 是真实的保险库(解析信封、检查版本)。
源码佐证:原子写入管线_atomic_write(vault.py)五步完整可复用:os.open0600 建临时文件 →os.write→os.fsync(fd)→os.replace原子重命名 → 父目录fsync。advisory 锁_file_lock(vault.py)通过 sidecarvault.json.lock文件实现fcntl.flock(LOCK_EX),备份读写都应包在锁内。
12. 在独立的pv web命令中提供 Web UI
做什么:一个仅本地访问的 Web 服务器(localhost:8080),提供浏览保险库的简单界面。10 分钟无操作后自动关闭。
为什么有趣:这会强迫你思考所有 CLI 时代不需要考虑的安全权衡:浏览器中的主密码处理、CSRF、XSS、会话超时、localhost 上的 HTTPS 与否、第二个标签页打开时怎么办。
从哪开始:用starlette或fastapi做服务器,Jinja2 模板渲染条目。不要用 cookie——用一个自动过期的内存会话。
当心什么:
- 这比看起来难得多。真实密码管理器的 Web UI 是全日制工程。本版本的意义是理解权衡,而不是交付生产工具;
- 浏览器现在成为威胁模型的一部分:浏览器扩展可以读取 DOM,其他标签页可以导航到你的
localhost:8080。同源策略是你唯一的朋友; - 日志——Starlette/FastAPI 会为每个请求打日志。确保访问日志不包含主密码(正确使用 form POST 时不会,但务必检查)。
Tier 4 — 研究型项目
这一层级没有「构建这个具体功能」的清晰形状,而是四条值得深挖的方向——适合已经吸收完上面所有内容、还想继续前进的人。
13. 审计一个真实密码管理器的威胁模型
选一个真实的开源密码管理器:Bitwarden、KeePassXC 或 pass。阅读其文档与源码,对应本项目逐项比较:它如何派生密钥、如何存储文件、如何处理主密码轮换?写一份对比报告。
你会学到:真实密码管理器做出了不同的权衡——有些出于合理理由,有些出于历史包袱。识别「哪个是哪个」正是安全工程师赖以谋生的分析能力。本项目对应的权衡基准就是 01-CONCEPTS.md §12 的威胁模型清单与 README.md 的「Cryptographic guarantees」表格(13 项防护措施与其对应威胁)。
14. 用另一种语言编写保险库格式的读取器
保险库格式在 02-ARCHITECTURE.md §3 有完整文档,JSON 键名常量集中在 constants.py。用 Rust、Go 或任何你正在学的新语言写一个只读客户端:找到对应的库(argon2、aes-gcm),匹配版本与参数,验证跨语言往返。
这是非常好的练习:它证明了为什么要把格式写下来——而且很可能暴露出格式中某些规范不足的地方(这正是现实世界互操作项目常发现的问题)。信封结构(见 vault.py)包含version、kdf{name, salt, time_cost, memory_cost, parallelism}、cipher{name, nonce, ciphertext}三个区块,其中 salt/nonce/ciphertext 均为 base64 编码。
15. 为一个刻意的弱点做威胁建模
从 01-CONCEPTS.md §12 的威胁模型中选择一个假设,尝试击败它。两个现成例子:
- 「我们不防御键盘记录器。」尝试写一个监视
pv进程 stdin 的 Python 键盘记录器(你会发现出乎意料地难——因为getpass读取的是终端设备而非 stdin)。然后尝试在 Linux 上从 OS 层挂钩——需要什么权限? - 「我们无法真正把密钥从内存中抹掉。」用内存调试工具(
gcore+strings)在运行中的pv进程里找到 AES 密钥。然后设计出真正能防御此问题的方案,并解释本项目为什么没有实现它——这正是 vault.py 中close()方法注释坦诚说明的部分:「Python bytes 是不可变的,原始密钥字节可能一直存活到 GC 运行……这个教学项目刻意回避 bytearray + ctypes 的真相清零技巧」。
目标不是武器化任何东西,而是真切感受「我们声明不防御 X」与「我们即使想防御也做不到」之间的差别。
完成之后的阅读路线
如果你读完了 03-IMPLEMENTATION.md 并且完成了这个项目:
- intermediate 层级—— 涉及 Web 服务器、数据库与多文件的项目。做完本项目后再跳过去,落差会小得多;
- advanced 层级—— 涉及真实分布式系统与严肃安全原语的项目。
最后值得暂停感叹一句:你完成了 foundations 层级中最难的项目。你现在对「真实世界密码存储」的理解,已经超过了那些导致 01-CONCEPTS.md §13 中大部分真实泄露事件的工程师。
【免费下载链接】Cybersecurity-Projects
Building 70 Projects ranging from beginner to advanced so anyone can — learn from, build upon, use as a reference, or even copy directly. Gamified Cybersecurity learning 👇
相关推荐
C2 Beacon 扩展挑战实战指南:从新增命令到进程注入的 15 级攻防进阶
C2 Beacon 扩展挑战实战指南:从新增命令到进程注入的 15 级攻防进阶 本文以本仓库 PROJECTS/beginner/c2 beacon 项目为基础
systemd-persistence-scanner 扩展实战指南:从基线扫描到 12 个进阶挑战
systemd persistence scanner 扩展实战指南:从基线扫描到 12 个进阶挑战 这是一份面向 systemd persistence sc
b64tool 进阶实战指南:从 ROT13 到 CyberChef 的 12 个编码工具扩展挑战
b64tool 进阶实战指南:从 ROT13 到 CyberChef 的 12 个编码工具扩展挑战 本指南以 b64tool https://link.gitc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考