/ test-api / src / utils / authUtils.js
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  };