技术文档

双端本机数据格式与兼容

本页记录任务、网站、记忆、附件和本机授权状态在当前源码中的形状。示例均为合成值,只用于解释字段;不提供读取用户私有数据或导出密钥的操作。原生磁盘格式是实现细节,没有通用跨平台导入承诺,修改前应保留旧数据兼容和失败保护。

数据分类

类别 iOS Android
工作台 workspace.json,JSON 对象快照 SharedPreferences 名称 starline_settings 中的 JSON 字符串
记忆索引 逻辑文件 vault/index.enc,解密后为记录数组 memories.bin,解密后为 JSON 数组
记忆附件 vault/files/<UUID>.enc memory-<id>.bin
聊天会话 独立 Keychain 项 session.bin,解密后为会话字符串
原生登录 pending Keychain 内 JSON native-auth.bin,解密后为 JSON
开关与默认页 UserDefaults / @AppStorage 同一个私有 SharedPreferences

这里的文件名是程序中的逻辑名称,不表示部署目录。任务、收藏和普通偏好不使用记忆 AES-GCM 容器;聊天 token、记忆正文和附件不能写回普通偏好或日志。记忆密钥为设备级随机值,同一安装中的聊天账号共用记忆库;改 Account 密码、退出或切换账号不会删除或重加密记忆。

iOS 工作台快照

LocalWorkspaceStore.Snapshot 的三个顶层键是 tasks、bookmarks、recentSites。tasks 解码时必需;旧快照缺少后两项时按空数组兼容。当前没有单独的 schemaVersion。示例:

{
  "tasks": [{
    "id": "11111111-1111-4111-8111-111111111111",
    "title": "示例任务",
    "notes": "合成说明",
    "dueDate": 3600,
    "sourceURL": "https://example.test/task",
    "isCompleted": false,
    "createdAt": 0,
    "updatedAt": 0
  }],
  "bookmarks": [{
    "id": "22222222-2222-4222-8222-222222222222",
    "title": "示例网站",
    "url": "https://example.test/",
    "createdAt": 0
  }],
  "recentSites": []
}

默认 JSONEncoder 把 Swift Date 编为从 2001-01-01 00:00:00 UTC 起算的秒数,可含小数;这里的 0 不是 1970 年,3600 也不是 Unix 毫秒。URL 编为字符串,UUID 编为字符串。可选 dueDate、sourceURL 为空时通常省略键。后端聊天时间使用 Unix 毫秒,不能把相同数字直接复制到本机 Date 字段。

快照一次编码三个数组,.atomic 写文件成功后再替换 Store 内存。损坏或缺少必需字段会抛出读取错误,根界面不会把它当新安装。它没有单记录日志、数据库事务回放或云端副本。具体筛选与网址规范化见工作台。

iOS 记忆明文模型

加密索引解开后是 [MemoryEntry]。医疗示例只填写一个自定义输入值:

{
  "id": "33333333-3333-4333-8333-333333333333",
  "title": "合成医疗记录",
  "text": "用于格式说明",
  "attachments": [{
    "id": "44444444-4444-4444-8444-444444444444",
    "name": "sample.txt",
    "storedName": "44444444-4444-4444-8444-444444444444",
    "byteCount": 6,
    "isImage": false
  }],
  "createdAt": 0,
  "updatedAt": 0,
  "kind": "health",
  "fields": {"healthSubject": "合成对象"}
}
字段 类型与语义
id UUID,编辑保持不变
title、text 字符串;正文名是 text,不能套用 Android 的 body
attachments 元数据数组,附件正文另存;byteCount 是原始字节数
createdAt、updatedAt Swift Date 默认参考日期秒数
kind note/health/visa/document/appointment/subscription
fields 字符串到字符串字典,日期和数字型表单值仍是字符串
generatedSchema 可选主题对象:category、icon、fields;每个生成字段含 id/title/type/options

生成字段 type 可为 text/choice/date/dateTime/number。表单日期为 yyyy-MM-dd,带时间为 yyyy-MM-dd'T'HH:mm,不含时区;不能等同为 UTC 瞬间。旧记录缺少 kind 时取 note,缺少 fields 时取空字典;旧生成 schema 缺 icon 时按主题选择兼容图标。保存层只接受当前模板或生成 schema 允许的字段 ID,因此未知字段不会自动透传到新 iOS 记录。

iOS vault 封装与密钥项

当前容器是 ASCII SLMV2 头加 CryptoKit AES.GCM.SealedBox.combined。combined 含 nonce、密文与认证标签;正文与索引不采用 JSON 内 Base64 封装。索引与附件使用同一随机 256 位密钥,但附加认证数据 AAD 分别是 UTF-8 的 index 与 attachment:<附件UUID的uuidString>,UUID 的具体字符串必须与保存时一致。换附件名不改变其绑定 ID;拿另一附件的密文替换而不匹配 purpose 会认证失败。

Keychain service 为 cn.thestarline.workbench.memory,account 为 device-vault-v2,属性为 WhenUnlockedThisDeviceOnly。这些名字不是密钥值,也没有基于账号密码的密钥推导或在线找回。正常写入使用 complete 文件保护;读取需设备安全存储与文件共同可用。文件头提供格式识别,真正的内容完整性由 GCM 认证负责。

Android JSON 与加密容器

工作台偏好键 tasks/bookmarks/recents 对应 JSON 数组字符串。任务字段常见为 id/title/notes/isCompleted/createdAt,编辑时增加 dueAt/updatedAt;网址任务还含 sourceURL。日期是 System.currentTimeMillis() 的 Unix 毫秒,dueAt=0 或缺失表示无到期。收藏和历史含 id/title/url/createdAt。数组的元素是 JSON 对象,不是 Swift Snapshot。

