fix: direct-download reads the sealed object for its CEK header

getObject decrypts by magic, so feeding its plaintext into openDirectHeader
always failed. getSealedObject exposes raw bytes for the header read;
DirectStorageMembers and the test fakes carry the new member.
This commit is contained in:
glass beadian 2026-10-16 08:30:25 +00:00
parent 0a1c3b392b
commit a7c3f26a4f
2 changed files with 11 additions and 2 deletions

View file

@ -665,7 +665,7 @@ export class GroupFilesService {
const storage = this.storage();
const [url, frame] = await Promise.all([
storage.presign(storageKey, DIRECT_GET_TTL_SECONDS),
storage.getObject(storageKey).then((stored) => storage.openDirectHeader(stored)),
storage.getSealedObject(storageKey).then((stored) => storage.openDirectHeader(stored)),
]).catch(() => { throw new ServiceUnavailableException("file storage is unavailable"); });
return { url, cekB64: frame.toString("base64"), frame: "SPENC2", sizeBytes: item.sizeBytes };
}

View file

@ -48,12 +48,14 @@ export interface StorageService {
headObject(key: string): Promise<{ exists: boolean; contentLength: number | null }>;
/** Direct-to-R2: unwrap the frame's CEK (header only) for /files/:id/direct-download. */
openDirectHeader(stored: Buffer): Promise<Buffer>;
/** Direct-to-R2: raw still-sealed object bytes — the CEK header lives only in the sealed form. */
getSealedObject(key: string): Promise<Buffer>;
}
/** The direct-only members of StorageService, as one spreadable group. */
export type DirectStorageMembers = Pick<
StorageService,
"directPublicJwk" | "presignPut" | "headObject" | "openDirectHeader"
"directPublicJwk" | "presignPut" | "headObject" | "openDirectHeader" | "getSealedObject"
>;
/**
@ -70,6 +72,7 @@ export function directStorageDisabled(): DirectStorageMembers {
presignPut: unavailable,
headObject: unavailable,
openDirectHeader: unavailable,
getSealedObject: unavailable,
};
}
@ -514,6 +517,12 @@ export class S3Storage implements StorageService {
if (this.directKey === null) throw missingDirectKey();
return unwrapDirectCek(stored, this.directKey.privateKey);
}
async getSealedObject(key: string): Promise<Buffer> {
const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
if (res.Body === undefined) throw new Error(`empty body for ${key}`);
return Buffer.from(await res.Body.transformToByteArray());
}
}
/** Build a DirectKeyPair from an already-loaded private key (config + tests). */