4 Commits

38 changed files with 430 additions and 598 deletions

View File

@@ -98,22 +98,6 @@ export const createUserClient = async (userId: number, clientId: number) => {
} }
}; };
export const getAllUserClients = async (userId: number) => {
const userClients = await db
.selectFrom("user_client")
.selectAll()
.where("user_id", "=", userId)
.execute();
return userClients.map(
({ user_id, client_id, state }) =>
({
userId: user_id,
clientId: client_id,
state,
}) satisfies UserClient,
);
};
export const getUserClient = async (userId: number, clientId: number) => { export const getUserClient = async (userId: number, clientId: number) => {
const userClient = await db const userClient = await db
.selectFrom("user_client") .selectFrom("user_client")

View File

@@ -0,0 +1,10 @@
export * as CategoryRepo from "./category";
export * as ClientRepo from "./client";
export * as FileRepo from "./file";
export * as HskRepo from "./hsk";
export * as MediaRepo from "./media";
export * as MekRepo from "./mek";
export * as SessionRepo from "./session";
export * as UserRepo from "./user";
export * from "./error";

View File

@@ -60,19 +60,6 @@ export const registerInitialMek = async (
}); });
}; };
export const getInitialMek = async (userId: number) => {
const mek = await db
.selectFrom("master_encryption_key")
.selectAll()
.where("user_id", "=", userId)
.where("version", "=", 1)
.limit(1)
.executeTakeFirst();
return mek
? ({ userId: mek.user_id, version: mek.version, state: mek.state } satisfies Mek)
: null;
};
export const getAllValidClientMeks = async (userId: number, clientId: number) => { export const getAllValidClientMeks = async (userId: number, clientId: number) => {
const clientMeks = await db const clientMeks = await db
.selectFrom("client_master_encryption_key") .selectFrom("client_master_encryption_key")

View File

@@ -4,7 +4,7 @@ import { authenticate, AuthenticationError } from "$lib/server/modules/auth";
export const authenticateMiddleware: Handle = async ({ event, resolve }) => { export const authenticateMiddleware: Handle = async ({ event, resolve }) => {
const { pathname, search } = event.url; const { pathname, search } = event.url;
if (pathname === "/api/auth/login") { if (pathname === "/api/auth/login" || pathname.startsWith("/api/trpc")) {
return await resolve(event); return await resolve(event);
} }

View File

@@ -11,10 +11,17 @@ interface Session {
clientId?: number; clientId?: number;
} }
interface ClientSession extends Session { export interface ClientSession extends Session {
clientId: number; clientId: number;
} }
export type SessionPermission =
| "any"
| "notClient"
| "anyClient"
| "pendingClient"
| "activeClient";
export class AuthenticationError extends Error { export class AuthenticationError extends Error {
constructor( constructor(
public status: 400 | 401, public status: 400 | 401,
@@ -25,6 +32,16 @@ export class AuthenticationError extends Error {
} }
} }
export class AuthorizationError extends Error {
constructor(
public status: 403 | 500,
message: string,
) {
super(message);
this.name = "AuthorizationError";
}
}
export const startSession = async (userId: number, ip: string, userAgent: string) => { export const startSession = async (userId: number, ip: string, userAgent: string) => {
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret); const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
await createSession(userId, sessionId, ip, userAgent); await createSession(userId, sessionId, ip, userAgent);
@@ -52,34 +69,12 @@ export const authenticate = async (sessionIdSigned: string, ip: string, userAgen
} }
}; };
export async function authorize(locals: App.Locals, requiredPermission: "any"): Promise<Session>; export const authorizeInternal = async (
export async function authorize(
locals: App.Locals, locals: App.Locals,
requiredPermission: "notClient", requiredPermission: SessionPermission,
): Promise<Session>; ): Promise<Session> => {
export async function authorize(
locals: App.Locals,
requiredPermission: "anyClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "pendingClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "activeClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "any" | "notClient" | "anyClient" | "pendingClient" | "activeClient",
): Promise<Session> {
if (!locals.session) { if (!locals.session) {
error(500, "Unauthenticated"); throw new AuthorizationError(500, "Unauthenticated");
} }
const { id: sessionId, userId, clientId } = locals.session; const { id: sessionId, userId, clientId } = locals.session;
@@ -89,39 +84,63 @@ export async function authorize(
break; break;
case "notClient": case "notClient":
if (clientId) { if (clientId) {
error(403, "Forbidden"); throw new AuthorizationError(403, "Forbidden");
} }
break; break;
case "anyClient": case "anyClient":
if (!clientId) { if (!clientId) {
error(403, "Forbidden"); throw new AuthorizationError(403, "Forbidden");
} }
break; break;
case "pendingClient": { case "pendingClient": {
if (!clientId) { if (!clientId) {
error(403, "Forbidden"); throw new AuthorizationError(403, "Forbidden");
} }
const userClient = await getUserClient(userId, clientId); const userClient = await getUserClient(userId, clientId);
if (!userClient) { if (!userClient) {
error(500, "Invalid session id"); throw new AuthorizationError(500, "Invalid session id");
} else if (userClient.state !== "pending") { } else if (userClient.state !== "pending") {
error(403, "Forbidden"); throw new AuthorizationError(403, "Forbidden");
} }
break; break;
} }
case "activeClient": { case "activeClient": {
if (!clientId) { if (!clientId) {
error(403, "Forbidden"); throw new AuthorizationError(403, "Forbidden");
} }
const userClient = await getUserClient(userId, clientId); const userClient = await getUserClient(userId, clientId);
if (!userClient) { if (!userClient) {
error(500, "Invalid session id"); throw new AuthorizationError(500, "Invalid session id");
} else if (userClient.state !== "active") { } else if (userClient.state !== "active") {
error(403, "Forbidden"); throw new AuthorizationError(403, "Forbidden");
} }
break; break;
} }
} }
return { sessionId, userId, clientId }; return { sessionId, userId, clientId };
};
export async function authorize(
locals: App.Locals,
requiredPermission: "any" | "notClient",
): Promise<Session>;
export async function authorize(
locals: App.Locals,
requiredPermission: "anyClient" | "pendingClient" | "activeClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: SessionPermission,
): Promise<Session> {
try {
return await authorizeInternal(locals, requiredPermission);
} catch (e) {
if (e instanceof AuthorizationError) {
error(e.status, e.message);
}
throw e;
}
} }

View File

@@ -1,25 +0,0 @@
import { error } from "@sveltejs/kit";
import { getUserClientWithDetails } from "$lib/server/db/client";
import { getInitialMek } from "$lib/server/db/mek";
import { verifySignature } from "$lib/server/modules/crypto";
export const isInitialMekNeeded = async (userId: number) => {
const initialMek = await getInitialMek(userId);
return !initialMek;
};
export const verifyClientEncMekSig = async (
userId: number,
clientId: number,
version: number,
encMek: string,
encMekSig: string,
) => {
const userClient = await getUserClientWithDetails(userId, clientId);
if (!userClient) {
error(500, "Invalid session id");
}
const data = JSON.stringify({ version, key: encMek });
return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey);
};