Android 医疗记忆的真实形状为:

{
  "id": "55555555-5555-4555-8555-555555555555",
  "title": "合成医疗记录",
  "body": "用于格式说明",
  "kind": "health",
  "createdAt": 978307200000,
  "updatedAt": 978307200000,
  "healthSubject": "合成对象",
  "attachments": [{
    "id": "66666666-6666-4666-8666-666666666666",
    "name": "sample.txt",
    "mimeType": "text/plain",
    "size": 6
  }]
}

正文为 body,模板字段直接位于记录对象中,没有 iOS 的 fields 外层。类型包含 general/pet/health/visa/document/appointment/subscription,本机通用推荐使用 suggested 加 suggestedCategory;旧 travel/calendar 仍可显示。附件使用 size、mimeType,没有 iOS 的 byteCount/isImage/storedName。JSONObject 的键顺序不是契约,未知历史键可能存在,不能按示例重建并丢弃其他字段。

SecureFile 从 Android Keystore 读取或生成别名 starline_android_data_v1 的 AES-256 密钥,使用 AES/GCM/NoPadding。磁盘字节为 IV || ciphertext-and-tag,读取按前 12 字节 IV、128 位 GCM 标签处理,至少 28 字节;当前没有 SLMV2 头,也没有 updateAAD 的索引/附件 purpose 绑定。格式上不与 iOS 互通,不能通过换后缀或复用 iOS key 读取。

写入先保存到同名 .tmp 文件,调用文件描述符同步,再重命名替换。这个流程不意味着多份附件与索引构成完整数据库事务。加密数组读失败会抛“原文件已保留”,AppState 将记忆设为 locked;普通工作台数组解析失败则回退空数组,两者保护级别不同。

偏好、会话与 pending

内容 iOS 键或结构 Android 键或结构
默认页面 workbench.defaultTab default_tab
底部记忆入口 workbench.memoryInTabBar show_memory_tab
页面扫码开关 workbench.scanOnChat/scanOnHome/scanOnProfile scan_on_chat/scan_on_work/scan_on_profile
聊天安全存储 service cn.thestarline.workbench.chat;account device-session 加密 session.bin
登录 pending codeVerifier/state/expiresAt/apiBaseURL/ticket? verifier/state/expiresAt

聊天 Keychain 的可访问属性为 AfterFirstUnlockThisDeviceOnly,与记忆密钥的 WhenUnlocked 不同。pending 的 expiresAt 两端都是协议 Unix 毫秒,不能套用 Swift Date 的默认 JSON 日期规则。Android 不持久化换票 ticket,也没有 pending API 基址字段;恢复边界详见客户端生命周期。本页不展示任何实际 token、verifier 或密钥。

旧数据迁移与失败处理

iOS 检测旧 memories.json 时先加载旧记录,在暂存 vault 中逐附件加密并回读验证,再写入并验证加密索引,随后移动成正式 vault,最后清理旧明文。旧附件 storedName 必须是安全单段文件名,不能包含路径穿越。清理旧明文失败有独立错误,不应描述为所有数据都已安全清理。

已有加密索引时不会为了读取失败而生成新密钥;出现无有效索引的残留 vault、认证失败或缺失密钥会留下错误状态,禁止覆盖。附件删除使用尽力清理,不能从索引删除成功推断闪存物理擦除。Android 兼容宠物和签证旧字段别名;同主题编辑保留未展示旧键,切换主题则明确替换相关字段。原子索引写入、旧格式解析和字段迁移需分别测试。

解析与扩展检查顺序

  1. 先确认数据类别。一个带有相同记录标题的工作台任务和记忆没有共享 ID 或自动关联;一个 Base64 字符串也不能据此判断是密钥、附件还是密文。按容器与字段定义解释,不猜测内容。
  2. 确认时间单位、基准和是否含时区。Swift 快照参考日期秒数、Android Unix 毫秒、协议 expiresAt 和表单日期分属不同规则。转换测试至少覆盖参考日期、非整秒、缺失截止、设备时区变化和日期型空值。
  3. 确认空值策略。iOS 的可选键可省略,但必需数组或 UUID 损坏会使解码失败;Android optString/optLong/optBoolean 可能给出空串、0 或 false。缺键不应自动解释为用户主动清空,也不能用默认值覆盖已经读错的数据。
  4. 确认元数据与文件一致。附件数、ID 和原字节大小应吻合;扩展名是名称信息,不能替代 MIME、图片解码或 GCM 认证。转出包附带的原始字节需要按记录关联;磁盘 vault 只靠元数据不能恢复丢失密钥。
  5. 用独立合成夹具验证旧版读取、新版保存、进程重开和错误保护。添加字段时明确默认值和未知字段保留策略,不能将 Android 的宽松 JSON 行为推到 Swift Codable;改变 AAD 或密钥别名须有明确版本迁移,不能只改字符串。

检查成功也只说明该类夹具符合当前解析器,不构成通用导入器、跨端转换器或断电恢复保证。当前没有自动修复受损 vault 的流程;应保留原文件和可复核错误,避免直接编辑加密字节、复制安全存储、生成新密钥或用空库覆盖作为修复。

代码入口包括 iOS LocalWorkspaceStore、MemoryStore、MemoryVaultCrypto、MemoryGeneratedSchema 与 Android AppData、MemoryScreens、MemoryTemplates。2026-10-23 本轮仅静态核对,未读取用户磁盘数据、运行迁移或执行产品测试。

Markdown 原文完整文档文本