一、概述
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 |
秒,自 2001-01-01 起算 |
值 + 978307200 |
Safari 与 Chrome 的差值(978307200)符号是加,Chrome 是减且多除 1000000。
写错会让全部时间落到 1601 年或 2