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, } from "@/libs/endorserServer"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; 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 ""; } }; /** @return payload of JWT pulled out of any recognized URL path (if any) */ export const getContactJwtFromJwtUrl = (jwtUrlText: string) => { let jwtText = jwtUrlText; const appImportConfirmUrlLoc = jwtText.indexOf( CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, ); if (appImportConfirmUrlLoc > -1) { jwtText = jwtText.substring( appImportConfirmUrlLoc + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI.length, ); } const appImportOneUrlLoc = jwtText.indexOf( CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI, ); if (appImportOneUrlLoc > -1) { jwtText = jwtText.substring( appImportOneUrlLoc + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI.length, ); } const endorserUrlPathLoc = jwtText.indexOf(CONTACT_URL_PATH_ENDORSER_CH_OLD); if (endorserUrlPathLoc > -1) { jwtText = jwtText.substring( endorserUrlPathLoc + CONTACT_URL_PATH_ENDORSER_CH_OLD.length, ); } return jwtText; }; 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"; console.log("Original message:", testMessage); // Test encryption console.log("Encrypting..."); const encrypted = await encryptMessage(testMessage, testPassword); console.log("Encrypted result:", encrypted); // Test decryption console.log("Decrypting..."); const decrypted = await decryptMessage(encrypted, testPassword); console.log("Decrypted result:", decrypted); // Verify const success = testMessage === decrypted; console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌")); console.log("Messages match:", success); // Test with wrong password console.log("\nTesting with wrong password..."); try { await decryptMessage(encrypted, "wrongPassword"); console.log("Should not reach here"); } catch (error) { console.log("Correctly failed with wrong password ✅"); } return success; } catch (error) { console.error("Test failed with error:", error); return false; } }