Add verifiable GVL revision evidence packages
Dieser Commit ist enthalten in:
@@ -0,0 +1,462 @@
|
||||
"use strict";
|
||||
|
||||
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT =
|
||||
"vendorget-gvl-evidence-export";
|
||||
const VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
|
||||
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT =
|
||||
"vendorget-gvl-revision-evidence";
|
||||
const VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION = 1;
|
||||
const VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND = "iab-gvl-revision";
|
||||
|
||||
const VENDORGET_GVL_EVIDENCE_STORE_NAMES = [
|
||||
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||
VENDORGET_STORE_NAMES.gvlVendors,
|
||||
VENDORGET_STORE_NAMES.gvlPurposes,
|
||||
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
|
||||
VENDORGET_STORE_NAMES.gvlFeatures,
|
||||
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
|
||||
VENDORGET_STORE_NAMES.gvlDataCategories,
|
||||
VENDORGET_STORE_NAMES.gvlVendorRelationships
|
||||
];
|
||||
|
||||
const VENDORGET_GVL_EVIDENCE_ARRAY_NAMES = [
|
||||
"gvl_raw_evidence",
|
||||
"gvl_snapshots",
|
||||
"gvl_vendors",
|
||||
"gvl_purposes",
|
||||
"gvl_special_purposes",
|
||||
"gvl_features",
|
||||
"gvl_special_features",
|
||||
"gvl_data_categories",
|
||||
"gvl_vendor_relationships"
|
||||
];
|
||||
|
||||
const VENDORGET_GVL_NORMALIZED_STORE_NAMES = [
|
||||
VENDORGET_STORE_NAMES.gvlVendors,
|
||||
VENDORGET_STORE_NAMES.gvlPurposes,
|
||||
VENDORGET_STORE_NAMES.gvlSpecialPurposes,
|
||||
VENDORGET_STORE_NAMES.gvlFeatures,
|
||||
VENDORGET_STORE_NAMES.gvlSpecialFeatures,
|
||||
VENDORGET_STORE_NAMES.gvlDataCategories,
|
||||
VENDORGET_STORE_NAMES.gvlVendorRelationships
|
||||
];
|
||||
|
||||
async function exportVendorGetGvlEvidenceJson() {
|
||||
const db = await openVendorGetDb();
|
||||
const exportedStores = await readGvlEvidenceStores(db);
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
exportFormat: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT,
|
||||
exportFormatVersion: VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
dbName: db.name,
|
||||
dbVersion: db.version
|
||||
},
|
||||
...exportedStores
|
||||
};
|
||||
}
|
||||
|
||||
async function exportVendorGetGvlRevisionEvidenceJson(snapshotSha256) {
|
||||
if (!snapshotSha256) {
|
||||
throw new Error("missing_snapshot_sha256");
|
||||
}
|
||||
|
||||
const db = await openVendorGetDb();
|
||||
const snapshot = await getGvlEvidenceRecordByKey(
|
||||
db,
|
||||
VENDORGET_STORE_NAMES.gvlSnapshots,
|
||||
snapshotSha256
|
||||
);
|
||||
|
||||
if (!snapshot) {
|
||||
throw new Error("gvl_snapshot_not_found");
|
||||
}
|
||||
|
||||
const rawGvlSha256 = snapshot.rawGvlSha256 ?? null;
|
||||
const rawEvidence = rawGvlSha256
|
||||
? await getGvlEvidenceRecordByKey(
|
||||
db,
|
||||
VENDORGET_STORE_NAMES.gvlRawEvidence,
|
||||
rawGvlSha256
|
||||
)
|
||||
: null;
|
||||
const normalized = await readGvlRevisionNormalizedRecords(db, snapshotSha256);
|
||||
const exportedAt = new Date();
|
||||
const exportedAtUtcCompact = formatGvlEvidenceUtcCompact(exportedAt);
|
||||
const payload = {
|
||||
metadata: {
|
||||
exportFormat: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT,
|
||||
exportFormatVersion: VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION,
|
||||
exportedAt: exportedAt.toISOString(),
|
||||
exportedAtUtcCompact,
|
||||
dbName: db.name,
|
||||
dbVersion: db.version,
|
||||
vendorListVersion: snapshot.vendorListVersion ?? null,
|
||||
snapshotSha256: snapshot.sha256 ?? snapshot.snapshotSha256 ?? null,
|
||||
rawGvlSha256,
|
||||
contentKind: VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND
|
||||
},
|
||||
rawEvidence,
|
||||
snapshot,
|
||||
normalized
|
||||
};
|
||||
const exportPayloadSha256 = await calculateGvlEvidencePayloadSha256(payload);
|
||||
|
||||
return {
|
||||
...payload,
|
||||
metadata: {
|
||||
...payload.metadata,
|
||||
exportPayloadSha256
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyVendorGetGvlRevisionEvidenceJson(exportContainer) {
|
||||
const errors = [];
|
||||
const metadata = exportContainer?.metadata ?? {};
|
||||
const normalized = exportContainer?.normalized ?? {};
|
||||
const rawBody = exportContainer?.rawEvidence?.rawBody ?? null;
|
||||
const snapshot = exportContainer?.snapshot ?? null;
|
||||
const snapshotSha256 = metadata.snapshotSha256 ?? null;
|
||||
const rawGvlSha256 = metadata.rawGvlSha256 ?? null;
|
||||
|
||||
if (!exportContainer || typeof exportContainer !== "object") {
|
||||
errors.push("invalid_export_container");
|
||||
}
|
||||
|
||||
if (metadata.exportFormat !== VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT) {
|
||||
errors.push("invalid_export_format");
|
||||
}
|
||||
|
||||
if (
|
||||
metadata.exportFormatVersion !==
|
||||
VENDORGET_GVL_REVISION_EVIDENCE_EXPORT_FORMAT_VERSION
|
||||
) {
|
||||
errors.push("unsupported_export_format_version");
|
||||
}
|
||||
|
||||
if (metadata.contentKind !== VENDORGET_GVL_REVISION_EVIDENCE_CONTENT_KIND) {
|
||||
errors.push("invalid_content_kind");
|
||||
}
|
||||
|
||||
if (metadata.vendorListVersion === null || metadata.vendorListVersion === undefined) {
|
||||
errors.push("missing_vendor_list_version");
|
||||
}
|
||||
|
||||
if (!snapshotSha256) {
|
||||
errors.push("missing_snapshot_sha256");
|
||||
}
|
||||
|
||||
if (!rawGvlSha256) {
|
||||
errors.push("missing_raw_gvl_sha256");
|
||||
}
|
||||
|
||||
if (typeof rawBody !== "string") {
|
||||
errors.push("missing_raw_body");
|
||||
} else if ((await VendorGetGvlService.calculateRawGvlSha256(rawBody)) !== rawGvlSha256) {
|
||||
errors.push("raw_body_sha256_mismatch");
|
||||
}
|
||||
|
||||
const snapshotRecordSha256 = snapshot?.sha256 ?? snapshot?.snapshotSha256 ?? null;
|
||||
|
||||
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
||||
errors.push("missing_snapshot");
|
||||
} else if (snapshotRecordSha256 !== snapshotSha256) {
|
||||
errors.push("snapshot_sha256_mismatch");
|
||||
} else if (
|
||||
(await VendorGetGvlService.calculateGvlSnapshotSha256(snapshot.rawJson)) !==
|
||||
snapshotSha256
|
||||
) {
|
||||
errors.push("snapshot_sha256_mismatch");
|
||||
}
|
||||
|
||||
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
|
||||
if (!Array.isArray(normalized[storeName])) {
|
||||
errors.push(`missing_normalized_store_${storeName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const record of normalized[storeName]) {
|
||||
if (record?.snapshotSha256 !== snapshotSha256) {
|
||||
errors.push(`normalized_record_snapshot_mismatch_${storeName}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.exportPayloadSha256) {
|
||||
errors.push("missing_export_payload_sha256");
|
||||
} else {
|
||||
const recalculatedPayloadSha256 =
|
||||
await calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(exportContainer);
|
||||
|
||||
if (recalculatedPayloadSha256 !== metadata.exportPayloadSha256) {
|
||||
errors.push("export_payload_sha256_mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
vendorListVersion: metadata.vendorListVersion ?? null,
|
||||
snapshotSha256,
|
||||
rawGvlSha256,
|
||||
exportPayloadSha256: metadata.exportPayloadSha256 ?? null,
|
||||
normalizedCounts: countGvlRevisionNormalizedRecords(normalized)
|
||||
};
|
||||
}
|
||||
|
||||
function getGvlEvidenceRecordByKey(db, storeName, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction([storeName], "readonly");
|
||||
const getRequest = tx.objectStore(storeName).get(key);
|
||||
let record = null;
|
||||
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
getRequest.onsuccess = () => {
|
||||
record = getRequest.result ?? null;
|
||||
};
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(record);
|
||||
});
|
||||
}
|
||||
|
||||
function readGvlRevisionNormalizedRecords(db, snapshotSha256) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const normalized = {};
|
||||
const tx = db.transaction(VENDORGET_GVL_NORMALIZED_STORE_NAMES, "readonly");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(normalized);
|
||||
|
||||
for (const storeName of VENDORGET_GVL_NORMALIZED_STORE_NAMES) {
|
||||
const records = [];
|
||||
const cursorRequest = tx
|
||||
.objectStore(storeName)
|
||||
.index("snapshotSha256")
|
||||
.openCursor(IDBKeyRange.only(snapshotSha256));
|
||||
|
||||
normalized[storeName] = records;
|
||||
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||
cursorRequest.onsuccess = () => {
|
||||
const cursor = cursorRequest.result;
|
||||
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.push(cursor.value);
|
||||
cursor.continue();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function calculateGvlEvidencePayloadSha256(payload) {
|
||||
return sha256Hex(stableStringify(payload));
|
||||
}
|
||||
|
||||
async function calculateGvlEvidencePayloadSha256WithoutEmbeddedHash(
|
||||
exportContainer
|
||||
) {
|
||||
const metadata = {
|
||||
...(exportContainer?.metadata ?? {})
|
||||
};
|
||||
|
||||
delete metadata.exportPayloadSha256;
|
||||
|
||||
return calculateGvlEvidencePayloadSha256({
|
||||
...exportContainer,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
function countGvlRevisionNormalizedRecords(normalized) {
|
||||
return Object.fromEntries(
|
||||
VENDORGET_GVL_NORMALIZED_STORE_NAMES.map((storeName) => [
|
||||
storeName,
|
||||
Array.isArray(normalized?.[storeName]) ? normalized[storeName].length : 0
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function formatGvlEvidenceUtcCompact(date) {
|
||||
return [
|
||||
date.getUTCFullYear(),
|
||||
padGvlEvidenceDatePart(date.getUTCMonth() + 1),
|
||||
padGvlEvidenceDatePart(date.getUTCDate()),
|
||||
"T",
|
||||
padGvlEvidenceDatePart(date.getUTCHours()),
|
||||
padGvlEvidenceDatePart(date.getUTCMinutes()),
|
||||
padGvlEvidenceDatePart(date.getUTCSeconds()),
|
||||
"Z"
|
||||
].join("");
|
||||
}
|
||||
|
||||
function padGvlEvidenceDatePart(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function readGvlEvidenceStores(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const exportContainer = {};
|
||||
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readonly");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => resolve(exportContainer);
|
||||
|
||||
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||
const records = [];
|
||||
const cursorRequest = tx.objectStore(storeName).openCursor();
|
||||
|
||||
exportContainer[storeName] = records;
|
||||
|
||||
cursorRequest.onerror = () => reject(cursorRequest.error);
|
||||
cursorRequest.onsuccess = () => {
|
||||
const cursor = cursorRequest.result;
|
||||
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
records.push(cursor.value);
|
||||
cursor.continue();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function importVendorGetGvlEvidenceJson(importContainer) {
|
||||
validateVendorGetGvlEvidenceImport(importContainer);
|
||||
|
||||
const db = await openVendorGetDb();
|
||||
|
||||
return importGvlEvidenceStores(db, importContainer);
|
||||
}
|
||||
|
||||
function validateVendorGetGvlEvidenceImport(importContainer) {
|
||||
if (!importContainer || typeof importContainer !== "object") {
|
||||
throw new Error("invalid_gvl_evidence_export");
|
||||
}
|
||||
|
||||
const metadata = importContainer.metadata ?? {};
|
||||
|
||||
if (metadata.exportFormat !== VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT) {
|
||||
throw new Error("invalid_gvl_evidence_export_format");
|
||||
}
|
||||
|
||||
if (
|
||||
metadata.exportFormatVersion !==
|
||||
VENDORGET_GVL_EVIDENCE_EXPORT_FORMAT_VERSION
|
||||
) {
|
||||
throw new Error("unsupported_gvl_evidence_export_version");
|
||||
}
|
||||
|
||||
for (const arrayName of VENDORGET_GVL_EVIDENCE_ARRAY_NAMES) {
|
||||
if (!Array.isArray(importContainer[arrayName])) {
|
||||
throw new Error(`missing_gvl_evidence_store_${arrayName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importGvlEvidenceStores(db, importContainer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const counts = buildEmptyGvlEvidenceImportCounts();
|
||||
const seenKeysByStore = new Map();
|
||||
const tx = db.transaction(VENDORGET_GVL_EVIDENCE_STORE_NAMES, "readwrite");
|
||||
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.oncomplete = () => {
|
||||
resolve({
|
||||
importedAt: new Date().toISOString(),
|
||||
counts
|
||||
});
|
||||
};
|
||||
|
||||
for (const storeName of VENDORGET_GVL_EVIDENCE_STORE_NAMES) {
|
||||
importGvlEvidenceStoreRecords(
|
||||
tx.objectStore(storeName),
|
||||
importContainer[storeName],
|
||||
counts[storeName],
|
||||
getSeenKeysForStore(seenKeysByStore, storeName)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildEmptyGvlEvidenceImportCounts() {
|
||||
return Object.fromEntries(
|
||||
VENDORGET_GVL_EVIDENCE_STORE_NAMES.map((storeName) => [
|
||||
storeName,
|
||||
{
|
||||
read: 0,
|
||||
inserted: 0,
|
||||
skippedExisting: 0,
|
||||
skippedInvalid: 0
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function getSeenKeysForStore(seenKeysByStore, storeName) {
|
||||
if (!seenKeysByStore.has(storeName)) {
|
||||
seenKeysByStore.set(storeName, new Set());
|
||||
}
|
||||
|
||||
return seenKeysByStore.get(storeName);
|
||||
}
|
||||
|
||||
function importGvlEvidenceStoreRecords(objectStore, records, counts, seenKeys) {
|
||||
for (const record of records) {
|
||||
counts.read += 1;
|
||||
|
||||
const key = getGvlEvidenceRecordKey(objectStore, record);
|
||||
const keySignature = JSON.stringify(key);
|
||||
|
||||
if (key === null || seenKeys.has(keySignature)) {
|
||||
counts.skippedInvalid += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
seenKeys.add(keySignature);
|
||||
|
||||
const getRequest = objectStore.get(key);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
if (getRequest.result !== undefined) {
|
||||
counts.skippedExisting += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const addRequest = objectStore.add(record);
|
||||
|
||||
addRequest.onsuccess = () => {
|
||||
counts.inserted += 1;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getGvlEvidenceRecordKey(objectStore, record) {
|
||||
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (objectStore.name === VENDORGET_STORE_NAMES.gvlSnapshots) {
|
||||
return record.sha256 ?? record.snapshotSha256 ?? null;
|
||||
}
|
||||
|
||||
const keyPath = objectStore.keyPath;
|
||||
|
||||
if (typeof keyPath !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return record[keyPath] ?? null;
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren