fix(ota): verify mirrored asset hashes
This commit is contained in:
parent
f176051cd0
commit
3ef0e1b494
|
|
@ -205,14 +205,15 @@ describe("listPublishedOtaReleases", () => {
|
|||
|
||||
describe("extractMirroredFiles", () => {
|
||||
it("extracts only referenced files from a tar archive", async () => {
|
||||
const iosBundle = textEncoder.encode("console.log('ios')")
|
||||
const archiveBuffer = await createTarArchive([
|
||||
{
|
||||
name: "bundles/ios-main.js",
|
||||
body: "console.log('ios')",
|
||||
body: iosBundle,
|
||||
},
|
||||
{
|
||||
name: "bundles/unused.js",
|
||||
body: "console.log('unused')",
|
||||
body: textEncoder.encode("console.log('unused')"),
|
||||
},
|
||||
])
|
||||
|
||||
|
|
@ -223,7 +224,7 @@ describe("extractMirroredFiles", () => {
|
|||
ios: {
|
||||
launchAsset: {
|
||||
path: "bundles/ios-main.js",
|
||||
sha256: "a".repeat(64),
|
||||
sha256: await sha256Hex(iosBundle),
|
||||
contentType: "application/javascript",
|
||||
},
|
||||
assets: [],
|
||||
|
|
@ -236,7 +237,7 @@ describe("extractMirroredFiles", () => {
|
|||
expect(files).toEqual([
|
||||
{
|
||||
key: "mobile/production/0.4.1/0.4.2/ios/bundles/ios-main.js",
|
||||
body: textEncoder.encode("console.log('ios')"),
|
||||
body: iosBundle,
|
||||
contentType: "application/javascript",
|
||||
},
|
||||
])
|
||||
|
|
@ -246,7 +247,7 @@ describe("extractMirroredFiles", () => {
|
|||
const archiveBuffer = await createTarArchive([
|
||||
{
|
||||
name: "bundles/unused.js",
|
||||
body: "console.log('unused')",
|
||||
body: textEncoder.encode("console.log('unused')"),
|
||||
},
|
||||
])
|
||||
|
||||
|
|
@ -269,6 +270,34 @@ describe("extractMirroredFiles", () => {
|
|||
}),
|
||||
).rejects.toThrow('Archive is missing referenced file "bundles/ios-main.js"')
|
||||
})
|
||||
|
||||
it("throws when a referenced archive file hash does not match metadata", async () => {
|
||||
const archiveBuffer = await createTarArchive([
|
||||
{
|
||||
name: "bundles/ios-main.js",
|
||||
body: textEncoder.encode("console.log('tampered')"),
|
||||
},
|
||||
])
|
||||
|
||||
await expect(
|
||||
extractMirroredFiles({
|
||||
release: {
|
||||
...baseRelease,
|
||||
platforms: {
|
||||
ios: {
|
||||
launchAsset: {
|
||||
path: "bundles/ios-main.js",
|
||||
sha256: await sha256Hex(textEncoder.encode("console.log('ios')")),
|
||||
contentType: "application/javascript",
|
||||
},
|
||||
assets: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
archiveBuffer,
|
||||
}),
|
||||
).rejects.toThrow('Archive file "bundles/ios-main.js" hash mismatch')
|
||||
})
|
||||
})
|
||||
|
||||
describe("putMirroredFiles", () => {
|
||||
|
|
@ -371,7 +400,7 @@ describe("mirrorReleaseToStorage", () => {
|
|||
async function createTarArchive(
|
||||
entries: Array<{
|
||||
name: string
|
||||
body: string
|
||||
body: Uint8Array
|
||||
}>,
|
||||
) {
|
||||
const pack = tar.pack()
|
||||
|
|
@ -389,11 +418,10 @@ async function createTarArchive(
|
|||
|
||||
for (const entry of entries) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const body = textEncoder.encode(entry.body)
|
||||
const tarEntry = pack.entry(
|
||||
{
|
||||
name: entry.name,
|
||||
size: body.byteLength,
|
||||
size: entry.body.byteLength,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
|
|
@ -406,7 +434,7 @@ async function createTarArchive(
|
|||
)
|
||||
|
||||
tarEntry.on("error", reject)
|
||||
tarEntry.end(body)
|
||||
tarEntry.end(entry.body)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -427,3 +455,13 @@ function concatenateChunks(chunks: readonly Uint8Array[]) {
|
|||
|
||||
return output
|
||||
}
|
||||
|
||||
async function sha256Hex(data: Uint8Array) {
|
||||
const digest = await crypto.subtle.digest("SHA-256", toDigestInput(data))
|
||||
|
||||
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
|
||||
function toDigestInput(data: Uint8Array) {
|
||||
return new Uint8Array(data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ interface MirroredFileRequest {
|
|||
archivePath: string
|
||||
key: string
|
||||
contentType: string
|
||||
sha256: string
|
||||
body?: Uint8Array
|
||||
}
|
||||
|
||||
|
|
@ -76,11 +77,21 @@ export async function extractMirroredFiles(input: {
|
|||
stream.on("error", (error) => {
|
||||
rejectOnce(toError(error))
|
||||
})
|
||||
stream.on("end", () => {
|
||||
stream.on("end", async () => {
|
||||
if (matchingRequests) {
|
||||
const body = concatenateChunks(chunks)
|
||||
const bodySha256 = await sha256Hex(body)
|
||||
|
||||
for (const request of matchingRequests) {
|
||||
if (request.sha256 !== bodySha256) {
|
||||
rejectOnce(
|
||||
new Error(
|
||||
`Archive file "${archivePath}" hash mismatch: expected ${request.sha256} but received ${bodySha256}`,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
request.body = body
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +162,7 @@ function createMirroredFileRequests(release: OtaRelease): MirroredFileRequest[]
|
|||
archivePath,
|
||||
key: buildMirroredAssetKey(release, platform, archivePath),
|
||||
contentType: asset.contentType,
|
||||
sha256: asset.sha256,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -209,3 +221,13 @@ function once<T extends (...args: never[]) => void>(callback: T): T {
|
|||
function toError(error: unknown) {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
async function sha256Hex(data: Uint8Array) {
|
||||
const digest = await crypto.subtle.digest("SHA-256", toDigestInput(data))
|
||||
|
||||
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
|
||||
function toDigestInput(data: Uint8Array) {
|
||||
return new Uint8Array(data)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue