UNPKG

smartledger-sdk

Version:

A JavaScript SDK for interacting with the SmartLedger.

1,023 lines (1,020 loc) 2.52 MB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SmartLedger = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./src/primitives/index.js"), exports); __exportStar(require("./src/script/index.js"), exports); __exportStar(require("./src/script/templates/index.js"), exports); __exportStar(require("./src/transaction/index.js"), exports); __exportStar(require("./src/transaction/fee-models/index.js"), exports); __exportStar(require("./src/transaction/broadcasters/index.js"), exports); __exportStar(require("./src/transaction/chaintrackers/index.js"), exports); __exportStar(require("./src/transaction/http/index.js"), exports); __exportStar(require("./src/messages/index.js"), exports); __exportStar(require("./src/compat/index.js"), exports); __exportStar(require("./src/totp/index.js"), exports); __exportStar(require("./src/wallet/index.js"), exports); __exportStar(require("./src/wallet/substrates/index.js"), exports); __exportStar(require("./src/auth/index.js"), exports); __exportStar(require("./src/overlay-tools/index.js"), exports); __exportStar(require("./src/storage/index.js"), exports); __exportStar(require("./src/identity/index.js"), exports); __exportStar(require("./src/registry/index.js"), exports); __exportStar(require("./src/kvstore/index.js"), exports); },{"./src/auth/index.js":11,"./src/compat/index.js":26,"./src/identity/index.js":29,"./src/kvstore/index.js":32,"./src/messages/index.js":35,"./src/overlay-tools/index.js":39,"./src/primitives/index.js":61,"./src/registry/index.js":64,"./src/script/index.js":72,"./src/script/templates/index.js":76,"./src/storage/index.js":80,"./src/totp/index.js":81,"./src/transaction/broadcasters/index.js":93,"./src/transaction/chaintrackers/index.js":97,"./src/transaction/fee-models/index.js":99,"./src/transaction/http/index.js":104,"./src/transaction/index.js":105,"./src/wallet/index.js":113,"./src/wallet/substrates/index.js":122}],2:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Peer = void 0; const SessionManager_js_1 = require("./SessionManager.js"); const index_js_1 = require("./utils/index.js"); const Random_js_1 = __importDefault(require("../primitives/Random.js")); const Utils = __importStar(require("../primitives/utils.js")); const AUTH_VERSION = '0.1'; /** * Represents a peer capable of performing mutual authentication. * Manages sessions, handles authentication handshakes, certificate requests and responses, * and sending and receiving general messages over a transport layer. * * This version supports multiple concurrent sessions per peer identityKey. */ class Peer { /** * Creates a new Peer instance * * @param {WalletInterface} wallet - The wallet instance used for cryptographic operations. * @param {Transport} transport - The transport mechanism used for sending and receiving messages. * @param {RequestedCertificateSet} [certificatesToRequest] - Optional set of certificates to request from a peer during the initial handshake. * @param {SessionManager} [sessionManager] - Optional SessionManager to be used for managing peer sessions. * @param {boolean} [autoPersistLastSession] - Whether to auto-persist the session with the last-interacted-with peer. Defaults to true. */ constructor(wallet, transport, certificatesToRequest, sessionManager, autoPersistLastSession, originator) { this.onGeneralMessageReceivedCallbacks = new Map(); this.onCertificatesReceivedCallbacks = new Map(); this.onCertificateRequestReceivedCallbacks = new Map(); this.onInitialResponseReceivedCallbacks = new Map(); // Single shared counter for all callback types this.callbackIdCounter = 0; // Whether to auto-persist the session with the last-interacted-with peer this.autoPersistLastSession = true; this.wallet = wallet; this.originator = originator; this.transport = transport; this.certificatesToRequest = certificatesToRequest ?? { certifiers: [], types: {} }; this.transport.onData(this.handleIncomingMessage.bind(this)).catch(e => { throw e; }); this.sessionManager = sessionManager != null ? sessionManager : new SessionManager_js_1.SessionManager(); if (autoPersistLastSession === false) { this.autoPersistLastSession = false; } else { this.autoPersistLastSession = true; } } /** * Sends a general message to a peer, and initiates a handshake if necessary. * * @param {number[]} message - The message payload to send. * @param {string} [identityKey] - The identity public key of the peer. If not provided, uses lastInteractedWithPeer (if any). * @param {number} [maxWaitTime] - optional max wait time in ms * @returns {Promise<void>} * @throws Will throw an error if the message fails to send. */ async toPeer(message, identityKey, maxWaitTime) { if (this.autoPersistLastSession && typeof this.lastInteractedWithPeer === 'string' && typeof identityKey !== 'string') { identityKey = this.lastInteractedWithPeer; } const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime); // Prepare the general message const requestNonce = Utils.toBase64((0, Random_js_1.default)(32)); const { signature } = await this.wallet.createSignature({ data: message, protocolID: [2, 'auth message signature'], keyID: `${requestNonce} ${peerSession.peerNonce ?? ''}`, counterparty: peerSession.peerIdentityKey }, this.originator); const generalMessage = { version: AUTH_VERSION, messageType: 'general', identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator)) .publicKey, nonce: requestNonce, yourNonce: peerSession.peerNonce, payload: message, signature }; peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); try { await this.transport.send(generalMessage); } catch (error) { const e = new Error(`Failed to send message to peer ${peerSession.peerIdentityKey ?? 'unknown'}: ${String(error.message)}`); e.stack = error.stack; throw e; } } /** * Sends a request for certificates to a peer. * This method allows a peer to dynamically request specific certificates after * an initial handshake or message has been exchanged. * * @param {RequestedCertificateSet} certificatesToRequest - Specifies the certifiers and types of certificates required from the peer. * @param {string} [identityKey] - The identity public key of the peer. If not provided, the current or last session identity is used. * @param {number} [maxWaitTime=10000] - Maximum time in milliseconds to wait for the peer session to be authenticated. * @returns {Promise<void>} Resolves if the certificate request message is successfully sent. * @throws Will throw an error if the peer session is not authenticated or if sending the request fails. */ async requestCertificates(certificatesToRequest, identityKey, maxWaitTime = 10000) { if (this.autoPersistLastSession && typeof this.lastInteractedWithPeer === 'string' && typeof identityKey !== 'string') { identityKey = this.lastInteractedWithPeer; } const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime); // Prepare the message const requestNonce = Utils.toBase64((0, Random_js_1.default)(32)); const { signature } = await this.wallet.createSignature({ data: Utils.toArray(JSON.stringify(certificatesToRequest), 'utf8'), protocolID: [2, 'auth message signature'], keyID: `${requestNonce} ${peerSession.peerNonce ?? ''}`, counterparty: peerSession.peerIdentityKey }, this.originator); const certRequestMessage = { version: AUTH_VERSION, messageType: 'certificateRequest', identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator)) .publicKey, nonce: requestNonce, initialNonce: peerSession.sessionNonce, yourNonce: peerSession.peerNonce, requestedCertificates: certificatesToRequest, signature }; // Update last-used timestamp peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); try { await this.transport.send(certRequestMessage); } catch (error) { throw new Error(`Failed to send certificate request message to peer ${peerSession.peerIdentityKey ?? 'unknown'}: ${String(error.message)}`); } } /** * Retrieves an authenticated session for a given peer identity. If no session exists * or the session is not authenticated, initiates a handshake to create or authenticate the session. * * - If `identityKey` is provided, we look up any existing session for that identity key. * - If none is found or not authenticated, we do a new handshake. * - If `identityKey` is not provided, but we have a `lastInteractedWithPeer`, we try that key. * * @param {string} [identityKey] - The identity public key of the peer. * @param {number} [maxWaitTime] - The maximum time in milliseconds to wait for the handshake. * @returns {Promise<PeerSession>} - A promise that resolves with an authenticated `PeerSession`. */ async getAuthenticatedSession(identityKey, maxWaitTime) { if (this.transport === undefined) { throw new Error('Peer transport is not connected!'); } let peerSession; if (typeof identityKey === 'string') { peerSession = this.sessionManager.getSession(identityKey); } // If that session doesn't exist or isn't authenticated, initiate handshake if ((peerSession == null) || !peerSession.isAuthenticated) { // This will create a brand-new session const sessionNonce = await this.initiateHandshake(identityKey, maxWaitTime); // Now retrieve it by the sessionNonce peerSession = this.sessionManager.getSession(sessionNonce); if ((peerSession == null) || !peerSession.isAuthenticated) { throw new Error('Unable to establish mutual authentication with peer!'); } } return peerSession; } /** * Registers a callback to listen for general messages from peers. * * @param {(senderPublicKey: string, payload: number[]) => void} callback - The function to call when a general message is received. * @returns {number} The ID of the callback listener. */ listenForGeneralMessages(callback) { const callbackID = this.callbackIdCounter++; this.onGeneralMessageReceivedCallbacks.set(callbackID, callback); return callbackID; } /** * Removes a general message listener. * * @param {number} callbackID - The ID of the callback to remove. */ stopListeningForGeneralMessages(callbackID) { this.onGeneralMessageReceivedCallbacks.delete(callbackID); } /** * Registers a callback to listen for certificates received from peers. * * @param {(senderPublicKey: string, certs: VerifiableCertificate[]) => void} callback - The function to call when certificates are received. * @returns {number} The ID of the callback listener. */ listenForCertificatesReceived(callback) { const callbackID = this.callbackIdCounter++; this.onCertificatesReceivedCallbacks.set(callbackID, callback); return callbackID; } /** * Cancels and unsubscribes a certificatesReceived listener. * * @param {number} callbackID - The ID of the certificates received callback to cancel. */ stopListeningForCertificatesReceived(callbackID) { this.onCertificatesReceivedCallbacks.delete(callbackID); } /** * Registers a callback to listen for certificates requested from peers. * * @param {(requestedCertificates: RequestedCertificateSet) => void} callback - The function to call when a certificate request is received * @returns {number} The ID of the callback listener. */ listenForCertificatesRequested(callback) { const callbackID = this.callbackIdCounter++; this.onCertificateRequestReceivedCallbacks.set(callbackID, callback); return callbackID; } /** * Cancels and unsubscribes a certificatesRequested listener. * * @param {number} callbackID - The ID of the requested certificates callback to cancel. */ stopListeningForCertificatesRequested(callbackID) { this.onCertificateRequestReceivedCallbacks.delete(callbackID); } /** * Initiates the mutual authentication handshake with a peer. * * @private * @param {string} [identityKey] - The identity public key of the peer. * @param {number} [maxWaitTime=10000] - how long to wait for handshake * @returns {Promise<string>} A promise that resolves to the session nonce. */ async initiateHandshake(identityKey, maxWaitTime = 10000) { const sessionNonce = await (0, index_js_1.createNonce)(this.wallet, undefined, this.originator); // Initial request nonce // Create the preliminary session (not yet authenticated) const now = Date.now(); this.sessionManager.addSession({ isAuthenticated: false, sessionNonce, peerIdentityKey: identityKey, lastUpdate: now }); const initialRequest = { version: AUTH_VERSION, messageType: 'initialRequest', identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator)) .publicKey, initialNonce: sessionNonce, requestedCertificates: this.certificatesToRequest }; await this.transport.send(initialRequest); return await this.waitForInitialResponse(sessionNonce, maxWaitTime); } /** * Waits for the initial response from the peer after sending an initial handshake request message. * * @param {string} sessionNonce - The session nonce created in the initial request. * @returns {Promise<string>} A promise that resolves with the session nonce when the initial response is received. */ async waitForInitialResponse(sessionNonce, maxWaitTime = 10000) { return await new Promise((resolve, reject) => { const callbackID = this.listenForInitialResponse(sessionNonce, nonce => { clearTimeout(timeoutHandle); this.stopListeningForInitialResponses(callbackID); resolve(nonce); }); const timeoutHandle = setTimeout(() => { this.stopListeningForInitialResponses(callbackID); reject(new Error('Initial response timed out.')); }, maxWaitTime); }); } /** * Adds a listener for an initial response message matching a specific initial nonce. * * @private * @param {string} sessionNonce - The session nonce to match. * @param {(sessionNonce: string) => void} callback - The callback to invoke when the initial response is received. * @returns {number} The ID of the callback listener. */ listenForInitialResponse(sessionNonce, callback) { const callbackID = this.callbackIdCounter++; this.onInitialResponseReceivedCallbacks.set(callbackID, { callback, sessionNonce }); return callbackID; } /** * Removes a listener for initial responses. * * @private * @param {number} callbackID - The ID of the callback to remove. */ stopListeningForInitialResponses(callbackID) { this.onInitialResponseReceivedCallbacks.delete(callbackID); } /** * Handles incoming messages from the transport. * * @param {AuthMessage} message - The incoming message to process. * @returns {Promise<void>} */ async handleIncomingMessage(message) { if (typeof message.version !== 'string' || message.version !== AUTH_VERSION) { throw new Error(`Invalid or unsupported message auth version! Received: ${message.version}, expected: ${AUTH_VERSION}`); } switch (message.messageType) { case 'initialRequest': await this.processInitialRequest(message); break; case 'initialResponse': await this.processInitialResponse(message); break; case 'certificateRequest': await this.processCertificateRequest(message); break; case 'certificateResponse': await this.processCertificateResponse(message); break; case 'general': await this.processGeneralMessage(message); break; default: throw new Error(`Unknown message type of ${String(message.messageType)} from ${String(message.identityKey)}`); } } /** * Processes an initial request message from a peer. * * @param {AuthMessage} message - The incoming initial request message. */ async processInitialRequest(message) { if (typeof message.identityKey !== 'string' || typeof message.initialNonce !== 'string' || message.initialNonce === '') { throw new Error('Missing required fields in initialRequest message.'); } // Create a new sessionNonce for our side const sessionNonce = await (0, index_js_1.createNonce)(this.wallet, undefined, this.originator); const now = Date.now(); // We'll treat this as fully authenticated from *our* perspective (the responding side). this.sessionManager.addSession({ isAuthenticated: true, sessionNonce, peerNonce: message.initialNonce, peerIdentityKey: message.identityKey, lastUpdate: now }); // Possibly handle the peer's requested certs let certificatesToInclude; if ((message.requestedCertificates != null) && Array.isArray(message.requestedCertificates.certifiers) && message.requestedCertificates.certifiers.length > 0) { if (this.onCertificateRequestReceivedCallbacks.size > 0) { // Let the application handle it this.onCertificateRequestReceivedCallbacks.forEach(cb => { cb(message.identityKey, message.requestedCertificates); }); } else { // Attempt to find automatically certificatesToInclude = await (0, index_js_1.getVerifiableCertificates)(this.wallet, message.requestedCertificates, message.identityKey, this.originator); } } // Create signature const { signature } = await this.wallet.createSignature({ data: Utils.toArray(message.initialNonce + sessionNonce, 'base64'), protocolID: [2, 'auth message signature'], keyID: `${message.initialNonce} ${sessionNonce}`, counterparty: message.identityKey }, this.originator); const initialResponseMessage = { version: AUTH_VERSION, messageType: 'initialResponse', identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator)) .publicKey, initialNonce: sessionNonce, yourNonce: message.initialNonce, certificates: certificatesToInclude, requestedCertificates: this.certificatesToRequest, signature }; // If we haven't interacted with a peer yet, store this identity as "lastInteracted" if (this.lastInteractedWithPeer === undefined) { this.lastInteractedWithPeer = message.identityKey; } // Send the response await this.transport.send(initialResponseMessage); } /** * Processes an initial response message from a peer. * * @private * @param {AuthMessage} message - The incoming initial response message. * @throws Will throw an error if nonce or signature verification fails. */ async processInitialResponse(message) { const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator); if (!validNonce) { throw new Error(`Initial response nonce verification failed from peer: ${message.identityKey}`); } // This is the session we previously created by calling initiateHandshake const peerSession = this.sessionManager.getSession(message.yourNonce); if (peerSession == null) { throw new Error(`Peer session not found for peer: ${message.identityKey}`); } // Validate message signature const dataToVerify = Utils.toArray((peerSession.sessionNonce ?? '') + (message.initialNonce ?? ''), 'base64'); const { valid } = await this.wallet.verifySignature({ data: dataToVerify, signature: message.signature, protocolID: [2, 'auth message signature'], keyID: `${peerSession.sessionNonce ?? ''} ${message.initialNonce ?? ''}`, counterparty: message.identityKey }, this.originator); if (!valid) { throw new Error(`Unable to verify initial response signature for peer: ${message.identityKey}`); } // Now mark the session as authenticated peerSession.peerNonce = message.initialNonce; peerSession.peerIdentityKey = message.identityKey; peerSession.isAuthenticated = true; peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); // If the handshake had requested certificates, validate them if (this.certificatesToRequest?.certifiers?.length > 0 && message.certificates?.length > 0) { await (0, index_js_1.validateCertificates)(this.wallet, message, this.certificatesToRequest, this.originator); // Notify listeners this.onCertificatesReceivedCallbacks.forEach(cb => cb(message.identityKey, message.certificates)); } // Update lastInteractedWithPeer this.lastInteractedWithPeer = message.identityKey; // Let the handshake wait-latch know we got our response this.onInitialResponseReceivedCallbacks.forEach(entry => { if (entry.sessionNonce === peerSession.sessionNonce) { entry.callback(peerSession.sessionNonce); } }); // The peer might also request certificates from us if ((message.requestedCertificates != null) && Array.isArray(message.requestedCertificates.certifiers) && message.requestedCertificates.certifiers.length > 0) { if (this.onCertificateRequestReceivedCallbacks.size > 0) { // Let the application handle it this.onCertificateRequestReceivedCallbacks.forEach(cb => { cb(message.identityKey, message.requestedCertificates); }); } else { // Attempt auto const verifiableCertificates = await (0, index_js_1.getVerifiableCertificates)(this.wallet, message.requestedCertificates, message.identityKey, this.originator); await this.sendCertificateResponse(message.identityKey, verifiableCertificates); } } } /** * Processes an incoming certificate request message from a peer. * Verifies nonce/signature and then possibly sends a certificateResponse. * * @param {AuthMessage} message - The certificate request message received from the peer. * @throws {Error} if nonce or signature is invalid. */ async processCertificateRequest(message) { const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator); if (!validNonce) { throw new Error(`Unable to verify nonce for certificate request message from: ${message.identityKey}`); } const peerSession = this.sessionManager.getSession(message.yourNonce); if (peerSession == null) { throw new Error(`Session not found for nonce: ${message.yourNonce}`); } const { valid } = await this.wallet.verifySignature({ data: Utils.toArray(JSON.stringify(message.requestedCertificates), 'utf8'), signature: message.signature, protocolID: [2, 'auth message signature'], keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`, counterparty: peerSession.peerIdentityKey }, this.originator); if (!valid) { throw new Error(`Invalid signature in certificate request message from ${peerSession.peerIdentityKey}`); } // Update usage peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); if ((message.requestedCertificates != null) && Array.isArray(message.requestedCertificates.certifiers) && message.requestedCertificates.certifiers.length > 0) { if (this.onCertificateRequestReceivedCallbacks.size > 0) { // Let the application handle it this.onCertificateRequestReceivedCallbacks.forEach(cb => { cb(message.identityKey, message.requestedCertificates); }); } else { // Attempt auto const verifiableCertificates = await (0, index_js_1.getVerifiableCertificates)(this.wallet, message.requestedCertificates, message.identityKey, this.originator); await this.sendCertificateResponse(message.identityKey, verifiableCertificates); } } } /** * Sends a certificate response message containing the specified certificates to a peer. * * @param {string} verifierIdentityKey - The identity key of the peer requesting the certificates. * @param {VerifiableCertificate[]} certificates - The list of certificates to include in the response. * @throws Will throw an error if the transport fails to send the message. */ async sendCertificateResponse(verifierIdentityKey, certificates) { const peerSession = await this.getAuthenticatedSession(verifierIdentityKey); const requestNonce = Utils.toBase64((0, Random_js_1.default)(32)); const { signature } = await this.wallet.createSignature({ data: Utils.toArray(JSON.stringify(certificates), 'utf8'), protocolID: [2, 'auth message signature'], keyID: `${requestNonce} ${peerSession.peerNonce ?? ''}`, counterparty: peerSession.peerIdentityKey }, this.originator); const certificateResponse = { version: AUTH_VERSION, messageType: 'certificateResponse', identityKey: (await this.wallet.getPublicKey({ identityKey: true }, this.originator)) .publicKey, nonce: requestNonce, initialNonce: peerSession.sessionNonce, yourNonce: peerSession.peerNonce, certificates, signature }; // Update usage peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); try { await this.transport.send(certificateResponse); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to send certificate response message to peer ${peerSession.peerIdentityKey ?? 'unknown'}: ${errorMessage}`); } } /** * Processes a certificate response message from a peer. * * @private * @param {AuthMessage} message - The incoming certificate response message. * @throws Will throw an error if nonce verification or signature verification fails. */ async processCertificateResponse(message) { const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator); if (!validNonce) { throw new Error(`Unable to verify nonce for certificate response from: ${message.identityKey}`); } const peerSession = this.sessionManager.getSession(message.yourNonce); if (peerSession == null) { throw new Error(`Session not found for nonce: ${message.yourNonce}`); } // Validate message signature const { valid } = await this.wallet.verifySignature({ data: Utils.toArray(JSON.stringify(message.certificates), 'utf8'), signature: message.signature, protocolID: [2, 'auth message signature'], keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`, counterparty: message.identityKey }, this.originator); if (!valid) { throw new Error(`Unable to verify certificate response signature for peer: ${message.identityKey}`); } // We also handle optional validation if there's a requestedCertificates field await (0, index_js_1.validateCertificates)(this.wallet, message, message.requestedCertificates, this.originator); // Notify any listeners this.onCertificatesReceivedCallbacks.forEach(cb => { cb(message.identityKey, message.certificates ?? []); }); peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); } /** * Processes a general message from a peer. * * @private * @param {AuthMessage} message - The incoming general message. * @throws Will throw an error if nonce or signature verification fails. */ async processGeneralMessage(message) { const validNonce = await (0, index_js_1.verifyNonce)(message.yourNonce, this.wallet, undefined, this.originator); if (!validNonce) { throw new Error(`Unable to verify nonce for general message from: ${message.identityKey}`); } const peerSession = this.sessionManager.getSession(message.yourNonce); if (peerSession == null) { throw new Error(`Session not found for nonce: ${message.yourNonce}`); } const { valid } = await this.wallet.verifySignature({ data: message.payload, signature: message.signature, protocolID: [2, 'auth message signature'], keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`, counterparty: peerSession.peerIdentityKey }, this.originator); if (!valid) { throw new Error(`Invalid signature in generalMessage from ${peerSession.peerIdentityKey}`); } // Mark last usage peerSession.lastUpdate = Date.now(); this.sessionManager.updateSession(peerSession); // Update lastInteractedWithPeer this.lastInteractedWithPeer = message.identityKey; // Dispatch callbacks this.onGeneralMessageReceivedCallbacks.forEach(cb => { cb(message.identityKey, message.payload ?? []); }); } } exports.Peer = Peer; },{"../primitives/Random.js":55,"../primitives/utils.js":62,"./SessionManager.js":3,"./utils/index.js":17}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionManager = void 0; /** * Manages sessions for peers, allowing multiple concurrent sessions * per identity key. Primary lookup is always by `sessionNonce`. */ class SessionManager { constructor() { this.sessionNonceToSession = new Map(); this.identityKeyToNonces = new Map(); } /** * Adds a session to the manager, associating it with its sessionNonce, * and also with its peerIdentityKey (if any). * * This does NOT overwrite existing sessions for the same peerIdentityKey, * allowing multiple concurrent sessions for the same peer. * * @param {PeerSession} session - The peer session to add. */ addSession(session) { if (typeof session.sessionNonce !== 'string') { throw new Error('Invalid session: sessionNonce is required to add a session.'); } // Use the sessionNonce as the primary key this.sessionNonceToSession.set(session.sessionNonce, session); // Also track it by identity key if present if (typeof session.peerIdentityKey === 'string') { let nonces = this.identityKeyToNonces.get(session.peerIdentityKey); if (nonces == null) { nonces = new Set(); this.identityKeyToNonces.set(session.peerIdentityKey, nonces); } nonces.add(session.sessionNonce); } } /** * Updates a session in the manager (primarily by re-adding it), * ensuring we record the latest data (e.g., isAuthenticated, lastUpdate, etc.). * * @param {PeerSession} session - The peer session to update. */ updateSession(session) { // Remove the old references (if any) and re-add this.removeSession(session); this.addSession(session); } /** * Retrieves a session based on a given identifier, which can be: * - A sessionNonce, or * - A peerIdentityKey. * * If it is a `sessionNonce`, returns that exact session. * If it is a `peerIdentityKey`, returns the "best" (e.g. most recently updated, * authenticated) session associated with that peer, if any. * * @param {string} identifier - The identifier for the session (sessionNonce or peerIdentityKey). * @returns {PeerSession | undefined} - The matching peer session, or undefined if not found. */ getSession(identifier) { // Check if this identifier is directly a sessionNonce const direct = this.sessionNonceToSession.get(identifier); if (direct != null) { return direct; } // Otherwise, interpret the identifier as an identity key const nonces = this.identityKeyToNonces.get(identifier); if ((nonces == null) || nonces.size === 0) { return undefined; } // Pick the "best" session. One sensible approach: // - Choose an authenticated session if available // - Among them, pick the most recently updated let best; for (const nonce of nonces) { const s = this.sessionNonceToSession.get(nonce); if (s == null) continue; // We can prefer authenticated sessions if (best == null) { best = s; } else { // If we want the "most recently updated" AND isAuthenticated if ((s.lastUpdate ?? 0) > (best.lastUpdate ?? 0)) { best = s; } } } // Optionally, you could also filter out isAuthenticated===false if you only want // an authenticated session. But for our usage, let's return the latest any session. return best; } /** * Removes a session from the manager by clearing all associated identifiers. * * @param {PeerSession} session - The peer session to remove. */ removeSession(session) { if (typeof session.sessionNonce === 'string') { this.sessionNonceToSession.delete(session.sessionNonce); } if (typeof session.peerIdentityKey === 'string') { const nonces = this.identityKeyToNonces.get(session.peerIdentityKey); if (nonces != null) { nonces.delete(session.sessionNonce ?? ''); if (nonces.size === 0) { this.identityKeyToNonces.delete(session.peerIdentityKey); } } } } /** * Checks if a session exists for a given identifier (either sessionNonce or identityKey). * * @param {string} identifier - The identifier to check. * @returns {boolean} - True if the session exists, false otherwise. */ hasSession(identifier) { const direct = this.sessionNonceToSession.has(identifier); if (direct) return true; // if not directly a nonce, interpret as identityKey const nonces = this.identityKeyToNonces.get(identifier); return !(nonces == null) && nonces.size > 0; } } exports.SessionManager = SessionManager; },{}],4:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Utils = __importStar(require("../../primitives/utils.js")); const ProtoWallet_js_1 = __importDefault(require("../../wallet/ProtoWallet.js")); const Signature_js_1 = __importDefault(require("../../primitives/Signature.js")); /** * Represents an Identity Certificate as per the Wallet interface specifications. * * This class provides methods to serialize and deserialize certificates, as well as signing and verifying the certificate's signature. */ class Certificate { /** * Constructs a new Certificate. * * @param {Base64String} type - Type identifier for the certificate, base64 encoded string, 32 bytes. * @param {Base64String} serialNumber - Unique serial number of the certificate, base64 encoded string, 32 bytes. * @param {PubKeyHex} subject - The public key belonging to the certificate's subject, compressed public key hex string. * @param {PubKeyHex} certifier - Public key of the certifier who issued the certificate, compressed public key hex string. * @param {OutpointString} revocationOutpoint - The outpoint used to confirm that the certificate has not been revoked (TXID.OutputIndex), as a string. * @param {Record<CertificateFieldNameUnder50Bytes, string>} fields - All the fields present in the certificate. * @param {HexString} signature - Certificate signature by the certifier's private key, DER encoded hex string. */ constructor(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature) { this.type = type; this.serialNumber = serialNumber; this.subject = subject; this.certifier = certifier; this.revocationOutpoint = revocationOutpoint; this.fields = fields; this.signature = signature; } /** * Serializes the certificate into binary format, with or without a signature. * * @param {boolean} [includeSignature=true] - Whether to include the signature in the serialization. * @returns {number[]} - The serialized certificate in binary format. */ toBinary(includeSignature = true) { const writer = new Utils.Writer(); // Write type (Base64String, 32 bytes) const typeBytes = Utils.toArray(this.type, 'base64'); writer.write(typeBytes); // Write serialNumber (Base64String, 32 bytes) const serialNumberBytes = Utils.toArray(this.serialNumber, 'base64'); writer.write(serialNumberBytes); // Write subject (33 bytes compressed PubKeyHex) const subjectBytes = Utils.toArray(this.subject, 'hex'); writer.write(subjectBytes); // Write certifier (33 bytes compressed PubKeyHex) const certifierBytes = Utils.toArray(this.certifier, 'hex'); writer.write(certifierBytes); // Write revocationOutpoint (TXID + OutputIndex) const [txid, outputIndex] = this.revocationOutpoint.split('.'); const txidBytes = Utils.toArray(txid, 'hex'); writer.write(txidBytes); writer.writeVarIntNum(Number(outputIndex)); // Write fields // Sort field names lexicographically const fieldNames = Object.keys(this.fields).sort(); writer.writeVarIntNum(fieldNames.length); for (const fieldName of fieldNames) { const fieldValue = this.fields[fieldName]; // Field name const fieldNameBytes = Utils.toArray(fieldName, 'utf8'); writer.writeVarIntNum(fieldNameBytes.length); writer.write(fieldNameBytes); // Field value const fieldValueBytes = Utils.toArray(fieldValue, 'utf8'); writer.writeVarIntNum(fieldValueBytes.length); writer.write(fieldValueBytes); } // Write signature if included if (includeSignature && (this.signature ?? '').length > 0) { // ✅ Explicitly handle nullish signature const signatureBytes = Utils.toArray(this.signature, 'hex'); // ✅ Type assertion ensures it's a string writer.write(signatureBytes); } return writer.toArray(); } /** * Deserializes a certificate from binary format. * * @param {number[]} bin - The binary data representing the certificate. * @returns {Certificate} - The deserialized Certificate object. */ static fromBinary(bin) { const reader = new Utils.Reader(bin); // Read type const typeBytes = reader.read(32); const type = Utils.toBase64(typeBytes); // Read serialNumber const serialNumberBytes = reader.read(32); const serialNumber = Utils.toBase64(serialNumberBytes); // Read subject (33 bytes) const subjectBytes = reader.read(33); const subject = Utils.toHex(subjectBytes); // Read certifier (33 bytes) const certifierBytes = reader.read(33); const certifier = Utils.toHex(certifierBytes); // Read revocationOutpoint const txidBytes = reader.read(32); const txid = Utils.toHex(txidBytes); const outputIndex = reader.readVarIntNum(); const revocationOutpoint = `${txid}.${outputIndex}`; // Read fields const numFields = reader.readVarIntNum(); const fields = {}; for (let i = 0; i < numFields; i++) { // Field name const fieldNameLength = reader.readVarIntNum(); const fieldNameBytes = reader.read(fieldNameLength); const fieldName = Utils.toUTF8(fieldNameBytes); // Field value const fieldValueLength = reader.readVarIntNum(); const fieldValueBytes = reader.read(fieldValueLength); const fieldValue = Utils.toUTF8(fieldValueBytes); fields[fieldName] = fieldValue; } // Read signature if present let signature; if (!reader.eof()) { const signatureBytes = reader.read(); const sig = Signature_js_1.default.fromDER(signatureBytes); signature = sig.toString('hex'); } return new Certificate(type, serialNumber, subject, certifier, revocationOutpoint, fields, signature); } /** * Verifies the certificate's signature. * * @returns {Promise<boolean>} - A promise that resolves to true if the signature is valid. */ async verify() { // A verifier can be any wallet capable of verifying signatures const verifier = new ProtoWallet_js_1.default('anyone'); const verificationData = this.toBinary(false); // Exclude the signature from the verification data const signatureHex = this.signature ?? ''; // Provide a fallback value (empty string) const { valid } = await verifier.verifySignature({ signature: Utils.toArray(signatureHex, 'hex'), data: verificationData, protocolID: [2, 'certificate signature'], keyID: `${this.type} ${this.serialNumber}`, counterparty: this.certifier // The certifier is the one who signed the certificate }); return valid; } /** * Signs the certificate using the provided certifier wallet. * * @param {Wallet} certifierWallet - The wallet representing the certifier. * @returns {Promise<void>} */ async sign(certifierWallet) { if (this.signature != null && this.signature.length > 0) { // ✅ Explicitly checking for null/undefined throw new Error(`Certificate has already been signed! Signature present: ${this.signature}`); } // Ensure the certifier declared is the one actually signing this.certifier = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey; const preimage = this.toBinary(false); // Exclude the signature when signing const { signature } = await certifierWallet.createSignature({ data: preimage, protocolID: [2, 'certificate signature'], keyID: `${this.type} ${this.serialNumber}` }); this.signature = Utils.toHex(signature); } /** * Helper function which retrieves the protocol ID and key ID for certificate field encryption. * * For master certificate creation, no serial number is provided because entropy is required * from both the client and the certifier. In this case, the `keyID` is simply the `fieldName`. * * For VerifiableCertificates verifier keyring creation, both the serial number and field name are available, * so the `keyID` is formed by concatenating the `serialNumber` and `fieldName`. * * @param fieldName - The name of the field within the certificate to be encrypted. * @param