Merge pull request #1 from kmc7468/add-client-signature-keypair

클라이언트 서명 시스템 도입
This commit is contained in:
static
2024-12-31 06:53:49 +09:00
committed by GitHub
36 changed files with 1331 additions and 284 deletions

View File

@@ -5,4 +5,5 @@ JWT_SECRET=
DATABASE_URL= DATABASE_URL=
JWT_ACCESS_TOKEN_EXPIRES= JWT_ACCESS_TOKEN_EXPIRES=
JWT_REFRESH_TOKEN_EXPIRES= JWT_REFRESH_TOKEN_EXPIRES=
PUBKEY_CHALLENGE_EXPIRES= USER_CLIENT_CHALLENGE_EXPIRES=
TOKEN_UPGRADE_CHALLENGE_EXPIRES=

View File

@@ -2,3 +2,6 @@
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
yarn.lock yarn.lock
# Output
/drizzle

View File

@@ -10,7 +10,8 @@ services:
- JWT_SECRET=${JWT_SECRET:?} # Required - JWT_SECRET=${JWT_SECRET:?} # Required
- JWT_ACCESS_TOKEN_EXPIRES - JWT_ACCESS_TOKEN_EXPIRES
- JWT_REFRESH_TOKEN_EXPIRES - JWT_REFRESH_TOKEN_EXPIRES
- PUBKEY_CHALLENGE_EXPIRES - USER_CLIENT_CHALLENGE_EXPIRES
- TOKEN_UPGRADE_CHALLENGE_EXPIRES
# SvelteKit # SvelteKit
- ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For} - ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For}
- XFF_DEPTH=${TRUST_PROXY:-} - XFF_DEPTH=${TRUST_PROXY:-}

View File

