fix(sync): secure encrypted snapshot lifecycle

This commit is contained in:
2026-07-10 06:24:53 -04:00
parent 556c5ff624
commit 540b901fd6
106 changed files with 18026 additions and 3309 deletions
+20 -9
View File
@@ -54,11 +54,19 @@ export function syncSnapshotKey(params: {
region: string;
userHash: string;
snapshotId: string;
payloadHash: string;
}): string {
assertRegion(params.region);
assertSha256Hex(params.userHash, "user_hash");
assertSegment(params.snapshotId, "snapshot_id");
return ["sync-snapshots", params.region, params.userHash, `${params.snapshotId}.bin`].join("/");
assertSha256Hex(params.payloadHash, "payload_hash");
return [
"sync-snapshots",
params.region,
params.userHash,
params.snapshotId,
`${params.payloadHash}.bin`,
].join("/");
}
export function pluginPackageKey(params: { pluginId: string; packageHash: string }): string {
@@ -101,9 +109,7 @@ export async function putVerifiedObject(
expectedSha256: string,
contentType: string,
): Promise<StoredObject> {
assertKnownObjectKey(key);
assertSha256Hex(expectedSha256, "sha256");
assertKeyChecksum(key, expectedSha256);
assertKnownObjectKeyHash(key, expectedSha256);
const actualSha256 = await sha256Hex(payload);
if (actualSha256 !== expectedSha256) {
throw new StorageObjectError("r2_checksum_mismatch");
@@ -122,9 +128,7 @@ export async function getVerifiedObject(
key: string,
expectedSha256: string,
): Promise<ArrayBuffer | null> {
assertKnownObjectKey(key);
assertSha256Hex(expectedSha256, "sha256");
assertKeyChecksum(key, expectedSha256);
assertKnownObjectKeyHash(key, expectedSha256);
const object = await bucket.get(key);
if (object === null) {
return null;
@@ -143,10 +147,11 @@ export async function deleteKnownObject(bucket: ElyR2Bucket, key: string): Promi
await bucket.delete(key);
}
function assertKnownObjectKey(key: string): void {
export function assertKnownObjectKey(key: string): void {
const matches = [
/^sync-payloads\/[a-z0-9][a-z0-9-]{1,31}\/[a-f0-9]{64}\/[a-z0-9][a-z0-9._-]{0,127}\/[a-z0-9][a-z0-9._-]{0,127}\/[a-f0-9]{64}\.bin$/,
/^sync-snapshots\/[a-z0-9][a-z0-9-]{1,31}\/[a-f0-9]{64}\/[a-z0-9][a-z0-9._-]{0,127}\.bin$/,
/^sync-snapshots\/[a-z0-9][a-z0-9-]{1,31}\/[a-f0-9]{64}\/[a-z0-9][a-z0-9._-]{0,127}\/[a-f0-9]{64}\.bin$/,
/^plugin-packages\/[a-z0-9][a-z0-9._-]{0,127}\/[a-f0-9]{64}\.rplug$/,
/^plugin-assets\/[a-z0-9][a-z0-9._-]{0,127}\/[a-f0-9]{64}$/,
/^user-avatars\/[a-f0-9]{64}\/[a-f0-9]{64}$/,
@@ -161,6 +166,12 @@ function assertKnownObjectKey(key: string): void {
}
}
export function assertKnownObjectKeyHash(key: string, expectedSha256: string): void {
assertKnownObjectKey(key);
assertSha256Hex(expectedSha256, "sha256");
assertKeyChecksum(key, expectedSha256);
}
function assertKeyChecksum(key: string, expectedSha256: string): void {
const keyChecksum = checksumFromKey(key);
if (keyChecksum !== null && keyChecksum !== expectedSha256) {
@@ -176,7 +187,7 @@ function checksumFromKey(key: string): string | null {
return null;
}
if (prefix === "sync-payloads") {
if (prefix === "sync-payloads" || (prefix === "sync-snapshots" && segments.length === 5)) {
return lastSegment.slice(0, -".bin".length);
}
if (prefix === "plugin-packages") {