Tinycast 解密 Raycast .rayconfig 的实现:纯 Swift 手写 Scrypt + AES-256-GCM + Gzip

📅 发布时间:2026/9/20 12:59:28
Tinycast 解密 Raycast .rayconfig 的实现:纯 Swift 手写 Scrypt + AES-256-GCM + Gzip
Tinycast 解密 Raycast .rayconfig 的实现纯 Swift 手写 Scrypt AES-256-GCM Gzip【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast如果你正想把 Raycast 的快捷键、收藏和剪贴板历史迁移到Tinycast那么它的「Import from Raycast」功能值得深入了解Tinycast 用约 400 行纯 Swift 代码零第三方依赖地读懂了 Raycast 加密备份文件.rayconfig。核心思路是把 Raycast 的 RAYCFG3 加密容器逐层拆开——先手写Scrypt口令派生出密钥再用系统的AES-256-GCM打开加密体最后用手写Gzip解压出明文 JSON。RAYCFG3 容器格式.rayconfig 长什么样Raycast v2.x 的导出文件并不是纯 JSON而是一个分层的加密容器。整个线格式在 docs/features/raycast-import.md 里只有三行描述file RAYCFG3\n ‖ UInt32LE(header.count) ‖ gzip(header JSON) ‖ ciphertext ‖ tag(16) body AES-256-GCM(gzip(payload JSON)) key scrypt(passphrase, salt, N16384, r8, p1, dkLen32)把它拆开看一个 .rayconfig 文件由五段组成区段内容说明魔数RAYCFG3\n8 字节只凭这 8 个字节就能识别文件无需口令头部长度UInt32 小端声明后面 gzip 头部的字节数头部gzip 压缩的 JSON含schemaVersion、hex 编码的iv和salt各 16 字节密文AES-256-GCM 密文正文是 gzip 压缩后的全部设置 JSON认证标签16 字节GCM 的校验和错一个字节就解密失败一个容易忽略的细节Raycast即使用户没设密码也会加密——它自动生成一个口令并存在登录钥匙串里。Tinycast 从不读钥匙串口令由用户在导入界面手动输入。Scrypt 密钥派生纯 Swift 手写不引一个第三方库.rayconfig的解密密钥来自 scryptRFC 7914。Tinycast 在 Scrypt.swift 里完整实现了它只依赖系统自带的 CryptoKit。结构是教科书式的三段式前处理PBKDF2-HMAC-SHA256 一轮把口令扩展成内存块Scrypt.swiftROMix 内存硬化循环 N16384 次每次调用 BlockMixBlockMix 内部对 128 个 64 字节块交替做 Salsa20/8 轮函数Scrypt.swift后处理再做一轮 PBKDF2取出 32 字节密钥其中工作量最大的 salsa20_8 是 16 轮「加、异或、循环移位」的字节码级别手写循环。参数n16384, r8, p1意味着一次派生要分配约 8 MiB 内存做混合——这也正是 scrypt 抵御暴力破解的原理代价是派生要花几百毫秒到几秒。Tinycast 用了一个很聪明的排序所有廉价校验都放在 scrypt 之前只有格式完全合法的文件才会付出这笔时间成本。AES-256-GCM 解密五步拆弹流程真正的解密入口是 RaycastDecoder.swift整个流程可以概括为五步识别isExport只检查文件头 8 字节的RAYCFG3\n魔数RaycastDecoder.swift所以在用户还没输入口令时Backup 面板就能先判断「这确实是个 Raycast 导出」解帧读小端头部长度上限 1 MB算出密文区起点和认证标签位置解头部手写 gunzip 解出头部 JSON取出 hex 编码的 iv 与 salt并确认schemaVersion 3RaycastDecoder.swift派生 解密scrypt 出 32 字节密钥用 CryptoKit 的AES.GCM.open打开密文RaycastDecoder.swift解正文再次 gunzip 得到明文 JSON上限 512 MB第 4 步有个产品细节GCM 解密一旦抛错一律归类为「口令错误」incorrectPassphrase——因为格式问题早已在前几步被拦下走到这一步还失败十有八九是口令不对。手写 Gzip为什么不用现成库你可能会问解压 gzip 何必自己写因为苹果的Compression框架只支持裸 DEFLATE 流gzip 的 10 字节文件头、FEXTRA/FNAME 等可选字段、以及结尾的 CRC32 校验和都没有人处理。Zlib.swift 用约 150 行补齐了这部分Zlib.swift顺带实现了 zlib 格式和 CRC32/Adler32 校验。这里有一个安全设计值得新手学习输出上限maxOutput不是 zip 炸弹防护而是内存保险。因为 AES-GCM 已经认证过正文的完整性恶意膨胀攻击在认证前就无从谈起上限存在的唯一目的是防止超大导出撑爆内存。两个上限的取值也很讲究——未认证的头部保持 1 MB而剪贴板历史密集的导出解压后可能超过 64 MB所以正文放宽到 512 MB。错误处理与测试每一字节都有断言用户能看到的错误只有四种定义在 RaycastImportError.swift不是 Raycast 文件、口令错误、文件损坏、文件过大。对应提示都很直白比如「Incorrect passphrase, or the file is corrupted.」而保证这些分支不出错的是一套相当硬核的测试 raycast-test.swift测试文件在内存里现造用同一份 Scrypt 实现反向加密一个 JSON 构建容器避免把真实 .rayconfig 提交进仓库256 次截断扫描把容器从第 0 字节到密文起点逐字节截断断言每一刀都抛出正确的错误类型raycast-test.swift切片索引测试验证非零起点的 Data 切片也能正确解密——因为解码器内部所有偏移都以startIndex为基准上限边界测试现场压缩 70 MB 数据验证 64 MB 默认上限拦截、512 MB 上限放行scrypt 派生慢是测试策略的关键约束整个测试只派生一次密钥并共享给所有用例凡是能在校验阶段提前失败的场景都保证不花 scrypt 的钱测试以-O编译后派生从 47 秒降到 1 秒。解密之后数据如何落到 Tinycast解出明文 JSON 只是起点。RaycastImportReader.swift 负责把 Raycast 的字段映射成 Tinycast 的领域类型几个有意思的转换快捷键Raycast 的修饰键名Meta/Ctrl/Alt/Shift转成 Carbon 键码一律导入为组合键——Raycast 没有双击绑定应用命令应用路径藏在命令 id 的::::之后通过Bundle解析成 bundle ID快捷键、收藏、别名三套映射共用同一解析剪贴板只认text/plain和文件仍存在的image/*记录图片文件已丢失的条目会计数上报而不是静默丢弃按类别选择导入RaycastImportOptions是一个 OptionSetRaycastImport.swift用户勾选快捷键、收藏、剪贴板历史、Emoji 肤色等任意子集selecting(_:)只保留勾选的类别小结这个实现好在哪里回看这套 Tinycast 导入 Raycast 备份的实现它示范了一个干净的三层结构RaycastDecoder 只管拆容器纯逻辑可独立编译测试→RaycastImportReader 负责字段映射依赖 AppKit留在 Service 层→RaycastImport 只是纯数据。手写 Scrypt 手写 Gzip 换来的是整个项目「零第三方依赖」承诺的延续——解密一个竞品格式的加密文件只用了系统框架和约 400 行 Swift。延伸阅读功能文档docs/features/raycast-import.md解密器RaycastDecoder.swift Scrypt.swift压缩层Zlib.swift独立测试raycast-test.swift【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考