233 lines
21 KiB
JavaScript
233 lines
21 KiB
JavaScript
// AERE Identity, Travel Rule post-cuantic (roadmap master punctul 13, pista B, 2026-09-30): datele initiatorului si ale
|
|
// beneficiarului unui transfer (un obiect IVMS101) trimise de VASP-ul care plateste VASP-ului care primeste, CIFRATE numai pentru el
|
|
// (KEM hibrid X25519 + ML-KEM-768, ambele secrete cerute, HKDF-SHA256, AES-256-GCM), SEMNATE de initiator (Ed25519 + ML-DSA-65), legate
|
|
// de transfer (lant, activ, suma, adresa, tranzactia) si CONFIRMATE semnat de beneficiar. Fiecare parte dovedeste ca e VASP cu o
|
|
// prezentare a unui credential AERE Identity emis de un registru in care celalalt are incredere, legata de el (public = id-ul lui).
|
|
//
|
|
// Drumul: (1) beneficiarul leaga cheile lui KEM de identitatea lui (`bindKemKeys`, semnat) si le da impreuna cu o prezentare a
|
|
// credentialului lui de VASP; (2) initiatorul le verifica (`acceptBeneficiary`) si sigileaza mesajul (`sealMessage`), cu prezentarea
|
|
// lui atasata; (3) beneficiarul il deschide (`openMessage`): semnatura, VASP-ul initiatorului, destinatarul, prospetimea, reluarea,
|
|
// apoi descifrarea; (4) confirmarea semnata (`acknowledge` / `verifyReceipt`); (5) inregistrarea fara date personale (plic AIP-23
|
|
// `compliance`, `travelRuleRecord`).
|
|
//
|
|
// Ce NU face: nu valideaza schema IVMS101 (poarta obiectul asa cum e dat, cere doar `originator` si `beneficiary`); nu descopera VASP-ul
|
|
// beneficiarului dupa o adresa (asta e treaba unui protocol de descoperire); nu spune ca un transfer e legal, ci ca datele au ajuns,
|
|
// cifrate, la VASP-ul numit si ca acesta a confirmat. Momentele sunt ale celor care semneaza, judecate pe ceasul celui care verifica.
|
|
//
|
|
// REGULILE NUMITE (2026-10-18, pentru suita de conformitate tools/aere-conformance): fiecare refuz al celor trei verificatoare
|
|
// (`acceptBeneficiary`, `openMessage`, `verifyReceipt`) numeste regula lui, din REGULI; fiecare verificare e un rand { rule, pass, detail }
|
|
// si poarta marcajul /*PAZA:<regula>*/ pe conditia ei BUNA (controlul negativ al suitei o face adevarata intr-o copie). Rezultatul poarta,
|
|
// pe langa `ok` si `motive` (textele de ieri, neschimbate), `rules` (id-urile refuzate, sortate) si `rows`. Prezentarea de VASP e judecata
|
|
// de verificatorul de identitate (identity.mjs, inrolat cu regulile lui); aici refuzurile ei poarta o singura regula, `vasp-presentation`.
|
|
// Verificarile antetului unui mesaj se fac toate; la orice refuz al lor mesajul NU se mai deschide (incapsularea si cifrul nu se judeca).
|
|
import crypto from 'node:crypto';
|
|
import { canonical, signText, verifyText, verifyPresentation, idOf, publicKeyObjects } from './identity.mjs';
|
|
|
|
export const KEM_ALG = 'x25519+ml-kem-768';
|
|
/** regulile pe care le poate numi un refuz (binding-*: acceptBeneficiary; vasp-*: prezentarea de VASP, in amandoua; message-*, replay*, kem-*, ciphertext: openMessage; receipt-*: verifyReceipt) */
|
|
export const REGULI = ['binding-form', 'binding-id', 'binding-signature', 'binding-window', 'vasp-presentation', 'vasp-status', 'vasp-identity', 'vasp-claim',
|
|
'message-form', 'message-id', 'message-signature', 'message-recipient', 'message-kem-keys', 'message-freshness', 'replay-store', 'replay', 'kem-alg', 'kem-open', 'ciphertext',
|
|
'receipt-form', 'receipt-from', 'receipt-signature', 'receipt-message'];
|
|
/** regulile dupa al caror refuz verificatorul s-a oprit (ce nu poate citi): celelalte verificari nu s-au mai facut */
|
|
export const OPRIRI = ['binding-form', 'message-form', 'receipt-form'];
|
|
// randurile unei verificari: ok(regula, conditia buna, motivul) -> conditia; gata() -> { ok, motive, rules, rows }
|
|
function colector() {
|
|
const rows = [];
|
|
const ok = (rule, pass, detail = '') => { if (!REGULI.includes(rule)) throw new Error('travel rule: undeclared rule ' + rule); rows.push({ rule, pass: !!pass, detail: pass ? '' : detail }); return !!pass; };
|
|
const gata = (extra = {}) => { const rele = rows.filter((r) => !r.pass); return { ok: !rele.length, motive: rele.map((r) => r.detail), rules: [...new Set(rele.map((r) => r.rule))].sort(), rows, ...extra }; };
|
|
return { ok, gata, rows };
|
|
}
|
|
const sha = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex');
|
|
const b64 = (b) => Buffer.from(b).toString('base64');
|
|
const SPKI_X25519 = Buffer.from('302a300506032b656e032100', 'hex');
|
|
const DATA = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
|
|
const timp = (s, ce) => { if (typeof s !== 'string' || !DATA.test(s) || Number.isNaN(Date.parse(s))) throw new Error(`travel rule: ${ce} is not an RFC 3339 UTC time`); return Date.parse(s); };
|
|
|
|
function cheieKem(b64s, tip) {
|
|
if (typeof b64s !== 'string') throw new Error(`travel rule: the ${tip} public key is missing`);
|
|
const k = crypto.createPublicKey({ key: Buffer.from(b64s, 'base64'), format: 'der', type: 'spki' });
|
|
if (k.asymmetricKeyType !== tip) throw new Error(`travel rule: the ${tip} public key is a ${k.asymmetricKeyType} key`);
|
|
if (k.export({ type: 'spki', format: 'der' }).toString('base64') !== b64s) throw new Error(`travel rule: the ${tip} public key is not in its canonical form`);
|
|
return k;
|
|
}
|
|
function kemPublicObjects(pub) {
|
|
if (!pub || pub.alg !== KEM_ALG || Object.keys(pub).some((k) => !['alg', 'x25519', 'mlkem768'].includes(k))) throw new Error(`travel rule: KEM public keys must be ${KEM_ALG}`);
|
|
return { x25519: cheieKem(pub.x25519, 'x25519'), mlkem768: cheieKem(pub.mlkem768, 'ml-kem-768') };
|
|
}
|
|
/** O pereche KEM hibrida noua (partea privata nu e enumerabila). */
|
|
export function generateKemKeys() {
|
|
const x = crypto.generateKeyPairSync('x25519'), m = crypto.generateKeyPairSync('ml-kem-768');
|
|
const k = { public: { alg: KEM_ALG, x25519: x.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'), mlkem768: m.publicKey.export({ type: 'spki', format: 'der' }).toString('base64') } };
|
|
Object.defineProperty(k, 'privat', { value: { x25519: x.privateKey, mlkem768: m.privateKey }, enumerable: false });
|
|
return k;
|
|
}
|
|
export function exportKemKeys(k) {
|
|
return { v: 1, kind: 'aere-travel-rule-kem-keys', public: k.public, private: { x25519: k.privat.x25519.export({ type: 'pkcs8', format: 'der' }).toString('base64'), mlkem768: k.privat.mlkem768.export({ type: 'pkcs8', format: 'der' }).toString('base64') } };
|
|
}
|
|
export function importKemKeys(j) {
|
|
if (!j || j.kind !== 'aere-travel-rule-kem-keys') throw new Error('travel rule: not an aere-travel-rule-kem-keys file');
|
|
const x = crypto.createPrivateKey({ key: Buffer.from(String(j.private.x25519), 'base64'), format: 'der', type: 'pkcs8' });
|
|
const m = crypto.createPrivateKey({ key: Buffer.from(String(j.private.mlkem768), 'base64'), format: 'der', type: 'pkcs8' });
|
|
const pub = { alg: KEM_ALG, x25519: crypto.createPublicKey(x).export({ type: 'spki', format: 'der' }).toString('base64'), mlkem768: crypto.createPublicKey(m).export({ type: 'spki', format: 'der' }).toString('base64') };
|
|
if (canonical(pub) !== canonical(j.public)) throw new Error('travel rule: the public KEM keys in the file are not those of its private keys');
|
|
const k = { public: pub }; Object.defineProperty(k, 'privat', { value: { x25519: x, mlkem768: m }, enumerable: false }); return k;
|
|
}
|
|
|
|
/** VASP-ul isi leaga cheile KEM de identitatea lui, semnat, cu o fereastra de valabilitate. */
|
|
export function bindKemKeys({ vasp, kem, validUntil, now = new Date() }) {
|
|
kemPublicObjects(kem.public || kem);
|
|
const statement = { v: 1, kind: 'aere-travel-rule-kem-binding', vasp: { id: vasp.id, keys: vasp.public }, kem: kem.public || kem, validFrom: new Date(now).toISOString(), validUntil };
|
|
timp(validUntil, 'validUntil');
|
|
return { statement, signature: signText('kem-binding', canonical(statement), vasp) };
|
|
}
|
|
|
|
// prezentarea credentialului de VASP: valida, de la un registru de incredere, facuta pentru `audience`, cu `nonce`, a detinatorului `id`,
|
|
// cu afirmatia vasp:true si starea judecata (un VASP revocat nu mai primeste date). `ok` e colectorul celui care cheama; `prefix` sta in
|
|
// fata fiecarui motiv (openMessage scrie 'originator VASP: ')
|
|
function eVasp(pres, { registries, audience, nonce, id, now, statusLists, maxAgeS }, ok, prefix = '') {
|
|
const v = verifyPresentation(pres, { audience, nonce, now, trustedIssuers: registries, statusLists, maxAgeS });
|
|
// 2026-10-18, gasit de randul intrarilor malformate al suitei de conformitate: fara publicul sau nonce-ul verificatorului (originatorId,
|
|
// nonce lipsa) verifyPresentation le lasa NEJUDECATE si ramane valid, iar beneficiarul iesea acceptat cu o prezentare reluabila; aici
|
|
// o prezentare al carei public sau nonce nu a fost judecat e refuzata, nu primita pe incredere
|
|
const nejudecate = v.rows.filter((r) => r.pass === null && (r.unjudged === 'audience' || r.unjudged === 'nonce'));
|
|
if (!((v.valid && nejudecate.length === 0) /*PAZA:vasp-presentation*/)) { for (const r of [...v.rows.filter((r) => r.pass === false), ...nejudecate]) ok('vasp-presentation', false, prefix + r.name + (r.detail ? ' (' + r.detail + ')' : '')); }
|
|
else ok('vasp-presentation', true);
|
|
const stare = v.rows.find((r) => /^credential: (not revoked|status)/.test(r.name));
|
|
ok('vasp-status', !!(stare && stare.pass === true) /*PAZA:vasp-status*/, prefix + 'the VASP credential status is not judged: ' + (stare ? stare.detail || 'not judged' : 'no status row'));
|
|
const holder = pres && pres.credential && pres.credential.statement && pres.credential.statement.holder;
|
|
ok('vasp-identity', !!(holder && holder.id === id) /*PAZA:vasp-identity*/, prefix + `the VASP credential belongs to ${holder && holder.id}, not to ${id}`);
|
|
ok('vasp-claim', (!v.valid || !!(v.claims && v.claims.vasp === true)) /*PAZA:vasp-claim*/, prefix + 'the credential does not say vasp: true');
|
|
return { claims: v.valid ? v.claims : null };
|
|
}
|
|
|
|
/**
|
|
* Initiatorul accepta un beneficiar: legarea cheilor KEM semnata de identitatea lui, valabila acum, si prezentarea credentialului lui
|
|
* de VASP de la un registru de incredere, facuta pentru initiator (`audience` = id-ul initiatorului) cu nonce-ul lui, a ACELEIASI
|
|
* identitati. Intoarce { ok, motive, rules, rows, beneficiary: { id, keys, kem, claims } }.
|
|
*/
|
|
export function acceptBeneficiary({ binding, presentation, registries, originatorId, nonce, now = new Date(), statusLists = [], maxAgeS = 300 }) {
|
|
const C = colector(); const acum = new Date(now).getTime();
|
|
const S = binding && binding.statement;
|
|
try {
|
|
if (!((!!S && S.kind === 'aere-travel-rule-kem-binding') /*PAZA:binding-form*/)) throw new Error('not an aere-travel-rule-kem-binding');
|
|
C.ok('binding-id', idOf(S.vasp.keys) === S.vasp.id /*PAZA:binding-id*/, 'the binding names an id not derived from its keys');
|
|
C.ok('binding-signature', verifyText('kem-binding', canonical(S), binding.signature, S.vasp.keys) /*PAZA:binding-signature*/, 'the binding is not signed by the VASP it names');
|
|
kemPublicObjects(S.kem);
|
|
C.ok('binding-window', (timp(S.validFrom, 'validFrom') <= acum && acum <= timp(S.validUntil, 'validUntil')) /*PAZA:binding-window*/, `the binding is valid from ${S.validFrom} until ${S.validUntil}`);
|
|
} catch (e) { if (!(false /*PAZA:binding-form*/)) C.ok('binding-form', false, 'the binding cannot be read: ' + e.message); return C.gata(); }
|
|
const v = eVasp(presentation, { registries, audience: originatorId, nonce, id: S.vasp.id, now, statusLists, maxAgeS }, C.ok);
|
|
return C.gata({ beneficiary: { id: S.vasp.id, keys: S.vasp.keys, kem: S.kem, claims: v.claims } });
|
|
}
|
|
|
|
const TRANSFER = ['chainId', 'asset', 'amount', 'beneficiaryAddress'];
|
|
function normalTransfer(t) {
|
|
if (!t || typeof t !== 'object') throw new Error('travel rule: a transfer is required');
|
|
for (const k of TRANSFER) if (t[k] == null || t[k] === '') throw new Error(`travel rule: transfer.${k} is required`);
|
|
const extra = Object.keys(t).filter((k) => ![...TRANSFER, 'txHash'].includes(k));
|
|
if (extra.length) throw new Error('travel rule: unknown transfer field ' + extra.join(', '));
|
|
if (!/^[0-9]+$/.test(String(t.amount))) throw new Error('travel rule: transfer.amount is a decimal integer in the asset\'s smallest unit');
|
|
return { chainId: Number(t.chainId), asset: String(t.asset), amount: String(t.amount), beneficiaryAddress: String(t.beneficiaryAddress), txHash: t.txHash ? String(t.txHash) : null };
|
|
}
|
|
// antetul mesajului fara cifru si semnatura: e AAD-ul cifrului si, cu cifrul, textul semnat
|
|
function antet(m) { const { ciphertext, signature, fromPresentation, ...h } = m; return h; }
|
|
function derive(ssK, ssX, transcript) {
|
|
const ikm = Buffer.concat([ssK, ssX]);
|
|
const okm = Buffer.from(crypto.hkdfSync('sha256', ikm, crypto.createHash('sha256').update(transcript).digest(), Buffer.from('aere-travel-rule/v1/aes-256-gcm'), 44));
|
|
ikm.fill(0); return { key: okm.subarray(0, 32), iv: okm.subarray(32, 44) };
|
|
}
|
|
|
|
/**
|
|
* Initiatorul sigileaza datele IVMS101 pentru beneficiarul acceptat. `presentation` e prezentarea credentialului lui de VASP, facuta
|
|
* pentru beneficiar (audience = id-ul beneficiarului) cu nonce = id-ul mesajului (`messageId`, dat sau generat).
|
|
*/
|
|
export function sealMessage({ from, beneficiary, ivms101, transfer, presentation, messageId = 'urn:uuid:' + crypto.randomUUID(), now = new Date() }) {
|
|
if (!from || !from.privat) throw new Error('travel rule: sealing needs the originator VASP\'s private keys');
|
|
if (!ivms101 || typeof ivms101 !== 'object' || !ivms101.originator || !ivms101.beneficiary) throw new Error('travel rule: the IVMS101 object needs originator and beneficiary');
|
|
const tr = normalTransfer(transfer); const k = kemPublicObjects(beneficiary.kem);
|
|
const eph = crypto.generateKeyPairSync('x25519');
|
|
const ePub = eph.publicKey.export({ type: 'spki', format: 'der' }).subarray(SPKI_X25519.length);
|
|
const ssX = crypto.diffieHellman({ privateKey: eph.privateKey, publicKey: k.x25519 });
|
|
const { sharedKey: ssK, ciphertext: ctK } = crypto.encapsulate(k.mlkem768);
|
|
const h = { v: 1, kind: 'aere-travel-rule-message', id: messageId, from: { id: from.id, keys: from.public }, to: { id: beneficiary.id, kem: beneficiary.kem },
|
|
transfer: tr, createdAt: new Date(now).toISOString(), kem: { alg: KEM_ALG, ephemeralX25519: b64(ePub), mlkemCiphertext: b64(ctK) } };
|
|
const aad = Buffer.from(canonical(h), 'utf8');
|
|
const d = derive(ssK, ssX, aad); ssX.fill(0); ssK.fill(0);
|
|
const c = crypto.createCipheriv('aes-256-gcm', d.key, d.iv); c.setAAD(aad);
|
|
const ct = Buffer.concat([c.update(Buffer.from(canonical(ivms101), 'utf8')), c.final(), c.getAuthTag()]);
|
|
d.key.fill(0);
|
|
const m = { ...h, ciphertext: b64(ct) };
|
|
return { ...m, fromPresentation: presentation, signature: signText('travel-rule', canonical(m), from) };
|
|
}
|
|
|
|
/**
|
|
* Beneficiarul deschide un mesaj: semnatura initiatorului, VASP-ul lui (prezentare pentru beneficiar, nonce = id-ul mesajului),
|
|
* destinatarul (cheile KEM ale beneficiarului), prospetimea, reluarea (`seen`: id-urile deja primite), apoi descifrarea.
|
|
* B-26 (2026-09-30, revizuirea adversariala): `seen` e OBLIGATORIU (un Set sau orice obiect cu has/add care tine id-urile cel putin
|
|
* maxAgeS); fara el, acelasi mesaj semnat si sigilat se deschidea de oricate ori, deci reluarea era prinsa numai de cine stia sa ceara.
|
|
* Intoarce { ok, motive, rules, rows, ivms101, transfer, from, messageHash }. Un ceas al verificatorului care nu se poate citi e refuz
|
|
* (`message-freshness`), nu un NaN care trece.
|
|
*/
|
|
export function openMessage(m, { me, kem, registries, now = new Date(), statusLists = [], seen = null, maxAgeS = 300 }) {
|
|
const C = colector(); const acum = new Date(now).getTime();
|
|
try {
|
|
if (!((!!m && m.kind === 'aere-travel-rule-message' && m.v === 1) /*PAZA:message-form*/)) throw new Error('not an aere-travel-rule-message');
|
|
const semnat = { ...antet(m), ciphertext: m.ciphertext };
|
|
C.ok('message-id', idOf(m.from.keys) === m.from.id /*PAZA:message-id*/, 'the message names an originator id not derived from its keys');
|
|
C.ok('message-signature', verifyText('travel-rule', canonical(semnat), m.signature, m.from.keys) /*PAZA:message-signature*/, 'the message is not signed by the originator it names');
|
|
C.ok('message-recipient', m.to.id === me /*PAZA:message-recipient*/, `the message is for ${m.to.id}, not for ${me}`);
|
|
C.ok('message-kem-keys', canonical(m.to.kem) === canonical(kem.public) /*PAZA:message-kem-keys*/, 'the message is sealed to other KEM keys');
|
|
C.ok('message-freshness', (Number.isFinite(acum) && Math.abs(acum - timp(m.createdAt, 'createdAt')) <= maxAgeS * 1000) /*PAZA:message-freshness*/, `the message was made at ${m.createdAt}, outside ${maxAgeS} s of this clock`);
|
|
if (C.ok('replay-store', (!!seen && typeof seen.has === 'function' && typeof seen.add === 'function') /*PAZA:replay-store*/, 'no replay store was given (seen: a set of the message ids already received, kept at least maxAgeS)'))
|
|
C.ok('replay', !seen.has(m.id) /*PAZA:replay*/, 'replay: this message id was received before');
|
|
const v = eVasp(m.fromPresentation, { registries, audience: me, nonce: m.id, id: m.from.id, now, statusLists, maxAgeS }, C.ok, 'originator VASP: ');
|
|
if (!C.gata().ok) return C.gata();
|
|
const ePub = Buffer.from(m.kem.ephemeralX25519, 'base64'), ctK = Buffer.from(m.kem.mlkemCiphertext, 'base64');
|
|
if (!C.ok('kem-alg', (m.kem.alg === KEM_ALG && ePub.length === 32) /*PAZA:kem-alg*/, 'the key encapsulation is not ' + KEM_ALG)) return C.gata();
|
|
let ssX, ssK;
|
|
try {
|
|
const { x25519: skX, mlkem768: skK } = kem.privat;
|
|
ssX = crypto.diffieHellman({ privateKey: skX, publicKey: crypto.createPublicKey({ key: Buffer.concat([SPKI_X25519, ePub]), format: 'der', type: 'spki' }) });
|
|
ssK = crypto.decapsulate(skK, ctK);
|
|
} catch { if (!(false /*PAZA:kem-open*/)) { C.ok('kem-open', false, 'the key encapsulation does not open with these keys'); return C.gata(); } }
|
|
const aad = Buffer.from(canonical(antet(m)), 'utf8');
|
|
const d = derive(ssK, ssX, aad); ssX.fill(0); ssK.fill(0);
|
|
const ct = Buffer.from(m.ciphertext, 'base64');
|
|
let pt;
|
|
try { const dc = crypto.createDecipheriv('aes-256-gcm', d.key, d.iv); dc.setAAD(aad); dc.setAuthTag(ct.subarray(ct.length - 16)); pt = Buffer.concat([dc.update(ct.subarray(0, ct.length - 16)), dc.final()]); }
|
|
catch { if (!(false /*PAZA:ciphertext*/)) { C.ok('ciphertext', false, 'the ciphertext or its header was modified, or it is not for these keys'); return C.gata(); } }
|
|
finally { d.key.fill(0); }
|
|
C.ok('kem-open', true); C.ok('ciphertext', true);
|
|
seen.add(m.id);
|
|
return C.gata({ ivms101: JSON.parse(pt.toString('utf8')), transfer: m.transfer, from: { id: m.from.id, claims: v.claims }, messageHash: sha(Buffer.from(canonical(semnat), 'utf8')), payloadHash: sha(pt) });
|
|
} catch (e) { if (!(false /*PAZA:message-form*/)) C.ok('message-form', false, 'the message cannot be read: ' + e.message); return C.gata(); }
|
|
}
|
|
|
|
/** Confirmarea semnata a beneficiarului: ce mesaj (hash), ce continut (hash-ul textului clar), acceptat sau nu si de ce. */
|
|
export function acknowledge({ opened, message, me, accepted = true, reason = null, now = new Date() }) {
|
|
const statement = { v: 1, kind: 'aere-travel-rule-receipt', messageId: message.id, messageHash: opened.messageHash, payloadHash: opened.payloadHash,
|
|
from: { id: me.id, keys: me.public }, to: message.from.id, accepted: !!accepted, ...(reason ? { reason: String(reason) } : {}), at: new Date(now).toISOString() };
|
|
return { statement, signature: signText('travel-rule-receipt', canonical(statement), me) };
|
|
}
|
|
/** Initiatorul verifica o confirmare: semnata de beneficiarul acceptat, pentru ACEST mesaj (hash-ul celui trimis). Intoarce { ok, accepted, motive, rules, rows }. */
|
|
export function verifyReceipt(r, { message, beneficiaryId }) {
|
|
const S = r && r.statement; const C = colector();
|
|
try {
|
|
if (!((!!S && S.kind === 'aere-travel-rule-receipt') /*PAZA:receipt-form*/)) throw new Error('not an aere-travel-rule-receipt');
|
|
C.ok('receipt-from', (S.from.id === beneficiaryId && idOf(S.from.keys) === S.from.id) /*PAZA:receipt-from*/, `the receipt is from ${S.from.id}, not from the accepted beneficiary`);
|
|
C.ok('receipt-signature', verifyText('travel-rule-receipt', canonical(S), r.signature, S.from.keys) /*PAZA:receipt-signature*/, 'the receipt is not signed by the beneficiary');
|
|
const semnat = { ...antet(message), ciphertext: message.ciphertext };
|
|
C.ok('receipt-message', (S.messageId === message.id && S.messageHash === sha(Buffer.from(canonical(semnat), 'utf8'))) /*PAZA:receipt-message*/, 'the receipt is for another message');
|
|
} catch (e) { if (!(false /*PAZA:receipt-form*/)) C.ok('receipt-form', false, 'the receipt cannot be read: ' + e.message); }
|
|
const g = C.gata();
|
|
return { ...g, accepted: g.ok && !!(S && S.accepted) };
|
|
}
|
|
|
|
/** Inregistrarea schimbului fara date personale (plic AIP-23 `compliance`): transferul prin hash, rezultatul, digestul mesajului. */
|
|
export function travelRuleRecord({ message, receipt, buildProof, createdAt = new Date().toISOString() }) {
|
|
const t = message.transfer;
|
|
const subject = 'transfer:' + sha(Buffer.from(canonical({ chainId: t.chainId, asset: t.asset, txHash: t.txHash, from: message.from.id, to: message.to.id, id: message.id }), 'utf8')).slice(2, 42);
|
|
return buildProof('compliance', { subject, policy: 'travel-rule/fatf-r16', result: receipt && receipt.statement.accepted ? 'delivered-and-acknowledged' : 'delivered',
|
|
evidenceHash: sha(Buffer.from(canonical({ message: antet(message), receipt: receipt ? receipt.statement : null }), 'utf8')), createdAt });
|
|
}
|
|
export { publicKeyObjects };
|