@@ -0,0 +1,20 @@
CREATE TABLE `token_upgrade_challenge` (
`id` integer PRIMARY KEY NOT NULL,
`refresh_token_id` text NOT NULL,
`client_id` integer NOT NULL,
`challenge` text NOT NULL,
`allowed_ip` text NOT NULL,
`expires_at` integer NOT NULL,
`is_used` integer DEFAULT false NOT NULL,
FOREIGN KEY (`refresh_token_id`) REFERENCES `refresh_token`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
ALTER TABLE `client` RENAME COLUMN `public_key` TO `encryption_public_key`;--> statement-breakpoint
DROP INDEX IF EXISTS `client_public_key_unique`;--> statement-breakpoint
ALTER TABLE `client` ADD `signature_public_key` text NOT NULL;--> statement-breakpoint
ALTER TABLE `user_client_challenge` ADD `is_used` integer DEFAULT false NOT NULL;--> statement-breakpoint
CREATE UNIQUE INDEX `token_upgrade_challenge_challenge_unique` ON `token_upgrade_challenge` (`challenge`);--> statement-breakpoint
CREATE UNIQUE INDEX `client_encryption_public_key_unique` ON `client` (`encryption_public_key`);--> statement-breakpoint
CREATE UNIQUE INDEX `client_signature_public_key_unique` ON `client` (`signature_public_key`);--> statement-breakpoint
CREATE UNIQUE INDEX `client_encryption_public_key_signature_public_key_unique` ON `client` (`encryption_public_key`,`signature_public_key`);

View File

@@ -0,0 +1,611 @@
{
"version": "6",
"dialect": "sqlite",
"id": "f5b74176-eb87-436d-8f32-6da01727b564",
"prevId": "64e2c1ed-92bf-44d1-9094-7e3610b3224f",
"tables": {
"client": {
"name": "client",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"encryption_public_key": {
"name": "encryption_public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"signature_public_key": {
"name": "signature_public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"client_encryption_public_key_unique": {
"name": "client_encryption_public_key_unique",
"columns": [
"encryption_public_key"
],
"isUnique": true
},
"client_signature_public_key_unique": {
"name": "client_signature_public_key_unique",
"columns": [
"signature_public_key"
],
"isUnique": true
},
"client_encryption_public_key_signature_public_key_unique": {
"name": "client_encryption_public_key_signature_public_key_unique",
"columns": [
"encryption_public_key",
"signature_public_key"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user_client": {
"name": "user_client",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"client_id": {
"name": "client_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'challenging'"
}
},
"indexes": {},
"foreignKeys": {
"user_client_user_id_user_id_fk": {
"name": "user_client_user_id_user_id_fk",
"tableFrom": "user_client",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"user_client_client_id_client_id_fk": {
"name": "user_client_client_id_client_id_fk",
"tableFrom": "user_client",
"tableTo": "client",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_client_user_id_client_id_pk": {
"columns": [
"client_id",
"user_id"
],
"name": "user_client_user_id_client_id_pk"
}
},
"uniqueConstraints": {}
},
"user_client_challenge": {
"name": "user_client_challenge",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"client_id": {
"name": "client_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"challenge": {
"name": "challenge",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"allowed_ip": {
"name": "allowed_ip",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_used": {
"name": "is_used",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
}
},
"indexes": {
"user_client_challenge_challenge_unique": {
"name": "user_client_challenge_challenge_unique",
"columns": [
"challenge"
],
"isUnique": true
}
},
"foreignKeys": {
"user_client_challenge_user_id_user_id_fk": {
"name": "user_client_challenge_user_id_user_id_fk",
"tableFrom": "user_client_challenge",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"user_client_challenge_client_id_client_id_fk": {
"name": "user_client_challenge_client_id_client_id_fk",
"tableFrom": "user_client_challenge",
"tableTo": "client",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"client_master_encryption_key": {
"name": "client_master_encryption_key",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"client_id": {
"name": "client_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"master_encryption_key_version": {
"name": "master_encryption_key_version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"encrypted_master_encryption_key": {
"name": "encrypted_master_encryption_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"client_master_encryption_key_user_id_user_id_fk": {
"name": "client_master_encryption_key_user_id_user_id_fk",
"tableFrom": "client_master_encryption_key",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"client_master_encryption_key_client_id_client_id_fk": {
"name": "client_master_encryption_key_client_id_client_id_fk",
"tableFrom": "client_master_encryption_key",
"tableTo": "client",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"client_master_encryption_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": {
"name": "client_master_encryption_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk",
"tableFrom": "client_master_encryption_key",
"tableTo": "master_encryption_key",
"columnsFrom": [
"user_id",
"master_encryption_key_version"
],
"columnsTo": [
"user_id",
"version"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk": {
"columns": [
"client_id",
"master_encryption_key_version",
"user_id"
],
"name": "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk"
}
},
"uniqueConstraints": {}
},
"master_encryption_key": {
"name": "master_encryption_key",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_by": {
"name": "created_by",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"retired_at": {
"name": "retired_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"master_encryption_key_user_id_user_id_fk": {
"name": "master_encryption_key_user_id_user_id_fk",
"tableFrom": "master_encryption_key",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"master_encryption_key_created_by_client_id_fk": {
"name": "master_encryption_key_created_by_client_id_fk",
"tableFrom": "master_encryption_key",
"tableTo": "client",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"master_encryption_key_user_id_version_pk": {
"columns": [
"user_id",
"version"
],
"name": "master_encryption_key_user_id_version_pk"
}
},
"uniqueConstraints": {}
},
"refresh_token": {
"name": "refresh_token",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"client_id": {
"name": "client_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"refresh_token_user_id_client_id_unique": {
"name": "refresh_token_user_id_client_id_unique",
"columns": [
"user_id",
"client_id"
],
"isUnique": true
}
},
"foreignKeys": {
"refresh_token_user_id_user_id_fk": {
"name": "refresh_token_user_id_user_id_fk",
"tableFrom": "refresh_token",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"refresh_token_client_id_client_id_fk": {
"name": "refresh_token_client_id_client_id_fk",
"tableFrom": "refresh_token",
"tableTo": "client",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"token_upgrade_challenge": {
"name": "token_upgrade_challenge",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"refresh_token_id": {
"name": "refresh_token_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"client_id": {
"name": "client_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"challenge": {
"name": "challenge",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"allowed_ip": {
"name": "allowed_ip",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_used": {
"name": "is_used",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
}
},
"indexes": {
"token_upgrade_challenge_challenge_unique": {
"name": "token_upgrade_challenge_challenge_unique",
"columns": [
"challenge"
],
"isUnique": true
}
},
"foreignKeys": {
"token_upgrade_challenge_refresh_token_id_refresh_token_id_fk": {
"name": "token_upgrade_challenge_refresh_token_id_refresh_token_id_fk",
"tableFrom": "token_upgrade_challenge",
"tableTo": "refresh_token",
"columnsFrom": [
"refresh_token_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"token_upgrade_challenge_client_id_client_id_fk": {
"name": "token_upgrade_challenge_client_id_client_id_fk",
"tableFrom": "token_upgrade_challenge",
"tableTo": "client",
"columnsFrom": [
"client_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"client\".\"public_key\"": "\"client\".\"encryption_public_key\""
}
},
"internal": {
"indexes": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1735525637133, "when": 1735525637133,
"tag": "0000_spicy_morgan_stark", "tag": "0000_spicy_morgan_stark",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1735588850570,
"tag": "0001_silly_vanisher",
"breakpoints": true
} }
] ]
} }

View File

@@ -2,7 +2,10 @@ import { redirect, type ServerInit, type Handle } from "@sveltejs/kit";
import schedule from "node-schedule"; import schedule from "node-schedule";
import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client"; import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client";
import { migrateDB } from "$lib/server/db/drizzle"; import { migrateDB } from "$lib/server/db/drizzle";
import { cleanupExpiredRefreshTokens } from "$lib/server/db/token"; import {
cleanupExpiredRefreshTokens,
cleanupExpiredTokenUpgradeChallenges,
} from "$lib/server/db/token";
export const init: ServerInit = () => { export const init: ServerInit = () => {
migrateDB(); migrateDB();
@@ -10,6 +13,7 @@ export const init: ServerInit = () => {
schedule.scheduleJob("0 * * * *", () => { schedule.scheduleJob("0 * * * *", () => {
cleanupExpiredUserClientChallenges(); cleanupExpiredUserClientChallenges();
cleanupExpiredRefreshTokens(); cleanupExpiredRefreshTokens();
cleanupExpiredTokenUpgradeChallenges();
}); });
}; };

View File

@@ -4,8 +4,12 @@ type Path = "/key/export";
interface KeyExportState { interface KeyExportState {
redirectPath: string; redirectPath: string;
pubKeyBase64: string;
privKeyBase64: string; encryptKeyBase64: string;
decryptKeyBase64: string;
signKeyBase64: string;
verifyKeyBase64: string;
mekDraft: ArrayBuffer; mekDraft: ArrayBuffer;
} }

View File

@@ -1,33 +1,43 @@
import { Dexie, type EntityTable } from "dexie"; import { Dexie, type EntityTable } from "dexie";
interface KeyPair { type RSAKeyUsage = "encrypt" | "decrypt" | "sign" | "verify";
type: "publicKey" | "privateKey";
interface RSAKey {
usage: RSAKeyUsage;
key: CryptoKey; key: CryptoKey;
} }
const keyStore = new Dexie("keyStore") as Dexie & { const keyStore = new Dexie("keyStore") as Dexie & {
keyPair: EntityTable<KeyPair, "type">; rsaKey: EntityTable<RSAKey, "usage">;
}; };
keyStore.version(1).stores({ keyStore.version(1).stores({
keyPair: "type", rsaKey: "usage",
}); });
export const getKeyPairFromIndexedDB = async () => { export const getRSAKey = async (usage: RSAKeyUsage) => {
const pubKey = await keyStore.keyPair.get("publicKey"); const key = await keyStore.rsaKey.get(usage);
const privKey = await keyStore.keyPair.get("privateKey"); return key?.key ?? null;
return {
pubKey: pubKey?.key ?? null,
privKey: privKey?.key ?? null,
};
}; };
export const storeKeyPairIntoIndexedDB = async (pubKey: CryptoKey, privKey: CryptoKey) => { export const storeRSAKey = async (key: CryptoKey, usage: RSAKeyUsage) => {
if (!pubKey.extractable) throw new Error("Public key must be extractable"); switch (usage) {
if (privKey.extractable) throw new Error("Private key must be non-extractable"); case "encrypt":
case "verify":
await keyStore.keyPair.bulkPut([ if (key.type !== "public") {
{ type: "publicKey", key: pubKey }, throw new Error("Public key required");
{ type: "privateKey", key: privKey }, } else if (!key.extractable) {
]); throw new Error("Public key must be extractable");
}
break;
case "decrypt":
case "sign":
if (key.type !== "private") {
throw new Error("Private key required");
} else if (key.extractable) {
throw new Error("Private key must be non-extractable");
}
break;
}
await keyStore.rsaKey.put({ usage, key });
}; };

View File

@@ -1,3 +1,4 @@
export type RSAKeyPurpose = "encryption" | "signature";
export type RSAKeyType = "public" | "private"; export type RSAKeyType = "public" | "private";
export const encodeToBase64 = (data: ArrayBuffer) => { export const encodeToBase64 = (data: ArrayBuffer) => {
@@ -8,42 +9,42 @@ export const decodeFromBase64 = (data: string) => {
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer;
}; };
export const generateRSAKeyPair = async () => { export const generateRSAKeyPair = async (purpose: RSAKeyPurpose) => {
const keyPair = await window.crypto.subtle.generateKey( return await window.crypto.subtle.generateKey(
{ {
name: "RSA-OAEP", name: purpose === "encryption" ? "RSA-OAEP" : "RSA-PSS",
modulusLength: 4096, modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]), publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256", hash: "SHA-256",
} satisfies RsaHashedKeyGenParams, } satisfies RsaHashedKeyGenParams,
true, true,
["encrypt", "decrypt"], purpose === "encryption" ? ["encrypt", "decrypt"] : ["sign", "verify"],
); );
return keyPair;
}; };
export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { export const makeRSAKeyNonextractable = async (key: CryptoKey) => {
const { format, key: exportedKey } = await exportRSAKey(key, type); const { format, key: exportedKey } = await exportRSAKey(key);
return await window.crypto.subtle.importKey( return await window.crypto.subtle.importKey(
format, format,
exportedKey, exportedKey,
{ key.algorithm,
name: "RSA-OAEP",
hash: "SHA-256",
} satisfies RsaHashedImportParams,
false, false,
[type === "public" ? "encrypt" : "decrypt"], key.usages,
); );
}; };
export const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { export const exportRSAKey = async (key: CryptoKey) => {
const format = type === "public" ? ("spki" as const) : ("pkcs8" as const); const format = key.type === "public" ? ("spki" as const) : ("pkcs8" as const);
return { return {
format, format,
key: await window.crypto.subtle.exportKey(format, key), key: await window.crypto.subtle.exportKey(format, key),
}; };
}; };
export const exportRSAKeyToBase64 = async (key: CryptoKey) => {
return encodeToBase64((await exportRSAKey(key)).key);
};
export const encryptRSAPlaintext = async (plaintext: ArrayBuffer, publicKey: CryptoKey) => { export const encryptRSAPlaintext = async (plaintext: ArrayBuffer, publicKey: CryptoKey) => {
return await window.crypto.subtle.encrypt( return await window.crypto.subtle.encrypt(
{ {
@@ -64,6 +65,17 @@ export const decryptRSACiphertext = async (ciphertext: ArrayBuffer, privateKey:
); );
}; };
export const signRSAMessage = async (message: ArrayBuffer, privateKey: CryptoKey) => {
return await window.crypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32,
} satisfies RsaPssParams,
privateKey,
message,
);
};
export const generateAESKey = async () => { export const generateAESKey = async () => {
return await window.crypto.subtle.generateKey( return await window.crypto.subtle.generateKey(
{ {
@@ -79,12 +91,9 @@ export const makeAESKeyNonextractable = async (key: CryptoKey) => {
return await window.crypto.subtle.importKey( return await window.crypto.subtle.importKey(
"raw", "raw",
await exportAESKey(key), await exportAESKey(key),
{ key.algorithm,
name: "AES-GCM",
length: 256,
} satisfies AesKeyAlgorithm,
false, false,
["encrypt", "decrypt"], key.usages,
); );
}; };

View File

@@ -1,10 +1,21 @@
import { and, eq, gt, lte } from "drizzle-orm"; import { and, or, eq, gt, lte, count } from "drizzle-orm";
import db from "./drizzle"; import db from "./drizzle";
import { client, userClient, userClientChallenge } from "./schema"; import { client, userClient, userClientChallenge } from "./schema";
export const createClient = async (pubKey: string, userId: number) => { export const createClient = async (encPubKey: string, sigPubKey: string, userId: number) => {
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id }); const clients = await tx
.select()
.from(client)
.where(or(eq(client.encPubKey, sigPubKey), eq(client.sigPubKey, encPubKey)));
if (clients.length > 0) {
throw new Error("Already used public key(s)");
}
const insertRes = await tx
.insert(client)
.values({ encPubKey, sigPubKey })
.returning({ id: client.id });
const { id: clientId } = insertRes[0]!; const { id: clientId } = insertRes[0]!;
await tx.insert(userClient).values({ userId, clientId }); await tx.insert(userClient).values({ userId, clientId });
@@ -12,11 +23,28 @@ export const createClient = async (pubKey: string, userId: number) => {
}); });
}; };
export const getClientByPubKey = async (pubKey: string) => { export const getClient = async (clientId: number) => {
const clients = await db.select().from(client).where(eq(client.pubKey, pubKey)).execute(); const clients = await db.select().from(client).where(eq(client.id, clientId)).execute();
return clients[0] ?? null; return clients[0] ?? null;
}; };
export const getClientByPubKeys = async (encPubKey: string, sigPubKey: string) => {
const clients = await db
.select()
.from(client)
.where(and(eq(client.encPubKey, encPubKey), eq(client.sigPubKey, sigPubKey)))
.execute();
return clients[0] ?? null;
};
export const countClientByPubKey = async (pubKey: string) => {
const clients = await db
.select({ count: count() })
.from(client)
.where(or(eq(client.encPubKey, pubKey), eq(client.encPubKey, pubKey)));
return clients[0]?.count ?? 0;
};
export const createUserClient = async (userId: number, clientId: number) => { export const createUserClient = async (userId: number, clientId: number) => {
await db.insert(userClient).values({ userId, clientId }).execute(); await db.insert(userClient).values({ userId, clientId }).execute();
}; };
@@ -62,7 +90,7 @@ export const setUserClientStateToActive = async (userId: number, clientId: numbe
.execute(); .execute();
}; };
export const createUserClientChallenge = async ( export const registerUserClientChallenge = async (
userId: number, userId: number,
clientId: number, clientId: number,
answer: string, answer: string,
@@ -90,12 +118,21 @@ export const getUserClientChallenge = async (answer: string, ip: string) => {
eq(userClientChallenge.answer, answer), eq(userClientChallenge.answer, answer),
eq(userClientChallenge.allowedIp, ip), eq(userClientChallenge.allowedIp, ip),
gt(userClientChallenge.expiresAt, new Date()), gt(userClientChallenge.expiresAt, new Date()),
eq(userClientChallenge.isUsed, false),
), ),
) )
.execute(); .execute();
return challenges[0] ?? null; return challenges[0] ?? null;
}; };
export const markUserClientChallengeAsUsed = async (id: number) => {
await db
.update(userClientChallenge)
.set({ isUsed: true })
.where(eq(userClientChallenge.id, id))
.execute();
};
export const cleanupExpiredUserClientChallenges = async () => { export const cleanupExpiredUserClientChallenges = async () => {
await db await db
.delete(userClientChallenge) .delete(userClientChallenge)

View File

@@ -1,10 +1,17 @@
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; import { sqliteTable, text, integer, primaryKey, unique } from "drizzle-orm/sqlite-core";
import { user } from "./user"; import { user } from "./user";
export const client = sqliteTable("client", { export const client = sqliteTable(
id: integer("id").primaryKey(), "client",
pubKey: text("public_key").notNull().unique(), // Base64 {
}); id: integer("id").primaryKey(),
encPubKey: text("encryption_public_key").notNull().unique(), // Base64
sigPubKey: text("signature_public_key").notNull().unique(), // Base64
},
(t) => ({
unq: unique().on(t.encPubKey, t.sigPubKey),
}),
);
export const userClient = sqliteTable( export const userClient = sqliteTable(
"user_client", "user_client",
@@ -35,4 +42,5 @@ export const userClientChallenge = sqliteTable("user_client_challenge", {
answer: text("challenge").notNull().unique(), // Base64 answer: text("challenge").notNull().unique(), // Base64
allowedIp: text("allowed_ip").notNull(), allowedIp: text("allowed_ip").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false),
}); });

View File

@@ -16,3 +16,17 @@ export const refreshToken = sqliteTable(
unq: unique().on(t.userId, t.clientId), unq: unique().on(t.userId, t.clientId),
}), }),
); );
export const tokenUpgradeChallenge = sqliteTable("token_upgrade_challenge", {
id: integer("id").primaryKey(),
refreshTokenId: text("refresh_token_id")
.notNull()
.references(() => refreshToken.id),
clientId: integer("client_id")
.notNull()
.references(() => client.id),
answer: text("challenge").notNull().unique(), // Base64
allowedIp: text("allowed_ip").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false),
});

View File

@@ -1,9 +1,9 @@
import { SqliteError } from "better-sqlite3"; import { SqliteError } from "better-sqlite3";
import { eq, lte } from "drizzle-orm"; import { and, eq, gt, lte } from "drizzle-orm";
import ms from "ms"; import ms from "ms";
import env from "$lib/server/loadenv"; import env from "$lib/server/loadenv";
import db from "./drizzle"; import db from "./drizzle";
import { refreshToken } from "./schema"; import { refreshToken, tokenUpgradeChallenge } from "./schema";
const expiresIn = ms(env.jwt.refreshExp); const expiresIn = ms(env.jwt.refreshExp);
const expiresAt = () => new Date(Date.now() + expiresIn); const expiresAt = () => new Date(Date.now() + expiresIn);
@@ -38,15 +38,20 @@ export const getRefreshToken = async (tokenId: string) => {
}; };
export const rotateRefreshToken = async (oldTokenId: string, newTokenId: string) => { export const rotateRefreshToken = async (oldTokenId: string, newTokenId: string) => {
const res = await db return await db.transaction(async (tx) => {
.update(refreshToken) await tx
.set({ .delete(tokenUpgradeChallenge)
id: newTokenId, .where(eq(tokenUpgradeChallenge.refreshTokenId, oldTokenId));
expiresAt: expiresAt(), const res = await db
}) .update(refreshToken)
.where(eq(refreshToken.id, oldTokenId)) .set({
.execute(); id: newTokenId,
return res.changes > 0; expiresAt: expiresAt(),
})
.where(eq(refreshToken.id, oldTokenId))
.execute();
return res.changes > 0;
});
}; };
export const upgradeRefreshToken = async ( export const upgradeRefreshToken = async (
@@ -54,16 +59,21 @@ export const upgradeRefreshToken = async (
newTokenId: string, newTokenId: string,
clientId: number, clientId: number,
) => { ) => {
const res = await db return await db.transaction(async (tx) => {
.update(refreshToken) await tx
.set({ .delete(tokenUpgradeChallenge)
id: newTokenId, .where(eq(tokenUpgradeChallenge.refreshTokenId, oldTokenId));
clientId, const res = await tx
expiresAt: expiresAt(), .update(refreshToken)
}) .set({
.where(eq(refreshToken.id, oldTokenId)) id: newTokenId,
.execute(); clientId,
return res.changes > 0; expiresAt: expiresAt(),
})
.where(eq(refreshToken.id, oldTokenId))
.execute();
return res.changes > 0;
});
}; };
export const revokeRefreshToken = async (tokenId: string) => { export const revokeRefreshToken = async (tokenId: string) => {
@@ -73,3 +83,53 @@ export const revokeRefreshToken = async (tokenId: string) => {
export const cleanupExpiredRefreshTokens = async () => { export const cleanupExpiredRefreshTokens = async () => {
await db.delete(refreshToken).where(lte(refreshToken.expiresAt, new Date())).execute(); await db.delete(refreshToken).where(lte(refreshToken.expiresAt, new Date())).execute();
}; };
export const registerTokenUpgradeChallenge = async (
tokenId: string,
clientId: number,
answer: string,
allowedIp: string,
expiresAt: Date,
) => {
await db
.insert(tokenUpgradeChallenge)
.values({
refreshTokenId: tokenId,
clientId,
answer,
allowedIp,
expiresAt,
})
.execute();
};
export const getTokenUpgradeChallenge = async (answer: string, ip: string) => {
const challenges = await db
.select()
.from(tokenUpgradeChallenge)
.where(
and(
eq(tokenUpgradeChallenge.answer, answer),
eq(tokenUpgradeChallenge.allowedIp, ip),
gt(tokenUpgradeChallenge.expiresAt, new Date()),
eq(tokenUpgradeChallenge.isUsed, false),
),
)
.execute();
return challenges[0] ?? null;
};
export const markTokenUpgradeChallengeAsUsed = async (id: number) => {
await db
.update(tokenUpgradeChallenge)
.set({ isUsed: true })
.where(eq(tokenUpgradeChallenge.id, id))
.execute();
};
export const cleanupExpiredTokenUpgradeChallenges = async () => {
await db
.delete(tokenUpgradeChallenge)
.where(lte(tokenUpgradeChallenge.expiresAt, new Date()))
.execute();
};

View File

@@ -13,6 +13,7 @@ export default {
refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d", refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d",
}, },
challenge: { challenge: {
pubKeyExp: env.PUBKEY_CHALLENGE_EXPIRES || "5m", userClientExp: env.USER_CLIENT_CHALLENGE_EXPIRES || "5m",
tokenUpgradeExp: env.TOKEN_UPGRADE_CHALLENGE_EXPIRES || "5m",
}, },
}; };

View File

@@ -0,0 +1,36 @@
import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto";
import { promisify } from "util";
const makePubKeyToPem = (pubKey: string) =>
`-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`;
export const verifyPubKey = (pubKey: string) => {
const pubKeyPem = makePubKeyToPem(pubKey);
const pubKeyObject = createPublicKey(pubKeyPem);
return (
pubKeyObject.asymmetricKeyType === "rsa" &&
pubKeyObject.asymmetricKeyDetails?.modulusLength === 4096
);
};
export const encryptAsymmetric = (data: Buffer, encPubKey: string) => {
return publicEncrypt({ key: makePubKeyToPem(encPubKey), oaepHash: "sha256" }, data);
};
export const verifySignature = (data: string, signature: string, sigPubKey: string) => {
return verify(
"rsa-sha256",
Buffer.from(data, "base64"),
{
key: makePubKeyToPem(sigPubKey),
padding: constants.RSA_PKCS1_PSS_PADDING,
},
Buffer.from(signature, "base64"),
);
};
export const generateChallenge = async (length: number, encPubKey: string) => {
const answer = await promisify(randomBytes)(length);
const challenge = encryptAsymmetric(answer, encPubKey);
return { answer, challenge };
};

View File

@@ -1,16 +1,22 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import argon2 from "argon2"; import argon2 from "argon2";
import ms from "ms";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { getClientByPubKey, getUserClient } from "$lib/server/db/client"; import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client";
import { getUserByEmail } from "$lib/server/db/user"; import { getUserByEmail } from "$lib/server/db/user";
import env from "$lib/server/loadenv";
import { import {
getRefreshToken, getRefreshToken,
registerRefreshToken, registerRefreshToken,
rotateRefreshToken, rotateRefreshToken,
upgradeRefreshToken, upgradeRefreshToken,
revokeRefreshToken, revokeRefreshToken,
registerTokenUpgradeChallenge,
getTokenUpgradeChallenge,
markTokenUpgradeChallengeAsUsed,
} from "$lib/server/db/token"; } from "$lib/server/db/token";
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
import { verifySignature, generateChallenge } from "$lib/server/modules/crypto";
const verifyPassword = async (hash: string, password: string) => { const verifyPassword = async (hash: string, password: string) => {
return await argon2.verify(hash, password); return await argon2.verify(hash, password);
@@ -30,23 +36,15 @@ const issueRefreshToken = async (userId: number, clientId?: number) => {
return token; return token;
}; };
export const login = async (email: string, password: string, pubKey?: string) => { export const login = async (email: string, password: string) => {
const user = await getUserByEmail(email); const user = await getUserByEmail(email);
if (!user || !(await verifyPassword(user.password, password))) { if (!user || !(await verifyPassword(user.password, password))) {
error(401, "Invalid email or password"); error(401, "Invalid email or password");
} }
const client = pubKey ? await getClientByPubKey(pubKey) : undefined;
const userClient = client ? await getUserClient(user.id, client.id) : undefined;
if (client === null) {
error(401, "Invalid public key");
} else if (client && (!userClient || userClient.state === "challenging")) {
error(401, "Unregistered public key");
}
return { return {
accessToken: issueAccessToken(user.id, client?.id), accessToken: issueAccessToken(user.id),
refreshToken: await issueRefreshToken(user.id, client?.id), refreshToken: await issueRefreshToken(user.id),
}; };
}; };
@@ -75,7 +73,7 @@ export const logout = async (refreshToken: string) => {
await revokeRefreshToken(jti); await revokeRefreshToken(jti);
}; };
export const refreshTokens = async (refreshToken: string) => { export const refreshToken = async (refreshToken: string) => {
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
const newJti = uuidv4(); const newJti = uuidv4();
@@ -88,20 +86,75 @@ export const refreshTokens = async (refreshToken: string) => {
}; };
}; };
export const upgradeTokens = async (refreshToken: string, pubKey: string) => { const expiresIn = ms(env.challenge.tokenUpgradeExp);
const expiresAt = () => new Date(Date.now() + expiresIn);
const createChallenge = async (
ip: string,
tokenId: string,
clientId: number,
encPubKey: string,
) => {
const { answer, challenge } = await generateChallenge(32, encPubKey);
await registerTokenUpgradeChallenge(
tokenId,
clientId,
answer.toString("base64"),
ip,
expiresAt(),
);
return challenge.toString("base64");
};
export const createTokenUpgradeChallenge = async (
refreshToken: string,
ip: string,
encPubKey: string,
sigPubKey: string,
) => {
const { jti, userId, clientId } = await verifyRefreshToken(refreshToken);
if (clientId) {
error(403, "Forbidden");
}
const client = await getClientByPubKeys(encPubKey, sigPubKey);
const userClient = client ? await getUserClient(userId, client.id) : undefined;
if (!client) {
error(401, "Invalid public key(s)");
} else if (!userClient || userClient.state === "challenging") {
error(401, "Unregistered client");
}
return { challenge: await createChallenge(ip, jti, client.id, encPubKey) };
};
export const upgradeToken = async (
refreshToken: string,
ip: string,
answer: string,
sigAnswer: string,
) => {
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
if (clientId) { if (clientId) {
error(403, "Forbidden"); error(403, "Forbidden");
} }
const client = await getClientByPubKey(pubKey); const challenge = await getTokenUpgradeChallenge(answer, ip);
const userClient = client ? await getUserClient(userId, client.id) : undefined; if (!challenge) {
if (!client) { error(401, "Invalid challenge answer");
error(401, "Invalid public key"); } else if (challenge.refreshTokenId !== oldJti) {
} else if (client && (!userClient || userClient.state === "challenging")) { error(403, "Forbidden");
error(401, "Unregistered public key");
} }
const client = await getClient(challenge.clientId);
if (!client) {
error(500, "Invalid challenge answer");
} else if (!verifySignature(answer, sigAnswer, client.sigPubKey)) {
error(401, "Invalid challenge answer signature");
}
await markTokenUpgradeChallengeAsUsed(challenge.id);
const newJti = uuidv4(); const newJti = uuidv4();
if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) { if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) {
error(500, "Refresh token not found"); error(500, "Refresh token not found");

View File

@@ -1,17 +1,19 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import { randomBytes, publicEncrypt, createPublicKey } from "crypto";
import ms from "ms"; import ms from "ms";
import { promisify } from "util";
import { import {
createClient, createClient,
getClientByPubKey, getClient,
getClientByPubKeys,
countClientByPubKey,
createUserClient, createUserClient,
getAllUserClients, getAllUserClients,
getUserClient, getUserClient,
createUserClientChallenge,
getUserClientChallenge,
setUserClientStateToPending, setUserClientStateToPending,
registerUserClientChallenge,
getUserClientChallenge,
markUserClientChallengeAsUsed,
} from "$lib/server/db/client"; } from "$lib/server/db/client";
import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto";
import { isInitialMekNeeded } from "$lib/server/modules/mek"; import { isInitialMekNeeded } from "$lib/server/modules/mek";
import env from "$lib/server/loadenv"; import env from "$lib/server/loadenv";
@@ -25,45 +27,53 @@ export const getUserClientList = async (userId: number) => {
}; };
}; };
const expiresIn = ms(env.challenge.pubKeyExp); const expiresIn = ms(env.challenge.userClientExp);
const expiresAt = () => new Date(Date.now() + expiresIn); const expiresAt = () => new Date(Date.now() + expiresIn);
const generateChallenge = async (userId: number, ip: string, clientId: number, pubKey: string) => { const createUserClientChallenge = async (
const answer = await promisify(randomBytes)(32); userId: number,
const answerBase64 = answer.toString("base64"); ip: string,
await createUserClientChallenge(userId, clientId, answerBase64, ip, expiresAt()); clientId: number,
encPubKey: string,
const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; ) => {
const challenge = publicEncrypt({ key: pubKeyPem, oaepHash: "sha256" }, answer); const { answer, challenge } = await generateChallenge(32, encPubKey);
await registerUserClientChallenge(userId, clientId, answer.toString("base64"), ip, expiresAt());
return challenge.toString("base64"); return challenge.toString("base64");
}; };
export const registerUserClient = async (userId: number, ip: string, pubKey: string) => { export const registerUserClient = async (
const client = await getClientByPubKey(pubKey); userId: number,
ip: string,
encPubKey: string,
sigPubKey: string,
) => {
let clientId; let clientId;
const client = await getClientByPubKeys(encPubKey, sigPubKey);
if (client) { if (client) {
const userClient = await getUserClient(userId, client.id); const userClient = await getUserClient(userId, client.id);
if (userClient) { if (userClient) {
error(409, "Public key already registered"); error(409, "Client already registered");
} }
await createUserClient(userId, client.id); await createUserClient(userId, client.id);
clientId = client.id; clientId = client.id;
} else { } else {
const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) {
const pubKeyObject = createPublicKey(pubKeyPem); error(400, "Invalid public key(s)");
if ( } else if (encPubKey === sigPubKey) {
pubKeyObject.asymmetricKeyType !== "rsa" || error(400, "Public keys must be different");
pubKeyObject.asymmetricKeyDetails?.modulusLength !== 4096 } else if (
(await countClientByPubKey(encPubKey)) > 0 ||
(await countClientByPubKey(sigPubKey)) > 0
) { ) {
error(400, "Invalid public key"); error(409, "Public key(s) already registered");
} }
clientId = await createClient(pubKey, userId); clientId = await createClient(encPubKey, sigPubKey, userId);
} }
return await generateChallenge(userId, ip, clientId, pubKey); return { challenge: await createUserClientChallenge(userId, ip, clientId, encPubKey) };
}; };
export const getUserClientStatus = async (userId: number, clientId: number) => { export const getUserClientStatus = async (userId: number, clientId: number) => {
@@ -78,7 +88,12 @@ export const getUserClientStatus = async (userId: number, clientId: number) => {
}; };
}; };
export const verifyUserClient = async (userId: number, ip: string, answer: string) => { export const verifyUserClient = async (
userId: number,
ip: string,
answer: string,
sigAnswer: string,
) => {
const challenge = await getUserClientChallenge(answer, ip); const challenge = await getUserClientChallenge(answer, ip);
if (!challenge) { if (!challenge) {
error(401, "Invalid challenge answer"); error(401, "Invalid challenge answer");
@@ -86,5 +101,13 @@ export const verifyUserClient = async (userId: number, ip: string, answer: strin
error(403, "Forbidden"); error(403, "Forbidden");
} }
const client = await getClient(challenge.clientId);
if (!client) {
error(500, "Invalid challenge answer");
} else if (!verifySignature(answer, sigAnswer, client.sigPubKey)) {
error(401, "Invalid challenge answer signature");
}
await markUserClientChallengeAsUsed(challenge.id);
await setUserClientStateToPending(userId, challenge.clientId); await setUserClientStateToPending(userId, challenge.clientId);
}; };

41
src/lib/services/auth.ts Normal file
View File

@@ -0,0 +1,41 @@
import {
encodeToBase64,
decodeFromBase64,
decryptRSACiphertext,
signRSAMessage,
} from "$lib/modules/crypto";
export const requestTokenUpgrade = async (
encryptKeyBase64: string,
decryptKey: CryptoKey,
verifyKeyBase64: string,
signKey: CryptoKey,
) => {
let res = await fetch("/api/auth/upgradeToken", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
encPubKey: encryptKeyBase64,
sigPubKey: verifyKeyBase64,
}),
});
if (!res.ok) return false;
const { challenge } = await res.json();
const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey);
const sigAnswer = await signRSAMessage(answer, signKey);
res = await fetch("/api/auth/upgradeToken/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
answer: encodeToBase64(answer),
sigAnswer: encodeToBase64(sigAnswer),
}),
});
return res.ok;
};

