Keychain 与 Safari 痕迹

Keychain 是 macOS 的凭据中枢,Safari 的历史数据库是 macOS 上最完整的浏览行为记录。 在 Windows 上这两件事分别对应「凭据管理器」和「Chrome/Edge 的 History 数据库」,但有两处差异必须先讲清。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:Keychain、login.keychain-db、security、kSecAttr、Safari、History.db、LastSession、Chrome 路径 难度:进阶 前置知识:macOS 目录结构(见01)、SQLite 基础

一、概述

Keychain 是 macOS 的凭据中枢,Safari 的历史数据库是 macOS 上最完整的浏览行为记录。 在 Windows 上这两件事分别对应「凭据管理器」和「Chrome/Edge 的 History 数据库」,但有两处差异必须先讲清。

第一,Keychain 条目不保存明文密码,但保存「这个凭据是给谁用的、什么时候建的」。 光是这条信息就有取证价值——一个标签为 mail.google.com 的条目,不解密也能证明该机登录过这个服务。条目上的 kSecAttrCreationDate 与 kSecAttrModificationDate 能给出凭据使用的时间线。

第二,Safari 的 History.db 在较新系统上是被加密的(SQLCipher),不是普通 SQLite。 直接用 sqlite3 打开会看到 file is not a database 或 database disk image is malformed。这不是文件损坏,是加密。 认错这一点,整个浏览器证据就会被写成「已损坏无法读取」,而实际只是解密方式不对。

两点合起来意味着:macOS 的凭据与浏览痕迹需要成对解读——Keychain 告诉你「这个站点被登录过、凭据何时创建」,Safari 告诉你「这个站点被访问过、访问了什么」。 单独的任一半都不完整。

这两个工件落在「文件已固定、开始逐工件判读」这一步,属分析环节里的高敏感环节:

  • 上游依赖三项前置。磁盘解密状态(FileVault 已解密 ≠ Keychain 已解锁,这是两个独立动作);用户身份的确认(Keychain 是按用户隔离的,~/Library/Keychains/ 下每个用户一份);以及 Safari 容器路径的确认,见macOS 取证工件地图。
  • 它自己要先判断可解性。不试解锁就先评估有没有必要,因为解锁动作本身会改变钥匙串状态,且需要合法来源的密码。
  • 下游支撑访问行为的定性。Keychain 的元数据给出「登录过哪些服务」,Safari 给出「访问了什么内容」,两者交叉才能画出完整的活动画像。
  • 它与其它浏览器材料是并列关系:这里只处理 Safari 与 Keychain,Chrome / Firefox 在 macOS 上的路径不同,需要单独处理,见 2.9。

落到具体物件上:

形态 位置 关键结构
登录钥匙串 ~/Library/Keychains/login.keychain-db 按用户隔离;条目含 kSecAttrLabel、kSecAttrAccount、kSecAttrCreationDate、kSecAttrModificationDate
系统钥匙串 /Library/Keychains/System.keychain 系统级,不含用户凭据
钥匙串工具 security 命令行 unlock-keychain 需要用户登录密码——密码由用户登录密码派生,不是硬件密钥
Safari 历史 ~/Library/Containers/com.apple.Safari/Data/Library/Safari/History.db 较新系统上为 SQLCipher 加密,密钥在 Keychain 里;明文可用 history_items 与 history_visits 两表
Safari 时间基准 Apple 纪元(2001-01-01) 时间戳要加 978307200 才是 Unix 秒,忘了加会全部显示为 2001 年附近
下载记录 .../Safari/Downloads.plist plist 格式,独立于 History.db
会话恢复 .../Safari/LastSession.plist 记录上次打开的标签页
Chrome 路径 ~/Library/Application Support/Google/Chrome/ Profile 目录结构,与 Safari 的容器路径不同
Firefox 路径 ~/Library/Application Support/Firefox/Profiles/ places.sqlite

三个判定要点:Keychain 的加密密钥来自用户登录口令而非硬件、Safari 的时间基准是 Apple 纪元、以及加密不是损坏。