View File

@@ -1,36 +0,0 @@
import { z } from "zod";
export const clientListResponse = z.object({
clients: z.array(
z.object({
id: z.number().int().positive(),
state: z.enum(["pending", "active"]),
}),
),
});
export type ClientListResponse = z.output<typeof clientListResponse>;
export const clientRegisterRequest = z.object({
encPubKey: z.string().base64().nonempty(),
sigPubKey: z.string().base64().nonempty(),
});
export type ClientRegisterRequest = z.input<typeof clientRegisterRequest>;
export const clientRegisterResponse = z.object({
id: z.number().int().positive(),
challenge: z.string().base64().nonempty(),
});
export type ClientRegisterResponse = z.output<typeof clientRegisterResponse>;
export const clientRegisterVerifyRequest = z.object({
id: z.number().int().positive(),
answerSig: z.string().base64().nonempty(),
});
export type ClientRegisterVerifyRequest = z.input<typeof clientRegisterVerifyRequest>;
export const clientStatusResponse = z.object({
id: z.number().int().positive(),
state: z.enum(["pending", "active"]),
isInitialMekNeeded: z.boolean(),
});
export type ClientStatusResponse = z.output<typeof clientStatusResponse>;

View File

@@ -1,19 +0,0 @@
import { z } from "zod";
export const hmacSecretListResponse = z.object({
hsks: z.array(
z.object({
version: z.number().int().positive(),
state: z.enum(["active"]),
mekVersion: z.number().int().positive(),
hsk: z.string().base64().nonempty(),
}),
),
});
export type HmacSecretListResponse = z.output<typeof hmacSecretListResponse>;
export const initialHmacSecretRegisterRequest = z.object({
mekVersion: z.number().int().positive(),
hsk: z.string().base64().nonempty(),
});
export type InitialHmacSecretRegisterRequest = z.input<typeof initialHmacSecretRegisterRequest>;

View File

@@ -1,8 +1,4 @@
export * from "./auth"; export * from "./auth";
export * from "./category"; export * from "./category";
export * from "./client";
export * from "./directory"; export * from "./directory";
export * from "./file"; export * from "./file";
export * from "./hsk";
export * from "./mek";
export * from "./user";