42
src/lib/services/key.ts Normal file
View File

@@ -0,0 +1,42 @@
import { callAPI } from "$lib/hooks";
import {
encodeToBase64,
decodeFromBase64,
decryptRSACiphertext,
signRSAMessage,
} from "$lib/modules/crypto";
export const requestClientRegistration = async (
encryptKeyBase64: string,
decryptKey: CryptoKey,
verifyKeyBase64: string,
signKey: CryptoKey,
) => {
let res = await callAPI("/api/client/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
encPubKey: encryptKeyBase64,
sigPubKey: verifyKeyBase64,
}),
});
if (!res.ok) return false;
const { challenge } = await res.json();
const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey);
const sigAnswer = await signRSAMessage(answer, signKey);
res = await callAPI("/api/client/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
answer: encodeToBase64(answer),
sigAnswer: encodeToBase64(sigAnswer),
}),
});
return res.ok;
};

View File

@@ -1,4 +1,11 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export const keyPairStore = writable<CryptoKeyPair | null>(null); export interface ClientKeys {
encryptKey: CryptoKey;
decryptKey: CryptoKey;
signKey: CryptoKey;
verifyKey: CryptoKey;
}
export const clientKeyStore = writable<ClientKeys | null>(null);
export const mekStore = writable<Map<number, CryptoKey>>(new Map()); export const mekStore = writable<Map<number, CryptoKey>>(new Map());

