authUtils.js
1 import axios from 'axios'; 2 import { argon2id } from 'hash-wasm'; 3 import CryptoJS from 'crypto-js'; 4 import { getProxyUrl, getApiConfig, handleApiError } from './apiUtils'; 5 6 /** 7 * Get a puzzle from the auth service 8 * @returns {Promise<Object>} Puzzle object or error 9 */ 10 export const getPuzzle = async () => { 11 try { 12 const proxyUrl = getProxyUrl(); 13 const response = await axios.get(`${proxyUrl}/auth/puzzle`, getApiConfig()); 14 return { success: true, data: response.data }; 15 } catch (error) { 16 return { success: false, error: handleApiError(error) }; 17 } 18 }; 19 20 /** 21 * Get auth service status 22 * @returns {Promise<Object>} Status object or error 23 */ 24 export const getAuthStatus = async () => { 25 try { 26 const proxyUrl = getProxyUrl(); 27 const response = await axios.get(`${proxyUrl}/auth/status`, getApiConfig()); 28 return { success: true, data: response.data }; 29 } catch (error) { 30 return { success: false, error: handleApiError(error) }; 31 } 32 }; 33 34 /** 35 * Verify a JWT token 36 * @param {string} token - JWT token to verify 37 * @returns {Promise<Object>} Verification result or error 38 */ 39 export const verifyToken = async (token) => { 40 try { 41 const proxyUrl = getProxyUrl(); 42 const response = await axios.get(`${proxyUrl}/auth/verify`, getApiConfig(token)); 43 return { 44 success: true, 45 data: { 46 status: response.status, 47 headers: response.headers, 48 data: response.data || 'Token valid' 49 } 50 }; 51 } catch (error) { 52 return { 53 success: false, 54 error: { 55 status: error.response?.status || 'Network Error', 56 headers: error.response?.headers || {}, 57 data: error.response?.data || error.message 58 } 59 }; 60 } 61 }; 62 63 /** 64 * Submit a puzzle solution to get JWT token 65 * @param {Object} solution - Puzzle solution object 66 * @returns {Promise<Object>} JWT token result or error 67 */ 68 export const submitSolution = async (solution) => { 69 try { 70 const proxyUrl = getProxyUrl(); 71 const response = await axios.post(`${proxyUrl}/auth/solve`, solution, getApiConfig()); 72 return { success: true, token: response.data.token }; 73 } catch (error) { 74 return { success: false, error: handleApiError(error) }; 75 } 76 }; 77 78 /** 79 * Check if hash meets difficulty requirement 80 * @param {string} hash - Hash to check 81 * @param {number} difficulty - Required difficulty (number of leading zeros) 82 * @returns {boolean} Whether hash meets difficulty 83 */ 84 export const checkDifficulty = (hash, difficulty) => { 85 if (hash.length < difficulty) return false; 86 for (let i = 0; i < difficulty; i++) { 87 if (hash[i] !== '0') return false; 88 } 89 return true; 90 }; 91 92 /** 93 * Compute HMAC-SHA256 94 * @param {string} data - Data to hash 95 * @param {string} secret - Secret key 96 * @returns {string} HMAC hex string 97 */ 98 export const computeHMAC = (data, secret) => { 99 const hmac = CryptoJS.HmacSHA256(data, secret); 100 return hmac.toString(CryptoJS.enc.Hex); 101 }; 102 103 /** 104 * Convert hex string to Uint8Array 105 * @param {string} hex - Hex string 106 * @returns {Uint8Array} Byte array 107 */ 108 export const hexToUint8Array = (hex) => { 109 const bytes = new Uint8Array(hex.length / 2); 110 for (let i = 0; i < hex.length; i += 2) { 111 bytes[i / 2] = parseInt(hex.substr(i, 2), 16); 112 } 113 return bytes; 114 }; 115 116 /** 117 * Solve puzzle using Argon2 118 * @param {Object} puzzle - Puzzle object from auth service 119 * @param {Function} onProgress - Progress callback (optional) 120 * @param {number} maxAttempts - Maximum attempts (default: 100000) 121 * @returns {Promise<Object>} Solution object or error 122 */ 123 export const solvePuzzle = async (puzzle, onProgress = null, maxAttempts = 100000) => { 124 const startTime = Date.now(); 125 126 try { 127 const { challenge, salt, difficulty, argon2_params } = puzzle; 128 129 // Convert salt from hex to Uint8Array 130 const saltBytes = hexToUint8Array(salt); 131 132 for (let nonce = 0; nonce < maxAttempts; nonce++) { 133 // Create input: challenge + salt + nonce 134 const input = `${challenge}${salt}${nonce}`; 135 136 try { 137 // Compute Argon2id hash using hash-wasm 138 const argonHash = await argon2id({ 139 password: input, 140 salt: saltBytes, 141 parallelism: argon2_params.threads, 142 iterations: argon2_params.time, 143 memorySize: argon2_params.memory_kb, 144 hashLength: argon2_params.key_len, 145 outputType: 'hex' 146 }); 147 148 // Check if this hash meets the difficulty requirement 149 if (checkDifficulty(argonHash, difficulty)) { 150 const endTime = Date.now(); 151 const solveTime = ((endTime - startTime) / 1000).toFixed(2); 152 153 const solution = { 154 challenge, 155 salt, 156 nonce, 157 argon_hash: argonHash, 158 hmac: puzzle.hmac, // Always use puzzle HMAC 159 expires_at: puzzle.expires_at 160 }; 161 162 return { 163 success: true, 164 solution, 165 solveTime: parseFloat(solveTime), 166 attempts: nonce + 1 167 }; 168 } 169 } catch (error) { 170 console.error('Argon2 computation error:', error); 171 continue; 172 } 173 174 // Report progress every 1000 attempts 175 if (nonce % 1000 === 0 && onProgress) { 176 onProgress(nonce, maxAttempts); 177 // Allow UI to update 178 await new Promise(resolve => setTimeout(resolve, 1)); 179 } 180 } 181 182 const endTime = Date.now(); 183 const solveTime = ((endTime - startTime) / 1000).toFixed(2); 184 return { 185 success: false, 186 error: { 187 message: `Failed to solve puzzle within ${maxAttempts} attempts`, 188 solveTime: parseFloat(solveTime) 189 } 190 }; 191 } catch (error) { 192 const endTime = Date.now(); 193 const solveTime = ((endTime - startTime) / 1000).toFixed(2); 194 return { 195 success: false, 196 error: { 197 message: error.message, 198 solveTime: parseFloat(solveTime) 199 } 200 }; 201 } 202 }; 203 204 /** 205 * Create solution from debug data 206 * @param {Object} puzzle - Puzzle object 207 * @returns {Object} Debug solution object 208 */ 209 export const createDebugSolution = (puzzle) => { 210 if (!puzzle.debug_solution) { 211 throw new Error('No debug solution available'); 212 } 213 214 return { 215 challenge: puzzle.challenge, 216 salt: puzzle.salt, 217 nonce: puzzle.debug_solution.nonce, 218 argon_hash: puzzle.debug_solution.argon_hash, 219 hmac: puzzle.hmac, // Always use puzzle HMAC 220 expires_at: puzzle.expires_at 221 }; 222 }; 223 224 /** 225 * Complete puzzle solving and token generation workflow 226 * @param {Function} onProgress - Progress callback for puzzle solving 227 * @param {Function} onStatusUpdate - Status update callback 228 * @returns {Promise<Object>} JWT token result or error 229 */ 230 export const generateJwtToken = async (onProgress = null, onStatusUpdate = null) => { 231 try { 232 // Step 1: Get puzzle 233 if (onStatusUpdate) onStatusUpdate('Getting puzzle...'); 234 const puzzleResult = await getPuzzle(); 235 if (!puzzleResult.success) { 236 return { success: false, error: puzzleResult.error }; 237 } 238 239 // Step 2: Solve puzzle 240 if (onStatusUpdate) onStatusUpdate('Solving puzzle...'); 241 const solveResult = await solvePuzzle(puzzleResult.data, onProgress); 242 if (!solveResult.success) { 243 return { success: false, error: solveResult.error }; 244 } 245 246 // Step 3: Submit solution 247 if (onStatusUpdate) onStatusUpdate('Submitting solution...'); 248 const submitResult = await submitSolution(solveResult.solution); 249 if (!submitResult.success) { 250 return { success: false, error: submitResult.error }; 251 } 252 253 if (onStatusUpdate) onStatusUpdate('JWT token generated successfully!'); 254 return { 255 success: true, 256 token: submitResult.token, 257 puzzle: puzzleResult.data, 258 solution: solveResult.solution, 259 solveTime: solveResult.solveTime, 260 attempts: solveResult.attempts 261 }; 262 } catch (error) { 263 return { success: false, error: { message: error.message } }; 264 } 265 };