92 lines
3.4 KiB
JavaScript
92 lines
3.4 KiB
JavaScript
(function (window) {
|
|
"use strict";
|
|
|
|
const keyRequests = new Map();
|
|
const encoder = new TextEncoder();
|
|
|
|
function toBase64Url(bytes) {
|
|
let binary = "";
|
|
const view = new Uint8Array(bytes);
|
|
for (let offset = 0; offset < view.length; offset += 0x8000) {
|
|
binary += String.fromCharCode(...view.subarray(offset, offset + 0x8000));
|
|
}
|
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
}
|
|
|
|
function fromBase64Url(text) {
|
|
const normalized = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
const binary = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
|
|
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
}
|
|
|
|
function fetchKey(keyUrl) {
|
|
if (!keyUrl) {
|
|
return Promise.reject(new Error("A payload encryption key URL is required."));
|
|
}
|
|
if (!keyRequests.has(keyUrl)) {
|
|
const request = fetch(keyUrl, {
|
|
credentials: "same-origin",
|
|
headers: { Accept: "application/json" }
|
|
}).then(async (response) => {
|
|
const body = await response.json();
|
|
if (!response.ok) throw body;
|
|
if (!body.keyId || !body.publicKey) {
|
|
throw new Error("The payload encryption key response is invalid.");
|
|
}
|
|
return body;
|
|
}).catch((error) => {
|
|
keyRequests.delete(keyUrl);
|
|
throw error;
|
|
});
|
|
keyRequests.set(keyUrl, request);
|
|
}
|
|
return keyRequests.get(keyUrl);
|
|
}
|
|
|
|
async function encrypt(payload, options) {
|
|
if (!window.crypto?.subtle) {
|
|
throw new Error("Secure payload encryption is not supported by this browser.");
|
|
}
|
|
const keyInfo = await fetchKey(options?.keyUrl);
|
|
const publicKey = await window.crypto.subtle.importKey(
|
|
"spki",
|
|
fromBase64Url(keyInfo.publicKey),
|
|
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
false,
|
|
["encrypt"]
|
|
);
|
|
const aesKey = await window.crypto.subtle.generateKey(
|
|
{ name: "AES-GCM", length: 256 }, true, ["encrypt"]
|
|
);
|
|
const initializationVector = window.crypto.getRandomValues(new Uint8Array(12));
|
|
const requestId = window.crypto.randomUUID();
|
|
const timestamp = new Date().toISOString();
|
|
const additionalData = encoder.encode(`${keyInfo.keyId}|${requestId}|${timestamp}`);
|
|
const encryptedPayload = await window.crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", iv: initializationVector, additionalData },
|
|
aesKey,
|
|
encoder.encode(JSON.stringify(payload))
|
|
);
|
|
const rawKey = await window.crypto.subtle.exportKey("raw", aesKey);
|
|
const encryptedKey = await window.crypto.subtle.encrypt(
|
|
{ name: "RSA-OAEP" }, publicKey, rawKey
|
|
);
|
|
|
|
return Object.freeze({
|
|
keyId: keyInfo.keyId,
|
|
encryptedKey: toBase64Url(encryptedKey),
|
|
initializationVector: toBase64Url(initializationVector),
|
|
encryptedPayload: toBase64Url(encryptedPayload),
|
|
requestId,
|
|
timestamp
|
|
});
|
|
}
|
|
|
|
function clearKeyCache(keyUrl) {
|
|
if (keyUrl) keyRequests.delete(keyUrl);
|
|
else keyRequests.clear();
|
|
}
|
|
|
|
window.CygnusPayloadCrypto = Object.freeze({ encrypt, clearKeyCache });
|
|
}(window));
|