View File

@@ -5,8 +5,8 @@
import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { TitleDiv, BottomDiv } from "$lib/components/divs";
import { TextInput } from "$lib/components/inputs"; import { TextInput } from "$lib/components/inputs";
import { refreshToken } from "$lib/hooks/callAPI"; import { refreshToken } from "$lib/hooks/callAPI";
import { keyPairStore } from "$lib/stores"; import { clientKeyStore } from "$lib/stores";
import { requestLogin } from "./service"; import { requestLogin, requestTokenUpgrade } from "./service";
let { data } = $props(); let { data } = $props();
@@ -16,14 +16,20 @@
const login = async () => { const login = async () => {
// TODO: Validation // TODO: Validation
if (await requestLogin(email, password, $keyPairStore)) { try {
if (!(await requestLogin(email, password))) throw new Error("Failed to login");
if ($clientKeyStore && !(await requestTokenUpgrade($clientKeyStore)))
throw new Error("Failed to upgrade token");
await goto( await goto(
$keyPairStore $clientKeyStore
? data.redirectPath ? data.redirectPath
: "/key/generate?redirect=" + encodeURIComponent(data.redirectPath), : "/key/generate?redirect=" + encodeURIComponent(data.redirectPath),
); );
} else { } catch (e) {
// TODO: Alert // TODO: Alert
throw e;
} }
}; };

