背景
游离文件(orphan files)是指存在于 S3 但不在元数据库中的文件。之前的实现逻辑复杂,分多个循环处理不同目录,且加密文件收养后会被错误标记为”文件损坏”。
核心设计
fileID 与加密密钥
每个文件的加密密钥由 masterKey 和 fileID 共同派生:
fileKey = DeriveFileKey(masterKey, fileID)- fileID:文件的 UUID,即 S3 路径
files/{uuid}.enc中的 uuid - masterKey:端到端加密主密钥
- fileKey:实际用于加密文件内容的密钥
这是标准的密码学设计(HKDF),确保每个文件有独立的密钥,互不影响。
尾部嵌入元数据
加密文件尾部存储原始文件名:
[加密内容][加密的元数据 {"name":"photo.jpg"}][4字节长度]用途:元数据库丢失后仍能恢复有意义的文件名。
重构后的逻辑
1. 游离文件扫描(极简化)
// 一次扫描所有 S3 对象
allObjects, _ := s3.ListAllObjects(ctx, "")
for _, obj := range allObjects {
// 跳过系统目录
if strings.HasPrefix(obj.Key, ".e2eepan/") || strings.HasPrefix(obj.Key, "thumbs/") {
continue
}
isEncrypted := strings.HasSuffix(obj.Key, ".enc")
if isEncrypted {
// 检查是否在元数据中
fileID := strings.TrimSuffix(filepath.Base(obj.Key), ".enc")
if knownFileIDs[fileID] {
continue // 不是游离文件
}
// 提取真实文件名
displayName = extractOriginalName(ctx, obj.Key, obj.Size)
}
orphans = append(orphans, ...)
}2. 游离文件收养(两种情况)
| 文件类型 | 处理方式 |
|---|---|
files/*.enc | 直接用原 fileID,提取真名,添加元数据 |
| 其他(明文或非 files/ 下的 .enc) | 加密后上传到 files/,添加元数据 |
if isEncrypted && isInFilesDir {
// 情况1:直接添加元数据
fileID = strings.TrimSuffix(filepath.Base(req.Key), ".enc")
// 提取真名、解密获取原始大小...
} else {
// 情况2:加密上传
if isEncrypted {
// 先解密原文件
}
// 加密后上传到 files/
fileID = uuid.New().String()
// ...
}3. 下载游离文件
if strings.HasSuffix(orphanKey, ".enc") {
// 派生 fileID
var fileID string
if strings.HasPrefix(orphanKey, "files/") {
fileID = strings.TrimSuffix(filepath.Base(orphanKey), ".enc")
} else {
fileID = strings.TrimSuffix(orphanKey, ".enc")
}
// 解密后返回
} else {
// 明文直接返回
}前端适配
OrphanFileInfo包含encrypted字段- 转换为
FileMetadata时设置isOrphan=true,isEncryptedOrphan=encrypted - UI 显示”加密的游离文件”或”明文的游离文件”
Debug 页新增功能
添加”游离文件诊断”选项,调用 GET /api/v1/orphans/debug 返回 S3 文件列表和分类信息,便于在移动端排查问题。
安全说明
Q: UUID 是明文存储,会不会泄露密钥?
A: 不会。fileKey = derive(masterKey, fileID) 是单向函数,攻击者知道 fileID 但不知道 masterKey,无法计算出 fileKey。这是标准密码学设计。