ChatExport/ChatExportKnowledge

GitHub: ChatExport/ChatExportKnowledge

系统记录 Apple 消息数据库 sms.db 的表结构、备份布局、加密机制及各 iOS 版本间的变化,帮助开发者快速理解并正确解析 iPhone 消息备份数据。

Stars: 0 | Forks: 0

# iOS 如何存储消息,以及如何读取备份 关于 Apple 设备端消息数据库以及 iPhone 备份布局的笔记。 它们记录的是 **Apple 的格式**,而不是任何特定程序的格式。 写这些是因为搞清楚这些问题花了很长时间,而且这些内容并不保密, 只是比较分散。如果你正在开发读取 `sms.db` 的工具,这应该 能帮你省下几周的时间。 ## 目录 | 文件 | 主题 | |---|---| | `README.md` | 消息数据库:表、列、iOS 版本间的变化以及 Apple 时间 | | [`attributedBody.md`](attributedBody.md) | 为什么从 iOS 16 开始 `text` 列通常为 NULL,以及如何提取字符串 | | [`manifest-structure.md`](manifest-structure.md) | 备份文件夹布局,SHA1 文件命名规则,`Manifest.db` | | [`encrypted-backups.md`](encrypted-backups.md) | Keybag,密钥派生,单文件密钥 | | [`what-cannot-be-recovered.md`](what-cannot-be-recovered.md) | 备份中不包含哪些内容的真实清单 | ## 数据库 消息存在于 `HomeDomain` 下 `Library/SMS/sms.db` 的 SQLite 数据库中。 历史上它在 macOS 上被称为 `chat.db`,在大多数相关文章中这两个名字 是交替使用的。它们的布局是一样的。 重要的表: | 表 | 存储内容 | |---|---| | `message` | 每条消息一行,另外每个回应和每个系统事件各占一行 | | `chat` | 每个对话(单聊或群聊)一行 | | `handle` | 每个对话方地址(电话号码或电子邮件)一行 | | `attachment` | 每个发送或接收的文件一行 | | `chat_message_join` | 哪些消息属于哪个对话 | | `chat_handle_join` | 哪些参与者属于哪个对话 | | `message_attachment_join` | 哪些附件属于哪条消息 | | `chat_recoverable_message_join` | iOS 16 及更高版本:最近删除中的消息 | 连接表是最让人意想不到的东西。一条消息并 不包含对话 ID:你需要通过 `chat_message_join` 来获取。群聊的 参与者不是一个列:它们是 `chat_handle_join` 中的行。 ### `message` 表中值得了解的列 | 列 | 含义 | |---|---| | `ROWID` | 本地整数 ID,供连接表使用 | | `guid` | 稳定的标识符,回复和回应指向的目标 | | `text` | 消息文本。**从 iOS 16 开始通常为 NULL。** 参见 [`attributedBody.md`](attributedBody.md) | | `attributedBody` | 当 `text` 为 NULL 时保存文本的 Blob | | `handle_id` | 对话方,指向 `handle.ROWID`。在某些发件行中为 0 | | `is_from_me` | 1 表示发出,0 表示接收 | | `date` | 消息发送时间。Apple 时间,见下文 | | `date_delivered` | 到达对方设备的时间。未确认时为 0 或 NULL | | `date_read` | 已读时间戳。具有不对称性,见下文 | | `date_edited` | iOS 16 及更高版本。0 或 NULL 表示从未被编辑 | | `date_retracted` | iOS 16 及更高版本。非零表示发送者撤回了消息 | | `associated_message_guid` | 对于回应:指向的目标,格式为 `p:0/GUID` 或 `bp:GUID` | | `associated_message_type` | 0 为正常。2000 到 2005 表示添加回应。3000 到 3005 表示移除回应 | | `thread_originator_guid` | iOS 14 及更高版本。此条消息回复的目标消息 | | `message_summary_info` | Blob。编辑历史和撤回详情 | | `item_type` | 0 代表真实消息。其他值为系统事件,如群组重命名 | | `service` | `iMessage` 或 `SMS` | 上述表格中会导致大多数实现出现问题的两个后果: **回应对应的是消息。** Tapback 是 `message` 表中的一行,其 `associated_message_type` 为非零值并指向另一条消息的 GUID。 如果简单地计算行数,每个对话都会被夸大。如果简单地渲染行, 每个“喜欢了一条消息”都会在记录中作为单独的一行出现。 **系统事件也是消息。** 非 0 的 `item_type` 标志着某事, 比如有人被添加到群组或群组被重命名。它们值得被显示, 但不能显示得好像有人真的这么说过一样。 ### `date_read` 是不对称的 根据方向的不同,同一个列有两种不同的含义。 - 在**发出**行中,只有当接收方开启了已读回执时才会进行设置。 空值并不能证明消息未被读取。它通常 意味着对方从未开启过此功能。 - 在**接收**行中,无论任何人的设置如何,iOS 都会记录设备所有者阅读它的时间。 将两者都呈现为“阅读于”是一种误解。这是导出此数据的工具中最常见的错误。 ## Apple 时间 时间戳并不是从 Unix 纪元开始计算的。它们是从 **2001-01-01T00:00:00Z**(即 Apple 或 Mac 绝对时间纪元)开始计算的。偏移量是 `978307200` 秒。 单位也发生了变化。较旧的备份以**秒**为单位存储。从 iOS 11 左右开始, 相同的列保存的是**纳秒**。 没有版本字段可供参考,因此实际的方法是看 数值大小。一个大于 `1e12` 的值不可能是秒数,因为那 大约是公元 33000 年。纳秒级时间戳大约在 `7e17` 左右。 因此,`1e12` 完美地将两者区分开来,而完全不需要知道 iOS 版本。 换算示例: ``` raw = 707521234000000000 # nanoseconds, because it exceeds 1e12 seconds = raw / 1e9 # 707521234 unix = seconds + 978307200 # 1685828434 ``` 零值、负值和缺失值在真实的数据库中会出现在草稿行和 损坏的行中。请将它们视为“无时间戳”,而不是 2001 年, 当然也不要因此抛出异常:一条损坏的行不应中断十万条消息的导出。 ## 区分 iOS 版本 Apple 没有在这个数据库中提供可靠的 schema 版本号。可行的 方法是寻找**标记列**,因为每一代功能 都会增加一个独特的标记列。 | `message` 中的证据 | 版本 | |---|---| | 存在 `thread_originator_guid` **且**存在 `date_edited` | iOS 16 及更高版本 | | 存在 `attributedBody` 或 `associated_message_guid`,缺少 `thread_originator_guid` | 大致为 iOS 11 至 15 | | 两者都缺少 | 较旧的版本,或无法识别的布局 | 存在 `chat_recoverable_message_join` 表是一个单独的信号, 表示是“最近删除”存储,适用于 iOS 16 及更高版本。请单独 检查它是否具有 `delete_date` 列,因为该列并不总是被填充数据。 `PRAGMA table_info('message')` 足以在一次查询中收集所有这些信息。 **宁可降级也不要拒绝执行。** 无法识别的布局并不是 向用户显示空内容的充分理由。读取你能识别的内容并报告你 无法识别的内容,要比直接抛出错误信息好,因为另一端的用户通常 有真正的需求,而且只有一个备份。 ## 这些笔记目前未涵盖的内容 - 数据库中记录的路径之外的设备端附件存储布局。 - `message_summary_info` 的 blob 格式细节(除了其在编辑历史中的作用)。 - 群聊重命名和参与者随时间的变化(这些被记录为系统事件,但这里未将其重构为时间轴)。 - 不同于 iMessage 行的 SMS 特定字段。 - 任何关于 Messages in iCloud 的内容,因为它是一个同步服务,而不是一种文件格式。 ## 勘误 公开这些内容的原因就是为了获取勘误。这里的每一个声明要么是从 真实的备份中读取的,要么被标记为推断的,而那些推断的内容正是最需要 外部慧眼的地方。 一个可以采取行动的 issue 应该说明三点:备份来自哪个 iOS 版本, 你观察到了什么,以及你是如何观察到的(例如 `PRAGMA table_info` 的转储、行数或你运行的查询)。这些文件中的 一半陈述本质上都依赖于版本,因此没有附带版本的“这是错的” 无法与任何内容进行核对。 不清楚的文件也值得提 issue。这些笔记是为了给某人 省下半个月的时间而存在的,如果一段话需要读两遍才能理解,那它就没有达到这个目的。 ## 许可证 CC BY 4.0。引用它,粘贴到你自己的文档中,翻译它,在它的基础上进行 构建。唯一的条件是注明出处,而指向此代码仓库的链接 即可满足此条件。 ## 作者信息 Krzysztof Kowalski,Bokart,华沙。这些笔记源自于开发 [ChatExport](https://getchatexport.com),这是一个 Windows 程序,可以将 iPhone 备份转换为可以提交给法庭或交给律师的文档。 该程序是闭源的,这个代码库刻意没有成为进入该程序的入口。 在上面的任何地方,都没有来自该程序的函数名、文件名和设计决策。 这里描述的是 Apple 的格式,它不属于我们,从来不是 秘密,只是比较分散:一半是文档记录,一半是口口相传,而且在 很多地方都是错误的,以至于根据真实备份验证它所花的时间比阅读 它的时间还要长。这就是其他任何人都不需要再重复的部分。
标签:iOS, SQLite, 技术文档, 数字取证, 数据备份, 数据恢复, 数据解析, 自动化脚本