import { IIdentifier } from "@veramo/core"; import { getRandomBytesSync } from "ethereum-cryptography/random"; import { entropyToMnemonic } from "ethereum-cryptography/bip39"; import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; import { HDNode } from "@ethersproject/hdnode"; import { CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, createEndorserJwtForDid, CONTACT_URL_PATH_ENDORSER_CH_OLD, CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, CONTACT_CONFIRM_URL_PATH_TIME_SAFARI, } from "../../libs/endorserServer"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { logger } from "../../utils/logger"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; export const LOCAL_KMS_NAME = "local"; /** * * * @param {string} address * @param {string} publicHex * @param {string} privateHex * @param {string} derivationPath * @return {*} {Omit} */ export const newIdentifier = ( address: string, publicHex: string, privateHex: string, derivationPath: string, ): Omit => { return { did: DEFAULT_DID_PROVIDER_NAME + ":" + address, keys: [ { kid: publicHex, kms: LOCAL_KMS_NAME, meta: { derivationPath: derivationPath }, privateKeyHex: privateHex, publicKeyHex: publicHex, type: "Secp256k1", }, ], provider: DEFAULT_DID_PROVIDER_NAME, services: [], }; }; /** * * * @param {string} mnemonic * @return {[string, string, string, string]} address, privateHex, publicHex, derivationPath */ export const deriveAddress = ( mnemonic: string, derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH, ): [string, string, string, string] => { mnemonic = mnemonic.trim().toLowerCase(); const hdnode: HDNode = HDNode.fromMnemonic(mnemonic); const rootNode: HDNode = hdnode.derivePath(derivationPath); const privateHex = rootNode.privateKey.substring(2); // original starts with '0x' const publicHex = rootNode.publicKey.substring(2); // original starts with '0x' const address = rootNode.address; return [address, privateHex, publicHex, derivationPath]; }; export const generateRandomBytes = (numBytes: number): Uint8Array => { return getRandomBytesSync(numBytes); }; /** * * * @return {*} {string} */ export const generateSeed = (): string => { const entropy: Uint8Array = getRandomBytesSync(32); const mnemonic = entropyToMnemonic(entropy, wordlist); return mnemonic; }; /** * Retrieve an access token, or "" if no DID is provided. * * @param {string} did * @return {string} JWT with basic payload */ export const accessToken = async (did?: string) => { if (did) { const nowEpoch = Math.floor(Date.now() / 1000); const endEpoch = nowEpoch + 60; // add one minute const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; return createEndorserJwtForDid(did, tokenPayload); } else { return ""; } }; /** * Extract JWT from various URL formats * @param jwtUrlText The URL containing the JWT * @returns The extracted JWT or null if not found */ export const getContactJwtFromJwtUrl = (jwtUrlText: string) => { try { let jwtText = jwtUrlText; // Try to extract JWT from URL paths const paths = [ CONTACT_CONFIRM_URL_PATH_TIME_SAFARI, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, CONTACT_URL_PATH_ENDORSER_CH_OLD, ]; for (const path of paths) { const pathIndex = jwtText.indexOf(path); if (pathIndex > -1) { jwtText = jwtText.substring(pathIndex + path.length); break; } } // Validate JWT format if (!jwtText.match(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/)) { logger.error("Invalid JWT format in URL:", jwtUrlText); return null; } return jwtText; } catch (error) { logger.error("Error extracting JWT from URL:", error); return null; } }; export const nextDerivationPath = (origDerivPath: string) => { let lastStr = origDerivPath.split("/").slice(-1)[0]; if (lastStr.endsWith("'")) { lastStr = lastStr.slice(0, -1); } const lastNum = parseInt(lastStr, 10); const newLastNum = lastNum + 1; const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : ""); const newDerivPath = origDerivPath .split("/") .slice(0, -1) .concat([newLastStr]) .join("/"); return newDerivPath; }; // Base64 encoding/decoding utilities for browser function base64ToArrayBuffer(base64: string): Uint8Array { const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } function arrayBufferToBase64(buffer: ArrayBuffer): string { const binary = String.fromCharCode(...new Uint8Array(buffer)); return btoa(binary); } const SALT_LENGTH = 16; const IV_LENGTH = 12; const KEY_LENGTH = 256; const ITERATIONS = 100000; // Encryption helper function export async function encryptMessage(message: string, password: string) { const encoder = new TextEncoder(); const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); // Derive key from password using PBKDF2 const keyMaterial = await crypto.subtle.importKey( "raw", encoder.encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"], ); const key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256", }, keyMaterial, { name: "AES-GCM", length: KEY_LENGTH }, false, ["encrypt"], ); // Encrypt the message const encryptedContent = await crypto.subtle.encrypt( { name: "AES-GCM", iv, }, key, encoder.encode(message), ); // Return a JSON structure with base64-encoded components const result = { salt: arrayBufferToBase64(salt), iv: arrayBufferToBase64(iv), encrypted: arrayBufferToBase64(encryptedContent), }; return btoa(JSON.stringify(result)); } // Decryption helper function export async function decryptMessage(encryptedJson: string, password: string) { const decoder = new TextDecoder(); const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson)); // Convert base64 components back to Uint8Arrays const saltArray = base64ToArrayBuffer(salt); const ivArray = base64ToArrayBuffer(iv); const encryptedContent = base64ToArrayBuffer(encrypted); // Derive the same key using PBKDF2 with the extracted salt const keyMaterial = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"], ); const key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: saltArray, iterations: ITERATIONS, hash: "SHA-256", }, keyMaterial, { name: "AES-GCM", length: KEY_LENGTH }, false, ["decrypt"], ); // Decrypt the content const decryptedContent = await crypto.subtle.decrypt( { name: "AES-GCM", iv: ivArray, }, key, encryptedContent, ); // Convert the decrypted content back to a string return decoder.decode(decryptedContent); } // Test function to verify encryption/decryption export async function testEncryptionDecryption() { try { const testMessage = "Hello, this is a test message! 🚀"; const testPassword = "myTestPassword123"; logger.log("Original message:", testMessage); // Test encryption logger.log("Encrypting..."); const encrypted = await encryptMessage(testMessage, testPassword); logger.log("Encrypted result:", encrypted); // Test decryption logger.log("Decrypting..."); const decrypted = await decryptMessage(encrypted, testPassword); logger.log("Decrypted result:", decrypted); // Verify const success = testMessage === decrypted; logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌")); logger.log("Messages match:", success); // Test with wrong password logger.log("\nTesting with wrong password..."); try { await decryptMessage(encrypted, "wrongPassword"); logger.log("Should not reach here"); } catch (error) { logger.log("Correctly failed with wrong password ✅"); } return success; } catch (error) { logger.error("Test failed with error:", error); return false; } }