"use node"; import { action } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import crypto from "crypto"; export const resetPasswordAction = action({ args: { username: v.string(), newSalt: v.string(), newEncryptedMK: v.string(), newHAK: v.string(), signature: v.string(), timestamp: v.number(), }, returns: v.union( v.object({ success: v.boolean() }), v.object({ error: v.string() }) ), handler: async (ctx, args) => { // Validate timestamp is within 5 minutes const now = Date.now(); if (Math.abs(now - args.timestamp) > 5 * 60 * 1000) { return { error: "Request expired. Please try again." }; } // Get user data const userData = await ctx.runQuery(internal.auth.getUserForRecovery, { username: args.username, }); if (!userData) { return { error: "User not found" }; } // Verify Ed25519 signature const message = `password-reset:${args.username}:${args.timestamp}`; try { const publicKeyObj = crypto.createPublicKey({ key: userData.publicSigningKey, format: "pem", type: "spki", }); const isValid = crypto.verify( null, Buffer.from(message), publicKeyObj, Buffer.from(args.signature, "hex") ); if (!isValid) { return { error: "Invalid recovery key" }; } } catch (e) { return { error: "Signature verification failed" }; } // Update credentials await ctx.runMutation(internal.auth.updateCredentials, { userId: userData.userId, clientSalt: args.newSalt, encryptedMasterKey: args.newEncryptedMK, hashedAuthKey: args.newHAK, }); return { success: true }; }, });