cross-crypto-ts
Version:
Cifrado híbrido AES-GCM + RSA-OAEP con interoperabilidad entre TypeScript y Python, con diseño compatible para Rust.
132 lines (131 loc) • 5.04 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.canonicalJson = canonicalJson;
exports.canonicalJsonBytes = canonicalJsonBytes;
exports.fingerprintBytes = fingerprintBytes;
exports.fingerprintPublicKey = fingerprintPublicKey;
exports.generateEd25519Keys = generateEd25519Keys;
exports.signPayload = signPayload;
exports.verifyPayload = verifyPayload;
// src/sign.ts
const crypto_1 = __importDefault(require("crypto"));
function pemToArrayBuffer(pem) {
const base64 = pem
.replace(/-----BEGIN [^-]+-----/g, "")
.replace(/-----END [^-]+-----/g, "")
.replace(/\s+/g, "");
const buffer = Buffer.from(base64, "base64");
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
function bytesToBase64(bytes) {
return Buffer.from(bytes).toString("base64");
}
function base64ToArrayBuffer(value) {
const buffer = Buffer.from(value, "base64");
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
function uint8ArrayToArrayBuffer(bytes) {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}
function assertJsonSafe(value) {
if (typeof value === "number" && !Number.isFinite(value)) {
throw new Error("JSON canónico no permite NaN ni Infinity.");
}
if (Array.isArray(value)) {
value.forEach(assertJsonSafe);
return;
}
if (value && typeof value === "object") {
for (const item of Object.values(value)) {
assertJsonSafe(item);
}
}
}
function sortDeep(value) {
if (Array.isArray(value)) {
return value.map(sortDeep);
}
if (value && typeof value === "object") {
const input = value;
const output = {};
for (const key of Object.keys(input).sort()) {
output[key] = sortDeep(input[key]);
}
return output;
}
return value;
}
function canonicalJson(payload) {
assertJsonSafe(payload);
return JSON.stringify(sortDeep(payload));
}
function canonicalJsonBytes(payload) {
return new TextEncoder().encode(canonicalJson(payload));
}
function fingerprintBytes(data) {
const buffer = typeof data === "string"
? Buffer.from(data, "utf8")
: Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data));
return crypto_1.default.createHash("sha256").update(buffer).digest("hex");
}
function fingerprintPublicKey(publicKeyPem) {
return fingerprintBytes(pemToArrayBuffer(publicKeyPem));
}
function generateEd25519Keys() {
const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync("ed25519");
return {
publicKey: publicKey.export({
type: "spki",
format: "pem",
}),
privateKey: privateKey.export({
type: "pkcs8",
format: "pem",
}),
};
}
async function signPayload(payload, privateKeyPem, options = {}) {
const data = canonicalJsonBytes(payload);
const privateKey = await crypto_1.default.webcrypto.subtle.importKey("pkcs8", pemToArrayBuffer(privateKeyPem), { name: "Ed25519" }, false, ["sign"]);
const signature = await crypto_1.default.webcrypto.subtle.sign({ name: "Ed25519" }, privateKey, uint8ArrayToArrayBuffer(data));
return {
alg: "Ed25519",
keyId: options.keyId ?? "v1",
signedAt: Math.floor(options.signedAt ?? Date.now() / 1000),
payloadHash: fingerprintBytes(data),
signature: bytesToBase64(signature),
};
}
async function verifyPayload(payload, signaturePayload, publicKeyPem, options = {}) {
try {
if (signaturePayload.alg !== "Ed25519")
return false;
const required = ["alg", "keyId", "signedAt", "payloadHash", "signature"];
for (const key of required) {
if (!(key in signaturePayload))
return false;
}
const signedAt = Number(signaturePayload.signedAt);
if (!Number.isFinite(signedAt))
return false;
if (options.maxAgeSeconds !== undefined) {
const now = Math.floor(options.now ?? Date.now() / 1000);
if (signedAt > now + 30)
return false;
if (now - signedAt > options.maxAgeSeconds)
return false;
}
const data = canonicalJsonBytes(payload);
const expectedHash = fingerprintBytes(data);
if (signaturePayload.payloadHash !== expectedHash)
return false;
const publicKey = await crypto_1.default.webcrypto.subtle.importKey("spki", pemToArrayBuffer(publicKeyPem), { name: "Ed25519" }, false, ["verify"]);
return await crypto_1.default.webcrypto.subtle.verify({ name: "Ed25519" }, publicKey, base64ToArrayBuffer(String(signaturePayload.signature)), uint8ArrayToArrayBuffer(data));
}
catch {
return false;
}
}