据此能回答的是:该机登录过哪些服务、每条凭据的创建与修改时间、某个站点的访问历史与访问次数、下载过哪些文件、上次会话打开的页面、以及某个凭据属于哪个账户。

回答不了的是这些,其中几条是必须先纠正的误解。

  • 「FileVault 恢复密钥能打开 Keychain」是错的。 FileVault 解决的是磁盘加密,Keychain 的加密密钥由用户登录口令派生。两个都没有时只能读元数据(标签、账户、日期),读不到明文。报告里必须区分「已解密磁盘」与「已解锁钥匙串」这两个独立动作。
  • 明文凭据。 这里只固定与识别凭据材料,不记录、不摘录口令本身。解锁用的口令必须合法取得,报告中只写「已使用合法取得的用户口令解锁」。
  • Safari 加密数据库的内容(在未取得密钥时)。 读到 file is not a database 时,正确写法是「该数据库经 SQLCipher 加密,需 Keychain 中的密钥解密;本次未取得密钥,仅记录文件存在与哈希」。绝不能写成「数据库损坏」或「数据已丢失」——这是事实性错误,会直接影响委托方对证据可用性的判断。
  • Chrome / Firefox 的浏览痕迹。 它们在 macOS 上的路径与数据结构都不同,不能用 Safari 的方法套用。
  • 凭据的实际使用行为。 Keychain 记录的是「凭据存在」,不是「某时用这个凭据访问了某站点」。
  • 设备上不存在的浏览器。 只装了 Safari 的机器上,Chrome 路径不存在,这是预期状态而非异常。

读到这里你应该已经知道:macOS 目录结构,特别是 ~/Library 的分层与沙盒容器路径(见macOS 取证工件地图);SQLite 基本操作与 file 命令的文件类型判断,否则无法区分「加密」与「损坏」;macOS 时间基准的特殊性——Apple 纪元与 Unix 纪元差 978307200 秒,见时间戳换算;以及 plist 格式的两种形态,见plist 与偏好设置。

还有一条原则不能松:明确授权与最小化原则。这里涉及的凭据材料属高敏感证据,摘录范围须与授权范围一致并做脱敏,见合规授权与法律边界。

二、核心原理

2.1 Keychain 的文件布局

路径 内容 权限
~/Library/Keychains/login.keychain-db 用户登录钥匙串(新版文件名) drwx------,属主用户
~/Library/Keychains/metadata.keychain-db 元数据钥匙串 同上
~/Library/Keychains/<Hardware-UUID>/ 按主机划分的子钥匙串 drwx------
/Library/Keychains/System.keychain 系统钥匙串(证书、系统凭据) root:wheel

实测本机布局:login.keychain-db、metadata.keychain-db,以及一个与 Hardware UUID 同名的子目录 3132C17B-F189-5363-B717-8ACDE9BAF01D/。security list-keychains 返回 "/Users/<u>/Library/Keychains/login.keychain-db" 与 "/Library/Keychains/System.keychain"。

<Hardware-UUID> 子目录是个常被忽略的高价值位置。

它与 Preferences/ByHost/ 的命名规则一致(见03),可以用来判断某些凭据是否属于本机。

目录内是按用途划分的 SQLite 数据库,实测含 com.apple.security.keychain-defaultContext.TrustedPeersHelper.db 等(TrustedPeers 机制用于 Safari/iCloud 的信任链)。

2.2 Keychain 条目的四个关键属性

每个 Keychain 条目携带四个对取证最有价值的属性:

属性 含义 取证价值
kSecAttrLabel 人类可读的服务名,如 mail.google.com、Wi-Fi SSID ★ 直接说明"这个凭据管什么"
kSecAttrAccount 账号标识(用户名、邮箱) ★ 关联到具体人员
kSecAttrCreationDate 条目创建时间 ★ 凭据首次使用的时间锚点
kSecAttrModificationDate 条目最后修改时间 ★ 凭据最后变更的时间锚点

这四个属性合起来就是"凭据使用时间线"。

比如一个 kSecAttrCreationDate 为 2024-03-15 的 mail.google.com 条目,配合 Safari 历史里同期的访问记录,可以支撑"该机在此期间处于登录并使用状态"。

关键限制:这四个属性是元数据,不需要解密就能读。

但口令内容需要用户口令解锁(见 2.4)。

报告里"证明登录过某服务"与"获取到该服务的口令"是两种完全不同的证据强度:前者可从元数据得出,后者需合法取得用户口令并说明依据。

2.3 security 命令

security 的核心子命令:list-keychains(列搜索路径)、find-generic-password -s <服务>(找通用密码条目)、find-internet-password -s <站点>、dump-keychain <路径>(全量导出)。

-g 参数在取证中属于敏感操作:它会尝试显示口令,且会修改钥匙串的访问控制列表。

只应在检材工作副本上使用,并记录这一事实。

2.4 解锁:为什么需要用户密码

Keychain 的加密密钥由用户登录口令派生,不是硬件密钥。后果是:

场景 能否读取
已有用户密码 可解锁,读取明文凭据
知道 FileVault 恢复密钥 ★ 不够——FileVault 解决的是磁盘加密,Keychain 需要的是用户登录密码
两者都无 只能读元数据(Label/Account/日期),读不到明文

这是最常见的误解之一:拿到 FileVault 恢复密钥 ≠ 能打开 Keychain。报告里必须明确区分"已解密磁盘"与"已解锁钥匙串"这两个独立动作。

security unlock-keychain <路径> 会交互式要求输入口令(password to unlock <path>:)。

取证场景下口令来源必须合法且可追溯(委托人当面提供、经书面授权的记忆、或依法调取),报告中只写"已使用合法取得的用户口令解锁",绝不记录口令本身。

2.5 Safari 的路径:两个位置

路径 适用 说明
~/Library/Safari/ 10.13 及以前 传统路径
~/Library/Containers/com.apple.Safari/Data/Library/Safari/ 10.14 Mojave 起 ★ 沙盒容器路径,现代系统的真实位置
~/Library/Containers/com.apple.Safari/Data/Library/Safari/
├── History.db              # 历史记录(★ 加密,见 2.6)
├── History.db-shm / -wal   # WAL 模式的伴随文件(★ 必须一起提取)
├── Bookmarks.plist         # 书签(二进制 plist)
├── Extensions/             # Safari 扩展
├── Downloads.plist         # ★ 下载记录(见 2.7)
├── LastSession.plist       # ★ 上次会话的标签页(见 2.8)
└── Favicons/  Snapshots/   # 站点图标与快照

.db-shm 与 .db-wal 必须与 History.db 一起提取。 SQLite 的 WAL 模式下,最近的事务可能只存在于 -wal 文件中而未回写主库。只拷 History.db 会丢掉最近几小时到几天的记录——这是 Safari 取证最常见的漏检点。

2.6 History.db 的加密

较新 macOS 上 Safari 的 History.db 使用 SQLCipher 加密,密钥存放在 Keychain 中(Safari 相关条目)。

-- 解密后可见的核心表
SELECT datetime(last_visit_time + 978307200, 'unixepoch', 'localtime') AS 本地时间,
       url, visit_count
FROM history_items
ORDER BY last_visit_time DESC LIMIT 20;

SELECT datetime(visit_time + 978307200, 'unixepoch', 'localtime') AS 本地时间, url
FROM history_visits
JOIN history_items ON history_items.id = history_visits.history_item
ORDER BY visit_time DESC LIMIT 50;

978307200 是 Cocoa/Apple 纪元(2001-01-01)与 Unix 纪元的差值,Safari 的时间戳要加上它才是 Unix 秒。忘了加这个常量,所有时间都会显示为 2001 年附近。

# 判断是否加密
file History.db
file -b History.db | head -c 16          # "SQLite format 3" = 未加密;否则为加密
sqlite3 History.db "select count(*) from history_items;"
# → Error: file is not a database     ← 这是加密,不是损坏

报告写法约束:读到 file is not a database 时,正确写法是"该数据库经 SQLCipher 加密,需 Keychain 中的密钥解密;本次未取得密钥,仅记录文件存在与哈希",绝不能写成"数据库损坏"或"数据已丢失"。这是事实性错误,会直接影响委托方对证据可用性的判断。

2.7 下载记录 Downloads.plist

Safari 的下载历史在 ~/Library/Containers/com.apple.Safari/Data/Library/Safari/Downloads.plist。

plutil -p ~/Library/Containers/com.apple.Safari/Data/Library/Safari/Downloads.plist | head -30
plutil -convert xml1 -o - ~/Library/Containers/com.apple.Safari/Data/Library/Safari/Downloads.plist \
  | grep -B2 -A6 'DownloadEntry'

它的价值在于记录的是"下载条目"而不是"文件"——即使用户后来把文件移到别处或删除,下载条目可能仍在(具体保留策略随系统版本变化)。

它应与 Quarantine 属性(见05)和 kMDItemWhereFroms(见04)三者对照使用。

工件 记录什么 是否需要解密
Downloads.plist Safari 下载条目 否(二进制 plist,plutil 可读)
com.apple.quarantine xattr 下载时间 + 来源应用 否
kMDItemWhereFroms 下载来源 URL 否(但需现场采集)
History.db 访问历史 是

2.8 LastSession.plist 与会话恢复

LastSession.plist 保存上次关闭 Safari 时打开的标签页。

plutil -p ~/Library/Containers/com.apple.Safari/Data/Library/Safari/LastSession.plist | head -20

取证价值有限,但不为零:它能反映"关机前用户在看什么页面",是案件时间点之前用户意图的最直接线索。

不过要注意:

  • 它的 mtime 是最后写入时间(通常是 Safari 退出时间),不代表页面访问时间
  • 它只反映"未关闭的标签页",用户正常关闭的页面不在其中
  • 必须与 History.db 的访问时间交叉,不能单独作为"访问过该网站"的证据

2.9 Chrome / Chromium / Firefox 在 macOS 上的路径

浏览器 路径 关键文件
Chrome ~/Library/Application Support/Google/Chrome/ Default/History(SQLite)、Default/Login Data(凭据)、Default/Bookmarks(JSON)
Chromium ~/Library/Application Support/Chromium/ 同上(Profile 目录名可能不同)
Edge ~/Library/Application Support/Microsoft Edge/ 同 Chromium 系
Firefox ~/Library/Application Support/Firefox/Profiles/<profile>/ places.sqlite、logins.json、key4.db
Arc / Brave ~/Library/Application Support/ 下同名目录 Brave 为 BraveSoftware/Brave-Browser/
# Chrome 历史(Chrome 的 History 无需 Keychain 即可读)
EV=/mnt/mac-data/Users/liwei
sqlite3 "$EV/Library/Application Support/Google/Chrome/Default/History" \
  "SELECT datetime((visits.visit_time/1000000)-11644473600,'unixepoch','localtime'), url
   FROM visits JOIN urls ON visits.url=urls.id ORDER BY visit_time DESC LIMIT 20;"

# Chrome 凭据表结构(值本身加密,键值表可读)
sqlite3 "$EV/Library/Application Support/Google/Chrome/Default/Login Data" \
  "SELECT origin_url, username_value, hex(encrypted_value) FROM logins LIMIT 10;"

# Firefox 历史
sqlite3 "$EV/Library/Application Support/Firefox/Profiles/default.default-release/places.sqlite" \
  "SELECT datetime(last_visit_date/1000000,'unixepoch','localtime'), url FROM moz_places ORDER BY last_visit_date DESC LIMIT 20;"

三个时间基数的差异是本节最易错的地方:

浏览器 存储形式 转 Unix 秒的方法
Chrome/Chromium 微秒(1e-6 s),且自 1601-01-01 起算 (值 / 1000000) - 11644473600
Firefox 微秒,自 Unix 纪元 值 / 1000000
Safari