十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Super Productivity 安全密钥存储架构:从 SecretRef 到 PortableVault 的设计解析

Super Productivity 安全密钥存储架构:从 SecretRef 到 PortableVault 的设计解析 Super Productivity 安全密钥存储架构从 SecretRef 到 PortableVault 的设计解析【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity本文基于 Super Productivity 仓库中的长期规划文档 secure-storage.md深入剖析该项目为全部应用托管密钥同步凭据、端到端加密口令、Issue 集成令牌、插件配置密钥、OAuth Token、原生后台同步凭据设计的安全存储架构。读完后你将理解它如何用SecretRef元数据与LocalSecretStore接口把明文凭据逐出同步状态、日志与备份如何在开启 E2EE 的用户之间通过 HKDF 派生密钥的 PortableVault 实现零重复输入的多设备体验以及各平台Electron/Android/iOS/Web后端选型背后的真实权衡。一、问题背景今天究竟有哪些密钥在同步规划文档开篇先做一次范围现实检查明确当前密钥暴露面的边界——这是整个方案设计的出发点同步提供方凭据WebDAV/Nextcloud 密码、Dropbox Token、SuperSync Token以及每个提供方的encryptKey已经是设备本地的存放在sup-syncIndexedDB 数据库中从不参与同步。用户今天每换一台设备就要重新输入一次这些凭据。插件 OAuth Tokensup-plugin-oauth与插件setSecret的值sup-plugin-secrets已在 #8633 中落地也已经是设备本地的。真正会同步出去的密钥只有两类7 个内置 Issue 提供方的机密字段Jirapassword、GitLabtoken、CalDAVpassword、OpenProjecttoken、Redmineapi_key、Nextcloud Deckpassword、Plainspacetoken以及 Issue 集成插件配置中声明为type: password的字段GitHub、ClickUp、Gitea、Linear、Trello、Azure DevOps。SuperSync 上传已强制 E2EEGHSA-9v8x 修复基于文件的提供方在设置阶段提供 E2EE 选项#8709。因此一切都能通过同步到达的体验只涉及 Issue 集成与插件凭据且对主流的 E2EE 用户群PortableVaultV1b能完全保留这一体验。仓库源码印证了这些设备本地的事实。例如 credential-store.service.ts 中的SyncCredentialStore把各同步提供方的私有配置密码、Token、encryptKey写入名为sup-sync的独立 IndexedDB键格式为__sp_cred_ProviderIdplugin-secret-store.ts 则是专为插件密钥建立的sup-plugin-secrets库其文件头注释明确写着该数据库不属于 op-log 同步系统、导出或备份的一部分。值得注意的是内置 Issue 提供方的机密字段目前仍活在 NgRx 的issueProvider状态中——见 issue.model.ts 中各提供方的配置接口。该状态属于 op-log 模型配置会进入快照、同步数据与备份这正是 V1b 的主要治理目标。二、核心权衡用可量化的 UX 成本换密钥出同步方案的核心主张是把密钥移出同步状态用一个小而精确的 UX 成本换取原始密钥不再出现在同步状态、op-log 操作、快照、备份、插件同步数据与日志包括 Electron 主进程日志中。引入 PortableVault 后成本只落在未开启同步 E2EE 的用户身上已开启 E2EE默认群体Issue 集成与插件密钥迁入用现有同步 E2EE 材料派生密钥加密的可移植金库。新设备反正要输入同步口令才能同步金库由同一材料解锁——零新增提示、零重复输入。未开启 E2EE 的同步已有同步密钥原地不动新输入/替换的密钥变为设备本地需要在每台设备上重新输入。已上线的 E2EE 引导会逐步缩小这一群体一旦用户开启 E2EE存量原始密钥会静默迁入金库。不同步的用户完全不受影响。针对设备本地场景的 UX 缓解措施包括同步提供方元数据以便设置表单预填只缺密钥本身、为每台设备展示明确状态凭据已保存在本设备/本设备缺少凭据、从每个受影响集成提供直接的重新授权/重新输入入口。文档还强调了一个重要的兜底事实每一个入金库或设备本地的密钥都可以通过向第三方服务重新认证来恢复因此任何金库/存储丢失的最坏情况就是设备本地基线绝不会造成数据丢失。这也直接推导出它的非目标它不是通用密码管理器无恢复密钥、无设备配对、无 passkey 托管。三、诚实的威胁模型每一层到底买到什么这是本文档最值得借鉴的部分——它要求所有对外文档只按以下声明撰写不得更强层次实际提供的保护明确不提供V1 本地 Profile 存储indexedDbProfile本地隔离密钥离开同步状态、备份、导出与日志静止态加密。任何能访问 Profile 磁盘、或运行于应用 Origin 的代码含插件都能读取与现有sup-sync、sup-plugin-oauth、sup-plugin-secrets同等PortableVault相对于同步目标/存储方对集成密钥的机密性叠加在同步 E2EE 之下继承同步 E2EE 口令强度恶意存储方仍可扣留或回滚金库记录机密性成立新鲜性不成立V1 之后的 OS 级存储safeStorage/Keystore/Keychain本地设备静止态保护磁盘被盗、其他 OS 用户应用与插件之间的隔离插件运行在宿主渲染进程中IPC 调用无法按调用方区分关于没有应用-插件边界这一点从源码结构看可以得到印证插件在宿主渲染进程内执行SecretAccessContext是单一 JS 域中由调用方提供的对象恶意插件可以伪造app上下文或直接打开 IndexedDB。文档因此把它重新定性为误用防护而非安全边界并规定其测试只能是 API 契约测试任何 release note 不得声称更强的隔离。四、当前密钥清单方案要治理的完整表面同步提供方密钥当前设备本地SyncCredentialStore在sup-syncIndexedDB 中明文保存各提供方的私有配置WebDAV/Nextcloud 的password与可选accessToken、Dropbox 的accessToken/refreshToken、SuperSync 的accessToken/refreshToken以及所有提供方含本地文件的encryptKey。源码中可以看到脱敏习惯已存在load()与_save()只记录encryptKey的长度[lengthN]从不记录值本身credential-store.service.ts。Android 后台同步密钥当前设备本地SuperSync access token 从 WebView 镜像到原生 Android 存储供后台同步与提醒取消使用。BackgroundSyncCredentialStore.kt 使用EncryptedSharedPreferencesAES256_GCM 主密钥存于 Android KeyStore但 KeyStore 损坏时会回退到明文SharedPreferences。而 AndroidManifest.xml 中android:allowBackuptrue且现有的备份规则文件data_extraction_rules.xml、backup_rules.xml没有排除SuperProductivitySync偏好文件——所以这个加密的或回退明文的Token 存储目前会被系统备份。修复只需要每个规则文件加一个exclude条目属于下文Quick Wins。内置 Issue 提供方密钥当前在同步——V1b 首要目标机密字段随issueProviderNgRx 状态进入 op-log 模型配置、快照、同步数据与备份。相关实现见 issue.model.ts、issue-provider.reducer.ts、model-config.ts 与 state-snapshot.service.ts。Gitea、Trello、Linear、Azure DevOps、GitHub、ClickUp 已不再内置——它们迁移为插件密钥变成了插件配置字段。插件密钥三套并行存储插件配置在同步——V1b 目标插件 JSON Schema 中声明type: password的字段如 GitHubtoken、ClickUpapiKey作为普通值存进同步的pluginUserData是内置 Issue 提供方泄漏的插件侧孪生。插件密钥存储设备本地#8633 已落地插件 API 的setSecret/getSecret/deleteSecret由 plugin-secret-store.ts 与 plugin-secret.service.ts 实现底层是独立的sup-plugin-secretsIndexedDB。明文静止态、按插件命名空间隔离卸载插件或清空插件缓存时一并清除。本计划在此之上构建而不是新增平行的第二套。插件 OAuth Token设备本地sup-plugin-oauthIndexedDB卸载/清缓存时清除。Electron 主进程泄漏早期草稿遗漏electron/jira.ts 通过 IPC 接收完整 Jira 配置含password并把原始错误响应经 electron-log 写到磁盘。渲染进程的全局错误处理器把错误对象整体转发到主进程 electron-log字符串化的 HTTP 错误经常内嵌含Authorization头的请求配置。electron-log 文件持久在磁盘上当前没有任何脱敏覆盖。相关代码见 global-error-handler.class.ts。现成的有利积木packages/sync-core/src/encryption.ts 已提供 Argon2id KDF、AES-256-GCMWebCrypto 加noble/ciphers回退、版本化 KDF 参数与会话密钥缓存HKDF 可经 WebCrypto 原生使用。其注释还明确了密钥与 IV 语义Argon2id 密文线格式为[SALT(16)][IV(12)][AES-GCM 密文认证标签]每次调用随机生成 12 字节 IV会话内复用 salt 以摊销约 500ms–2s 的 KDF 开销——PortableVault 不需要任何新的加密依赖。privacy-export.ts 已经对password、token、apiKey、secret、authorization、accessToken、authCode、api_key等键做掩码但缺少refreshToken、clientSecret/client_secret、encryptKey、apiToken且是精确键大小写敏感匹配——改进项列入 Quick Wins。plugin-persistence-key.util.ts 中的composeId是分隔符安全复合 ID的参考实现插件 ID 含:会直接抛错因为它把冒号保留为键分隔符——这正是 V1 槽位 ID 编码需要解决的问题。五、架构核心SecretRef 与 LocalSecretStore方案只有两个核心概念LocalSecretStore设备本地密钥存储。V1 在所有平台使用专用本地 IndexedDBindexedDbProfileV1 之后用原生 OS 后端在同一接口后替换存储实现。PortableVault为 E2EE 用户准备的、以金库加密的同步密钥记录作为普通 op-log 实体承载。SecretRef是纯元数据只有SecretRef与非敏感元数据允许进入 NgRx 状态、op-log 操作、快照、备份与插件同步数据密钥值只存在于LocalSecretStore或PortableVault之后。接口定义如下export interface SecretRef { kind: SecretRef; version: 1; id: string; // 分隔符安全的复合 ID见槽位 ID ownerType: | syncProvider | issueProvider | pluginConfig | pluginOAuth | nativeBackground; ownerId: string; field: string; storageMode: device | portableEncrypted; updatedAt: number; } export type SecretAccessContext | { callerType: app | nativeBridge; expectedOwnerType: SecretRef[ownerType]; expectedOwnerId: string; expectedField: string; } | { callerType: plugin; callerId: string; expectedOwnerType: pluginConfig | pluginOAuth; expectedOwnerId: string; expectedField: string; }; export interface LocalSecretStoreCapabilities { // 仅在某个后端真正落地时扩展这些联合类型YAGNI mode: localProfile | unavailable; backend: indexedDbProfile; canPersistDeviceSecrets: boolean; canUsePortableVault: boolean; } export interface LocalSecretStore { capabilities(): PromiseLocalSecretStoreCapabilities; set( input: SecretRefInput, value: string, context: SecretAccessContext, ): PromiseSecretRef; useSecretT( ref: SecretRef, context: SecretAccessContext, fn: (value: string) PromiseT, ): PromiseT | null; delete(ref: SecretRef, context: SecretAccessContext): Promisevoid; exists(ref: SecretRef, context: SecretAccessContext): Promiseboolean; }几个设计要点值得注意useSecret(ref, context, fn)的回调式取值密钥值只在回调作用域内短暂存在调用方拿不到可持有的字符串引用天然避免解析出的配置对象被到处传递。SecretAccessContext的诚实定位它是捕获意外跨属主读取与错误接线错误的 ownerType/ownerId/field 会被拒绝宿主按callerId ownerId映射插件属主 ref的断言机制。不是安全边界——上下文是调用方在单 JS 域中自供的对象插件与宿主共享渲染进程。真正的调用方边界要等插件进程/Origin 隔离加主进程强制校验落地届时才能声称。能力上报capabilitiesmode: unavailable是显式状态配套规则是降级平台必须显式化而不是静默回退到明文。槽位 IDSlot ids同步配置中不得包含每设备随机的密钥 ID——否则两台设备迁移同一提供方会各自铸造不同的SecretRefLWW 冲突处理无法避免孤儿。方案要求用稳定元数据生成确定性槽位 ID每段编码必须分隔符安全复用/对齐 plugin-persistence-key.util.ts 的composeId思路插件 ID 与 Schema 字段名是第三方控制字符串朴素拼接v1:${ownerType}:${ownerId}:${field}会产生歧义(a,b:c)vs(a:b,c)。ownerType对封闭枚举做校验。孤儿 GC周期性清扫本地存储中属主配置已不存在的条目并为同步竞态留宽限窗口。清除集成时同时删除同步的SecretRef与本地/金库值而在一台设备上替换密钥值只更新存储、不动同步元数据因此不会使另一台设备的本地条目失效。六、存储模式与 PortableVault 机制两种存储模式device非 E2EE 同步与所有非同步密钥的默认值只存在当前设备的LocalSecretStore其他设备显示本设备缺少凭据并提供重新输入。portableEncrypted同步 E2EE 开启时的默认密文作为普通 op-log 记录同步在到达状态/op-log/快照代码之前已被金库加密——即传输中还被同步 E2EE 再包一层双重封装。硬约束不得用它引导 SuperSync access token 或作为同步加密口令的唯一副本——同步凭据与encryptKey保持设备本地确保金库解锁永不依赖自身。PortableVault 细节V1b方案刻意保持极简——金库只有几条亚 KB 记录不需要通用金库的 DEK/manifest/epoch 机器密钥vaultKey HKDF-SHA-256(syncE2EEKey, salt 每金库随机 salt, info super-productivity-portable-vault-v1)。Salt 在金库创建时随机铸造以明文元数据存入同步的金库配置记录salt 不是机密。绝不直接使用同步内容密钥。若 E2EE 输入是口令它已经过现有 Argon2id KDF与同步 E2EE 同一实现与参数版本化——不养 PBKDF2 分支、不维护第二套 KDF。记录每条密钥用vaultKey下的 AES-256-GCM 加密每次加密含更新使用全新 CSPRNG nonce——多台设备在同一密钥下加密nonce 绝不能由计数器或元数据派生AAD 绑定{recordId, ownerType, ownerId, field, schemaVersion, updatedAt}。同步记录是普通同步实体LWW 冲突处理与删除墓碑直接复用现有 op-log无需独立 manifest。解锁同步 E2EE 密钥可用时即派生vaultKey现有会话缓存使这一步零成本。可选在LocalSecretStore中持久化一份封装副本供同步解锁前访问绝不持久化明文密钥。轮换更换同步 E2EE 口令派生新vaultKey新 salt并重加密全部记录——记录量小所以 trivial且是_真_轮换。文档同时要求诚实表述轮换只保护未来记录攻击者曾经能解密的旧历史必须视为已暴露正解是轮换第三方 Token 本身UI 应如此说明。残余风险写文档、不工程化对抗恶意同步目标可回滚整个数据集让已删除金库记录复活。这属于既有同步信任模型E2EE 给机密性不给新鲜性且恢复出来的记录至多是一个可以在上游吊销的过期凭据。per-vault epoch/MAC 方案复杂度不划算除非金库规模超出当前量级再重审。不做弱口令门槛同一密钥材料本就在保护用户全部同步数据金库化不增加任何新的离线爆破暴露面口令强度提示归 E2EE 设置流程所有。早期草稿中砍掉的内容vaultDek间接层、认证 manifest、金库 epoch、wrapper 集合、宽限重封装、recoveryKey、devicePairing、passkey 托管、金库导入/导出——依据是向上游重新认证即可覆盖恢复路径这一非目标。七、未开 E2EE 的同步用户不提示、不静默迁移对这类用户的处理原则很克制已同步的集成密钥留在同步配置中不迁移。其整个任务数据集本就以明文同步到同一目标Token 相对该目标的机密性是边际性的而迁移提示正是被强加的决策。新输入/替换的密钥写成设备本地SecretRef原始值绝不再进入同步状态明文暴露面停止增长。既有的平静 E2EE 横幅与设置时引导仍是迁移路径用户开启 E2EE 时存量原始密钥在每台升级设备上静默迁入金库。固定应用密钥、静态标准密钥或内置混淆密钥只算混淆禁止用于新同步密钥写入如仅用于一次性遗留读取/迁移兼容必须绑定明确的移除版本。八、平台后端V1本地 Profile 存储全平台统一一个后端走天下专用本地 IndexedDB 存放密钥值与同步模型数据分离Electron、浏览器/PWA、Android/iOS Capacitor 全部相同。理由很务实sup-sync已经在所有平台把 WebDAV 密码与encryptKey明文存在 IndexedDB 里如果 Web/移动端拒绝同一层级等于保护不了任何东西却把 V1b 变成移动端阻断项集成每次会话死亡或要求重新输入。统一后端还彻底删除了仅会话/不可用这类 UX 状态Web 上 IndexedDB 被驱逐的风险与应用数据本身相当并不更差。配套规则同步应用状态中只存SecretRef元数据该库不进入同步、备份、日志或正常应用流导出保留LocalSecretStore接口让原生后端日后替换存储实现而不触发第二次数据模型迁移。对pluginConfig属主的值复用既有sup-plugin-secrets库宿主保留键命名空间作为后端而不是第二个插件密钥表面——卸载与清缓存时的按插件清除生命周期自动继承。V1 之后ElectronsafeStorage主进程 IPC 作为唯一桥获得 OS 钥匙串的静止态保护并把密钥移出渲染进程可读的 Profile 库——但不获得插件隔离。落地形态electron/ipc-handlers/local-secret-store.ts注册进electron/ipc-handler.tselectron/preload.ts与electron/electronAPI.d.ts中暴露收窄的localSecretStoreSet/Resolve/Delete/Capabilities方法主进程用safeStorage.encryptString()/decryptString()密文存app.getPath(userData)下的小文件/库。关键约束Linuxbasic_text后端不得成为静默的明文等价回退——要求显式降级同意或提供仅会话模式升级时若存在遗留明文凭据展示显式选择而非静默删除。V1 之后Android 与 iOSAndroid原生LocalSecretStore由AndroidKeyStore中的 AES-GCM 密钥支持密文存私有应用存储。没有明文SharedPreferences回退——要么存加密的要么不持久化。Quick Wins 的备份排除必须先于原生密钥写入落地并扩展到覆盖新密钥存储文件。替换现存的BackgroundSyncCredentialStore明文回退见 BackgroundSyncCredentialStore.kt 的三级回退逻辑。iOSKeychain Services 经原生 Capacitor 桥kSecAttrSynchronizablefalse、whenUnlockedThisDeviceOnly仅当后台任务需要时才用afterFirstUnlockThisDeviceOnly显式 access group。Keychain 条目可存活卸载——首装流程中检测并清除过期 Token。Web/PWAV1 使用与所有平台相同的indexedDbProfile层级本地隔离诚实标注。若将来要更强的浏览器故事仅会话只意味着内存态——绝不使用sessionStorage、localStorage、window.name或BroadcastChannel浏览器会把sessionStorage持久化到磁盘用于会话恢复并为此类落点加金丝雀测试。永远不得声称浏览器存储等价于 OS 钥匙串。九、数据流表单、运行时解析与插件配置空控件模型密钥字段用初始空控件加每设备提示凭据已保存在本设备/本设备缺少凭据未触碰或清空 →unchanged脏状态追踪免费给出输入后又改回的坍缩语义输入值 →replace值写入金库/本地存储只派发新的SecretRef显式移除操作 →clear存储条目与同步 ref 一并删除。模型中不存在任何掩码哨兵值********因此不需要哨兵拒绝层。表单模型绝不允许把密钥值发往 NgRx金库/本地存储写入必须先于任何持久动作派发完成。运行时解析服务尽可能晚地解析凭据1) 从 NgRx 或提供方私有配置加载公开配置2) 携带SecretAccessContext经由LocalSecretStore/PortableVault解析所需SecretRef3) 构造短命运行时配置对象用于请求4) 绝不派发、持久化或记录解析出的对象。当解析后的密钥必须跨 IPC如经 Electron 主进程的 Jira时接收侧同样属于脱敏面。插件配置拦截Schema 中type: password字段由宿主拦截同步的插件配置存SecretRef值真实值在sup-plugin-secrets支撑的存储device 模式或 PortableVaultE2EE 模式。PluginAPI.getConfig()返回配置元数据与SecretRef值绝不返回解析后的密钥解析走收窄的PluginAPI.useSecret(ref, fn)宿主断言callerId ownerId误用防护能力边界见威胁模型。已落地的setSecret/getSecret/deleteSecret保持原样供插件自管密钥schema-password拦截是宿主侧互补共享同一存储与清除生命周期。persistDataSynced保持非密钥存储其负载日志已移除并有 spec 强制追加注册金丝雀使已注册的密钥值在测试中被拒绝。插件 OAuth Token 迁移延后V1 仅将其注册进脱敏/金丝雀检查。同步提供方私有配置sup-sync中的密码、Token、encryptKey在 V1 不迁移——它本就本地且不驱动同步泄漏风险V1 只把它全部字段注册进脱敏与金丝雀检查确保不再新出现在 op-log 负载、快照、备份、日志与导出中V1 之后再迁入 OS 级LocalSecretStore后端。十、Quick Wins立即可发、独立于 V1 的四个洞每一项都小、无 Schema/UX 影响、且关闭真实泄漏在data_extraction_rules.xml与backup_rules.xml中为SuperProductivitySync偏好文件加exclude条目KeyStore 密钥反正过不了恢复备份的密文至多是死重最坏情况是明文回退泄漏。停止在electron/jira.ts中记录原始 Jira 响应只记录状态码与脱敏元数据。扩展 privacy-export 掩码加入refreshToken、clientSecret、client_secret、encryptKey、apiToken并使匹配大小写不敏感对照 privacy-export.ts 的现有KEY_TO_REPLACE表可见当前缺口。在转发渲染进程错误到主进程 electron-log 之前清洗/截断错误对象丢弃请求配置/头信息块。十一、迁移策略Phase 0 → V1a → V1bPhase 0注册表、脱敏、守卫随 V1a按域建立敏感路径的类型化注册表同步提供方私有配置、内置 Issue 提供方字段、迁移后的 GitHub/ClickUp 插件配置、插件 Schemapassword字段、插件 OAuth Token 记录、插件setSecret值。一个注册表支撑的redactSecrets(value)同时用于日志记录、日志导出、隐私导出、崩溃/错误附加数据、插件/配置负载日志以及 Electron 主进程electron-log 写入、转发的渲染进程错误、electron/jira.ts。键集合含apiKey、api_key、apiToken、refreshToken、clientSecret、client_secret、encryptKey、authorization、大小写变体与嵌套插件配置密码字段。金丝雀测试助手扫描快照、操作负载、备份、渲染与主进程日志输出中的金丝雀密钥值op-log 捕获守卫在已注册金丝雀出现在持久动作负载时使测试失败。持久化前的AppDataComplete净化器用于备份导入、远程同步水合、文件同步快照下载、全状态尾操作水合、状态缓存写入与loadAllData。显式处理SYNC_IMPORT/BACKUP_IMPORT的替换语义导入/水合期间阻塞并发密钥写入、重跑净化器、若导入替换了确定性SecretRef元数据则重新发出ref 不得在本地条目仍存时丢失。迁移标记只存本地 Profile 存储绝不同步。退出标准可证伪所有新生成的序列化输出——当前状态、持久动作、op-log 条目、快照、备份、渲染与主进程日志、隐私导出、插件同步数据——中已注册密钥的原始命中为零。旧历史/备份在延后清理前可能仍含密钥V1 只警告不声称已清除。V1a兼容与护栏独立发布、无 Schema 破坏包含 Phase 0 全部内容加全平台indexedDbProfile后端的LocalSecretStore、保留未知/不支持SecretRef值的容忍型读取器、暗中的双写管线置于 flag 后。文档特别指出这一版对 E2EE 用户已捕获大部分实际价值其远端负载本就是密文——本地产物备份、导出、日志停止泄漏应尽早独立发布。V1b同步密钥迁移 PortableVault同一版本内完成内置 Issue 提供方密钥、插件 Schemapassword字段、遗留 GitHub/ClickUp 配置的迁移并发布 PortableVault使 E2EE 用户永不重新输入凭据、后续也不需要第二次 Schema/兼容事件。过渡 静默双写无阻塞对话框升级后的客户端同时写SecretRef金库/本地值与遗留原始字段——过渡期间同步可见的安全性不变UX 成本为零。原始字段仅在该账户的兼容门禁通过后剥离自动且静默。若剥离后原始密钥经同步到达掉队旧客户端写入把它净化进金库/本地存储并给出一次性、非阻塞提示凭据可能存在于同步历史/备份中、可考虑轮换。绝不静默吞掉事件也绝不阻塞同步。兼容门禁按传输方式拆分旧客户端无法发出新信号因此信号缺席永远不是证明——所以是双写 净化而非信任SuperSync服务端强制minClientVersion拒绝 op 上传——唯一硬仲裁。不依赖 vector-clock 条目有界、可裁剪。基于文件的同步WebDAV/Dropbox/本地需实证验证当前已发布客户端在同步格式版本提升后是否拒绝写入。若是格式提升即门禁若忽略未知版本则文档明言文件同步没有硬门禁永久依赖双写 接收端净化 轮换提示在长弃用窗口后再剥离原始字段。迁移机制确定性槽位 ID 使迁移跨设备幂等LWW 对相同元数据不会孤儿化任何一端的值E2EE 用户存量原始密钥静默迁入金库任何持有同步口令的设备无需重新输入非 E2EE 用户按未开 E2EE一节处理遗留 GitHub/ClickUp reducer 迁移不得把原始字段复制进插件配置原始值进存储、ref 进配置存储写入失败时遗留值只保留在其原遗留源中供下次启动幂等重试绝不重新经 NgRx、持久动作、导入、备份或同步状态持久化原始值设备迁移成功且门禁剥离同步原始字段后删除明文遗留值、兼容读保留一到两个版本之后永不再写明文。延后项本地密钥加固sup-sync私有配置、sup-plugin-oauth、sup-plugin-secrets值、Android 后台同步镜像迁入 OS 级后端同一流水标记 → 存储写入 → 替换/移除遗留 → 成功后才清除明文与历史数据清理重写/清除OPS、STATE_CACHE、IMPORT_BACKUP、文件同步sync-data.json.staterecentOps、SuperSync 远端快照/操作门禁通过后压缩本地 op-log、对文件同步强制上传剥离后的快照均为后续工作。SuperSync 服务端保留/压缩是否限定旧原始负载需要验证若不能文档须说明服务端历史可能保留遗留密钥。Release notes 必须说清新备份不再含原始凭据但旧备份、被复制的同步文件与保留的历史中可能仍含——建议删除/保护旧备份文件并轮换可能已暴露的第三方 Token。十二、错误处理与备份恢复规则方案为每个失败场景定义了明确处置可归纳为一张表场景处置设备本地条目缺失本设备缺少凭据 直接重新输入/重新授权动作本地 Profile 存储不可用受影响集成在本设备标记缺失/只读绝不写原始回退状态本地条目损坏不自动删除SecretRef提供重连/替换与不含密钥的诊断导出迁移中途失败遗留值只保留在原遗留源中供重试不再经 NgRx/op-log/备份/同步状态持久化同步期间密钥查找失败停止同步并索要凭据不覆盖或禁用配置剥离后原始密钥到达净化进存储/金库一次性非阻塞轮换提示绝不阻塞同步金库记录 AAD/解密失败视为缺失而非配置损坏提供重新输入记录脱敏诊断V1 之后 OS 级存储不可用仅会话内存或显式降级模式Linuxbasic_text需显式同意绝不静默明文等价备份与恢复规则正常应用备份只含设备本地密钥的SecretRef 提供方元数据以及金库密钥的金库密文离线暴露面与金库本身相同——绝不包含明文值。在新设备恢复备份后集成显示已配置、凭据缺失device 模式或同步解锁后即可用金库模式并附直接重新授权动作。V1 之后平台备份排除密钥解密键为设备绑定的原生密钥密文Android 规则按 Quick Wins/原生阶段执行。十三、安全不变量与测试策略安全不变量V1a 护栏注册表覆盖的路径上持久动作捕获、op-log 操作、BACKUP_IMPORT、SYNC_IMPORT、状态缓存、水合负载、插件同步数据、persistDataSynced负载、渲染日志、主进程日志、错误附加数据、隐私导出与日志导出中不得出现新的原始密钥值延后的本地密钥留在原存储但须有注册表/金丝雀覆盖证明无新泄漏路径新同步密钥写入禁止固定密钥混淆。V1b 不变量门禁对该账户清除后已迁移密钥绝不再原始出现在 NgRx 状态、动作负载、op-log 操作、备份、插件同步数据或日志中双写期间暴露等于现状且永不超过存储写入失败时集成在该设备变缺失/只读绝不写原始回退状态解析断言声明的属主元数据误用防护但调用方身份在渲染进程内不可验证、不作更强声明另一台设备的SecretRef 本地 Profile 库解析不出任何东西——这是隔离声明不是静止态加密声明明文vaultKey材料绝不同步或无封装持久化只存在于内存与既有 E2EE 会话缓存中运行时解析出的配置对象只留在调用路径本地。测试策略indexedDbProfileLocalSecretStore单测金丝雀值覆盖 web/Capacitor 构建目标处处单后端。SecretAccessContext的 API 契约测试错误 ownerType/id/field 拒绝插件 ref 仅当callerId ownerId才可解析——明确标注为契约测试而非安全测试。槽位 ID 编码测试含分隔符的插件 ID/字段名不能别名到其他槽位。多设备确定性A、B 两台客户端迁移同一集成同步 B 的元数据不会孤儿 A 的值。金库测试AAD 不匹配被拒绝每次写入新 nonce口令变更重加密记录且旧密钥材料不再能解密新记录经会话缓存 E2EE 材料解锁无需提示。双写/门禁测试过渡期写两种形式门禁信号前不剥离剥离后原始到达被净化 提示且同步不阻塞兼容客户端保留SecretRef旧客户端原始/空值覆盖 ref 可被检测并从本地存储修复。空控件表单测试未触碰/清空 → unchanged输入 → replace移除 → clear。迁移测试全新安装、存量凭据、部分迁移、存储写入失败、启用 E2EE 触发静默金库迁移。金丝雀集成测试持久动作捕获、OPS、BACKUP_IMPORT、SYNC_IMPORT、STATE_CACHE、文件同步sync-data.json.staterecentOps、SuperSync 快照上传、插件persistDataSynced、日志导出、隐私导出、electron-log 输出主进程 转发的渲染进程错误以及任何仅会话模式的纯内存规则无sessionStorage/localStorage/window.name落点。脱敏测试大小写变体与嵌套键apiKey、api_key、apiToken、refreshToken、authorization、clientSecret、client_secret、encryptKey、插件密码字段。E2E 冒烟Electron 上配置提供方、重载、凭据可用新 Profile 恢复备份 → 配置存在、凭据按设计缺失/锁定两台升级的 E2EE 客户端同步一个 Issue 提供方持久存储、op-log 文件与同步快照中零金丝雀命中且客户端 B 无需重新输入。预期新增文件实现草图V1 大概率新增src/app/core/secret-storage/下的local-secret-store.model.ts、local-secret-store.service.ts、secret-registry.ts、secret-migration.service.ts、redact-secrets.ts与electron/共享及 V1b 的portable-vault.service.ts改动面覆盖 Issue 提供方配置表单与 API 服务解析、持久派发前的动作创建、插件配置服务与桥useSecret、Schema-password拦截、备份导入/同步水合/快照下载/状态缓存写入/隐私与日志导出、electron/jira.ts与主进程日志接线脱敏以及 op-log 实体注册表与校验承载 V1b 金库记录类型。十四、V1a 之前必须拍板与 V1b 之前的前置条件规划文档最后留出了明确的决策清单与前置条件值得在同类项目中借鉴V1a 前决策遗留明文读兼容保留多久确认宿主管理的pluginConfig密钥复用既有sup-plugin-secrets库推荐继承清除生命周期还是新存储的独立命名空间评估 Electron 上跳过indexedDbProfile直达主进程safeStorage的成本推荐默认仍是统一indexedDbProfile一个后端、一条迁移路径之后换safeStorage只是LocalSecretStore内部实现替换。V1b 前必需实现 SuperSync 服务端minClientVersion上传拒绝实证验证旧发布客户端对文件同步格式版本提升的行为决定门禁存在还是永久双写验证 op-log 实体注册表 typia 校验能承载新金库记录类型而不破坏 V1a 前客户端若新实体种类破坏旧客户端校验金库记录必须与同一双写门禁同版本顺序发布。延后至 V1 之后的决策SuperSync 保留/压缩在迁移后是否限定旧原始密钥负载历史清理自动化的范围 vs 仅文档化轮换建议。总结Super Productivity 这份安全存储规划的核心价值不在某一段加密代码而在于三件事的严格自洽先精确盘点今天什么真的在同步范围现实检查、按诚实威胁模型划定每层存储的能力上限本地隔离 ≠ 静止态加密、SecretAccessContext是误用防护而非边界、用可证伪的退出标准驱动迁移金丝雀值全扫描、双写 门禁 静默净化全程无阻塞对话框。配合 packages/sync-core/src/encryption.ts 已有的 Argon2id/AES-256-GCM 积木与 src/app/plugins/secret/ 已落地的插件密钥存储整个方案没有引入新的密码学依赖把工程重心放在了元数据模型SecretRef、确定性槽位 ID 与全链路脱敏/金丝雀守卫上——这套方法论对任何需要把凭据逐出同步/持久化面的应用都可直接参照。【免费下载链接】super-productivitySuper Productivity is an advanced todo list app with integrated Timeboxing and time tracking capabilities. It also comes with integrations for Jira, GitLab, GitHub and Open Project.项目地址: https://gitcode.com/GitHub_Trending/su/super-productivity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表