mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
DB 접근 시 Executor를 명시적으로 전달하고 서비스에서 트랜잭션 경계를 관리하도록 인증 및 키 관련 코드를 개선하고 직렬화 실패 시 트랜잭션을 재시도하도록 변경
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b8621e524
commit
73b2d29b2c
18 files changed
+564
-385
No files matched your search
@@ -1,5 +1,12 @@
|
||||
import argon2 from "argon2";
|
||||
import { ClientRepo, SessionRepo, UserRepo } from "#lib/server/db/index.ts";
|
||||
import {
|
||||
ClientRepo,
|
||||
SessionRepo,
|
||||
UserRepo,
|
||||
db,
|
||||
isUniqueViolation,
|
||||
transaction,
|
||||
} from "#lib/server/db/index.ts";
|
||||
import env from "#lib/server/config.ts";
|
||||
import { issueSessionId } from "#lib/server/crypto.ts";
|
||||
import { AppError, invalidSessionError } from "#lib/server/errors.ts";
|
||||
@@ -8,18 +15,18 @@ import { createChallenge, verifyChallengeAnswer } from "./client.ts";
|
||||
const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
export const login = async (email: string, password: string, ip: string, userAgent: string) => {
|
||||
const user = await UserRepo.getUserByEmail(email);
|
||||
const user = await UserRepo.getUserByEmail(db, email);
|
||||
if (!user || !(await argon2.verify(user.password, password))) {
|
||||
throw new AppError("UNAUTHORIZED", "Invalid email or password");
|
||||
}
|
||||
|
||||
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
|
||||
await SessionRepo.createSession(user.id, sessionId, ip, userAgent);
|
||||
await SessionRepo.createSession(db, user.id, sessionId, ip, userAgent);
|
||||
return { sessionIdSigned };
|
||||
};
|
||||
|
||||
export const logout = async (sessionId: string) => {
|
||||
await SessionRepo.deleteSession(sessionId);
|
||||
await SessionRepo.deleteSession(db, sessionId);
|
||||
};
|
||||
|
||||
export const changePassword = async (
|
||||
@@ -34,15 +41,18 @@ export const changePassword = async (
|
||||
throw new AppError("BAD_REQUEST", "Too short password");
|
||||
}
|
||||
|
||||
const user = await UserRepo.getUser(userId);
|
||||
const user = await UserRepo.getUser(db, userId);
|
||||
if (!user) {
|
||||
throw invalidSessionError();
|
||||
} else if (!(await argon2.verify(user.password, oldPassword))) {
|
||||
throw new AppError("FORBIDDEN", "Invalid password");
|
||||
}
|
||||
|
||||
await UserRepo.setUserPassword(userId, await argon2.hash(newPassword));
|
||||
await SessionRepo.deleteAllOtherSessions(userId, sessionId);
|
||||
const passwordHash = await argon2.hash(newPassword);
|
||||
await transaction(async (trx) => {
|
||||
await UserRepo.setUserPassword(trx, userId, passwordHash);
|
||||
await SessionRepo.deleteOtherSessions(trx, userId, sessionId);
|
||||
});
|
||||
};
|
||||
|
||||
export const requestSessionUpgrade = async (
|
||||
@@ -52,25 +62,33 @@ export const requestSessionUpgrade = async (
|
||||
encPubKey: string,
|
||||
sigPubKey: string,
|
||||
) => {
|
||||
const client = await ClientRepo.getClientByPubKeys(encPubKey, sigPubKey);
|
||||
const client = await ClientRepo.getClientByPubKeys(db, encPubKey, sigPubKey);
|
||||
if (!client) {
|
||||
throw new AppError("UNAUTHORIZED", "Invalid public key(s)");
|
||||
}
|
||||
|
||||
const userClient = await ClientRepo.getUserClient(userId, client.id);
|
||||
const userClient = await ClientRepo.getUserClient(db, userId, client.id);
|
||||
if (!userClient || userClient.state === "challenging") {
|
||||
throw new AppError("FORBIDDEN", "Unregistered client");
|
||||
}
|
||||
|
||||
return await createChallenge(encPubKey, (answer) =>
|
||||
SessionRepo.registerSessionUpgradeChallenge(
|
||||
sessionId,
|
||||
client.id,
|
||||
answer,
|
||||
ip,
|
||||
new Date(Date.now() + env.challenge.sessionUpgradeExp),
|
||||
),
|
||||
);
|
||||
try {
|
||||
return await createChallenge(encPubKey, (answer) =>
|
||||
SessionRepo.createSessionUpgradeChallenge(
|
||||
db,
|
||||
sessionId,
|
||||
client.id,
|
||||
answer,
|
||||
ip,
|
||||
new Date(Date.now() + env.challenge.sessionUpgradeExp),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e)) {
|
||||
throw new AppError("CONFLICT", "Challenge already registered");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifySessionUpgrade = async (
|
||||
@@ -81,14 +99,34 @@ export const verifySessionUpgrade = async (
|
||||
answerSig: string,
|
||||
force: boolean,
|
||||
) => {
|
||||
const challenge = await SessionRepo.consumeSessionUpgradeChallenge(challengeId, sessionId, ip);
|
||||
const challenge = await SessionRepo.consumeSessionUpgradeChallenge(
|
||||
db,
|
||||
challengeId,
|
||||
sessionId,
|
||||
ip,
|
||||
);
|
||||
const client = await verifyChallengeAnswer(challenge, answerSig);
|
||||
await SessionRepo.upgradeSession(userId, sessionId, client.id, force);
|
||||
|
||||
try {
|
||||
await transaction(async (trx) => {
|
||||
if (force) {
|
||||
await SessionRepo.deleteOtherSessions(trx, userId, sessionId, client.id);
|
||||
}
|
||||
if (!(await SessionRepo.setSessionClient(trx, sessionId, client.id))) {
|
||||
throw new AppError("UNAUTHORIZED", "Session not found");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e, "session_ak01")) {
|
||||
throw new AppError("CONFLICT", "Already logged in");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanupExpiredSessions = async () => {
|
||||
await Promise.all([
|
||||
SessionRepo.cleanupExpiredSessions(),
|
||||
SessionRepo.cleanupExpiredSessionUpgradeChallenges(),
|
||||
SessionRepo.deleteExpiredSessions(db),
|
||||
SessionRepo.deleteExpiredSessionUpgradeChallenges(db),
|
||||
]);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ClientRepo } from "#lib/server/db/index.ts";
|
||||
import { ClientRepo, db, isUniqueViolation, transaction } from "#lib/server/db/index.ts";
|
||||
import env from "#lib/server/config.ts";
|
||||
import { generateChallenge, verifyPubKey, verifySignature } from "#lib/server/crypto.ts";
|
||||
import { AppError } from "#lib/server/errors.ts";
|
||||
@@ -22,7 +22,7 @@ export const verifyChallengeAnswer = async (
|
||||
throw new AppError("FORBIDDEN", "Invalid challenge answer");
|
||||
}
|
||||
|
||||
const client = await ClientRepo.getClient(challenge.clientId);
|
||||
const client = await ClientRepo.getClient(db, challenge.clientId);
|
||||
if (!client) {
|
||||
throw new AppError("INTERNAL_SERVER_ERROR", "Invalid challenge answer");
|
||||
} else if (
|
||||
@@ -33,28 +33,66 @@ export const verifyChallengeAnswer = async (
|
||||
return client;
|
||||
};
|
||||
|
||||
const addExistingClient = async (userId: number, clientId: number) => {
|
||||
try {
|
||||
await ClientRepo.createUserClient(db, userId, clientId);
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e)) {
|
||||
throw new AppError("CONFLICT", "User client already exists");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const addNewClient = async (userId: number, encPubKey: string, sigPubKey: string) => {
|
||||
if (encPubKey === sigPubKey) {
|
||||
throw new AppError("BAD_REQUEST", "Same public keys");
|
||||
} else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
|
||||
throw new AppError("BAD_REQUEST", "Invalid public key(s)");
|
||||
}
|
||||
|
||||
const publicKeysRegistered = () => new AppError("CONFLICT", "Public key(s) already registered");
|
||||
try {
|
||||
// Serializable, as a key must not be used as either key of another client, which can't be
|
||||
// ensured by unique constraints
|
||||
return await transaction(
|
||||
async (trx) => {
|
||||
if (await ClientRepo.isAnyPublicKeyRegistered(trx, encPubKey, sigPubKey)) {
|
||||
throw publicKeysRegistered();
|
||||
}
|
||||
|
||||
const { id } = await ClientRepo.createClient(trx, encPubKey, sigPubKey);
|
||||
await ClientRepo.createUserClient(trx, userId, id);
|
||||
return id;
|
||||
},
|
||||
{ isolationLevel: "serializable" },
|
||||
);
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e)) {
|
||||
throw publicKeysRegistered();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const registerClient = async (
|
||||
userId: number,
|
||||
ip: string,
|
||||
encPubKey: string,
|
||||
sigPubKey: string,
|
||||
) => {
|
||||
const getClientId = async () => {
|
||||
const client = await ClientRepo.getClientByPubKeys(encPubKey, sigPubKey);
|
||||
if (client) {
|
||||
await ClientRepo.createUserClient(userId, client.id);
|
||||
return client.id;
|
||||
} else if (encPubKey === sigPubKey) {
|
||||
throw new AppError("BAD_REQUEST", "Same public keys");
|
||||
} else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
|
||||
throw new AppError("BAD_REQUEST", "Invalid public key(s)");
|
||||
}
|
||||
return (await ClientRepo.createClient(encPubKey, sigPubKey, userId)).id;
|
||||
};
|
||||
const client = await ClientRepo.getClientByPubKeys(db, encPubKey, sigPubKey);
|
||||
let clientId;
|
||||
if (client) {
|
||||
await addExistingClient(userId, client.id);
|
||||
clientId = client.id;
|
||||
} else {
|
||||
clientId = await addNewClient(userId, encPubKey, sigPubKey);
|
||||
}
|
||||
|
||||
const clientId = await getClientId();
|
||||
return await createChallenge(encPubKey, (answer) =>
|
||||
ClientRepo.registerUserClientChallenge(
|
||||
ClientRepo.createUserClientChallenge(
|
||||
db,
|
||||
userId,
|
||||
clientId,
|
||||
answer,
|
||||
@@ -70,11 +108,14 @@ export const verifyClient = async (
|
||||
challengeId: number,
|
||||
answerSig: string,
|
||||
) => {
|
||||
const challenge = await ClientRepo.consumeUserClientChallenge(challengeId, userId, ip);
|
||||
const challenge = await ClientRepo.consumeUserClientChallenge(db, challengeId, userId, ip);
|
||||
const client = await verifyChallengeAnswer(challenge, answerSig);
|
||||
await ClientRepo.setUserClientStateToPending(userId, client.id);
|
||||
await ClientRepo.transitionUserClientState(db, userId, client.id, {
|
||||
from: "challenging",
|
||||
to: "pending",
|
||||
});
|
||||
};
|
||||
|
||||
export const cleanupExpiredChallenges = async () => {
|
||||
await ClientRepo.cleanupExpiredUserClientChallenges();
|
||||
await ClientRepo.deleteExpiredUserClientChallenges(db);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HskRepo, MekRepo, type Tx } from "#lib/server/db/index.ts";
|
||||
import { AppError } from "#lib/server/errors.ts";
|
||||
|
||||
const DEK_VERSION_TOLERANCE = 60 * 1000; // 1 minute
|
||||
@@ -8,3 +9,17 @@ export const ensureFreshDekVersion = (dekVersion: Date) => {
|
||||
throw new AppError("BAD_REQUEST", "Invalid DEK version");
|
||||
}
|
||||
};
|
||||
|
||||
// New data must be encrypted with the active keys, which are locked until the transaction ends
|
||||
|
||||
export const ensureActiveMekVersion = async (trx: Tx, userId: number, mekVersion: number) => {
|
||||
if ((await MekRepo.lockActiveMekVersion(trx, userId)) !== mekVersion) {
|
||||
throw new AppError("BAD_REQUEST", "Inactive MEK version");
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureActiveHskVersion = async (trx: Tx, userId: number, hskVersion: number) => {
|
||||
if ((await HskRepo.lockActiveHskVersion(trx, userId)) !== hskVersion) {
|
||||
throw new AppError("BAD_REQUEST", "Inactive HSK version");
|
||||
}
|
||||
};
|
||||
@@ -1,26 +1,34 @@
|
||||
import { ClientRepo, HskRepo, MekRepo } from "#lib/server/db/index.ts";
|
||||
import {
|
||||
ClientRepo,
|
||||
HskRepo,
|
||||
LogRepo,
|
||||
MekRepo,
|
||||
db,
|
||||
isUniqueViolation,
|
||||
transaction,
|
||||
} from "#lib/server/db/index.ts";
|
||||
import { verifySignature } from "#lib/server/crypto.ts";
|
||||
import { AppError, invalidSessionError } from "#lib/server/errors.ts";
|
||||
import { serializeMasterKeyWrapped } from "#lib/shared/crypto/index.ts";
|
||||
import { ensureActiveMekVersion } from "./common.ts";
|
||||
|
||||
export const getClientMeks = async (userId: number, clientId: number) => {
|
||||
return await MekRepo.getAllValidClientMeks(userId, clientId);
|
||||
return await MekRepo.getAllValidClientMeks(db, userId, clientId);
|
||||
};
|
||||
|
||||
const verifyClientEncMekSig = async (
|
||||
userId: number,
|
||||
clientId: number,
|
||||
version: number,
|
||||
encMek: string,
|
||||
encMekSig: string,
|
||||
) => {
|
||||
const userClient = await ClientRepo.getUserClientWithDetails(userId, clientId);
|
||||
if (!userClient) {
|
||||
const client = await ClientRepo.getClient(db, clientId);
|
||||
if (!client) {
|
||||
throw invalidSessionError();
|
||||
}
|
||||
|
||||
const data = serializeMasterKeyWrapped(encMek, version);
|
||||
return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey);
|
||||
return verifySignature(Buffer.from(data), encMekSig, client.sigPubKey);
|
||||
};
|
||||
|
||||
export const registerInitialMek = async (
|
||||
@@ -29,16 +37,35 @@ export const registerInitialMek = async (
|
||||
encMek: string,
|
||||
encMekSig: string,
|
||||
) => {
|
||||
if (!(await verifyClientEncMekSig(userId, clientId, 1, encMek, encMekSig))) {
|
||||
if (!(await verifyClientEncMekSig(clientId, 1, encMek, encMekSig))) {
|
||||
throw new AppError("BAD_REQUEST", "Invalid signature");
|
||||
}
|
||||
|
||||
await MekRepo.registerInitialMek(userId, clientId, encMek, encMekSig);
|
||||
await ClientRepo.setUserClientStateToActive(userId, clientId);
|
||||
try {
|
||||
// The client that registered the master key is approved to access it at the same time
|
||||
await transaction(async (trx) => {
|
||||
const { version } = await MekRepo.createInitialMek(trx, userId, clientId, encMek, encMekSig);
|
||||
await LogRepo.appendMekLog(trx, {
|
||||
user_id: userId,
|
||||
master_encryption_key_version: version,
|
||||
action: "create",
|
||||
action_by: clientId,
|
||||
});
|
||||
await ClientRepo.transitionUserClientState(trx, userId, clientId, {
|
||||
from: "pending",
|
||||
to: "active",
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e)) {
|
||||
throw new AppError("CONFLICT", "MEK already registered");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getHsks = async (userId: number) => {
|
||||
return await HskRepo.getAllValidHsks(userId);
|
||||
return await HskRepo.getAllValidHsks(db, userId);
|
||||
};
|
||||
|
||||
export const registerInitialHsk = async (
|
||||
@@ -47,5 +74,21 @@ export const registerInitialHsk = async (
|
||||
mekVersion: number,
|
||||
encHsk: string,
|
||||
) => {
|
||||
await HskRepo.registerInitialHsk(userId, clientId, mekVersion, encHsk);
|
||||
try {
|
||||
await transaction(async (trx) => {
|
||||
await ensureActiveMekVersion(trx, userId, mekVersion);
|
||||
const { version } = await HskRepo.createInitialHsk(trx, userId, mekVersion, encHsk);
|
||||
await LogRepo.appendHskLog(trx, {
|
||||
user_id: userId,
|
||||
hmac_secret_key_version: version,
|
||||
action: "create",
|
||||
action_by: clientId,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e)) {
|
||||
throw new AppError("CONFLICT", "HSK already registered");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UserRepo } from "#lib/server/db/index.ts";
|
||||
import { UserRepo, db } from "#lib/server/db/index.ts";
|
||||
import { invalidSessionError } from "#lib/server/errors.ts";
|
||||
|
||||
export const getUser = async (userId: number) => {
|
||||
const user = await UserRepo.getUser(userId);
|
||||
const user = await UserRepo.getUser(db, userId);
|
||||
if (!user) {
|
||||
throw invalidSessionError();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user