View File

@@ -1,48 +1,38 @@
import { encodeToBase64, exportRSAKey } from "$lib/modules/crypto"; import { exportRSAKeyToBase64 } from "$lib/modules/crypto";
import { requestPubKeyRegistration } from "../../key/export/service"; import { requestTokenUpgrade as requestTokenUpgradeInternal } from "$lib/services/auth";
import { requestClientRegistration } from "$lib/services/key";
import type { ClientKeys } from "$lib/stores";
const callLoginAPI = async (email: string, password: string, pubKeyBase64?: string) => { export const requestLogin = async (email: string, password: string) => {
return await fetch("/api/auth/login", { const res = await fetch("/api/auth/login", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({ email, password }),
email,
password,
pubKey: pubKeyBase64,
}),
}); });
return res.ok;
}; };
export const requestLogin = async ( export const requestTokenUpgrade = async ({
email: string, encryptKey,
password: string, decryptKey,
keyPair: CryptoKeyPair | null, signKey,
registerPubKey = true, verifyKey,
): Promise<boolean> => { }: ClientKeys) => {
const pubKeyBase64 = keyPair const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey);
? encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key) const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey);
: undefined; if (await requestTokenUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) {
let loginRes = await callLoginAPI(email, password, pubKeyBase64);
if (loginRes.ok) {
return true; return true;
} else if (loginRes.status !== 401 || !keyPair || !registerPubKey) {
return false;
} }
const { message } = await loginRes.json(); if (await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) {
if (message !== "Unregistered public key") { return await requestTokenUpgradeInternal(
return false; encryptKeyBase64,
} decryptKey,
verifyKeyBase64,
loginRes = await callLoginAPI(email, password); signKey,
if (!loginRes.ok) { );
return false;
}
if (await requestPubKeyRegistration(pubKeyBase64!, keyPair.privateKey)) {
return requestLogin(email, password, keyPair, false);
} else { } else {
return false; return false;
} }

View File

@@ -3,13 +3,13 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { Button, TextButton } from "$lib/components/buttons"; import { Button, TextButton } from "$lib/components/buttons";
import { BottomDiv } from "$lib/components/divs"; import { BottomDiv } from "$lib/components/divs";
import { keyPairStore } from "$lib/stores"; import { clientKeyStore } from "$lib/stores";
import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte"; import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte";
import BeforeContinueModal from "./BeforeContinueModal.svelte"; import BeforeContinueModal from "./BeforeContinueModal.svelte";
import { import {
createBlobFromKeyPairBase64, exportClientKeys,
requestPubKeyRegistration, requestClientRegistration,
storeKeyPairPersistently, storeClientKeys,
requestTokenUpgrade, requestTokenUpgrade,
requestInitialMekRegistration, requestInitialMekRegistration,
} from "./service"; } from "./service";
@@ -22,8 +22,16 @@
let isBeforeContinueBottomSheetOpen = $state(false); let isBeforeContinueBottomSheetOpen = $state(false);
const exportKeyPair = () => { const exportKeyPair = () => {
const keyPairBlob = createBlobFromKeyPairBase64(data.pubKeyBase64, data.privKeyBase64); const clientKeysExported = exportClientKeys(
saveAs(keyPairBlob, "arkvalut-keypair.pem"); data.encryptKeyBase64,
data.decryptKeyBase64,
data.signKeyBase64,
data.verifyKeyBase64,
);
const clientKeysBlob = new Blob([JSON.stringify(clientKeysExported)], {
type: "application/json",
});
saveAs(clientKeysBlob, "arkvalut-clientkey.json");
if (!isBeforeContinueBottomSheetOpen) { if (!isBeforeContinueBottomSheetOpen) {
setTimeout(() => { setTimeout(() => {
@@ -33,7 +41,7 @@
}; };
const registerPubKey = async () => { const registerPubKey = async () => {
if (!$keyPairStore) { if (!$clientKeyStore) {
throw new Error("Failed to find key pair"); throw new Error("Failed to find key pair");
} }
@@ -41,15 +49,29 @@
isBeforeContinueBottomSheetOpen = false; isBeforeContinueBottomSheetOpen = false;
try { try {
if (!(await requestPubKeyRegistration(data.pubKeyBase64, $keyPairStore.privateKey))) if (
throw new Error("Failed to register public key"); !(await requestClientRegistration(
data.encryptKeyBase64,
$clientKeyStore.decryptKey,
data.verifyKeyBase64,
$clientKeyStore.signKey,
))
)
throw new Error("Failed to register client");
await storeKeyPairPersistently($keyPairStore); await storeClientKeys($clientKeyStore);
if (!(await requestTokenUpgrade(data.pubKeyBase64))) if (
!(await requestTokenUpgrade(
data.encryptKeyBase64,
$clientKeyStore.decryptKey,
data.verifyKeyBase64,
$clientKeyStore.signKey,
))
)
throw new Error("Failed to upgrade token"); throw new Error("Failed to upgrade token");
if (!(await requestInitialMekRegistration(data.mekDraft, $keyPairStore.publicKey))) if (!(await requestInitialMekRegistration(data.mekDraft, $clientKeyStore.encryptKey)))
throw new Error("Failed to register initial MEK"); throw new Error("Failed to register initial MEK");
await goto(data.redirectPath); await goto(data.redirectPath);

View File

@@ -1,68 +1,51 @@
import { callAPI } from "$lib/hooks"; import { callAPI } from "$lib/hooks";
import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; import { storeRSAKey } from "$lib/indexedDB";
import { import { encodeToBase64, encryptRSAPlaintext } from "$lib/modules/crypto";
encodeToBase64, import type { ClientKeys } from "$lib/stores";
decodeFromBase64,
encryptRSAPlaintext,
decryptRSACiphertext,
} from "$lib/modules/crypto";
export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: string) => { export { requestTokenUpgrade } from "$lib/services/auth";
const pubKeyFormatted = pubKeyBase64.match(/.{1,64}/g)?.join("\n"); export { requestClientRegistration } from "$lib/services/key";
const privKeyFormatted = privKeyBase64.match(/.{1,64}/g)?.join("\n");
if (!pubKeyFormatted || !privKeyFormatted) {
throw new Error("Failed to format key pair");
}
const pubKeyPem = `-----BEGIN RSA PUBLIC KEY-----\n${pubKeyFormatted}\n-----END RSA PUBLIC KEY-----`; type ExportedKeyPairs = {
const privKeyPem = `-----BEGIN RSA PRIVATE KEY-----\n${privKeyFormatted}\n-----END RSA PRIVATE KEY-----`; generator: "ArkVault";
return new Blob([`${pubKeyPem}\n${privKeyPem}\n`], { type: "text/plain" }); exportedAt: Date;
} & {
version: 1;
encryptKey: string;
decryptKey: string;
signKey: string;
verifyKey: string;
}; };
export const requestPubKeyRegistration = async (pubKeyBase64: string, privateKey: CryptoKey) => { export const exportClientKeys = (
let res = await callAPI("/api/client/register", { encryptKeyBase64: string,
method: "POST", decryptKeyBase64: string,
headers: { signKeyBase64: string,
"Content-Type": "application/json", verifyKeyBase64: string,
}, ) => {
body: JSON.stringify({ pubKey: pubKeyBase64 }), return {
}); version: 1,
if (!res.ok) return false; generator: "ArkVault",
exportedAt: new Date(),
const data = await res.json(); encryptKey: encryptKeyBase64,
const challenge = data.challenge as string; decryptKey: decryptKeyBase64,
const answer = await decryptRSACiphertext(decodeFromBase64(challenge), privateKey); signKey: signKeyBase64,
verifyKey: verifyKeyBase64,
res = await callAPI("/api/client/verify", { } satisfies ExportedKeyPairs;
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ answer: encodeToBase64(answer) }),
});
return res.ok;
}; };
export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => { export const storeClientKeys = async (clientKeys: ClientKeys) => {
await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey); await storeRSAKey(clientKeys.encryptKey, "encrypt");
}; await storeRSAKey(clientKeys.decryptKey, "decrypt");
await storeRSAKey(clientKeys.signKey, "sign");
export const requestTokenUpgrade = async (pubKeyBase64: string) => { await storeRSAKey(clientKeys.verifyKey, "verify");
const res = await fetch("/api/auth/upgradeToken", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ pubKey: pubKeyBase64 }),
});
return res.ok;
}; };
export const requestInitialMekRegistration = async ( export const requestInitialMekRegistration = async (
mekDraft: ArrayBuffer, mekDraft: ArrayBuffer,
publicKey: CryptoKey, encryptKey: CryptoKey,
) => { ) => {
const mekDraftEncrypted = await encryptRSAPlaintext(mekDraft, publicKey); const mekDraftEncrypted = await encryptRSAPlaintext(mekDraft, encryptKey);
const res = await callAPI("/api/mek/register/initial", { const res = await callAPI("/api/mek/register/initial", {
method: "POST", method: "POST",
headers: { headers: {

View File

@@ -3,14 +3,15 @@
import { Button, TextButton } from "$lib/components/buttons"; import { Button, TextButton } from "$lib/components/buttons";
import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { TitleDiv, BottomDiv } from "$lib/components/divs";
import { gotoStateful } from "$lib/hooks"; import { gotoStateful } from "$lib/hooks";
import { keyPairStore } from "$lib/stores"; import { clientKeyStore } from "$lib/stores";
import Order from "./Order.svelte"; import Order from "./Order.svelte";
import { generateKeyPair, generateMekDraft } from "./service"; import { generateClientKeys, generateMekDraft } from "./service";
import IconKey from "~icons/material-symbols/key"; import IconKey from "~icons/material-symbols/key";
let { data } = $props(); let { data } = $props();
// TODO: Update
const orders = [ const orders = [
{ {
title: "암호 키는 공개 키와 개인 키로 구성돼요.", title: "암호 키는 공개 키와 개인 키로 구성돼요.",
@@ -33,19 +34,18 @@
const generate = async () => { const generate = async () => {
// TODO: Loading indicator // TODO: Loading indicator
const { pubKeyBase64, privKeyBase64 } = await generateKeyPair(); const clientKeys = await generateClientKeys();
const { mekDraft } = await generateMekDraft(); const { mekDraft } = await generateMekDraft();
await gotoStateful("/key/export", { await gotoStateful("/key/export", {
...clientKeys,
redirectPath: data.redirectPath, redirectPath: data.redirectPath,
pubKeyBase64,
privKeyBase64,
mekDraft, mekDraft,
}); });
}; };
$effect(() => { $effect(() => {
if ($keyPairStore) { if ($clientKeyStore) {
goto(data.redirectPath); goto(data.redirectPath);
} }
}); });

View File

@@ -1,26 +1,29 @@
import { import {
encodeToBase64,
generateRSAKeyPair, generateRSAKeyPair,
makeRSAKeyNonextractable, makeRSAKeyNonextractable,
exportRSAKey, exportRSAKeyToBase64,
generateAESKey, generateAESKey,
makeAESKeyNonextractable, makeAESKeyNonextractable,
exportAESKey, exportAESKey,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import { keyPairStore, mekStore } from "$lib/stores"; import { clientKeyStore, mekStore } from "$lib/stores";
export const generateKeyPair = async () => { export const generateClientKeys = async () => {
const keyPair = await generateRSAKeyPair(); const encKeyPair = await generateRSAKeyPair("encryption");
const privKeySecured = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); const sigKeyPair = await generateRSAKeyPair("signature");
keyPairStore.set({ clientKeyStore.set({
publicKey: keyPair.publicKey, encryptKey: encKeyPair.publicKey,
privateKey: privKeySecured, decryptKey: await makeRSAKeyNonextractable(encKeyPair.privateKey),
signKey: await makeRSAKeyNonextractable(sigKeyPair.privateKey),
verifyKey: sigKeyPair.publicKey,
}); });
return { return {
pubKeyBase64: encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key), encryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey),
privKeyBase64: encodeToBase64((await exportRSAKey(keyPair.privateKey, "private")).key), decryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey),
signKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey),
verifyKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey),
}; };
}; };
@@ -29,7 +32,7 @@ export const generateMekDraft = async () => {
const mekSecured = await makeAESKeyNonextractable(mek); const mekSecured = await makeAESKeyNonextractable(mek);
mekStore.update((meks) => { mekStore.update((meks) => {
meks.set(meks.size, mekSecured); meks.set(0, mekSecured);
return meks; return meks;
}); });

View File

@@ -1,21 +1,19 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { getKeyPairFromIndexedDB } from "$lib/indexedDB";
import { keyPairStore } from "$lib/stores";
import "../app.css"; import "../app.css";
import { prepareClientKeyStore } from "./services";
let { children } = $props(); let { children } = $props();
onMount(async () => { onMount(() => {
const { pubKey, privKey } = await getKeyPairFromIndexedDB(); prepareClientKeyStore().then(async (ok) => {
if (pubKey && privKey) { if (!ok && !["/auth", "/key"].some((path) => location.pathname.startsWith(path))) {
keyPairStore.set({ publicKey: pubKey, privateKey: privKey }); await goto(
} else if (!["/auth", "/key/generate"].some((path) => location.pathname.startsWith(path))) { "/key/generate?redirect=" + encodeURIComponent(location.pathname + location.search),
await goto( );
"/key/generate?redirect=" + encodeURIComponent(location.pathname + location.search), }
); });
}
}); });
</script> </script>

View File

@@ -10,14 +10,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
.object({ .object({
email: z.string().email().nonempty(), email: z.string().email().nonempty(),
password: z.string().nonempty(), password: z.string().nonempty(),
pubKey: z.string().base64().nonempty().optional(),
}) })
.safeParse(await request.json()); .safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body"); if (!zodRes.success) error(400, "Invalid request body");
const { email, password } = zodRes.data;
const { email, password, pubKey } = zodRes.data; const { accessToken, refreshToken } = await login(email.trim(), password.trim());
const { accessToken, refreshToken } = await login(email.trim(), password.trim(), pubKey?.trim());
cookies.set("accessToken", accessToken, { cookies.set("accessToken", accessToken, {
path: "/", path: "/",
maxAge: Math.floor(ms(env.jwt.accessExp) / 1000), maxAge: Math.floor(ms(env.jwt.accessExp) / 1000),
@@ -28,5 +26,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000), maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000),
sameSite: "strict", sameSite: "strict",
}); });
return text("Logged in", { headers: { "Content-Type": "text/plain" } }); return text("Logged in", { headers: { "Content-Type": "text/plain" } });
}; };

View File

@@ -7,8 +7,8 @@ export const POST: RequestHandler = async ({ cookies }) => {
if (!token) error(401, "Refresh token not found"); if (!token) error(401, "Refresh token not found");
await logout(token.trim()); await logout(token.trim());
cookies.delete("accessToken", { path: "/" }); cookies.delete("accessToken", { path: "/" });
cookies.delete("refreshToken", { path: "/api/auth" }); cookies.delete("refreshToken", { path: "/api/auth" });
return text("Logged out", { headers: { "Content-Type": "text/plain" } }); return text("Logged out", { headers: { "Content-Type": "text/plain" } });
}; };

View File

@@ -1,13 +1,12 @@
import { error, text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { refreshTokens } from "$lib/server/services/auth"; import { refreshToken as doRefreshToken } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ cookies }) => { export const POST: RequestHandler = async ({ cookies }) => {
const token = cookies.get("refreshToken"); const token = cookies.get("refreshToken");
if (!token) error(401, "Refresh token not found"); if (!token) error(401, "Refresh token not found");
const { accessToken, refreshToken } = await refreshTokens(token.trim()); const { accessToken, refreshToken } = await doRefreshToken(token.trim());
cookies.set("accessToken", accessToken, { cookies.set("accessToken", accessToken, {
path: "/", path: "/",
sameSite: "strict", sameSite: "strict",
@@ -16,5 +15,6 @@ export const POST: RequestHandler = async ({ cookies }) => {
path: "/api/auth", path: "/api/auth",
sameSite: "strict", sameSite: "strict",
}); });
return text("Token refreshed", { headers: { "Content-Type": "text/plain" } }); return text("Token refreshed", { headers: { "Content-Type": "text/plain" } });
}; };

View File

@@ -1,29 +1,26 @@
import { error, text } from "@sveltejs/kit"; import { error, json } from "@sveltejs/kit";
import { z } from "zod"; import { z } from "zod";
import { upgradeTokens } from "$lib/server/services/auth"; import { createTokenUpgradeChallenge } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, cookies }) => { export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
const token = cookies.get("refreshToken"); const token = cookies.get("refreshToken");
if (!token) error(401, "Refresh token not found"); if (!token) error(401, "Refresh token not found");
const zodRes = z const zodRes = z
.object({ .object({
pubKey: z.string().base64().nonempty(), encPubKey: z.string().base64().nonempty(),
sigPubKey: z.string().base64().nonempty(),
}) })
.safeParse(await request.json()); .safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body"); if (!zodRes.success) error(400, "Invalid request body");
const { encPubKey, sigPubKey } = zodRes.data;
const { pubKey } = zodRes.data; const { challenge } = await createTokenUpgradeChallenge(
const { accessToken, refreshToken } = await upgradeTokens(token.trim(), pubKey.trim()); token.trim(),
getClientAddress(),
cookies.set("accessToken", accessToken, { encPubKey.trim(),
path: "/", sigPubKey.trim(),
sameSite: "strict", );
}); return json({ challenge });
cookies.set("refreshToken", refreshToken, {
path: "/api/auth",
sameSite: "strict",
});
return text("Token upgraded", { headers: { "Content-Type": "text/plain" } });
}; };

View File

@@ -0,0 +1,35 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { upgradeToken } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
const token = cookies.get("refreshToken");
if (!token) error(401, "Refresh token not found");
const zodRes = z
.object({
answer: z.string().base64().nonempty(),
sigAnswer: z.string().base64().nonempty(),
})
.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { answer, sigAnswer } = zodRes.data;
const { accessToken, refreshToken } = await upgradeToken(
token.trim(),
getClientAddress(),
answer.trim(),
sigAnswer.trim(),
);
cookies.set("accessToken", accessToken, {
path: "/",
sameSite: "strict",
});
cookies.set("refreshToken", refreshToken, {
path: "/api/auth",
sameSite: "strict",
});
return text("Token upgraded", { headers: { "Content-Type": "text/plain" } });
};

View File

@@ -12,12 +12,18 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress
const zodRes = z const zodRes = z
.object({ .object({
pubKey: z.string().base64().nonempty(), encPubKey: z.string().base64().nonempty(),
sigPubKey: z.string().base64().nonempty(),
}) })
.safeParse(await request.json()); .safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body"); if (!zodRes.success) error(400, "Invalid request body");
const { pubKey } = zodRes.data; const { encPubKey, sigPubKey } = zodRes.data;
const challenge = await registerUserClient(userId, getClientAddress(), pubKey.trim()); const { challenge } = await registerUserClient(
userId,
getClientAddress(),
encPubKey.trim(),
sigPubKey.trim(),
);
return json({ challenge }); return json({ challenge });
}; };

View File

@@ -13,11 +13,12 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress
const zodRes = z const zodRes = z
.object({ .object({
answer: z.string().base64().nonempty(), answer: z.string().base64().nonempty(),
sigAnswer: z.string().base64().nonempty(),
}) })
.safeParse(await request.json()); .safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body"); if (!zodRes.success) error(400, "Invalid request body");
const { answer } = zodRes.data; const { answer, sigAnswer } = zodRes.data;
await verifyUserClient(userId, getClientAddress(), answer.trim()); await verifyUserClient(userId, getClientAddress(), answer.trim(), sigAnswer.trim());
return text("Client verified", { headers: { "Content-Type": "text/plain" } }); return text("Client verified", { headers: { "Content-Type": "text/plain" } });
}; };

15
src/routes/services.ts Normal file
View File

@@ -0,0 +1,15 @@
import { getRSAKey } from "$lib/indexedDB";
import { clientKeyStore } from "$lib/stores";
export const prepareClientKeyStore = async () => {
const encryptKey = await getRSAKey("encrypt");
const decryptKey = await getRSAKey("decrypt");
const signKey = await getRSAKey("sign");
const verifyKey = await getRSAKey("verify");
if (encryptKey && decryptKey && signKey && verifyKey) {
clientKeyStore.set({ encryptKey, decryptKey, signKey, verifyKey });
return true;
} else {
return false;
}
};