Added recovery keys
This commit is contained in:
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -22,6 +22,7 @@ import type * as messages from "../messages.js";
|
||||
import type * as presence from "../presence.js";
|
||||
import type * as reactions from "../reactions.js";
|
||||
import type * as readState from "../readState.js";
|
||||
import type * as recovery from "../recovery.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as serverSettings from "../serverSettings.js";
|
||||
import type * as storageUrl from "../storageUrl.js";
|
||||
@@ -50,6 +51,7 @@ declare const fullApi: ApiFromModules<{
|
||||
presence: typeof presence;
|
||||
reactions: typeof reactions;
|
||||
readState: typeof readState;
|
||||
recovery: typeof recovery;
|
||||
roles: typeof roles;
|
||||
serverSettings: typeof serverSettings;
|
||||
storageUrl: typeof storageUrl;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { query, mutation, internalQuery, internalMutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { getPublicStorageUrl } from "./storageUrl";
|
||||
|
||||
@@ -284,3 +284,74 @@ export const updateStatus = mutation({
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Get encrypted private keys + public signing key for recovery
|
||||
export const getRecoveryData = query({
|
||||
args: { username: v.string() },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
encryptedPrivateKeys: v.string(),
|
||||
publicSigningKey: v.string(),
|
||||
}),
|
||||
v.object({ error: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await ctx.db
|
||||
.query("userProfiles")
|
||||
.withIndex("by_username", (q) => q.eq("username", args.username))
|
||||
.unique();
|
||||
|
||||
if (!user) {
|
||||
return { error: "User not found" };
|
||||
}
|
||||
|
||||
return {
|
||||
encryptedPrivateKeys: user.encryptedPrivateKeys,
|
||||
publicSigningKey: user.publicSigningKey,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Internal: get userId + publicSigningKey for recovery action verification
|
||||
export const getUserForRecovery = internalQuery({
|
||||
args: { username: v.string() },
|
||||
returns: v.union(
|
||||
v.object({
|
||||
userId: v.id("userProfiles"),
|
||||
publicSigningKey: v.string(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const user = await ctx.db
|
||||
.query("userProfiles")
|
||||
.withIndex("by_username", (q) => q.eq("username", args.username))
|
||||
.unique();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
userId: user._id,
|
||||
publicSigningKey: user.publicSigningKey,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Internal: update credentials after password reset
|
||||
export const updateCredentials = internalMutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
clientSalt: v.string(),
|
||||
encryptedMasterKey: v.string(),
|
||||
hashedAuthKey: v.string(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.userId, {
|
||||
clientSalt: args.clientSalt,
|
||||
encryptedMasterKey: args.encryptedMasterKey,
|
||||
hashedAuthKey: args.hashedAuthKey,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
70
convex/recovery.ts
Normal file
70
convex/recovery.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
"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 };
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user