import { randomBytes } from 'crypto'; // Import createRequire for CJS dependencies in ESM // import { createRequire } from 'module'; // No longer needed for bitcoinjs-message import * as bitcoinMessage from 'bitcoinjs-message'; import { DigiIDCallbackData, DigiIDError, DigiIDUriOptions, DigiIDVerificationResult, DigiIDVerifyOptions } from './types'; // Moved require inside the function that uses it to potentially help mocking // and avoid top-level side effects if require itself does something complex. /** * INTERNAL: Verifies the signature using the bitcoinjs-message library. * Exported primarily for testing purposes (mocking/spying). * @internal */ export async function _internalVerifySignature( uri: string, address: string, signature: string ): Promise { // DigiByte Message Prefix const messagePrefix = '\x19DigiByte Signed Message:\n'; try { // bitcoinjs-message verify function const isValidSignature = bitcoinMessage.verify( uri, // The message that was signed (the DigiID URI) address, // The DigiByte address (D..., S..., or dgb1...) signature, // The signature string (Base64 encoded) messagePrefix, // The DigiByte specific message prefix true // Set checkSegwitAlways to true to handle all address types correctly ); return !!isValidSignature; // Ensure boolean return } catch (e: unknown) { // Catch potential errors from bitcoinjs-message (e.g., invalid address format, invalid signature format) const errorMessage = e instanceof Error ? e.message : String(e); throw new DigiIDError(`Signature verification failed: ${errorMessage}`); } } /** * Generates a secure random nonce (hex string). * @param length - The number of bytes to generate (default: 16, resulting in 32 hex chars). * @returns A hex-encoded random string. */ function generateNonce(length = 16): string { return randomBytes(length).toString('hex'); } /** * Generates a DigiID authentication URI. * * @param options - Options for URI generation, including the callback URL. * @returns The generated DigiID URI string. * @throws {DigiIDError} If the callback URL is invalid or missing. */ export function generateDigiIDUri(options: DigiIDUriOptions): string { if (!options.callbackUrl) { throw new DigiIDError('Callback URL is required.'); } let parsedUrl: URL; try { parsedUrl = new URL(options.callbackUrl); } catch (e) { throw new DigiIDError(`Invalid callback URL: ${(e as Error).message}`); } // DigiID spec requires stripping the scheme (http/https) const domainAndPath = parsedUrl.host + parsedUrl.pathname; const nonce = options.nonce || generateNonce(); const unsecureFlag = options.unsecure ? '1' : '0'; // 1 for http, 0 for https // Validate scheme based on unsecure flag if (options.unsecure && parsedUrl.protocol !== 'http:') { throw new DigiIDError('Unsecure flag is true, but callback URL does not use http protocol.'); } if (!options.unsecure && parsedUrl.protocol !== 'https:') { throw new DigiIDError('Callback URL must use https protocol unless unsecure flag is set to true.'); } // Construct the URI // Example: digiid://example.com/callback?x=nonce_value&u=0 const uri = `digiid://${domainAndPath}?x=${nonce}&u=${unsecureFlag}`; // Clean up potential trailing slash in path if no query params exist (though DigiID always has params) // This check might be redundant given DigiID structure, but good practice // const cleanedUri = uri.endsWith('/') && parsedUrl.search === '' ? uri.slice(0, -1) : uri; return uri; } /** * Verifies the signature and data received from a DigiID callback. * * @param callbackData - The data received from the wallet (address, uri, signature). * @param verifyOptions - Options for verification, including the expected callback URL and nonce. * @returns {Promise} A promise that resolves with verification details if successful. * @throws {DigiIDError} If validation or signature verification fails. */ export async function verifyDigiIDCallback( callbackData: DigiIDCallbackData, verifyOptions: DigiIDVerifyOptions ): Promise { const { address, uri, signature } = callbackData; const { expectedCallbackUrl, expectedNonce } = verifyOptions; if (!address || !uri || !signature) { throw new DigiIDError('Missing required callback data: address, uri, or signature.'); } // 1. Parse the received URI let parsedReceivedUri: URL; try { // Temporarily replace digiid:// with http:// for standard URL parsing const parsableUri = uri.replace(/^digiid:/, 'http:'); parsedReceivedUri = new URL(parsableUri); } catch (e) { throw new DigiIDError(`Invalid URI received in callback: ${(e as Error).message}`); } const receivedNonce = parsedReceivedUri.searchParams.get('x'); const receivedUnsecure = parsedReceivedUri.searchParams.get('u'); // 0 or 1 const receivedDomainAndPath = parsedReceivedUri.host + parsedReceivedUri.pathname; if (receivedNonce === null || receivedUnsecure === null) { throw new DigiIDError('URI missing nonce (x) or unsecure (u) parameter.'); } // 2. Validate Callback URL let parsedExpectedUrl: URL; try { // Allow expectedCallbackUrl to be a string or URL object parsedExpectedUrl = typeof expectedCallbackUrl === 'string' ? new URL(expectedCallbackUrl) : expectedCallbackUrl; } catch (e) { throw new DigiIDError(`Invalid expectedCallbackUrl provided: ${(e as Error).message}`); } const expectedDomainAndPath = parsedExpectedUrl.host + parsedExpectedUrl.pathname; if (receivedDomainAndPath !== expectedDomainAndPath) { throw new DigiIDError(`Callback URL mismatch: URI contained "${receivedDomainAndPath}", expected "${expectedDomainAndPath}"`); } // Validate scheme consistency const expectedScheme = parsedExpectedUrl.protocol; if (receivedUnsecure === '1' && expectedScheme !== 'http:') { throw new DigiIDError('URI indicates unsecure (u=1), but expectedCallbackUrl is not http.'); } if (receivedUnsecure === '0' && expectedScheme !== 'https:') { throw new DigiIDError('URI indicates secure (u=0), but expectedCallbackUrl is not https.'); } // 3. Validate Nonce (optional) if (expectedNonce && receivedNonce !== expectedNonce) { throw new DigiIDError(`Nonce mismatch: URI contained "${receivedNonce}", expected "${expectedNonce}". Possible replay attack.`); } // 4. Verify Signature using internal helper try { const isValidSignature = await _internalVerifySignature(uri, address, signature); if (!isValidSignature) { // If the helper returns false, throw the standard invalid signature error throw new DigiIDError('Invalid signature.'); } } catch (error) { // If _internalVerifySignature throws (e.g., due to format/checksum errors from the lib, or our re-thrown error), // re-throw it. It should already be a DigiIDError. if (error instanceof DigiIDError) { throw error; } else { // Catch any unexpected errors and wrap them throw new DigiIDError(`Unexpected error during signature verification: ${(error as Error).message}`); } } // 5. Return successful result return { isValid: true, address: address, nonce: receivedNonce, // Return the nonce from the URI }; }