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:
parent
0a1c3b392b
commit
a7c3f26a4f
2 changed files with 11 additions and 2 deletions
|
|
@ -665,7 +665,7 @@ export class GroupFilesService {
|
||||||
const storage = this.storage();
|
const storage = this.storage();
|
||||||
const [url, frame] = await Promise.all([
|
const [url, frame] = await Promise.all([
|
||||||
storage.presign(storageKey, DIRECT_GET_TTL_SECONDS),
|
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"); });
|
]).catch(() => { throw new ServiceUnavailableException("file storage is unavailable"); });
|
||||||
return { url, cekB64: frame.toString("base64"), frame: "SPENC2", sizeBytes: item.sizeBytes };
|
return { url, cekB64: frame.toString("base64"), frame: "SPENC2", sizeBytes: item.sizeBytes };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,14 @@ export interface StorageService {
|
||||||
headObject(key: string): Promise<{ exists: boolean; contentLength: number | null }>;
|
headObject(key: string): Promise<{ exists: boolean; contentLength: number | null }>;
|
||||||
/** Direct-to-R2: unwrap the frame's CEK (header only) for /files/:id/direct-download. */
|
/** Direct-to-R2: unwrap the frame's CEK (header only) for /files/:id/direct-download. */
|
||||||
openDirectHeader(stored: Buffer): Promise<Buffer>;
|
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. */
|
/** The direct-only members of StorageService, as one spreadable group. */
|
||||||
export type DirectStorageMembers = Pick<
|
export type DirectStorageMembers = Pick<
|
||||||
StorageService,
|
StorageService,
|
||||||
"directPublicJwk" | "presignPut" | "headObject" | "openDirectHeader"
|
"directPublicJwk" | "presignPut" | "headObject" | "openDirectHeader" | "getSealedObject"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -70,6 +72,7 @@ export function directStorageDisabled(): DirectStorageMembers {
|
||||||
presignPut: unavailable,
|
presignPut: unavailable,
|
||||||
headObject: unavailable,
|
headObject: unavailable,
|
||||||
openDirectHeader: unavailable,
|
openDirectHeader: unavailable,
|
||||||
|
getSealedObject: unavailable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,6 +517,12 @@ export class S3Storage implements StorageService {
|
||||||
if (this.directKey === null) throw missingDirectKey();
|
if (this.directKey === null) throw missingDirectKey();
|
||||||
return unwrapDirectCek(stored, this.directKey.privateKey);
|
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). */
|
/** Build a DirectKeyPair from an already-loaded private key (config + tests). */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue