147 lines
5.8 KiB
TypeScript
147 lines
5.8 KiB
TypeScript
export type VerifyResult = {
|
|
ok: boolean;
|
|
errors: string[];
|
|
summary: {
|
|
total: number;
|
|
missing: number;
|
|
integrityMismatches: number;
|
|
sizeMismatches: number;
|
|
bundleHashMismatch: boolean;
|
|
};
|
|
}
|
|
|
|
// Minimal synchronous SHA-256 implementation returning base64 string.
|
|
function sha256Base64(bytes: Uint8Array): string {
|
|
const K = new Uint32Array([
|
|
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
|
])
|
|
const H = new Uint32Array([
|
|
0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19
|
|
])
|
|
const l = bytes.length
|
|
const bitLenHi = Math.floor((l >>> 29))
|
|
const bitLenLo = (l << 3) >>> 0
|
|
const withOne = l + 1
|
|
const padLen = ((withOne % 64) <= 56) ? (56 - (withOne % 64)) : (56 + 64 - (withOne % 64))
|
|
const totalLen = withOne + padLen + 8
|
|
const m = new Uint8Array(totalLen)
|
|
m.set(bytes)
|
|
m[l] = 0x80
|
|
m[totalLen - 8] = (bitLenHi >>> 24) & 0xff
|
|
m[totalLen - 7] = (bitLenHi >>> 16) & 0xff
|
|
m[totalLen - 6] = (bitLenHi >>> 8) & 0xff
|
|
m[totalLen - 5] = (bitLenHi >>> 0) & 0xff
|
|
m[totalLen - 4] = (bitLenLo >>> 24) & 0xff
|
|
m[totalLen - 3] = (bitLenLo >>> 16) & 0xff
|
|
m[totalLen - 2] = (bitLenLo >>> 8) & 0xff
|
|
m[totalLen - 1] = (bitLenLo >>> 0) & 0xff
|
|
const W = new Uint32Array(64)
|
|
for (let i = 0; i < totalLen; i += 64) {
|
|
for (let t = 0; t < 16; t++) {
|
|
const j = i + t * 4
|
|
W[t] = (m[j] << 24) | (m[j + 1] << 16) | (m[j + 2] << 8) | (m[j + 3])
|
|
}
|
|
for (let t = 16; t < 64; t++) {
|
|
const s0 = (rotr(W[t - 15], 7) ^ rotr(W[t - 15], 18) ^ (W[t - 15] >>> 3)) >>> 0
|
|
const s1 = (rotr(W[t - 2], 17) ^ rotr(W[t - 2], 19) ^ (W[t - 2] >>> 10)) >>> 0
|
|
W[t] = (W[t - 16] + s0 + W[t - 7] + s1) >>> 0
|
|
}
|
|
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7]
|
|
for (let t = 0; t < 64; t++) {
|
|
const S1 = (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0
|
|
const ch = ((e & f) ^ (~e & g)) >>> 0
|
|
const temp1 = (h + S1 + ch + K[t] + W[t]) >>> 0
|
|
const S0 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0
|
|
const maj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0
|
|
const temp2 = (S0 + maj) >>> 0
|
|
h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0
|
|
}
|
|
H[0] = (H[0] + a) >>> 0
|
|
H[1] = (H[1] + b) >>> 0
|
|
H[2] = (H[2] + c) >>> 0
|
|
H[3] = (H[3] + d) >>> 0
|
|
H[4] = (H[4] + e) >>> 0
|
|
H[5] = (H[5] + f) >>> 0
|
|
H[6] = (H[6] + g) >>> 0
|
|
H[7] = (H[7] + h) >>> 0
|
|
}
|
|
const out = new Uint8Array(32)
|
|
for (let i = 0; i < 8; i++) {
|
|
out[i*4+0]=(H[i]>>>24)&0xff; out[i*4+1]=(H[i]>>>16)&0xff; out[i*4+2]=(H[i]>>>8)&0xff; out[i*4+3]=(H[i]>>>0)&0xff
|
|
}
|
|
if (typeof Buffer !== 'undefined') return Buffer.from(out).toString('base64')
|
|
let s = ''; for (let i=0;i<out.length;i++) s += String.fromCharCode(out[i])
|
|
const btoaFn = (globalThis as unknown as { btoa?: (s: string) => string }).btoa
|
|
if (!btoaFn) throw new Error('btoa not available')
|
|
return btoaFn(s)
|
|
function rotr(x: number, n: number) { return (x >>> n) | (x << (32 - n)) }
|
|
}
|
|
|
|
export function verifyIntegrity(
|
|
extras: Record<string, Uint8Array>,
|
|
assets: Record<string, Uint8Array>
|
|
): VerifyResult {
|
|
const errors: string[] = []
|
|
const key = 'assets.integrity.index.json'
|
|
if (!Object.prototype.hasOwnProperty.call(extras, key)) {
|
|
return { ok: false, errors: ['Missing assets.integrity.index.json in extras'], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } }
|
|
}
|
|
let parsed: { entries: Array<{ path: string; integrity: string; size?: number }>; bundleHash: string }
|
|
try {
|
|
parsed = JSON.parse(new TextDecoder().decode(extras[key]))
|
|
} catch {
|
|
return { ok: false, errors: ['Invalid assets.integrity.index.json JSON'], summary: { total: 0, missing: 0, integrityMismatches: 0, sizeMismatches: 0, bundleHashMismatch: false } }
|
|
}
|
|
// verify each entry exists and integrity matches
|
|
const recomputedLines: string[] = []
|
|
let missing = 0
|
|
let integrityMismatches = 0
|
|
let sizeMismatches = 0
|
|
for (const e of parsed.entries) {
|
|
const bytes = assets[e.path]
|
|
if (!bytes) {
|
|
errors.push(`Missing asset for path in integrity index: ${e.path}`)
|
|
missing++
|
|
continue
|
|
}
|
|
// optional size check when present in index
|
|
if (typeof e.size === 'number' && e.size !== bytes.length) {
|
|
errors.push(`Size mismatch for ${e.path}: expected ${e.size}, got ${bytes.length}`)
|
|
sizeMismatches++
|
|
}
|
|
const b64 = sha256Base64(bytes)
|
|
const integ = `sha256-${b64}`
|
|
if (integ !== e.integrity) {
|
|
errors.push(`Integrity mismatch for ${e.path}`)
|
|
integrityMismatches++
|
|
}
|
|
recomputedLines.push(e.path)
|
|
recomputedLines.push(integ)
|
|
}
|
|
// recompute bundle hash
|
|
const joined = new TextEncoder().encode(recomputedLines.join('\n'))
|
|
const bundle = `sha256-${sha256Base64(joined)}`
|
|
const bundleHashMismatch = bundle !== parsed.bundleHash
|
|
if (bundleHashMismatch) {
|
|
errors.push('BundleHash mismatch')
|
|
}
|
|
return {
|
|
ok: errors.length === 0,
|
|
errors,
|
|
summary: {
|
|
total: parsed.entries.length,
|
|
missing,
|
|
integrityMismatches,
|
|
sizeMismatches,
|
|
bundleHashMismatch,
|
|
},
|
|
}
|
|
}
|