先看结论与判断条件

  • 撤权首先是读取门禁切换,其次才是物理清理,不能在漫长删除期间继续返回旧向量结果。
  • 每份原文及所有派生节点绑定 documentId、subjectId、permissionEpoch、sourceRevision 和 policyVersion。
  • 清理对象不只包括向量,还包括分块文本、摘要、检索缓存、查询历史、临时文件、日志字段和任务队列。
  • 索引、检索与摘要任务提交前再次读取当前 permissionEpoch,拒绝旧代际写回和迟到回调。
  • 密钥失效可以缩短残留数据可读窗口,但不能替代明文缓存、数据库记录和备份策略中的实际清理。
  • 完成回执要列出派生图覆盖、逐类计数、失败重试和候选版本,不能把一次 delete 返回成功当作清理完成。

先把撤权拆成阻断、清理和证明三个阶段

用户撤销某份文档的访问权限时,最先要改变的是读取资格,而不是等待数据库逐表删除。权限服务应原子递增 permissionEpoch,写入 revoked 状态,并让检索入口在组装查询前检查当前主体、文档和代际。旧索引文件即使暂时仍在磁盘,也不能再成为候选结果;否则清理耗时越长,越容易继续泄露已撤销内容。

第二阶段沿派生图执行清理。原文可能被复制到应用私有目录、解析缓存、OCR 临时文件、分块表、向量分片、摘要表、关键字索引、查询缓存和模型会话。只删除文档主表会留下可检索或可还原的副本。每个派生节点必须能回答来自哪个 sourceDocumentId、哪一版 sourceRevision、哪个 permissionEpoch,以及由哪个任务生成。

第三阶段产生可审核的完成回执。回执列出计划节点、实际删除、已失效密钥引用、无法立即删除的介质、排队重试和仍受保留策略约束的最小记录。它证明系统执行了哪些动作,不等于绝对证明所有物理介质已不可恢复。缓存、系统备份和第三方 SDK 的实际行为要按各自边界核验。

撤权处理阶段与安全终态
阶段关键写入检索状态完成条件
开始撤权epoch 递增并标记 revoked立即拒绝状态事务提交
派生清理逐节点删除或加密失效持续拒绝任务无遗漏
迟到任务收敛旧 epoch 写回被拒绝持续拒绝活动任务归零
回执完成记录计数和失败项持续拒绝无未解释节点
重新授权创建新 epoch 和新任务仅新代际可用不得复活旧缓存

用派生数据图覆盖原文到答案缓存的全部路径

端侧 RAG 的数据链通常从文档 URI 或文件副本开始,经解析器产生文本与版面信息,再切成 chunk,计算 embedding,写入向量索引,并生成摘要、关键词或重排特征。查询时又会产生候选集合、相似度缓存、拼接后的上下文和答案缓存。任何节点仍携带足以重建内容的信息,都属于撤权范围。

派生图不应依靠文件名约定推断。每个节点保存稳定 nodeId、parentNodeIds、documentId、subjectId、permissionEpoch、dataClass 和 storageLocator;多文档摘要或共享索引分片要记录所有父节点。若一个分片混合了已撤权与仍授权内容,清理任务必须重建分片,不能粗暴删除影响其他用户,也不能因为共享而保留撤权向量。

图中还要包含间接副本。诊断事件可能记录原文片段,崩溃附件可能带查询上下文,模型会话可能保存拼接 prompt,第三方 SDK 可能收集文件名或用户输入。OWASP 关于日志敏感数据的风险提示可用于建立检查表,但项目仍要逐 SDK、逐字段核对实际收集和传输,不能假设应用私有存储内的数据天然安全。

RAG 派生节点与清理动作
节点类型可能含有的数据撤权动作核对证据
source_copy原文件或规范化文本删除文件与元数据路径和哈希回执
chunk局部正文和位置按 documentId 删除行数与父节点
embedding语义表示和标签删除向量并重建分片索引版本
summary压缩后的敏感事实删除摘要和引用来源集合
query_cache候选、上下文或答案按 epoch 失效并清除缓存键清单
diagnostic文件名、片段或错误输入脱敏或按策略删除字段级审计

permissionEpoch 必须贯穿索引、检索和展示

布尔 authorized 无法区分先授权、撤销、再授权的多个生命周期。permissionEpoch 在每次授权边界变化时单调递增,所有派生节点和任务都绑定创建时看到的 epoch。读取条件不只是 documentId 存在,还要同时满足 subjectId、当前 authorized 状态、节点 epoch 等于当前 epoch,以及 sourceRevision 仍在允许范围。

检索应在多个位置复核。查询规划时过滤无权文档,向量候选返回后再次核对节点标签,拼接上下文前确认父节点仍有效,答案提交前检查会话 generation 和权限快照。多次检查不是为了掩盖不一致,而是应对撤权与长耗时任务并发;每次失败都返回 permission_changed,并丢弃当前候选。

重新授权不能复用旧 epoch 的索引,即使文档内容哈希相同。用户可能更改了允许用途、账号、目录范围或数据处理选择。新授权创建新 epoch,按当前政策重新导入并产生新节点;旧节点继续处于 revoked 清理流程。这样可以避免撤权后仅翻转布尔值就让残留缓存重新可见。

各执行点的 permissionEpoch 门禁
执行点输入代际当前代际来源不匹配终态
导入任务任务创建 epoch权限事务表取消解析与写入
向量写入chunk epoch当前文档授权拒绝提交分片
查询规划会话权限快照权限服务排除文档
上下文组装候选节点 epoch批量当前状态丢弃整次候选
答案缓存请求 epoch 集合当前文档集合禁止缓存
页面展示响应 generation页面当前请求忽略迟到结果

先取消新工作,再处理正在运行的迟到任务

撤权事务提交后,调度器应停止为旧 epoch 领取新任务,并向正在运行的解析、embedding、摘要和压缩任务发送取消信号。取消只是优化,不能作为正确性前提,因为线程可能已经越过检查点,底层库也可能不支持即时中断。真正的门禁位于每次持久化提交前:任务重新读取当前 epoch,不一致就放弃输出。

队列消息至少携带 documentId、subjectId、permissionEpoch、sourceRevision、taskId 和 outputNodeIds。消费者不能仅凭 taskId 去重,因为撤权后新授权可能产生新的合法任务。写入事务把当前权限检查、节点唯一键和输出提交放在同一一致性边界;如果存储不支持跨组件事务,就使用准备态、提交标记和补偿清理,并保留每一步回执。

迟到任务最容易复活已经删除的数据。比如向量计算完成后写回旧分片,摘要回调重新放入内存缓存,或 WorkManager 重试恢复过期任务。清理完成前要枚举活动任务,完成后还要维持旧 epoch 的拒绝墓碑,直到所有可能重试窗口结束。墓碑只保存身份和代际,不保存文档内容。

后台任务在撤权期间的处理
任务状态撤权动作提交前检查回执
queued取消或标记不可领取epoch 必须当前cancelled_before_start
running请求取消输出写入前复核discarded_stale_output
committing由事务裁决权限与输出同事务committed 或 rejected
retry_wait永久拒绝旧 epoch墓碑匹配retry_suppressed
completed纳入派生图清理节点父关系完整deleted 或 unresolved

密钥失效是辅助边界,不能替代派生数据清理

可以为文档或权限域使用数据加密密钥,并用 Android Keystore 保护包裹密钥或执行受限密钥操作。撤权时删除可用密钥引用,能使仍在介质上的密文更难被正常解密,尤其适合无法瞬时重写的大文件。但 Keystore 只约束密钥使用,不会自动删除已经解密的 chunk、向量、摘要、内存页和日志。

密钥层级要避免一把全局密钥覆盖所有文档,否则撤销单份文档时无法独立失效;也不能为每个临时对象创建无界密钥导致管理失控。工程上常用主密钥包裹文档数据密钥,派生节点记录 keyRef 和 permissionEpoch。具体方案取决于设备能力、备份、迁移和恢复需求,不能从通用平台文档推导出某项目已启用硬件保护。

加密擦除也有适用限制。明文在生成 embedding 前会进入内存,某些向量本身可能仍泄露语义,系统备份可能保留旧密文,截图或导出文件则超出本地索引控制范围。因此回执要区分物理删除、逻辑不可检索、密钥失效和受保留策略约束,不用一个 cleaned 布尔值遮蔽不同证据等级。

用派生图校验器阻止遗漏节点和旧任务写回

下面的 Python 校验器读取公开安全的清理计划 JSON,逐个检查节点的 documentId、permissionEpoch、父节点和清理状态。它要求计划 epoch 与撤权记录一致,任何节点仍标记 active、父关系缺失或任务持有旧代际都会以非零状态退出。示例只处理元数据,不接触真实文档、向量或密钥。

图遍历从 source 节点开始,确认所有声明的派生节点都可达;若存在孤儿节点,说明来源链不完整,不能宣布完成。对 shared_shard 之类共享节点,计划应先生成只含授权成员的新版本,再把旧版本标记 replaced。代码并不执行删除,目的是让发布或测试流水线拒绝一份不完整的清理回执。

输入验证故意采用 fail closed:缺少 revocation、epoch 不是正整数、节点状态未知或任务仍可能提交时全部失败。生产系统还要校验签名、事务版本和存储实际计数。校验器能证明清单内部一致,不能证明磁盘块不可恢复,也不能替代对 SDK、备份和云同步路径的独立审查。

RAG 撤权派生图回执校验器
import json
import sys
from pathlib import Path

ALLOWED_STATES = {"deleted", "key_revoked", "replaced"}

def stop(message):
    print(f"cleanup receipt rejected: {message}", file=sys.stderr)
    raise SystemExit(2)

def load_plan(path_text):
    path = Path(path_text)
    if not path.is_file():
        stop("plan file is missing")
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        stop(f"cannot read plan: {exc}")

def validate(plan):
    revocation = plan.get("revocation", {})
    document_id = revocation.get("documentId")
    epoch = revocation.get("permissionEpoch")
    if not document_id or not isinstance(epoch, int) or epoch < 1:
        stop("invalid revocation identity")
    nodes = plan.get("nodes", [])
    if not nodes:
        raise SystemExit("cleanup receipt rejected: derivation graph is empty")
    by_id = {node.get("nodeId"): node for node in nodes}
    if None in by_id or len(by_id) != len(nodes):
        stop("node ids are missing or duplicated")
    for node in nodes:
        if node.get("documentId") != document_id:
            stop(f"foreign document node: {node.get('nodeId')}")
        if node.get("permissionEpoch") != epoch:
            stop(f"epoch mismatch: {node.get('nodeId')}")
        if node.get("cleanupState") not in ALLOWED_STATES:
            stop(f"unfinished node: {node.get('nodeId')}")
        for parent in node.get("parentNodeIds", []):
            if parent not in by_id:
                stop(f"missing parent {parent}")
    for task in plan.get("tasks", []):
        if task.get("permissionEpoch") <= epoch and task.get("canCommit"):
            stop(f"stale task can still commit: {task.get('taskId')}")
    print("cleanup receipt is internally consistent")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        stop("usage: validate_cleanup.py plan.json")
    validate(load_plan(sys.argv[1]))

测试要证明撤权瞬间、清理过程和重新授权都不串线

基本用例先导入文档并确认存在原文、chunk、embedding、摘要和缓存节点,然后在查询与索引任务运行时触发撤权。门禁应立即让新查询返回 permission_changed,活动任务即使完成计算也无法提交。测试随后等待清理器收敛,核对每类节点计数、共享分片替换和旧 epoch 墓碑。

竞争用例要控制事件顺序,而不是依赖随机压力。分别覆盖向量写入前撤权、写入事务中撤权、候选返回后撤权、答案缓存前撤权、应用被杀后任务重试,以及撤权后马上重新授权。每个事件记录 requestGeneration、permissionEpoch、taskId 和 candidateHash,才能判断迟到结果究竟来自哪一代。

隐私测试还要搜索日志、崩溃附件、临时目录、剪贴板、导出路径和第三方 SDK 事件。Data safety 声明反映应用与 SDK 实际收集和共享的数据,但声明本身不会清除数据;测试发现的实际路径要反馈到数据地图和商店披露。没有对应回执时,应把该路径标记为未覆盖。

撤权回归矩阵
用例控制点必须观察失败信号
查询并发撤权候选返回前后旧候选不进上下文页面仍显示旧答案
索引任务迟到embedding 提交前旧 epoch 写入被拒绝索引节点复活
进程被杀清理中断后重启从回执断点继续状态回到 active
快速重新授权新旧任务交错仅新 epoch 可见旧缓存被复用
共享分片部分文档撤权重建后成员正确误删或残留
诊断路径错误与崩溃上报无正文和敏感片段日志仍含内容

发布门禁需要清理回执、数据地图和明确限制

发布前的数据地图要把每类输入、派生节点、存储位置、密钥、任务和 SDK 接收者关联起来,并为每条边指定撤权动作。OWASP MASVS 的存储、隐私、密码学、平台交互等控制域适合作为审查索引,但不能代替具体业务分类、用户同意、法律保留和第三方合同审查。

门禁至少检查 permissionEpoch 一致性、旧任务写回拒绝、派生图可达性、每类删除计数、重试队列、墓碑保留和日志脱敏。候选的数据结构、解析器、向量库、缓存键、SDK 或备份设置变化后,旧回执不再充分,应基于新版本重跑。NIST SSDF 可支持变更与验证证据流程,但不提供某个实现的清理结论。

站内关于端侧 RAG 索引完整性的文章可继续核对索引文件来源与版本;准备评估商业 App 的 AI 数据边界和加固范围时,可通过页面行动按钮进入御盾中央平台提交数据地图、撤权序列与候选包。提交只启动评估,清理是否完整仍以同一候选的节点回执和设备测试为准。

  • 撤权事务先递增 permissionEpoch 并立即阻断新检索。
  • 原文、分块、向量、摘要、缓存、日志和任务都进入派生图。
  • 每个节点保存 documentId、subjectId、sourceRevision 与 permissionEpoch。
  • 所有后台任务在提交输出前重新读取当前权限代际。
  • 密钥失效与物理删除、逻辑隔离和保留策略分别记账。
  • 共享分片重建后核对成员,不误删仍授权文档。
  • 撤权后重新授权创建新 epoch,不复活旧缓存。
  • 候选变化后重跑并发、重启、SDK 和诊断路径测试。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
Android Keystore 可约束密钥用途,并可在设备支持时使用更强的密钥保护。Android Keystore 说明密钥材料、用途限制和设备保护相关的平台能力。Keystore 不清除解密后的原文、向量、摘要、缓存或日志,也不证明项目已采用硬件保护。
应用的数据披露需要包含自身代码和第三方 SDK 的实际收集与共享。Declare Android app data use 说明开发者对应用及集成 SDK 的 Data safety 信息负责。商店声明不是运行时权限门禁、数据最小化或清理机制。
生产日志可能暴露令牌、个人信息和内部诊断数据。OWASP MASWE-0001 描述敏感数据写入应用日志的风险和缓解方向。该页面处于 Beta,本文只采用稳定的风险与最小化原则,项目日志仍需字段级验证。
移动端应最小化敏感数据访问,并纳入第三方 SDK 的数据行为。OWASP MASVS-PRIVACY-1 要求应用最小化敏感数据访问。该控制不能替具体业务的数据分类、用户同意记录、保留策略和合规审查。
移动安全验收需要按不同控制域分别取证。OWASP MASVS overview 给出存储、密码学、认证、网络、平台、代码、韧性和隐私等控制组。控制分类不提供端侧 RAG 清理实现,也不形成任何具体产品的验收结论。
安全发布应保留需求、变更、构建和验证证据。NIST SP 800-218 SSDF 把安全开发与供应链风险管理组织为可执行实践。SSDF 不规定 permissionEpoch 或向量索引清理算法,具体结果仍要项目回执。
撤权后先阻断检索、再异步清理,可以减少清理窗口中的继续暴露。工程判断:读取门禁是即时一致性边界,物理清理通常跨多个存储和任务,无法假定同事务完成。具体延迟与覆盖率需要在目标存储、设备和任务系统上测量,本文不提供性能数字。
御盾项目中的派生图覆盖和撤权清理效果当前不能由通用资料推出。项目证据尚未接入:需要候选哈希、节点清单、epoch 事件、设备矩阵和真实完成回执。证据齐备前不得声称已清除全部副本、阻断攻击、通过兼容测试或产生客户效果。

工程常见问题

撤销权限后只删除向量数据库中的记录可以吗?

不够。原文副本、chunk、摘要、关键词索引、查询缓存、临时文件、日志和后台任务都可能保留内容或重新生成向量,必须按派生图处理。

为什么要先递增 permissionEpoch,再慢慢删除文件?

物理清理跨多个介质和任务,无法保证瞬时完成。先切换代际能让检索立即拒绝旧节点,并让迟到任务在提交前确定性失败。

删除 Keystore 中的密钥是否等于数据已经清理?

不等于。它可让相应密文失去正常解密路径,但已经解密的内存、缓存、向量、摘要、日志和备份仍需单独清理和验证。

用户重新授权同一文档时可以复用旧索引吗?

不应直接复用。新授权可能对应不同账号、用途和政策,应创建新 permissionEpoch,按当前数据边界重新导入,旧节点继续清理。

后台任务收到取消信号后还要检查 epoch 吗?

必须检查。取消可能迟到或底层计算不可中断,只有输出提交前重新读取当前 epoch,才能阻止旧任务把数据写回索引或缓存。

怎样证明撤权清理已经完成?

用同一候选的派生图、逐类节点计数、旧任务拒绝、共享分片替换、日志与 SDK 检查以及设备回归形成回执,同时明确备份和物理介质边界。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: 移动 AI 应用的运行时安全边界