Added recovery keys

This commit is contained in:
Bryan1029384756
2026-02-18 09:24:53 -06:00
parent bebf0bf989
commit ce9902d95d
16 changed files with 642 additions and 44 deletions

View File

@@ -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;

View File

@@ -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
View 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 };
},
});