钥匙串 Passkeys 全链路3MB 浏览器如何把密码安全做到系统级【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search一台只有 3MB 的浏览器凭什么敢谈系统级安全Search 给出的答案不是自己造一个加密保险箱而是把密码直接交给 macOS 登录钥匙串、把 Passkeys 流程从 WebKit 手里接管过来自己执行五重安全校验后再调用系统 AuthenticationServices。没有自有账本、没有云同步、没有遥测——所有秘密只存在于操作系统的保险库里解锁靠 Touch ID 与系统账户而不是浏览器自己的任何一道门。这篇文章从源码出发逐层拆解这条从表单提交到钥匙串写入、从页面请求到系统通行证的完整链路。密码全部入钥匙串标签隔离与生物识别校验不造账本直接把密码写进系统保险库多数浏览器把密码管理做成自有数据库 自带加密层密码先经自己的主密钥加密封存进 SQLite 账本再落进用户目录下的文件里。Search 选择了完全不同的一条路——Vault.swift 的文件头注释写得很直白Where passwords live: the macOS keychain, under this apps own name, as internet passwords keyed by site and account. Nothing is written to disk by this app in any other form, and nothing is ever logged.密码以kSecClassInternetPassword条目存进系统登录钥匙串由 site account 两个属性标识读写全部走SecItemCopyMatching/SecItemUpdate/SecItemAdd。文件头还点破了一个关键区别Safari 的钥匙串条目被 Apple 用专属 entitlement 隔开其他浏览器拿不到而 Search 的条目就在同一个系统保险库里只是以Search标签归拢——同一个保险柜不同的抽屉。抽屉的隔离不止是标签。写入时除了kSecAttrServer、kSecAttrAccount和kSecAttrLabel三个身份字段还额外带上kSecAttrAuthenticationTypeHTMLForm与协议标记。注释解释了原因服务器名 账户名在钥匙串眼里也会匹配到其他 app 的同站条目——比如 git 为 github.com 存的 token——不加这层身份一次更新就会把别人家的密码覆盖掉。列表与秘密分离四百条密码不是四百次解密钥匙串的一次调用要么列出条目、要么交出一个 secret两者不可兼得而SecItemCopyMatching同时索取全部数据时会静默返回errSecParam。这个坑在 Vault.swift 里被记录为一个真实的教训for a while this app saved passwords it could never read back。于是有了类型层面的隔离Login持有完整密码只用于填充表单与判断是否已保存Kept只有 host/user/used/clear 四个字段不含密码专门给列表和面板用。Vault.all()列出的[Kept]一行秘密都没有密码列表 UI 也就无从泄密每个 secret 是一次独立的secret(of:)调用只在显示或复制的那一下才发生。一个装了四百个密码的抽屉否则要在面板画出来之前做四百次钥匙串调用——这就是设计取舍读列表永远不碰秘密秘密永远按需现取。生物识别是显示的唯一入场券密码面板 Passwords.swift 的注释定下纪律a password is never shown until you have proved you are you。点击 Show 的路径是private func reveal() { Vault.prove(show the password for \(login.host)) { ok in guard ok, let password Vault.secret(of: login) else { return } secret password shown true // Long enough to read or type across, and not a minute more. let work DispatchWorkItem { conceal() } hide?.cancel() hide work DispatchQueue.main.asyncAfter(deadline: .now() 15, execute: work) } }Vault.prove底层是LAContext().evaluatePolicy(.deviceOwnerAuthentication)——也就是 macOS 本身的那道门Touch ID、Apple Watch 或账户密码系统怎么说就怎么算。密码只在屏幕上停留 15 秒然后连secret变量一并清空conceal()里secret nil这是全代码里唯一的秘密持有点。复制的路径更讲究Browser.copy(_:)在生物识别通过后把密码写入剪贴板时同时打上org.nspasteboard.ConcealedType与TransientType两个标记——前者告诉剪贴板管理器别收进历史后者让密码在 90 秒后自动从剪贴板消失。一个面向别人可能看到你屏幕的场景连密码的位数都不会暴露面板上永远是十个点行不知道自己存的是什么直到秘密被读出来。只在登录真正成功后保存保存时机同样拒绝抢跑。表单脚本 Forms.swift 监听 submit、回车与按钮点击把用户名密码桥接给浏览器Tab.swift 的sentSignIn先记下刚刚发出的登录settleSignIn则在页面跳转后确认密码框已经消失——即登录真的成功了——才触发保存询问45 秒内有效。密码框还在说明服务器拒绝了此时弹出的保存offer 存的是错密码。私密标签页、扩展自己的页面、用户说过never的站点、以及已被密码管理扩展接管的保存全部被排除在 offer 之外。自动填充同样克制点击输入框才从输入框下方挂出账户列表绝不主动填。Passkeys 五重校验从接管到调用系统 AuthenticationServices为什么一个浏览器要自己扛下整个 ceremonyPasskeys 在非 Safari 浏览器上的默认路径有个隐蔽的坑。文件头注释记录了一次真实的踩雷2026 年 9 月 24 日把 conditional mediation 交给 WebKit它会让 macOS 的 AuthenticationServicesAgent 持有一个 AutoFill 操作不释放一旦 Search 中途退出agent 就会拒绝此后的一切 passkey 请求报Request already in progress for specified application identifier所有站点登录全部失败直到重启 Mac。于是 Search 的决定是自己扛 ceremony像 Chrome 和 Firefox 在 Mac 上做的那样从页面收下请求逐项校验再通过专为浏览器设计的 API 交给 AuthenticationServices。五重校验每一道都针对一类攻击Passkeys.swift 的perform是完整的安检线顺序严格安全上下文只有https或本机localhost/127.0.0.1才放行否则NotAllowedError——明文页面上任何人都能改写请求前台焦点NSApp.isActive caller.window?.isKeyWindow后台标签页、后台窗口、乃至 Search 自身被遮挡时都不允许弹出系统面板盖在你正在看的内容之上frame 来源caller.mainFrame || caller.pageHost host跨站 iframe 的请求一律拒绝——另一个站点的 frame 不能替这个页面索要 passkeyRelying Party 归属fits(rp, host)要求 RP ID 是当前域名本身或其上级可注册域后缀且拒绝com、co.uk、github.io这类公开后缀publicSuffix直接动态加载了 CFNetwork 里_CFHostIsDomainTopLevel的私有符号没有它页面自己的 host 就是它唯一能拿到的 RPchallenge 必填guard let challenge Passkeys.data(body[challenge]), !challenge.isEmpty。校验通过后clientData 由 Search 自己写——origin 取的是 WebKit 报告的 frame origin从来不是页面自述的那个。这正是钓鱼防护的落点页面说什么都不算浏览器看见的才算数。权限链entitlement 系统隐私开关能走到系统层前提是两道权限。一道在构建期Prefs.entitledToPasskeys通过SecTaskCreateFromSelf读取运行进程自身的签名检查com.apple.developer.web-browser.public-key-credentialentitlement——profile 文件放在 bundle 里什么都不证明只有签名才算数。另一道在运行时Passkeys.access查询ASAuthorizationWebBrowserPublicKeyCredentialManager的授权状态首次请求时用requestAuthorizationForPublicKeyCredentials弹系统授权回答保存在系统设置里设置面板 Settings.swift 会如实显示三种状态——没 entitlement、被 macOS 拒绝、正常可用。conditional mediation先列清单绝不先弹面板navigator.credentials.get({mediation: conditional})的请求走wait(_:)先用platformCredentials(forRelyingParty:)问系统这台 Mac 为该站点持有哪些 passkey一次无界面的静默查询过滤allowCredentials白名单后最多列出 8 个挂在输入框下方与密码账户并列。在你亲手挑选之前什么都不会弹、什么都不会发生挑中一个之后才为这一个 credential 单独打开系统的 Touch ID / iCloud 钥匙串面板。等待中的请求若遇到页面离开、新请求顶替或标签页关闭都会被妥善终结。页面侧由注入在atDocumentStart的脚本接管navigator.credentials.get/create用 window 事件桥接PasskeyRelay.page/bridge浏览器侧则在PasskeyRelay里拿到message.frameInfo.securityOrigin——frame 来源由 WebKit 说了算事件本身说了不算。系统调用层还做了双路并行平台 passkeymacOS 15.0 起的 PRF 扩展也在这里挂上与安全密钥ASAuthorizationSecurityKeyPublicKeyCredentialProvidermacOS 14.4同时发起USB/NFC/BLE 传输逐项映射。回传的 attestation object 用一段自写的轻量 CBOR 解析器拆解把 P-256 与 Ed25519 公钥按 WebAuthn 标准重新编码成 DER 交给页面——包括 GitHub 这类库直接调用getPublicKey()的场景。甚至 PRF 扩展的输出都做了端到端保护页面脚本生成一次性 ECDH P-256 密钥对浏览器侧结果用 HKDF-SHA256 AES-GCM 密封后回传没有那把一次性钥匙谁都无法打开。无明文落盘、无遥测这套设计与 Chrome 的账本式存储对比存储模型的分水岭Chromium 系浏览器的密码是账本式的profile 目录下的Login DataSQLite 里每条记录以主密钥加密的 blob 存放主密钥又锁在操作系统的保险库里。这意味着密码最终仍是一个文件里的加密账本读账本、取密钥、解密是三道独立的工序安全边界由浏览器自己的代码维持。Search 直接跳过了整个账本层——秘密只在钥匙串条目里存在浏览器的进程里连一份明文副本都没有。这不只是实现差异而是信任模型差异加密、解锁、存储全部由操作系统完成浏览器只负责问系统要。连带的好处是迁移成本趋近于零换机、重装、备份钥匙串跟着系统走即便有人拿到了 Application Support 目录里面也只有历史、书签这类 JSONREADME.md 的隐私表格写得很清楚密码一个字节都不在。导入路径反而成了账本式存储的活证据有意思的是这个仓库里最生动的Chrome 账本证据来自导入功能本身。Import.swift 为了把用户从其他浏览器带过来不得不实现完整的逆向读取Chromium 系走Login DataSQLite encrypted_value字段、Firefox 走logins.jsonkey4.db从 key4.db 解出主密钥、用空主密码校验、再逐条解密甚至要连带复制-wal/-shm旁挂文件才能读到最新写入的密码——这正是账本式存储的日常样貌。导入完成后所有条目统一落进钥匙串Vault.take(csv:)对 Safari 导出的明文 CSV 尤其谨慎读完即弃、不留副本界面还会专门提示导出文件里的密码是明文请立即删除。这套方案还有一层签名级隔离钥匙串按应用签名区分条目自己./build.sh打出来的未签名 build 与官方发布版各占一格你自己的 build 的密码与签名版 Search 的互不相通。没有遥测比没有同步更进一步无同步只是不把数据送出去无遥测则是不把任何行为送出去。Search 的隐私承诺是离开 Mac 的只有你主动请求的页面、页面图标以及每天一次检查更新的小请求。密码面板、passkey ceremony 全程无日志记录nothing is ever loggedOSLog 只记事件计数与 refused 原因不记秘密。对比 Chrome 生态中与账号体系捆绑的密码同步、密码安全检查这类云端功能这里的取舍不是做得更少而是把安全边界重新画在了本机操作系统这条线上——浏览器退回到门童的角色验证你是谁、把请求递给系统、把系统的答案递回页面仅此而已。从钥匙串条目的标签隔离到显示密码时的生物识别从五重校验后接管 Passkeys到把 clientData 的 origin 牢牢握在 WebKit 手里——3MB 的体积没有省掉任何一道安全工序它只是把每道工序都推给了 macOS 本身。密码安全做到系统级本质上是承认一个朴素的事实浏览器不应该是秘密的主人它只该是秘密的搬运工。【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考