View File

@@ -1,19 +0,0 @@
import { z } from "zod";
export const masterKeyListResponse = z.object({
meks: z.array(
z.object({
version: z.number().int().positive(),
state: z.enum(["active", "retired"]),
mek: z.string().base64().nonempty(),
mekSig: z.string().base64().nonempty(),
}),
),
});
export type MasterKeyListResponse = z.output<typeof masterKeyListResponse>;
export const initialMasterKeyRegisterRequest = z.object({
mek: z.string().base64().nonempty(),
mekSig: z.string().base64().nonempty(),
});
export type InitialMasterKeyRegisterRequest = z.input<typeof initialMasterKeyRegisterRequest>;

View File

@@ -1,12 +0,0 @@
import { z } from "zod";
export const userInfoResponse = z.object({
email: z.string().email(),
nickname: z.string().nonempty(),
});
export type UserInfoResponse = z.output<typeof userInfoResponse>;
export const nicknameChangeRequest = z.object({
newNickname: z.string().trim().min(2).max(8),
});
export type NicknameChangeRequest = z.input<typeof nicknameChangeRequest>;

View File

@@ -1,116 +0,0 @@
import { error } from "@sveltejs/kit";
import {
createClient,
getClient,
getClientByPubKeys,
createUserClient,
getAllUserClients,
getUserClient,
setUserClientStateToPending,
registerUserClientChallenge,
consumeUserClientChallenge,
} from "$lib/server/db/client";
import { IntegrityError } from "$lib/server/db/error";
import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto";
import { isInitialMekNeeded } from "$lib/server/modules/mek";
import env from "$lib/server/loadenv";
export const getUserClientList = async (userId: number) => {
const userClients = await getAllUserClients(userId);
return {
userClients: userClients.map(({ clientId, state }) => ({
id: clientId,
state: state as "pending" | "active",
})),
};
};
const expiresAt = () => new Date(Date.now() + env.challenge.userClientExp);
const createUserClientChallenge = async (
ip: string,
userId: number,
clientId: number,
encPubKey: string,
) => {
const { answer, challenge } = await generateChallenge(32, encPubKey);
const { id } = await registerUserClientChallenge(
userId,
clientId,
answer.toString("base64"),
ip,
expiresAt(),
);
return { id, challenge: challenge.toString("base64") };
};
export const registerUserClient = async (
userId: number,
ip: string,
encPubKey: string,
sigPubKey: string,
) => {
const client = await getClientByPubKeys(encPubKey, sigPubKey);
if (client) {
try {
await createUserClient(userId, client.id);
return await createUserClientChallenge(ip, userId, client.id, encPubKey);
} catch (e) {
if (e instanceof IntegrityError && e.message === "User client already exists") {
error(409, "Client already registered");
}
throw e;
}
} else {
if (encPubKey === sigPubKey) {
error(400, "Same public keys");
} else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
error(400, "Invalid public key(s)");
}
try {
const { id: clientId } = await createClient(encPubKey, sigPubKey, userId);
return await createUserClientChallenge(ip, userId, clientId, encPubKey);
} catch (e) {
if (e instanceof IntegrityError && e.message === "Public key(s) already registered") {
error(409, "Public key(s) already used");
}
throw e;
}
}
};
export const verifyUserClient = async (
userId: number,
ip: string,
challengeId: number,
answerSig: string,
) => {
const challenge = await consumeUserClientChallenge(challengeId, userId, ip);
if (!challenge) {
error(403, "Invalid challenge answer");
}
const client = await getClient(challenge.clientId);
if (!client) {
error(500, "Invalid challenge answer");
} else if (
!verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey)
) {
error(403, "Invalid challenge answer signature");
}
await setUserClientStateToPending(userId, client.id);
};
export const getUserClientStatus = async (userId: number, clientId: number) => {
const userClient = await getUserClient(userId, clientId);
if (!userClient) {
error(500, "Invalid session id");
}
return {
state: userClient.state as "pending" | "active",
isInitialMekNeeded: await isInitialMekNeeded(userId),
};
};

View File

@@ -1,31 +0,0 @@
import { error } from "@sveltejs/kit";
import { IntegrityError } from "$lib/server/db/error";
import { registerInitialHsk, getAllValidHsks } from "$lib/server/db/hsk";
export const getHskList = async (userId: number) => {
const hsks = await getAllValidHsks(userId);
return {
encHsks: hsks.map(({ version, state, mekVersion, encHsk }) => ({
version,
state,
mekVersion,
encHsk,
})),
};
};
export const registerInitialActiveHsk = async (
userId: number,
createdBy: number,
mekVersion: number,
encHsk: string,
) => {
try {
await registerInitialHsk(userId, createdBy, mekVersion, encHsk);
} catch (e) {
if (e instanceof IntegrityError && e.message === "HSK already registered") {
error(409, "Initial HSK already registered");
}
throw e;
}
};

View File

@@ -1,38 +0,0 @@
import { error } from "@sveltejs/kit";
import { setUserClientStateToActive } from "$lib/server/db/client";
import { IntegrityError } from "$lib/server/db/error";
import { registerInitialMek, getAllValidClientMeks } from "$lib/server/db/mek";
import { verifyClientEncMekSig } from "$lib/server/modules/mek";
export const getClientMekList = async (userId: number, clientId: number) => {
const clientMeks = await getAllValidClientMeks(userId, clientId);
return {
encMeks: clientMeks.map(({ version, state, encMek, encMekSig }) => ({
version,
state,
encMek,
encMekSig,
})),
};
};
export const registerInitialActiveMek = async (
userId: number,
createdBy: number,
encMek: string,
encMekSig: string,
) => {
if (!(await verifyClientEncMekSig(userId, createdBy, 1, encMek, encMekSig))) {
error(400, "Invalid signature");
}
try {
await registerInitialMek(userId, createdBy, encMek, encMekSig);
await setUserClientStateToActive(userId, createdBy);
} catch (e) {
if (e instanceof IntegrityError && e.message === "MEK already registered") {
error(409, "Initial MEK already registered");
}
throw e;
}
};

View File

@@ -1,15 +0,0 @@
import { error } from "@sveltejs/kit";
import { getUser, setUserNickname } from "$lib/server/db/user";
export const getUserInformation = async (userId: number) => {
const user = await getUser(userId);
if (!user) {
error(500, "Invalid session id");
}
return { email: user.email, nickname: user.nickname };
};
export const changeNickname = async (userId: number, nickname: string) => {
await setUserNickname(userId, nickname);
};

View File

@@ -1,4 +1,4 @@
import { callGetApi, callPostApi } from "$lib/hooks"; import { TRPCClientError } from "@trpc/client";
import { storeMasterKeys } from "$lib/indexedDB"; import { storeMasterKeys } from "$lib/indexedDB";
import { import {
encodeToBase64, encodeToBase64,
@@ -9,16 +9,9 @@ import {
signMasterKeyWrapped, signMasterKeyWrapped,
verifyMasterKeyWrapped, verifyMasterKeyWrapped,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import type {
ClientRegisterRequest,
ClientRegisterResponse,
ClientRegisterVerifyRequest,
InitialHmacSecretRegisterRequest,
MasterKeyListResponse,
InitialMasterKeyRegisterRequest,
} from "$lib/server/schemas";
import { requestSessionUpgrade } from "$lib/services/auth"; import { requestSessionUpgrade } from "$lib/services/auth";
import { masterKeyStore, type ClientKeys } from "$lib/stores"; import { masterKeyStore, type ClientKeys } from "$lib/stores";
import { useTRPC } from "$trpc/client";
export const requestClientRegistration = async ( export const requestClientRegistration = async (
encryptKeyBase64: string, encryptKeyBase64: string,
@@ -26,21 +19,24 @@ export const requestClientRegistration = async (
verifyKeyBase64: string, verifyKeyBase64: string,
signKey: CryptoKey, signKey: CryptoKey,
) => { ) => {
let res = await callPostApi<ClientRegisterRequest>("/api/client/register", { const trpc = useTRPC();
encPubKey: encryptKeyBase64,
sigPubKey: verifyKeyBase64,
});
if (!res.ok) return false;
const { id, challenge }: ClientRegisterResponse = await res.json(); try {
const answer = await decryptChallenge(challenge, decryptKey); const { id, challenge } = await trpc.client.register.mutate({
const answerSig = await signMessageRSA(answer, signKey); encPubKey: encryptKeyBase64,
sigPubKey: verifyKeyBase64,
res = await callPostApi<ClientRegisterVerifyRequest>("/api/client/register/verify", { });
id, const answer = await decryptChallenge(challenge, decryptKey);
answerSig: encodeToBase64(answerSig), const answerSig = await signMessageRSA(answer, signKey);
}); await trpc.client.verify.mutate({
return res.ok; id,
answerSig: encodeToBase64(answerSig),
});
return true;
} catch {
// TODO: Error Handling
return false;
}
}; };
export const requestClientRegistrationAndSessionUpgrade = async ( export const requestClientRegistrationAndSessionUpgrade = async (
@@ -73,10 +69,16 @@ export const requestClientRegistrationAndSessionUpgrade = async (
}; };
export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => { export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => {
const res = await callGetApi("/api/mek/list"); const trpc = useTRPC();
if (!res.ok) return false;
let masterKeysWrapped;
try {
masterKeysWrapped = await trpc.mek.list.query();
} catch {
// TODO: Error Handling
return false;
}
const { meks: masterKeysWrapped }: MasterKeyListResponse = await res.json();
const masterKeys = await Promise.all( const masterKeys = await Promise.all(
masterKeysWrapped.map( masterKeysWrapped.map(
async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => { async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => {
@@ -108,17 +110,32 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async (
hmacSecretWrapped: string, hmacSecretWrapped: string,
signKey: CryptoKey, signKey: CryptoKey,
) => { ) => {
let res = await callPostApi<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", { const trpc = useTRPC();
mek: masterKeyWrapped,
mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), try {
}); await trpc.mek.registerInitial.mutate({
if (!res.ok) { mek: masterKeyWrapped,
return res.status === 403 || res.status === 409; mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey),
});
} catch (e) {
if (
e instanceof TRPCClientError &&
(e.data?.code === "FORBIDDEN" || e.data?.code === "CONFLICT")
) {
return true;
}
// TODO: Error Handling
return false;
} }
res = await callPostApi<InitialHmacSecretRegisterRequest>("/api/hsk/register/initial", { try {
mekVersion: 1, await trpc.hsk.registerInitial.mutate({
hsk: hmacSecretWrapped, mekVersion: 1,
}); hsk: hmacSecretWrapped,
return res.ok; });
return true;
} catch {
// TODO: Error Handling
return false;
}
}; };

View File

@@ -1,5 +1,5 @@
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { callGetApi, callPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import { storeHmacSecrets } from "$lib/indexedDB"; import { storeHmacSecrets } from "$lib/indexedDB";
import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto"; import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto";
import { import {
@@ -13,10 +13,10 @@ import type {
DirectoryRenameRequest, DirectoryRenameRequest,
DirectoryCreateRequest, DirectoryCreateRequest,
FileRenameRequest, FileRenameRequest,
HmacSecretListResponse,
DirectoryDeleteResponse, DirectoryDeleteResponse,
} from "$lib/server/schemas"; } from "$lib/server/schemas";
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
import { useTRPC } from "$trpc/client";
export interface SelectedEntry { export interface SelectedEntry {
type: "directory" | "file"; type: "directory" | "file";
@@ -40,10 +40,16 @@ export const useContext = () => {
export const requestHmacSecretDownload = async (masterKey: CryptoKey) => { export const requestHmacSecretDownload = async (masterKey: CryptoKey) => {
// TODO: MEK rotation // TODO: MEK rotation
const res = await callGetApi("/api/hsk/list"); const trpc = useTRPC();
if (!res.ok) return false;
let hmacSecretsWrapped;
try {
hmacSecretsWrapped = await trpc.hsk.list.query();
} catch {
// TODO: Error Handling
return false;
}
const { hsks: hmacSecretsWrapped }: HmacSecretListResponse = await res.json();
const hmacSecrets = await Promise.all( const hmacSecrets = await Promise.all(
hmacSecretsWrapped.map(async ({ version, state, hsk: hmacSecretWrapped }) => { hmacSecretsWrapped.map(async ({ version, state, hsk: hmacSecretWrapped }) => {
const { hmacSecret } = await unwrapHmacSecret(hmacSecretWrapped, masterKey); const { hmacSecret } = await unwrapHmacSecret(hmacSecretWrapped, masterKey);

View File

@@ -1,14 +1,14 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import { callGetApi } from "$lib/hooks"; import { useTRPC } from "$trpc/client";
import type { UserInfoResponse } from "$lib/server/schemas";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch }) => {
const res = await callGetApi("/api/user", fetch); const trpc = useTRPC(fetch);
if (!res.ok) {
try {
const { nickname } = await trpc.user.info.query();
return { nickname };
} catch {
error(500, "Internal server error"); error(500, "Internal server error");
} }
const { nickname }: UserInfoResponse = await res.json();
return { nickname };
}; };

View File

@@ -1,11 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { clientListResponse, type ClientListResponse } from "$lib/server/schemas";
import { getUserClientList } from "$lib/server/services/client";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
const { userId } = await authorize(locals, "anyClient");
const { userClients } = await getUserClientList(userId);
return json(clientListResponse.parse({ clients: userClients } satisfies ClientListResponse));
};

View File

@@ -1,20 +0,0 @@
import { error, json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import {
clientRegisterRequest,
clientRegisterResponse,
type ClientRegisterResponse,
} from "$lib/server/schemas";
import { registerUserClient } from "$lib/server/services/client";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "notClient");
const zodRes = clientRegisterRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { encPubKey, sigPubKey } = zodRes.data;
const { id, challenge } = await registerUserClient(userId, locals.ip, encPubKey, sigPubKey);
return json(clientRegisterResponse.parse({ id, challenge } satisfies ClientRegisterResponse));
};

View File

@@ -1,16 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { clientRegisterVerifyRequest } from "$lib/server/schemas";
import { verifyUserClient } from "$lib/server/services/client";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "notClient");
const zodRes = clientRegisterVerifyRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { id, answerSig } = zodRes.data;
await verifyUserClient(userId, locals.ip, id, answerSig);
return text("Client verified", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,17 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { clientStatusResponse, type ClientStatusResponse } from "$lib/server/schemas";
import { getUserClientStatus } from "$lib/server/services/client";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
const { userId, clientId } = await authorize(locals, "anyClient");
const { state, isInitialMekNeeded } = await getUserClientStatus(userId, clientId);
return json(
clientStatusResponse.parse({
id: clientId,
state,
isInitialMekNeeded,
} satisfies ClientStatusResponse),
);
};

View File

@@ -1,20 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { hmacSecretListResponse, type HmacSecretListResponse } from "$lib/server/schemas";
import { getHskList } from "$lib/server/services/hsk";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
const { userId } = await authorize(locals, "activeClient");
const { encHsks } = await getHskList(userId);
return json(
hmacSecretListResponse.parse({
hsks: encHsks.map(({ version, state, mekVersion, encHsk }) => ({
version,
state,
mekVersion,
hsk: encHsk,
})),
} satisfies HmacSecretListResponse),
);
};

View File

@@ -1,16 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { initialHmacSecretRegisterRequest } from "$lib/server/schemas";
import { registerInitialActiveHsk } from "$lib/server/services/hsk";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId, clientId } = await authorize(locals, "activeClient");
const zodRes = initialHmacSecretRegisterRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { mekVersion, hsk } = zodRes.data;
await registerInitialActiveHsk(userId, clientId, mekVersion, hsk);
return text("HSK registered", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -1,20 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { masterKeyListResponse, type MasterKeyListResponse } from "$lib/server/schemas";
import { getClientMekList } from "$lib/server/services/mek";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
const { userId, clientId } = await authorize(locals, "activeClient");
const { encMeks } = await getClientMekList(userId, clientId);
return json(
masterKeyListResponse.parse({
meks: encMeks.map(({ version, state, encMek, encMekSig }) => ({
version,
state,
mek: encMek,
mekSig: encMekSig,
})),
} satisfies MasterKeyListResponse),
);
};

View File

@@ -1,16 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { initialMasterKeyRegisterRequest } from "$lib/server/schemas";
import { registerInitialActiveMek } from "$lib/server/services/mek";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId, clientId } = await authorize(locals, "pendingClient");
const zodRes = initialMasterKeyRegisterRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { mek, mekSig } = zodRes.data;
await registerInitialActiveMek(userId, clientId, mek, mekSig);
return text("MEK registered", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -5,7 +5,7 @@ import type { RequestHandler } from "./$types";
const trpcHandler: RequestHandler = (event) => const trpcHandler: RequestHandler = (event) =>
fetchRequestHandler({ fetchRequestHandler({
endpoint: "/trpc", endpoint: "/api/trpc",
req: event.request, req: event.request,
router: appRouter, router: appRouter,
createContext: () => createContext(event), createContext: () => createContext(event),

View File

@@ -1,11 +0,0 @@
import { json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { userInfoResponse, type UserInfoResponse } from "$lib/server/schemas";
import { getUserInformation } from "$lib/server/services/user";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
const { userId } = await authorize(locals, "any");
const { email, nickname } = await getUserInformation(userId);
return json(userInfoResponse.parse({ email, nickname } satisfies UserInfoResponse));
};

View File

@@ -1,16 +0,0 @@
import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import { nicknameChangeRequest } from "$lib/server/schemas";
import { changeNickname } from "$lib/server/services/user";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "any");
const zodRes = nicknameChangeRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { newNickname } = zodRes.data;
await changeNickname(userId, newNickname);
return text("Nickname changed", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -6,7 +6,7 @@ const createClient = (fetch: typeof globalThis.fetch) =>
createTRPCClient<AppRouter>({ createTRPCClient<AppRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: "/trpc", url: "/api/trpc",
fetch, fetch,
}), }),
], ],
@@ -14,7 +14,7 @@ const createClient = (fetch: typeof globalThis.fetch) =>
let browserClient: ReturnType<typeof createClient>; let browserClient: ReturnType<typeof createClient>;
export const trpc = (fetch = globalThis.fetch) => { export const useTRPC = (fetch = globalThis.fetch) => {
const client = browserClient ?? createClient(fetch); const client = browserClient ?? createClient(fetch);
if (browser) { if (browser) {
browserClient ??= client; browserClient ??= client;

View File

@@ -1,9 +1,25 @@
import type { RequestEvent } from "@sveltejs/kit"; import type { RequestEvent } from "@sveltejs/kit";
import { initTRPC } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import { authorizeMiddleware, authorizeClientMiddleware } from "./middlewares/authorize";
export const createContext = (event: RequestEvent) => event; export const createContext = (event: RequestEvent) => event;
const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create(); export const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create();
export const router = t.router; export const router = t.router;
export const publicProcedure = t.procedure; export const publicProcedure = t.procedure;
const authedProcedure = publicProcedure.use(async ({ ctx, next }) => {
if (!ctx.locals.session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next();
});
export const roleProcedure = {
any: authedProcedure.use(authorizeMiddleware("any")),
notClient: authedProcedure.use(authorizeMiddleware("notClient")),
anyClient: authedProcedure.use(authorizeClientMiddleware("anyClient")),
pendingClient: authedProcedure.use(authorizeClientMiddleware("pendingClient")),
activeClient: authedProcedure.use(authorizeClientMiddleware("activeClient")),
};

View File

@@ -0,0 +1,36 @@
import { TRPCError } from "@trpc/server";
import {
AuthorizationError,
authorizeInternal,
type ClientSession,
type SessionPermission,
} from "$lib/server/modules/auth";
import { t } from "../init.server";
const authorize = async (locals: App.Locals, requiredPermission: SessionPermission) => {
try {
return await authorizeInternal(locals, requiredPermission);
} catch (e) {
if (e instanceof AuthorizationError) {
throw new TRPCError({
code: e.status === 403 ? "FORBIDDEN" : "INTERNAL_SERVER_ERROR",
message: e.message,
});
}
throw e;
}
};
export const authorizeMiddleware = (requiredPermission: "any" | "notClient") =>
t.middleware(async ({ ctx, next }) => {
const session = await authorize(ctx.locals, requiredPermission);
return next({ ctx: { session } });
});
export const authorizeClientMiddleware = (
requiredPermission: "anyClient" | "pendingClient" | "activeClient",
) =>
t.middleware(async ({ ctx, next }) => {
const session = (await authorize(ctx.locals, requiredPermission)) as ClientSession;
return next({ ctx: { session } });
});

View File

@@ -1,9 +1,13 @@
import type { RequestEvent } from "@sveltejs/kit"; import type { RequestEvent } from "@sveltejs/kit";
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { createContext, router } from "./init.server"; import { createContext, router } from "./init.server";
import { clientRouter, hskRouter, mekRouter, userRouter } from "./routers";
export const appRouter = router({ export const appRouter = router({
// TODO client: clientRouter,
hsk: hskRouter,
mek: mekRouter,
user: userRouter,
}); });
export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event)); export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event));

View File

@@ -0,0 +1,96 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { ClientRepo, IntegrityError } from "$lib/server/db";
import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto";
import env from "$lib/server/loadenv";
import { router, roleProcedure } from "../init.server";
const createUserClientChallenge = async (
ip: string,
userId: number,
clientId: number,
encPubKey: string,
) => {
const { answer, challenge } = await generateChallenge(32, encPubKey);
const { id } = await ClientRepo.registerUserClientChallenge(
userId,
clientId,
answer.toString("base64"),
ip,
new Date(Date.now() + env.challenge.userClientExp),
);
return { id, challenge: challenge.toString("base64") };
};
const clientRouter = router({
register: roleProcedure["notClient"]
.input(
z.object({
encPubKey: z.string().base64().nonempty(),
sigPubKey: z.string().base64().nonempty(),
}),
)
.mutation(async ({ ctx, input }) => {
const { userId } = ctx.session;
const { encPubKey, sigPubKey } = input;
const client = await ClientRepo.getClientByPubKeys(encPubKey, sigPubKey);
if (client) {
try {
await ClientRepo.createUserClient(userId, client.id);
return await createUserClientChallenge(ctx.locals.ip, userId, client.id, encPubKey);
} catch (e) {
if (e instanceof IntegrityError && e.message === "User client already exists") {
throw new TRPCError({ code: "CONFLICT", message: "Client already registered" });
}
throw e;
}
} else {
if (encPubKey === sigPubKey) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Same public keys" });
} else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid public key(s)" });
}
try {
const { id: clientId } = await ClientRepo.createClient(encPubKey, sigPubKey, userId);
return await createUserClientChallenge(ctx.locals.ip, userId, clientId, encPubKey);
} catch (e) {
if (e instanceof IntegrityError && e.message === "Public key(s) already registered") {
throw new TRPCError({ code: "CONFLICT", message: "Public key(s) already used" });
}
throw e;
}
}
}),
verify: roleProcedure["notClient"]
.input(
z.object({
id: z.number().int().positive(),
answerSig: z.string().base64().nonempty(),
}),
)
.mutation(async ({ ctx, input }) => {
const challenge = await ClientRepo.consumeUserClientChallenge(
input.id,
ctx.session.userId,
ctx.locals.ip,
);
if (!challenge) {
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" });
}
const client = await ClientRepo.getClient(challenge.clientId);
if (!client) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" });
} else if (
!verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey)
) {
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer signature" });
}
await ClientRepo.setUserClientStateToPending(ctx.session.userId, client.id);
}),
});
export default clientRouter;

41
src/trpc/routers/hsk.ts Normal file
View File

@@ -0,0 +1,41 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { HskRepo, IntegrityError } from "$lib/server/db";
import { router, roleProcedure } from "../init.server";
const hskRouter = router({
list: roleProcedure["activeClient"].query(async ({ ctx }) => {
const hsks = await HskRepo.getAllValidHsks(ctx.session.userId);
return hsks.map(({ version, state, mekVersion, encHsk }) => ({
version,
state,
mekVersion,
hsk: encHsk,
}));
}),
registerInitial: roleProcedure["activeClient"]
.input(
z.object({
mekVersion: z.number().int().positive(),
hsk: z.string().base64().nonempty(),
}),
)
.mutation(async ({ ctx, input }) => {
try {
await HskRepo.registerInitialHsk(
ctx.session.userId,
ctx.session.clientId,
input.mekVersion,
input.hsk,
);
} catch (e) {
if (e instanceof IntegrityError && e.message === "HSK already registered") {
throw new TRPCError({ code: "CONFLICT", message: "Initial HSK already registered" });
}
throw e;
}
}),
});
export default hskRouter;

View File

@@ -0,0 +1,4 @@
export { default as clientRouter } from "./client";
export { default as hskRouter } from "./hsk";
export { default as mekRouter } from "./mek";
export { default as userRouter } from "./user";

63
src/trpc/routers/mek.ts Normal file
View File

@@ -0,0 +1,63 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { ClientRepo, MekRepo, IntegrityError } from "$lib/server/db";
import { verifySignature } from "$lib/server/modules/crypto";
import { router, roleProcedure } from "../init.server";
const verifyClientEncMekSig = async (
userId: number,
clientId: number,
version: number,
encMek: string,
encMekSig: string,
) => {
const userClient = await ClientRepo.getUserClientWithDetails(userId, clientId);
if (!userClient) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
}
const data = JSON.stringify({ version, key: encMek });
return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey);
};
const mekRouter = router({
list: roleProcedure["activeClient"].query(async ({ ctx }) => {
const clientMeks = await MekRepo.getAllValidClientMeks(
ctx.session.userId,
ctx.session.clientId,
);
return clientMeks.map(({ version, state, encMek, encMekSig }) => ({
version,
state,
mek: encMek,
mekSig: encMekSig,
}));
}),
registerInitial: roleProcedure["pendingClient"]
.input(
z.object({
mek: z.string().base64().nonempty(),
mekSig: z.string().base64().nonempty(),
}),
)
.mutation(async ({ ctx, input }) => {
const { userId, clientId } = ctx.session;
const { mek, mekSig } = input;
if (!(await verifyClientEncMekSig(userId, clientId, 1, mek, mekSig))) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid signature" });
}
try {
await MekRepo.registerInitialMek(userId, clientId, mek, mekSig);
await ClientRepo.setUserClientStateToActive(userId, clientId);
} catch (e) {
if (e instanceof IntegrityError && e.message === "MEK already registered") {
throw new TRPCError({ code: "CONFLICT", message: "Initial MEK already registered" });
}
throw e;
}
}),
});
export default mekRouter;

27
src/trpc/routers/user.ts Normal file
View File

@@ -0,0 +1,27 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { UserRepo } from "$lib/server/db";
import { router, roleProcedure } from "../init.server";
const userRouter = router({
info: roleProcedure.any.query(async ({ ctx }) => {
const user = await UserRepo.getUser(ctx.session.userId);
if (!user) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
}
return { email: user.email, nickname: user.nickname };
}),
changeNickname: roleProcedure.any
.input(
z.object({
newNickname: z.string().trim().min(2).max(8),
}),
)
.mutation(async ({ ctx, input }) => {
await UserRepo.setUserNickname(ctx.session.userId, input.newNickname);
}),
});
export default userRouter;