From 8771b324a190668c64f49e1564f1453ec19db7a3 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 00:32:33 +0900 Subject: [PATCH 001/115] =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.html | 6 +++ src/lib/components/AdaptiveDiv.svelte | 7 +++ src/lib/components/buttons/Button.svelte | 28 ++++++++++++ src/lib/components/buttons/TextButton.svelte | 14 ++++++ src/lib/components/buttons/index.ts | 2 + src/lib/components/index.ts | 1 + src/lib/components/inputs/TextInput.svelte | 35 ++++++++++++++ src/lib/components/inputs/index.ts | 1 + tailwind.config.ts | 48 +++++++++++++++++++- 9 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/lib/components/AdaptiveDiv.svelte create mode 100644 src/lib/components/buttons/Button.svelte create mode 100644 src/lib/components/buttons/TextButton.svelte create mode 100644 src/lib/components/buttons/index.ts create mode 100644 src/lib/components/index.ts create mode 100644 src/lib/components/inputs/TextInput.svelte create mode 100644 src/lib/components/inputs/index.ts diff --git a/src/app.html b/src/app.html index 84ffad1..4471298 100644 --- a/src/app.html +++ b/src/app.html @@ -3,6 +3,12 @@ + %sveltekit.head% diff --git a/src/lib/components/AdaptiveDiv.svelte b/src/lib/components/AdaptiveDiv.svelte new file mode 100644 index 0000000..ee845cc --- /dev/null +++ b/src/lib/components/AdaptiveDiv.svelte @@ -0,0 +1,7 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte new file mode 100644 index 0000000..db27c33 --- /dev/null +++ b/src/lib/components/buttons/Button.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/buttons/TextButton.svelte b/src/lib/components/buttons/TextButton.svelte new file mode 100644 index 0000000..6ccb71f --- /dev/null +++ b/src/lib/components/buttons/TextButton.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/buttons/index.ts b/src/lib/components/buttons/index.ts new file mode 100644 index 0000000..0b93571 --- /dev/null +++ b/src/lib/components/buttons/index.ts @@ -0,0 +1,2 @@ +export { default as Button } from "./Button.svelte"; +export { default as TextButton } from "./TextButton.svelte"; diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts new file mode 100644 index 0000000..85508da --- /dev/null +++ b/src/lib/components/index.ts @@ -0,0 +1 @@ +export { default as AdaptiveDiv } from "./AdaptiveDiv.svelte"; diff --git a/src/lib/components/inputs/TextInput.svelte b/src/lib/components/inputs/TextInput.svelte new file mode 100644 index 0000000..e8bee93 --- /dev/null +++ b/src/lib/components/inputs/TextInput.svelte @@ -0,0 +1,35 @@ + + +
+ + + +
+ + diff --git a/src/lib/components/inputs/index.ts b/src/lib/components/inputs/index.ts new file mode 100644 index 0000000..c2c534d --- /dev/null +++ b/src/lib/components/inputs/index.ts @@ -0,0 +1 @@ +export { default as TextInput } from "./TextInput.svelte"; diff --git a/tailwind.config.ts b/tailwind.config.ts index 9d75afa..e8c5803 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -4,7 +4,53 @@ export default { content: ["./src/**/*.{html,js,svelte,ts}"], theme: { - extend: {}, + extend: { + colors: { + primary: { + "000": "#FFF5F5", + 100: "#FFE3E3", + 200: "#FFC9C9", + 300: "#FFA8A8", + 400: "#FF8787", + 500: "#FF6B6B", + 600: "#FA5252", + 700: "#F03E3E", + 800: "#E03131", + 900: "#C92A2A", + }, + gray: { + "000": "#F8F9FA", + 100: "#F1F3F5", + 200: "#E9ECEF", + 300: "#DEE2E6", + 400: "#CED4DA", + 500: "#ADB5BD", + 600: "#868E96", + 700: "#495057", + 800: "#343A40", + 900: "#212529", + }, + }, + fontFamily: { + sans: [ + '"Pretendard Variable"', + "Pretendard", + "-apple-system", + "BlinkMacSystemFont", + "system-ui", + "Roboto", + '"Helvetica Neue"', + '"Segoe UI"', + '"Apple SD Gothic Neo"', + '"Noto Sans KR"', + '"Malgun Gothic"', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + "sans-serif", + ], + }, + }, }, plugins: [], From 07252126eceb6eacd921495a72bbaaa8317333c6 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 01:20:01 +0900 Subject: [PATCH 002/115] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/buttons/Button.svelte | 2 +- src/lib/components/inputs/TextInput.svelte | 2 +- src/routes/(fullscreen)/+layout.svelte | 11 ++++++++ src/routes/(fullscreen)/login/+page.svelte | 29 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/routes/(fullscreen)/+layout.svelte create mode 100644 src/routes/(fullscreen)/login/+page.svelte diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte index db27c33..78025ff 100644 --- a/src/lib/components/buttons/Button.svelte +++ b/src/lib/components/buttons/Button.svelte @@ -21,7 +21,7 @@ ); - + +
+ 계정이 없어요 +
+ + From d83dc6b8d20cfb7d8316f26737da4bcddf14cbad Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 16:01:14 +0900 Subject: [PATCH 003/115] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20DB=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++ drizzle.config.ts | 2 +- package.json | 1 + src/lib/server/db/schema/device.ts | 29 +++++++++++++++++++ src/lib/server/db/schema/index.ts | 2 ++ .../server/db/{schema.ts => schema/user.ts} | 3 +- src/routes/(fullscreen)/login/+page.svelte | 2 +- 7 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 src/lib/server/db/schema/device.ts create mode 100644 src/lib/server/db/schema/index.ts rename src/lib/server/db/{schema.ts => schema/user.ts} (64%) diff --git a/.gitignore b/.gitignore index 171f629..17c5531 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,15 @@ node_modules .wrangler /.svelte-kit /build +/drizzle # OS .DS_Store Thumbs.db +# VSCode +/.vscode + # Env .env .env.* diff --git a/drizzle.config.ts b/drizzle.config.ts index 68872c7..9a64b26 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "drizzle-kit"; if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); export default defineConfig({ - schema: "./src/lib/server/db/schema.ts", + schema: "./src/lib/server/db/schema", dbCredentials: { url: process.env.DATABASE_URL, diff --git a/package.json b/package.json index 4b09ce6..66d9525 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "format": "prettier --write .", "lint": "prettier --check . && eslint .", "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio" }, diff --git a/src/lib/server/db/schema/device.ts b/src/lib/server/db/schema/device.ts new file mode 100644 index 0000000..44526c6 --- /dev/null +++ b/src/lib/server/db/schema/device.ts @@ -0,0 +1,29 @@ +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; +import { user } from "./user"; + +export enum UserDeviceState { + PENDING = 0, + ACTIVE = 1, +} + +export const device = sqliteTable("device", { + id: integer("id").primaryKey(), + pubKey: text("pub_key").notNull().unique(), +}); + +export const userDevice = sqliteTable( + "user_device", + { + userId: integer("user_id") + .notNull() + .references(() => user.id), + deviceId: integer("device_id") + .notNull() + .references(() => device.id), + state: integer("state").notNull().default(0), + encKey: text("enc_key"), + }, + (t) => ({ + pk: primaryKey({ columns: [t.userId, t.deviceId] }), + }), +); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts new file mode 100644 index 0000000..bcd10e8 --- /dev/null +++ b/src/lib/server/db/schema/index.ts @@ -0,0 +1,2 @@ +export * from "./device"; +export * from "./user"; diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema/user.ts similarity index 64% rename from src/lib/server/db/schema.ts rename to src/lib/server/db/schema/user.ts index d6309dd..2ad0e3c 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema/user.ts @@ -2,5 +2,6 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; export const user = sqliteTable("user", { id: integer("id").primaryKey(), - age: integer("age"), + email: text("email").notNull().unique(), + password: text("password").notNull(), }); diff --git a/src/routes/(fullscreen)/login/+page.svelte b/src/routes/(fullscreen)/login/+page.svelte index 191f4e2..db09d0a 100644 --- a/src/routes/(fullscreen)/login/+page.svelte +++ b/src/routes/(fullscreen)/login/+page.svelte @@ -14,7 +14,7 @@

서비스를 이용하려면 로그인을 해야해요.

- +
From fac876457226ef8463a4c7bf9b2ff16b864cfaeb Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 16:50:13 +0900 Subject: [PATCH 004/115] =?UTF-8?q?/api/auth/login=20Endpoint=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +- package.json | 6 +- pnpm-lock.yaml | 117 ++++++++++++++++++++++++++- src/lib/server/auth.ts | 27 +++++++ src/lib/server/db/drizzle.ts | 7 ++ src/lib/server/db/index.ts | 6 -- src/lib/server/db/user.ts | 8 ++ src/lib/server/loadenv.ts | 12 +++ src/routes/api/auth/login/+server.ts | 22 +++++ tsconfig.json | 1 + 10 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/db/drizzle.ts delete mode 100644 src/lib/server/db/index.ts create mode 100644 src/lib/server/db/user.ts create mode 100644 src/lib/server/loadenv.ts create mode 100644 src/routes/api/auth/login/+server.ts diff --git a/.env.example b/.env.example index d59bf33..e76f3cd 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ -DATABASE_URL=local.db +# Required environment variables +JWT_SECRET= + +# Optional environment variables +DATABASE_URL= +JWT_ACCESS_TOKEN_EXPIRES= +JWT_REFRESH_TOKEN_EXPIRES= diff --git a/package.json b/package.json index 66d9525..010856a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/better-sqlite3": "^7.6.11", + "@types/jsonwebtoken": "^9.0.7", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.22.0", "eslint": "^9.7.0", @@ -39,7 +40,10 @@ "vite": "^5.4.11" }, "dependencies": { + "argon2": "^0.41.1", "better-sqlite3": "^11.1.2", - "drizzle-orm": "^0.33.0" + "drizzle-orm": "^0.33.0", + "jsonwebtoken": "^9.0.2", + "zod": "^3.24.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59254a4..0a0e593 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,12 +5,21 @@ settings: excludeLinksFromLockfile: false dependencies: + argon2: + specifier: ^0.41.1 + version: 0.41.1 better-sqlite3: specifier: ^11.1.2 version: 11.7.0 drizzle-orm: specifier: ^0.33.0 version: 0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.0) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + zod: + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@eslint/compat': @@ -28,6 +37,9 @@ devDependencies: '@types/better-sqlite3': specifier: ^7.6.11 version: 7.6.12 + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.7 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -887,6 +899,11 @@ packages: fastq: 1.18.0 dev: true + /@phc/format@1.0.0: + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + dev: false + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1205,6 +1222,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonwebtoken@9.0.7: + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + dependencies: + '@types/node': 22.10.2 + dev: true + /@types/node@22.10.2: resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} dependencies: @@ -1399,6 +1422,16 @@ packages: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} dev: true + /argon2@0.41.1: + resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==} + engines: {node: '>=16.17.0'} + requiresBuild: true + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.3.0 + node-gyp-build: 4.8.4 + dev: false + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -1495,6 +1528,10 @@ packages: update-browserslist-db: 1.1.1(browserslist@4.24.3) dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -1761,6 +1798,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /electron-to-chromium@1.5.76: resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} dev: true @@ -2371,6 +2414,37 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -2419,10 +2493,38 @@ packages: p-locate: 5.0.0 dev: true + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} dev: true @@ -2489,7 +2591,6 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2520,6 +2621,16 @@ packages: semver: 7.6.3 dev: false + /node-addon-api@8.3.0: + resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} + engines: {node: ^18 || ^20 || >= 21} + dev: false + + /node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + dev: false + /node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} dev: true @@ -3411,3 +3522,7 @@ packages: /zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} dev: true + + /zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + dev: false diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..85efdff --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,27 @@ +import argon2 from "argon2"; +import jwt from "jsonwebtoken"; +import { getUserByEmail } from "$lib/server/db/user"; +import env from "$lib/server/loadenv"; + +const verifyPassword = async (hash: string, password: string) => { + return await argon2.verify(hash, password); +}; + +const issueToken = (id: number, type: "access" | "refresh") => { + return jwt.sign({ id, type }, env.jwt.secret, { + expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, + }); +}; + +export const login = async (email: string, password: string) => { + const user = await getUserByEmail(email); + if (!user) return null; + + const valid = await verifyPassword(user.password, password); + if (!valid) return null; + + return { + accessToken: issueToken(user.id, "access"), + refreshToken: issueToken(user.id, "refresh"), + }; +}; diff --git a/src/lib/server/db/drizzle.ts b/src/lib/server/db/drizzle.ts new file mode 100644 index 0000000..385ac23 --- /dev/null +++ b/src/lib/server/db/drizzle.ts @@ -0,0 +1,7 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import env from "$lib/server/loadenv"; + +const client = new Database(env.databaseUrl); + +export default drizzle(client); diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts deleted file mode 100644 index 8093e19..0000000 --- a/src/lib/server/db/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { drizzle } from "drizzle-orm/better-sqlite3"; -import Database from "better-sqlite3"; -import { env } from "$env/dynamic/private"; -if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); -const client = new Database(env.DATABASE_URL); -export const db = drizzle(client); diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts new file mode 100644 index 0000000..38a53f0 --- /dev/null +++ b/src/lib/server/db/user.ts @@ -0,0 +1,8 @@ +import { eq } from "drizzle-orm"; +import db from "./drizzle"; +import { user } from "./schema"; + +export const getUserByEmail = async (email: string) => { + const users = await db.select().from(user).where(eq(user.email, email)).execute(); + return users[0] ?? null; +}; diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts new file mode 100644 index 0000000..83f4514 --- /dev/null +++ b/src/lib/server/loadenv.ts @@ -0,0 +1,12 @@ +import { env } from "$env/dynamic/private"; + +if (!env.JWT_SECRET) throw new Error("JWT_SECRET is not set"); + +export default { + databaseUrl: env.DATABASE_URL || "local.db", + jwt: { + secret: env.JWT_SECRET, + accessExp: env.JWT_ACCESS_TOKEN_EXPIRES || "5m", + refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d", + }, +}; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts new file mode 100644 index 0000000..09bcdcf --- /dev/null +++ b/src/routes/api/auth/login/+server.ts @@ -0,0 +1,22 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { login } from "$lib/server/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request }) => { + const zodRes = z + .object({ + email: z.string().email().nonempty(), + password: z.string().nonempty(), + }) + .safeParse(await request.json()); + if (!zodRes.success) error(400, zodRes.error.message); + + const { email, password } = zodRes.data; + const loginRes = await login(email.trim(), password.trim()); + + if (!loginRes) error(401, "Invalid email or password"); + const { accessToken, refreshToken } = loginRes; + + return json({ accessToken, refreshToken }); +}; diff --git a/tsconfig.json b/tsconfig.json index f4d0a0e..33a6652 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, + "noUncheckedIndexedAccess": true, "moduleResolution": "bundler" } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias From 45e214d49f86a77426e19f562c955119e117664d Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 17:04:52 +0900 Subject: [PATCH 005/115] =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=97=90=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=A0=80=EC=9E=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/auth.ts | 35 ++++++++++++++----- src/lib/server/db/client.ts | 17 +++++++++ .../server/db/schema/{device.ts => client.ts} | 18 +++++----- src/lib/server/db/schema/index.ts | 2 +- src/routes/api/auth/login/+server.ts | 8 ++--- 5 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 src/lib/server/db/client.ts rename src/lib/server/db/schema/{device.ts => client.ts} (51%) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 85efdff..87b3bf7 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,27 +1,44 @@ import argon2 from "argon2"; import jwt from "jsonwebtoken"; +import { getClientByPubKey } from "$lib/server/db/client"; import { getUserByEmail } from "$lib/server/db/user"; import env from "$lib/server/loadenv"; +interface TokenData { + type: "access" | "refresh"; + userId: number; + clientId?: number; +} + const verifyPassword = async (hash: string, password: string) => { return await argon2.verify(hash, password); }; -const issueToken = (id: number, type: "access" | "refresh") => { - return jwt.sign({ id, type }, env.jwt.secret, { - expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, - }); +const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => { + return jwt.sign( + { + type, + userId, + clientId, + } satisfies TokenData, + env.jwt.secret, + { + expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, + }, + ); }; -export const login = async (email: string, password: string) => { +export const login = async (email: string, password: string, pubKey?: string) => { const user = await getUserByEmail(email); if (!user) return null; - const valid = await verifyPassword(user.password, password); - if (!valid) return null; + const isValid = await verifyPassword(user.password, password); + if (!isValid) return null; + + const client = pubKey ? await getClientByPubKey(pubKey) : null; return { - accessToken: issueToken(user.id, "access"), - refreshToken: issueToken(user.id, "refresh"), + accessToken: issueToken("access", user.id, client?.id), + refreshToken: issueToken("refresh", user.id, client?.id), }; }; diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts new file mode 100644 index 0000000..a4e2cdb --- /dev/null +++ b/src/lib/server/db/client.ts @@ -0,0 +1,17 @@ +import { and, eq } from "drizzle-orm"; +import db from "./drizzle"; +import { client, userClient } from "./schema"; + +export const getClientByPubKey = async (pubKey: string) => { + const clients = await db.select().from(client).where(eq(client.pubKey, pubKey)).execute(); + return clients[0] ?? null; +}; + +export const getUserClient = async (userId: number, clientId: number) => { + const userClients = await db + .select() + .from(userClient) + .where(and(eq(userClient.userId, userId), eq(userClient.clientId, clientId))) + .execute(); + return userClients[0] ?? null; +}; diff --git a/src/lib/server/db/schema/device.ts b/src/lib/server/db/schema/client.ts similarity index 51% rename from src/lib/server/db/schema/device.ts rename to src/lib/server/db/schema/client.ts index 44526c6..ad5d678 100644 --- a/src/lib/server/db/schema/device.ts +++ b/src/lib/server/db/schema/client.ts @@ -1,29 +1,29 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; import { user } from "./user"; -export enum UserDeviceState { +export enum UserClientState { PENDING = 0, ACTIVE = 1, } -export const device = sqliteTable("device", { +export const client = sqliteTable("client", { id: integer("id").primaryKey(), - pubKey: text("pub_key").notNull().unique(), + pubKey: text("public_key").notNull().unique(), }); -export const userDevice = sqliteTable( - "user_device", +export const userClient = sqliteTable( + "user_client", { userId: integer("user_id") .notNull() .references(() => user.id), - deviceId: integer("device_id") + clientId: integer("client_id") .notNull() - .references(() => device.id), + .references(() => client.id), state: integer("state").notNull().default(0), - encKey: text("enc_key"), + encKey: text("encrypted_key"), }, (t) => ({ - pk: primaryKey({ columns: [t.userId, t.deviceId] }), + pk: primaryKey({ columns: [t.userId, t.clientId] }), }), ); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index bcd10e8..6674d19 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,2 +1,2 @@ -export * from "./device"; +export * from "./client"; export * from "./user"; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 09bcdcf..2ec24c3 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -8,15 +8,15 @@ export const POST: RequestHandler = async ({ request }) => { .object({ email: z.string().email().nonempty(), password: z.string().nonempty(), + pubKey: z.string().nonempty().optional(), }) .safeParse(await request.json()); if (!zodRes.success) error(400, zodRes.error.message); - const { email, password } = zodRes.data; - const loginRes = await login(email.trim(), password.trim()); + const { email, password, pubKey } = zodRes.data; + const loginRes = await login(email.trim(), password.trim(), pubKey?.trim()); + if (!loginRes) error(401, "Invalid email, password, or public key"); - if (!loginRes) error(401, "Invalid email or password"); const { accessToken, refreshToken } = loginRes; - return json({ accessToken, refreshToken }); }; From a42f26bab1fa313819effbbec85e88a6e5f243e2 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 17:44:44 +0900 Subject: [PATCH 006/115] =?UTF-8?q?/api/auth/logout,=20/api/auth/refreshTo?= =?UTF-8?q?ken=20Endpoint=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/auth.ts | 44 --------------- src/lib/server/db/schema/user.ts | 6 +++ src/lib/server/db/user.ts | 21 +++++++- src/lib/server/modules/auth.ts | 38 +++++++++++++ src/lib/server/services/auth.ts | 60 +++++++++++++++++++++ src/routes/api/auth/login/+server.ts | 8 +-- src/routes/api/auth/logout/+server.ts | 18 +++++++ src/routes/api/auth/refreshToken/+server.ts | 16 ++++++ 8 files changed, 160 insertions(+), 51 deletions(-) delete mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/modules/auth.ts create mode 100644 src/lib/server/services/auth.ts create mode 100644 src/routes/api/auth/logout/+server.ts create mode 100644 src/routes/api/auth/refreshToken/+server.ts diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts deleted file mode 100644 index 87b3bf7..0000000 --- a/src/lib/server/auth.ts +++ /dev/null @@ -1,44 +0,0 @@ -import argon2 from "argon2"; -import jwt from "jsonwebtoken"; -import { getClientByPubKey } from "$lib/server/db/client"; -import { getUserByEmail } from "$lib/server/db/user"; -import env from "$lib/server/loadenv"; - -interface TokenData { - type: "access" | "refresh"; - userId: number; - clientId?: number; -} - -const verifyPassword = async (hash: string, password: string) => { - return await argon2.verify(hash, password); -}; - -const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => { - return jwt.sign( - { - type, - userId, - clientId, - } satisfies TokenData, - env.jwt.secret, - { - expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, - }, - ); -}; - -export const login = async (email: string, password: string, pubKey?: string) => { - const user = await getUserByEmail(email); - if (!user) return null; - - const isValid = await verifyPassword(user.password, password); - if (!isValid) return null; - - const client = pubKey ? await getClientByPubKey(pubKey) : null; - - return { - accessToken: issueToken("access", user.id, client?.id), - refreshToken: issueToken("refresh", user.id, client?.id), - }; -}; diff --git a/src/lib/server/db/schema/user.ts b/src/lib/server/db/schema/user.ts index 2ad0e3c..474db82 100644 --- a/src/lib/server/db/schema/user.ts +++ b/src/lib/server/db/schema/user.ts @@ -5,3 +5,9 @@ export const user = sqliteTable("user", { email: text("email").notNull().unique(), password: text("password").notNull(), }); + +export const revokedToken = sqliteTable("revoked_token", { + id: integer("id").primaryKey(), + token: text("token").notNull().unique(), + revokedAt: integer("revoked_at").notNull(), +}); diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index 38a53f0..f564680 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -1,8 +1,27 @@ import { eq } from "drizzle-orm"; import db from "./drizzle"; -import { user } from "./schema"; +import { user, revokedToken } from "./schema"; export const getUserByEmail = async (email: string) => { const users = await db.select().from(user).where(eq(user.email, email)).execute(); return users[0] ?? null; }; + +export const revokeToken = async (token: string) => { + await db + .insert(revokedToken) + .values({ + token, + revokedAt: Date.now(), + }) + .execute(); +}; + +export const isTokenRevoked = async (token: string) => { + const tokens = await db + .select() + .from(revokedToken) + .where(eq(revokedToken.token, token)) + .execute(); + return tokens.length > 0; +}; diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts new file mode 100644 index 0000000..9ab7420 --- /dev/null +++ b/src/lib/server/modules/auth.ts @@ -0,0 +1,38 @@ +import jwt from "jsonwebtoken"; +import env from "$lib/server/loadenv"; + +interface TokenData { + type: "access" | "refresh"; + userId: number; + clientId?: number; +} + +export enum TokenError { + EXPIRED, + INVALID, +} + +export const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => { + return jwt.sign( + { + type, + userId, + clientId, + } satisfies TokenData, + env.jwt.secret, + { + expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, + }, + ); +}; + +export const verifyToken = (token: string) => { + try { + return jwt.verify(token, env.jwt.secret) as TokenData; + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + return TokenError.EXPIRED; + } + return TokenError.INVALID; + } +}; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts new file mode 100644 index 0000000..af2a9ab --- /dev/null +++ b/src/lib/server/services/auth.ts @@ -0,0 +1,60 @@ +import { error } from "@sveltejs/kit"; +import argon2 from "argon2"; +import { getClientByPubKey } from "$lib/server/db/client"; +import { getUserByEmail, revokeToken, isTokenRevoked } from "$lib/server/db/user"; +import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; + +const verifyPassword = async (hash: string, password: string) => { + return await argon2.verify(hash, password); +}; + +export const login = async (email: string, password: string, pubKey?: string) => { + const user = await getUserByEmail(email); + if (!user) { + error(401, "Invalid email or password"); + } + + const isValid = await verifyPassword(user.password, password); + if (!isValid) { + error(401, "Invalid email or password"); + } + + const client = pubKey ? await getClientByPubKey(pubKey) : undefined; + if (client === null) { + error(401, "Invalid public key"); + } + + return { + accessToken: issueToken("access", user.id, client?.id), + refreshToken: issueToken("refresh", user.id, client?.id), + }; +}; + +const verifyRefreshToken = async (refreshToken: string) => { + const tokenData = verifyToken(refreshToken); + if (tokenData === TokenError.EXPIRED) { + error(401, "Token expired"); + } else if ( + tokenData === TokenError.INVALID || + tokenData.type !== "refresh" || + (await isTokenRevoked(refreshToken)) + ) { + error(401, "Invalid token"); + } + return tokenData; +}; + +export const logout = async (refreshToken: string) => { + await verifyRefreshToken(refreshToken); + await revokeToken(refreshToken); +}; + +export const refreshToken = async (refreshToken: string) => { + const tokenData = await verifyRefreshToken(refreshToken); + + await revokeToken(refreshToken); + return { + accessToken: issueToken("access", tokenData.userId, tokenData.clientId), + refreshToken: issueToken("refresh", tokenData.userId, tokenData.clientId), + }; +}; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 2ec24c3..8364a76 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -1,6 +1,6 @@ import { error, json } from "@sveltejs/kit"; import { z } from "zod"; -import { login } from "$lib/server/auth"; +import { login } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request }) => { @@ -14,9 +14,5 @@ export const POST: RequestHandler = async ({ request }) => { if (!zodRes.success) error(400, zodRes.error.message); const { email, password, pubKey } = zodRes.data; - const loginRes = await login(email.trim(), password.trim(), pubKey?.trim()); - if (!loginRes) error(401, "Invalid email, password, or public key"); - - const { accessToken, refreshToken } = loginRes; - return json({ accessToken, refreshToken }); + return json(await login(email.trim(), password.trim(), pubKey?.trim())); }; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts new file mode 100644 index 0000000..29df035 --- /dev/null +++ b/src/routes/api/auth/logout/+server.ts @@ -0,0 +1,18 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { logout } from "$lib/server/services/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request }) => { + const zodRes = z + .object({ + refreshToken: z.string().nonempty(), + }) + .safeParse(await request.json()); + if (!zodRes.success) error(400, zodRes.error.message); + + const { refreshToken } = zodRes.data; + await logout(refreshToken.trim()); + + return text("Logged out"); +}; diff --git a/src/routes/api/auth/refreshToken/+server.ts b/src/routes/api/auth/refreshToken/+server.ts new file mode 100644 index 0000000..f07de53 --- /dev/null +++ b/src/routes/api/auth/refreshToken/+server.ts @@ -0,0 +1,16 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { refreshToken } from "$lib/server/services/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request }) => { + const zodRes = z + .object({ + refreshToken: z.string().nonempty(), + }) + .safeParse(await request.json()); + if (!zodRes.success) error(400, zodRes.error.message); + + const { refreshToken: token } = zodRes.data; + return json(await refreshToken(token.trim())); +}; From b6fbd83d6f7b937d02939089851de91267551011 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 18:54:31 +0900 Subject: [PATCH 007/115] =?UTF-8?q?Refresh=20Token=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callAPI.ts | 47 +++++++++++++++++++++ src/lib/stores/auth.ts | 3 ++ src/routes/api/auth/login/+server.ts | 12 +++++- src/routes/api/auth/logout/+server.ts | 15 ++----- src/routes/api/auth/refreshToken/+server.ts | 22 +++++----- 5 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 src/lib/hooks/callAPI.ts create mode 100644 src/lib/stores/auth.ts diff --git a/src/lib/hooks/callAPI.ts b/src/lib/hooks/callAPI.ts new file mode 100644 index 0000000..a26ff6f --- /dev/null +++ b/src/lib/hooks/callAPI.ts @@ -0,0 +1,47 @@ +import { accessToken } from "$lib/stores/auth"; + +const refreshToken = async () => { + const res = await fetch("/api/auth/refreshtoken", { + method: "POST", + credentials: "same-origin", + }); + if (!res.ok) { + accessToken.set(null); + throw new Error("Failed to refresh token"); + } + + const data = await res.json(); + const token = data.accessToken as string; + + accessToken.set(token); + return token; +}; + +const callAPIInternal = async ( + input: RequestInfo, + init?: RequestInit, + token?: string | null, + retryIfUnauthorized = true, +): Promise => { + if (token === null) { + token = await refreshToken(); + retryIfUnauthorized = false; + } + + const res = await fetch(input, { + ...init, + headers: { + ...init?.headers, + Authorization: `Bearer ${token}`, + }, + }); + if (res.status === 401 && retryIfUnauthorized) { + return await callAPIInternal(input, init, null, false); + } + + return res; +}; + +export const callAPI = async (input: RequestInfo, init?: RequestInit, token?: string | null) => { + return await callAPIInternal(input, init, token); +}; diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts new file mode 100644 index 0000000..954cee0 --- /dev/null +++ b/src/lib/stores/auth.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export const accessToken = writable(null); diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 8364a76..dec6e55 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { login } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request }) => { +export const POST: RequestHandler = async ({ request, cookies }) => { const zodRes = z .object({ email: z.string().email().nonempty(), @@ -14,5 +14,13 @@ export const POST: RequestHandler = async ({ request }) => { if (!zodRes.success) error(400, zodRes.error.message); const { email, password, pubKey } = zodRes.data; - return json(await login(email.trim(), password.trim(), pubKey?.trim())); + const { accessToken, refreshToken } = await login(email.trim(), password.trim(), pubKey?.trim()); + + cookies.set("refreshToken", refreshToken, { + path: "/api/auth", + httpOnly: true, + secure: true, + sameSite: "strict", + }); + return json({ accessToken }); }; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts index 29df035..0499b87 100644 --- a/src/routes/api/auth/logout/+server.ts +++ b/src/routes/api/auth/logout/+server.ts @@ -1,18 +1,11 @@ import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; import { logout } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request }) => { - const zodRes = z - .object({ - refreshToken: z.string().nonempty(), - }) - .safeParse(await request.json()); - if (!zodRes.success) error(400, zodRes.error.message); - - const { refreshToken } = zodRes.data; - await logout(refreshToken.trim()); +export const POST: RequestHandler = async ({ cookies }) => { + const token = cookies.get("refreshToken"); + if (!token) error(401, "Token not found"); + await logout(token.trim()); return text("Logged out"); }; diff --git a/src/routes/api/auth/refreshToken/+server.ts b/src/routes/api/auth/refreshToken/+server.ts index f07de53..62f2a77 100644 --- a/src/routes/api/auth/refreshToken/+server.ts +++ b/src/routes/api/auth/refreshToken/+server.ts @@ -1,16 +1,18 @@ import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; import { refreshToken } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request }) => { - const zodRes = z - .object({ - refreshToken: z.string().nonempty(), - }) - .safeParse(await request.json()); - if (!zodRes.success) error(400, zodRes.error.message); +export const POST: RequestHandler = async ({ cookies }) => { + const token = cookies.get("refreshToken"); + if (!token) error(401, "Token not found"); - const { refreshToken: token } = zodRes.data; - return json(await refreshToken(token.trim())); + const { accessToken, refreshToken: newToken } = await refreshToken(token.trim()); + + cookies.set("refreshToken", newToken, { + path: "/api/auth", + httpOnly: true, + secure: true, + sameSite: "strict", + }); + return json({ accessToken }); }; From bd1cc9ea38bffd5e8c7acef365a2eca8119e05fd Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 19:08:28 +0900 Subject: [PATCH 008/115] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/buttons/Button.svelte | 4 +++- src/lib/components/buttons/TextButton.svelte | 3 ++- src/lib/hooks/callAPI.ts | 4 ++-- src/lib/hooks/index.ts | 1 + src/routes/(fullscreen)/login/+page.svelte | 18 ++++++++++++++--- src/routes/(fullscreen)/login/service.ts | 21 ++++++++++++++++++++ 6 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 src/lib/hooks/index.ts create mode 100644 src/routes/(fullscreen)/login/service.ts diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte index 78025ff..08cc1f8 100644 --- a/src/lib/components/buttons/Button.svelte +++ b/src/lib/components/buttons/Button.svelte @@ -1,6 +1,8 @@ @@ -14,13 +26,13 @@

서비스를 이용하려면 로그인을 해야해요.

- - + +
- +
계정이 없어요 diff --git a/src/routes/(fullscreen)/login/service.ts b/src/routes/(fullscreen)/login/service.ts new file mode 100644 index 0000000..4799ea5 --- /dev/null +++ b/src/routes/(fullscreen)/login/service.ts @@ -0,0 +1,21 @@ +import { callAPI } from "$lib/hooks"; +import { accessToken } from "$lib/stores/auth"; + +export const requestLogin = async (email: string, password: string) => { + const res = await callAPI("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + if (!res.ok) { + return false; + } + + const data = await res.json(); + const token = data.accessToken as string; + + accessToken.set(token); + return true; +}; From da4b753c41d9507156ee5d288bdcb6210bd5b38f Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 19:23:39 +0900 Subject: [PATCH 009/115] =?UTF-8?q?Refresh=20Token=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EC=9D=98=20=EC=9C=A0=ED=9A=A8=20=EA=B8=B0=EA=B0=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drizzle.config.ts | 3 +-- package.json | 2 ++ pnpm-lock.yaml | 10 ++++++++++ src/routes/api/auth/login/+server.ts | 3 +++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/drizzle.config.ts b/drizzle.config.ts index 9a64b26..c0b54d5 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,11 +1,10 @@ import { defineConfig } from "drizzle-kit"; -if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); export default defineConfig({ schema: "./src/lib/server/db/schema", dbCredentials: { - url: process.env.DATABASE_URL, + url: process.env.DATABASE_URL || "local.db", }, verbose: true, diff --git a/package.json b/package.json index 010856a..f626acf 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/better-sqlite3": "^7.6.11", "@types/jsonwebtoken": "^9.0.7", + "@types/ms": "^0.7.34", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.22.0", "eslint": "^9.7.0", @@ -44,6 +45,7 @@ "better-sqlite3": "^11.1.2", "drizzle-orm": "^0.33.0", "jsonwebtoken": "^9.0.2", + "ms": "^2.1.3", "zod": "^3.24.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a0e593..c33311f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + ms: + specifier: ^2.1.3 + version: 2.1.3 zod: specifier: ^3.24.1 version: 3.24.1 @@ -40,6 +43,9 @@ devDependencies: '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.7 + '@types/ms': + specifier: ^0.7.34 + version: 0.7.34 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -1228,6 +1234,10 @@ packages: '@types/node': 22.10.2 dev: true + /@types/ms@0.7.34: + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + dev: true + /@types/node@22.10.2: resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} dependencies: diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index dec6e55..65f7623 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -1,5 +1,7 @@ import { error, json } from "@sveltejs/kit"; +import ms from "ms"; import { z } from "zod"; +import env from "$lib/server/loadenv"; import { login } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; @@ -18,6 +20,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { cookies.set("refreshToken", refreshToken, { path: "/api/auth", + maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000), httpOnly: true, secure: true, sameSite: "strict", From 552681115ad52c7c3d18f24ebf0393ea5c14fdcf Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 21:21:20 +0900 Subject: [PATCH 010/115] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 145 ++++++++++++++++++ src/app.d.ts | 3 + .../auth/generateKey/+page.svelte | 57 +++++++ .../auth/generateKey/Order.svelte | 33 ++++ .../{ => auth}/login/+page.svelte | 4 +- .../(fullscreen)/{ => auth}/login/service.ts | 0 vite.config.ts | 8 +- 8 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 src/routes/(fullscreen)/auth/generateKey/+page.svelte create mode 100644 src/routes/(fullscreen)/auth/generateKey/Order.svelte rename src/routes/(fullscreen)/{ => auth}/login/+page.svelte (94%) rename src/routes/(fullscreen)/{ => auth}/login/service.ts (100%) diff --git a/package.json b/package.json index f626acf..98aad58 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@eslint/compat": "^1.2.3", + "@iconify-json/material-symbols": "^1.2.12", "@sveltejs/adapter-node": "^5.2.9", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", @@ -38,6 +39,7 @@ "tailwindcss": "^3.4.9", "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", + "unplugin-icons": "^0.22.0", "vite": "^5.4.11" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c33311f..8608e1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ devDependencies: '@eslint/compat': specifier: ^1.2.3 version: 1.2.4(eslint@9.17.0) + '@iconify-json/material-symbols': + specifier: ^1.2.12 + version: 1.2.12 '@sveltejs/adapter-node': specifier: ^5.2.9 version: 5.2.11(@sveltejs/kit@2.15.0) @@ -88,6 +91,9 @@ devDependencies: typescript-eslint: specifier: ^8.0.0 version: 8.18.2(eslint@9.17.0)(typescript@5.7.2) + unplugin-icons: + specifier: ^0.22.0 + version: 0.22.0(svelte@5.16.0) vite: specifier: ^5.4.11 version: 5.4.11 @@ -107,6 +113,24 @@ packages: '@jridgewell/trace-mapping': 0.3.25 dev: true + /@antfu/install-pkg@0.4.1: + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + dependencies: + package-manager-detector: 0.2.8 + tinyexec: 0.3.1 + dev: true + + /@antfu/install-pkg@0.5.0: + resolution: {integrity: sha512-dKnk2xlAyC7rvTkpkHmu+Qy/2Zc3Vm/l8PtNyIOGDBtXPY3kThfU4ORNEp3V7SXw5XSOb+tOJaUYpfquPzL/Tg==} + dependencies: + package-manager-detector: 0.2.8 + tinyexec: 0.3.1 + dev: true + + /@antfu/utils@0.7.10: + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + dev: true + /@esbuild-kit/core-utils@3.3.2: resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -842,6 +866,31 @@ packages: engines: {node: '>=18.18'} dev: true + /@iconify-json/material-symbols@1.2.12: + resolution: {integrity: sha512-2p2T13Kccy7R2HNbdiVsIcHxjp4s9a+iKlfbtt29hldG1pVNaPIlMALNA9bjdEwPjwsVFe06INCbjCRc68JysQ==} + dependencies: + '@iconify/types': 2.0.0 + dev: true + + /@iconify/types@2.0.0: + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + dev: true + + /@iconify/utils@2.2.1: + resolution: {integrity: sha512-0/7J7hk4PqXmxo5PDBDxmnecw5PxklZJfNjIVG9FM0mEfVrvfudS22rYWsqVk6gR3UJ/mSYS90X4R3znXnqfNA==} + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/types': 2.0.0 + debug: 4.4.0 + globals: 15.14.0 + kolorist: 1.8.0 + local-pkg: 0.5.1 + mlly: 1.7.3 + transitivePeerDependencies: + - supports-color + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1630,6 +1679,10 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + dev: true + /cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -2470,6 +2523,10 @@ packages: resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} dev: true + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: true + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2492,6 +2549,14 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + dependencies: + mlly: 1.7.3 + pkg-types: 1.2.1 + dev: true + /locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} dev: true @@ -2589,6 +2654,15 @@ packages: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false + /mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + dev: true + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2701,6 +2775,10 @@ packages: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} dev: true + /package-manager-detector@0.2.8: + resolution: {integrity: sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==} + dev: true + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2730,6 +2808,10 @@ packages: minipass: 7.1.2 dev: true + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + /picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} dev: true @@ -2754,6 +2836,14 @@ packages: engines: {node: '>= 6'} dev: true + /pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + dev: true + /postcss-import@15.1.0(postcss@8.4.49): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -3346,6 +3436,10 @@ packages: globrex: 0.1.2 dev: true + /tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + dev: true + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3406,9 +3500,56 @@ packages: hasBin: true dev: true + /ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + dev: true + /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + /unplugin-icons@0.22.0(svelte@5.16.0): + resolution: {integrity: sha512-CP+iZq5U7doOifer5bcM0jQ9t3Is7EGybIYt3myVxceI8Zuk8EZEpe1NPtJvh7iqMs1VdbK0L41t9+um9VuuLw==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + svelte: + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + dependencies: + '@antfu/install-pkg': 0.5.0 + '@antfu/utils': 0.7.10 + '@iconify/utils': 2.2.1 + debug: 4.4.0 + kolorist: 1.8.0 + local-pkg: 0.5.1 + svelte: 5.16.0 + unplugin: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + dev: true + /update-browserslist-db@1.1.1(browserslist@4.24.3): resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -3478,6 +3619,10 @@ packages: vite: 5.4.11 dev: true + /webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + dev: true + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} diff --git a/src/app.d.ts b/src/app.d.ts index 520c421..0904582 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,8 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces + +import "unplugin-icons/types/svelte"; + declare global { namespace App { // interface Error {} diff --git a/src/routes/(fullscreen)/auth/generateKey/+page.svelte b/src/routes/(fullscreen)/auth/generateKey/+page.svelte new file mode 100644 index 0000000..64104ea --- /dev/null +++ b/src/routes/(fullscreen)/auth/generateKey/+page.svelte @@ -0,0 +1,57 @@ + + + + 암호 키 생성하기 + + +
+
+
+

암호 키 생성하기

+

회원님의 디바이스 간의 안전한 데이터 동기화를 위해 암호 키를 생성해야 해요.

+
+
+
+ +

왜 암호 키가 필요한가요?

+
+
+ {#each orders as { title, description }, i} + + {/each} +
+
+
+
+
+ +
+
+ 키를 갖고 있어요 +
+
+
diff --git a/src/routes/(fullscreen)/auth/generateKey/Order.svelte b/src/routes/(fullscreen)/auth/generateKey/Order.svelte new file mode 100644 index 0000000..5e71e7c --- /dev/null +++ b/src/routes/(fullscreen)/auth/generateKey/Order.svelte @@ -0,0 +1,33 @@ + + +
+
+

+ {order} +

+ {#if !isLast} +
+ {/if} +
+
+

+ {title} +

+

+ {#if description} + {description} + {/if} +

+
+
diff --git a/src/routes/(fullscreen)/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte similarity index 94% rename from src/routes/(fullscreen)/login/+page.svelte rename to src/routes/(fullscreen)/auth/login/+page.svelte index 79e9156..23a3a0b 100644 --- a/src/routes/(fullscreen)/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -25,12 +25,12 @@

환영합니다!

서비스를 이용하려면 로그인을 해야해요.

-
+
-
+
diff --git a/src/routes/(fullscreen)/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts similarity index 100% rename from src/routes/(fullscreen)/login/service.ts rename to src/routes/(fullscreen)/auth/login/service.ts diff --git a/vite.config.ts b/vite.config.ts index 80864b9..1e576b9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,12 @@ import { sveltekit } from "@sveltejs/kit/vite"; +import Icons from "unplugin-icons/vite"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [sveltekit()], + plugins: [ + sveltekit(), + Icons({ + compiler: "svelte", + }), + ], }); From 5aefbcf9d91ab071ebb26c36ffba241794abbf58 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 21:30:07 +0900 Subject: [PATCH 011/115] =?UTF-8?q?TitleDiv,=20BottomDiv=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/{ => divs}/AdaptiveDiv.svelte | 0 src/lib/components/divs/BottomDiv.svelte | 7 +++++++ src/lib/components/divs/TitleDiv.svelte | 7 +++++++ src/lib/components/divs/index.ts | 3 +++ src/lib/components/index.ts | 1 - src/routes/(fullscreen)/+layout.svelte | 2 +- src/routes/(fullscreen)/auth/generateKey/+page.svelte | 9 +++++---- src/routes/(fullscreen)/auth/login/+page.svelte | 9 +++++---- 8 files changed, 28 insertions(+), 10 deletions(-) rename src/lib/components/{ => divs}/AdaptiveDiv.svelte (100%) create mode 100644 src/lib/components/divs/BottomDiv.svelte create mode 100644 src/lib/components/divs/TitleDiv.svelte create mode 100644 src/lib/components/divs/index.ts delete mode 100644 src/lib/components/index.ts diff --git a/src/lib/components/AdaptiveDiv.svelte b/src/lib/components/divs/AdaptiveDiv.svelte similarity index 100% rename from src/lib/components/AdaptiveDiv.svelte rename to src/lib/components/divs/AdaptiveDiv.svelte diff --git a/src/lib/components/divs/BottomDiv.svelte b/src/lib/components/divs/BottomDiv.svelte new file mode 100644 index 0000000..cfcfdd1 --- /dev/null +++ b/src/lib/components/divs/BottomDiv.svelte @@ -0,0 +1,7 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/divs/TitleDiv.svelte b/src/lib/components/divs/TitleDiv.svelte new file mode 100644 index 0000000..5b225e6 --- /dev/null +++ b/src/lib/components/divs/TitleDiv.svelte @@ -0,0 +1,7 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/divs/index.ts b/src/lib/components/divs/index.ts new file mode 100644 index 0000000..7bd6a34 --- /dev/null +++ b/src/lib/components/divs/index.ts @@ -0,0 +1,3 @@ +export { default as AdaptiveDiv } from "./AdaptiveDiv.svelte"; +export { default as BottomDiv } from "./BottomDiv.svelte"; +export { default as TitleDiv } from "./TitleDiv.svelte"; diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts deleted file mode 100644 index 85508da..0000000 --- a/src/lib/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as AdaptiveDiv } from "./AdaptiveDiv.svelte"; diff --git a/src/routes/(fullscreen)/+layout.svelte b/src/routes/(fullscreen)/+layout.svelte index 48c7d63..27e0b1c 100644 --- a/src/routes/(fullscreen)/+layout.svelte +++ b/src/routes/(fullscreen)/+layout.svelte @@ -1,5 +1,5 @@ diff --git a/src/routes/(fullscreen)/auth/generateKey/+page.svelte b/src/routes/(fullscreen)/auth/generateKey/+page.svelte index 64104ea..29bb56c 100644 --- a/src/routes/(fullscreen)/auth/generateKey/+page.svelte +++ b/src/routes/(fullscreen)/auth/generateKey/+page.svelte @@ -1,5 +1,6 @@ -
-

+

{title}

-

+

{#if description} {description} {/if} diff --git a/src/routes/(fullscreen)/auth/generateKey/done/+page.svelte b/src/routes/(fullscreen)/auth/generateKey/done/+page.svelte new file mode 100644 index 0000000..fd84277 --- /dev/null +++ b/src/routes/(fullscreen)/auth/generateKey/done/+page.svelte @@ -0,0 +1,29 @@ + + + + 암호 키 생성하기 + + +

+
+ +

암호 키가 생성되었어요!

+
+

모든 디바이스의 암호 키가 유실되면 서버에 저장된 데이터를 영원히 복호화할 수 없게 돼요.

+

복원을 위해 암호 키를 파일로 내보낼 수 있어요.

+
+
+ +
+ +
+
+ 내보내지 않을래요 +
+
+
From 400438c395712362531e81e8cc83fa01194be60f Mon Sep 17 00:00:00 2001 From: static Date: Fri, 27 Dec 2024 21:23:47 +0900 Subject: [PATCH 014/115] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=9B=84=20=EB=82=B4=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/goto.ts | 21 +++++++++ src/lib/hooks/index.ts | 1 + src/lib/stores/key.ts | 4 ++ .../auth/generateKey/done/+page.svelte | 29 ------------- .../(fullscreen)/key/export/+page.svelte | 43 +++++++++++++++++++ src/routes/(fullscreen)/key/export/+page.ts | 14 ++++++ .../generateKey => key/generate}/+page.svelte | 8 +++- .../generateKey => key/generate}/Order.svelte | 0 .../generateKey => key/generate}/service.ts | 35 ++++++++------- 9 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 src/lib/hooks/goto.ts create mode 100644 src/lib/stores/key.ts delete mode 100644 src/routes/(fullscreen)/auth/generateKey/done/+page.svelte create mode 100644 src/routes/(fullscreen)/key/export/+page.svelte create mode 100644 src/routes/(fullscreen)/key/export/+page.ts rename src/routes/(fullscreen)/{auth/generateKey => key/generate}/+page.svelte (88%) rename src/routes/(fullscreen)/{auth/generateKey => key/generate}/Order.svelte (100%) rename src/routes/(fullscreen)/{auth/generateKey => key/generate}/service.ts (69%) diff --git a/src/lib/hooks/goto.ts b/src/lib/hooks/goto.ts new file mode 100644 index 0000000..c2cef9c --- /dev/null +++ b/src/lib/hooks/goto.ts @@ -0,0 +1,21 @@ +import { writable } from "svelte/store"; +import { goto as svelteGoto } from "$app/navigation"; + +type Path = "/key/export"; + +interface KeyExportState { + pubKeyBase64: string; + privKeyBase64: string; +} + +export const keyExportState = writable(null); + +export function goto(path: "/key/export", state: KeyExportState): Promise; + +export function goto(path: Path, state: unknown) { + switch (path) { + case "/key/export": + keyExportState.set(state as KeyExportState); + return svelteGoto(path); + } +} diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index 54f1be9..48c19b9 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -1 +1,2 @@ export { default as callAPI } from "./callAPI"; +export { goto } from "./goto"; diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts new file mode 100644 index 0000000..e11209c --- /dev/null +++ b/src/lib/stores/key.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; + +export const pubKey = writable(null); +export const privKey = writable(null); diff --git a/src/routes/(fullscreen)/auth/generateKey/done/+page.svelte b/src/routes/(fullscreen)/auth/generateKey/done/+page.svelte deleted file mode 100644 index fd84277..0000000 --- a/src/routes/(fullscreen)/auth/generateKey/done/+page.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - 암호 키 생성하기 - - -
-
- -

암호 키가 생성되었어요!

-
-

모든 디바이스의 암호 키가 유실되면 서버에 저장된 데이터를 영원히 복호화할 수 없게 돼요.

-

복원을 위해 암호 키를 파일로 내보낼 수 있어요.

-
-
- -
- -
-
- 내보내지 않을래요 -
-
-
diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte new file mode 100644 index 0000000..ca53016 --- /dev/null +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -0,0 +1,43 @@ + + + + 암호 키 생성하기 + + +
+
+ +
+
+
+

암호 키를 파일로 내보낼까요?

+
+

+ 모든 디바이스의 암호 키가 유실되면, 서버에 저장된 데이터를 영원히 복호화할 수 없게 돼요. +

+

만약의 상황을 위해 암호 키를 파일로 내보낼 수 있어요.

+
+
+ +
+ +
+
+ 내보내지 않을래요 +
+
+
+
diff --git a/src/routes/(fullscreen)/key/export/+page.ts b/src/routes/(fullscreen)/key/export/+page.ts new file mode 100644 index 0000000..994640b --- /dev/null +++ b/src/routes/(fullscreen)/key/export/+page.ts @@ -0,0 +1,14 @@ +import { error } from "@sveltejs/kit"; +import { get } from "svelte/store"; +import { keyExportState } from "$lib/hooks/goto"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async () => { + const state = get(keyExportState); + if (!state) { + error(403, "Forbidden"); + } + + keyExportState.set(null); + return state; +}; diff --git a/src/routes/(fullscreen)/auth/generateKey/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte similarity index 88% rename from src/routes/(fullscreen)/auth/generateKey/+page.svelte rename to src/routes/(fullscreen)/key/generate/+page.svelte index 29bb56c..eddcf85 100644 --- a/src/routes/(fullscreen)/auth/generateKey/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -1,7 +1,9 @@ @@ -49,7 +55,7 @@
- +
키를 갖고 있어요 diff --git a/src/routes/(fullscreen)/auth/generateKey/Order.svelte b/src/routes/(fullscreen)/key/generate/Order.svelte similarity index 100% rename from src/routes/(fullscreen)/auth/generateKey/Order.svelte rename to src/routes/(fullscreen)/key/generate/Order.svelte diff --git a/src/routes/(fullscreen)/auth/generateKey/service.ts b/src/routes/(fullscreen)/key/generate/service.ts similarity index 69% rename from src/routes/(fullscreen)/auth/generateKey/service.ts rename to src/routes/(fullscreen)/key/generate/service.ts index 5653703..aa46c87 100644 --- a/src/routes/(fullscreen)/auth/generateKey/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,4 +1,5 @@ import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; +import { pubKey, privKey } from "$lib/stores/key"; type KeyType = "public" | "private"; @@ -16,20 +17,6 @@ const generateRSAKeyPair = async () => { return keyPair; }; -const exportKeyAsPem = async (key: CryptoKey, type: KeyType) => { - const exportedKey = await window.crypto.subtle.exportKey( - type === "public" ? "spki" : "pkcs8", - key, - ); - const exportedKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(exportedKey))) - .match(/.{1,64}/g)! - .join("\n"); - - const pemHeader = type === "public" ? "PUBLIC" : "PRIVATE"; - const pem = `-----BEGIN ${pemHeader} KEY-----\n${exportedKeyBase64}\n-----END ${pemHeader} KEY-----\n`; - return pem; -}; - const makeRSAKeyNonextractable = async (key: CryptoKey, type: KeyType) => { const format = type === "public" ? "spki" : "pkcs8"; const keyUsage = type === "public" ? "encrypt" : "decrypt"; @@ -45,13 +32,25 @@ const makeRSAKeyNonextractable = async (key: CryptoKey, type: KeyType) => { ); }; +const exportKeyToBase64 = async (key: CryptoKey, type: KeyType) => { + const exportedKey = await window.crypto.subtle.exportKey( + type === "public" ? "spki" : "pkcs8", + key, + ); + return btoa(String.fromCharCode(...new Uint8Array(exportedKey))); +}; + export const generateKeyPair = async () => { const keyPair = await generateRSAKeyPair(); - const privKeySecure = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); + + pubKey.set(keyPair.publicKey); + privKey.set(privKeySecure); + await storeKeyPairIntoIndexedDB(keyPair.publicKey, privKeySecure); - const pubKeyPem = await exportKeyAsPem(keyPair.publicKey, "public"); - const privKeyPem = await exportKeyAsPem(keyPair.privateKey, "private"); - return { pubKeyPem, privKeyPem }; + return { + pubKeyBase64: await exportKeyToBase64(keyPair.publicKey, "public"), + privKeyBase64: await exportKeyToBase64(keyPair.privateKey, "private"), + }; }; From 1aafe126d6b0dc814e0698c1963d3683294facea Mon Sep 17 00:00:00 2001 From: static Date: Fri, 27 Dec 2024 21:50:04 +0900 Subject: [PATCH 015/115] =?UTF-8?q?hook,=20store=20=EB=A6=AC=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callAPI.ts | 21 ++++++------ src/lib/hooks/goto.ts | 21 ------------ src/lib/hooks/gotoStateful.ts | 33 +++++++++++++++++++ src/lib/hooks/index.ts | 4 +-- src/lib/stores/auth.ts | 2 +- src/lib/stores/index.ts | 2 ++ src/lib/stores/key.ts | 4 +-- src/routes/(fullscreen)/auth/login/service.ts | 4 +-- src/routes/(fullscreen)/key/export/+page.ts | 7 ++-- .../(fullscreen)/key/generate/+page.svelte | 5 +-- .../(fullscreen)/key/generate/service.ts | 6 ++-- 11 files changed, 61 insertions(+), 48 deletions(-) delete mode 100644 src/lib/hooks/goto.ts create mode 100644 src/lib/hooks/gotoStateful.ts create mode 100644 src/lib/stores/index.ts diff --git a/src/lib/hooks/callAPI.ts b/src/lib/hooks/callAPI.ts index 60311b1..73df085 100644 --- a/src/lib/hooks/callAPI.ts +++ b/src/lib/hooks/callAPI.ts @@ -1,29 +1,30 @@ -import { accessToken } from "$lib/stores/auth"; +import { get } from "svelte/store"; +import { accessTokenStore } from "$lib/stores"; const refreshToken = async () => { - const res = await fetch("/api/auth/refreshtoken", { + const res = await fetch("/api/auth/refreshToken", { method: "POST", credentials: "same-origin", }); if (!res.ok) { - accessToken.set(null); + accessTokenStore.set(null); throw new Error("Failed to refresh token"); } const data = await res.json(); const token = data.accessToken as string; - accessToken.set(token); + accessTokenStore.set(token); return token; }; const callAPIInternal = async ( input: RequestInfo, - init?: RequestInit, - token?: string | null, + init: RequestInit | undefined, + token: string | null, retryIfUnauthorized = true, ): Promise => { - if (token === null) { + if (!token) { token = await refreshToken(); retryIfUnauthorized = false; } @@ -35,13 +36,13 @@ const callAPIInternal = async ( Authorization: `Bearer ${token}`, }, }); - if (res.status === 401 && retryIfUnauthorized && token !== undefined) { + if (res.status === 401 && retryIfUnauthorized) { return await callAPIInternal(input, init, null, false); } return res; }; -export default async (input: RequestInfo, init?: RequestInit, token?: string | null) => { - return await callAPIInternal(input, init, token); +export const callAPI = async (input: RequestInfo, init?: RequestInit) => { + return await callAPIInternal(input, init, get(accessTokenStore)); }; diff --git a/src/lib/hooks/goto.ts b/src/lib/hooks/goto.ts deleted file mode 100644 index c2cef9c..0000000 --- a/src/lib/hooks/goto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { writable } from "svelte/store"; -import { goto as svelteGoto } from "$app/navigation"; - -type Path = "/key/export"; - -interface KeyExportState { - pubKeyBase64: string; - privKeyBase64: string; -} - -export const keyExportState = writable(null); - -export function goto(path: "/key/export", state: KeyExportState): Promise; - -export function goto(path: Path, state: unknown) { - switch (path) { - case "/key/export": - keyExportState.set(state as KeyExportState); - return svelteGoto(path); - } -} diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts new file mode 100644 index 0000000..1a05294 --- /dev/null +++ b/src/lib/hooks/gotoStateful.ts @@ -0,0 +1,33 @@ +import { goto } from "$app/navigation"; + +type Path = "/key/export"; + +interface KeyExportState { + pubKeyBase64: string; + privKeyBase64: string; +} + +const useAutoNull = (value: T | null) => { + return { + get: () => { + const result = value; + value = null; + return result; + }, + set: (newValue: T) => { + value = newValue; + }, + }; +}; + +export const keyExportState = useAutoNull(null); + +export function gotoStateful(path: "/key/export", state: KeyExportState): Promise; + +export function gotoStateful(path: Path, state: unknown) { + switch (path) { + case "/key/export": + keyExportState.set(state as KeyExportState); + return goto(path); + } +} diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index 48c19b9..e2f0392 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -1,2 +1,2 @@ -export { default as callAPI } from "./callAPI"; -export { goto } from "./goto"; +export { callAPI } from "./callAPI"; +export { gotoStateful } from "./gotoStateful"; diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts index 954cee0..93e276c 100644 --- a/src/lib/stores/auth.ts +++ b/src/lib/stores/auth.ts @@ -1,3 +1,3 @@ import { writable } from "svelte/store"; -export const accessToken = writable(null); +export const accessTokenStore = writable(null); diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts new file mode 100644 index 0000000..86b0be9 --- /dev/null +++ b/src/lib/stores/index.ts @@ -0,0 +1,2 @@ +export * from "./auth"; +export * from "./key"; diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index e11209c..40458bb 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -1,4 +1,4 @@ import { writable } from "svelte/store"; -export const pubKey = writable(null); -export const privKey = writable(null); +export const pubKeyStore = writable(null); +export const privKeyStore = writable(null); diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 4799ea5..f88dbc8 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,5 +1,5 @@ import { callAPI } from "$lib/hooks"; -import { accessToken } from "$lib/stores/auth"; +import { accessTokenStore } from "$lib/stores"; export const requestLogin = async (email: string, password: string) => { const res = await callAPI("/api/auth/login", { @@ -16,6 +16,6 @@ export const requestLogin = async (email: string, password: string) => { const data = await res.json(); const token = data.accessToken as string; - accessToken.set(token); + accessTokenStore.set(token); return true; }; diff --git a/src/routes/(fullscreen)/key/export/+page.ts b/src/routes/(fullscreen)/key/export/+page.ts index 994640b..a64ea53 100644 --- a/src/routes/(fullscreen)/key/export/+page.ts +++ b/src/routes/(fullscreen)/key/export/+page.ts @@ -1,14 +1,11 @@ import { error } from "@sveltejs/kit"; -import { get } from "svelte/store"; -import { keyExportState } from "$lib/hooks/goto"; +import { keyExportState } from "$lib/hooks/gotoStateful"; import type { PageLoad } from "./$types"; export const load: PageLoad = async () => { - const state = get(keyExportState); + const state = keyExportState.get(); if (!state) { error(403, "Forbidden"); } - - keyExportState.set(null); return state; }; diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index eddcf85..af84aa8 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index aa46c87..7b869d4 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,5 +1,5 @@ import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; -import { pubKey, privKey } from "$lib/stores/key"; +import { pubKeyStore, privKeyStore } from "$lib/stores"; type KeyType = "public" | "private"; @@ -44,8 +44,8 @@ export const generateKeyPair = async () => { const keyPair = await generateRSAKeyPair(); const privKeySecure = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); - pubKey.set(keyPair.publicKey); - privKey.set(privKeySecure); + pubKeyStore.set(keyPair.publicKey); + privKeyStore.set(privKeySecure); await storeKeyPairIntoIndexedDB(keyPair.publicKey, privKeySecure); From 5a9ea3d91b9c20ad6a05965569e9808a397cb78a Mon Sep 17 00:00:00 2001 From: static Date: Fri, 27 Dec 2024 22:19:09 +0900 Subject: [PATCH 016/115] =?UTF-8?q?eslint-plugin-tailwindcss=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.js | 2 ++ package.json | 1 + pnpm-lock.yaml | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index 612cb6b..4027dc6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,7 @@ import prettier from "eslint-config-prettier"; import js from "@eslint/js"; import { includeIgnoreFile } from "@eslint/compat"; import svelte from "eslint-plugin-svelte"; +import tailwind from "eslint-plugin-tailwindcss"; import globals from "globals"; import { fileURLToPath } from "node:url"; import ts from "typescript-eslint"; @@ -12,6 +13,7 @@ export default ts.config( js.configs.recommended, ...ts.configs.recommended, ...svelte.configs["flat/recommended"], + ...tailwind.configs["flat/recommended"], prettier, ...svelte.configs["flat/prettier"], { diff --git a/package.json b/package.json index 7df57b1..7ade218 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", + "eslint-plugin-tailwindcss": "^3.17.5", "globals": "^15.0.0", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b8d506..cb35a7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ devDependencies: eslint-plugin-svelte: specifier: ^2.36.0 version: 2.46.1(eslint@9.17.0)(svelte@5.16.0) + eslint-plugin-tailwindcss: + specifier: ^3.17.5 + version: 3.17.5(tailwindcss@3.4.17) globals: specifier: ^15.0.0 version: 15.14.0 @@ -2051,6 +2054,17 @@ packages: - ts-node dev: true + /eslint-plugin-tailwindcss@3.17.5(tailwindcss@3.4.17): + resolution: {integrity: sha512-8Mi7p7dm+mO1dHgRHHFdPu4RDTBk69Cn4P0B40vRQR+MrguUpwmKwhZy1kqYe3Km8/4nb+cyrCF+5SodOEmaow==} + engines: {node: '>=18.12.0'} + peerDependencies: + tailwindcss: ^3.4.0 + dependencies: + fast-glob: 3.3.2 + postcss: 8.4.49 + tailwindcss: 3.4.17 + dev: true + /eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} From dec17ecba8a8b24716ed4cbe0cd6a7d8beb20c62 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 27 Dec 2024 22:56:34 +0900 Subject: [PATCH 017/115] =?UTF-8?q?Modal,=20BeforeContinueModal=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/Modal.svelte | 32 +++++++++++++++++ src/lib/components/buttons/Button.svelte | 9 ++++- src/lib/components/buttons/TextButton.svelte | 6 +++- src/lib/components/index.ts | 1 + .../(fullscreen)/key/export/+page.svelte | 36 ++++++++++++++----- .../key/export/BeforeContinueModal.svelte | 31 ++++++++++++++++ 6 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/lib/components/Modal.svelte create mode 100644 src/lib/components/index.ts create mode 100644 src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte new file mode 100644 index 0000000..36c1171 --- /dev/null +++ b/src/lib/components/Modal.svelte @@ -0,0 +1,32 @@ + + +{#if isOpen} + + +
+
e.stopPropagation()} + class="max-w-full rounded-2xl bg-white p-4" + transition:fade={{ duration: 100 }} + > + {@render children?.()} +
+
+{/if} diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte index 08cc1f8..484a2ab 100644 --- a/src/lib/components/buttons/Button.svelte +++ b/src/lib/components/buttons/Button.svelte @@ -23,7 +23,14 @@ ); - +
- 내보내지 않을래요 + { + isBeforeContinueModalOpen = true; + }} + > + 내보내지 않을래요 +
+ + diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte new file mode 100644 index 0000000..984f553 --- /dev/null +++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte @@ -0,0 +1,31 @@ + + + +
+
+

내보내지 않고 계속할까요?

+

+ 보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. +

+
+
+ + +
+
+
From c00dbe70248f02546bb7109c87ac5ad5670afc95 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 01:05:31 +0900 Subject: [PATCH 018/115] =?UTF-8?q?=EA=B3=B5=EA=B0=9C=20=ED=82=A4=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 8 +++++++ src/lib/server/modules/auth.ts | 20 +++++++++++++++++ src/lib/server/services/key.ts | 10 +++++++++ src/routes/(fullscreen)/auth/login/service.ts | 3 +-- .../(fullscreen)/key/export/+page.svelte | 9 +++++++- src/routes/(fullscreen)/key/export/service.ts | 12 ++++++++++ src/routes/api/auth/login/+server.ts | 2 +- src/routes/api/key/register/+server.ts | 22 +++++++++++++++++++ 8 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/lib/server/services/key.ts create mode 100644 src/routes/(fullscreen)/key/export/service.ts create mode 100644 src/routes/api/key/register/+server.ts diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index a4e2cdb..c3c64ad 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -2,6 +2,14 @@ import { and, eq } from "drizzle-orm"; import db from "./drizzle"; import { client, userClient } from "./schema"; +export const createClient = async (pubKey: string, userId: number) => { + await db.transaction(async (tx) => { + const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id }); + const { id: clientId } = insertRes[0]!; + await tx.insert(userClient).values({ userId, clientId }); + }); +}; + export const getClientByPubKey = async (pubKey: string) => { const clients = await db.select().from(client).where(eq(client.pubKey, pubKey)).execute(); return clients[0] ?? null; diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index 9ab7420..4d456b1 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -1,3 +1,4 @@ +import { error } from "@sveltejs/kit"; import jwt from "jsonwebtoken"; import env from "$lib/server/loadenv"; @@ -36,3 +37,22 @@ export const verifyToken = (token: string) => { return TokenError.INVALID; } }; + +export const authenticate = (request: Request) => { + const accessToken = request.headers.get("Authorization"); + if (!accessToken?.startsWith("Bearer ")) { + error(401, "Token required"); + } + + const tokenData = verifyToken(accessToken.slice(7)); + if (tokenData === TokenError.EXPIRED) { + error(401, "Token expired"); + } else if (tokenData === TokenError.INVALID || tokenData.type !== "access") { + error(401, "Invalid token"); + } + + return { + userId: tokenData.userId, + clientId: tokenData.clientId, + }; +}; diff --git a/src/lib/server/services/key.ts b/src/lib/server/services/key.ts new file mode 100644 index 0000000..4d57e08 --- /dev/null +++ b/src/lib/server/services/key.ts @@ -0,0 +1,10 @@ +import { error } from "@sveltejs/kit"; +import { createClient, getClientByPubKey } from "$lib/server/db/client"; + +export const registerPubKey = async (userId: number, pubKey: string) => { + if (await getClientByPubKey(pubKey)) { + error(409, "Public key already registered"); + } + + await createClient(pubKey, userId); +}; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index f88dbc8..47f8f5b 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,8 +1,7 @@ -import { callAPI } from "$lib/hooks"; import { accessTokenStore } from "$lib/stores"; export const requestLogin = async (email: string, password: string) => { - const res = await callAPI("/api/auth/login", { + const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index 14d2656..73fa06a 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -2,6 +2,7 @@ import { Button, TextButton } from "$lib/components/buttons"; import { BottomDiv } from "$lib/components/divs"; import BeforeContinueModal from "./BeforeContinueModal.svelte"; + import { requestPubKeyRegistration } from "./service"; import IconKey from "~icons/material-symbols/key"; @@ -15,9 +16,15 @@ console.log(data.privKeyBase64); }; - const continueWithoutExport = () => { + const continueWithoutExport = async () => { isBeforeContinueModalOpen = false; + const ok = await requestPubKeyRegistration(data.pubKeyBase64); + if (!ok) { + // TODO + return; + } + // TODO }; diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts new file mode 100644 index 0000000..3cef2bc --- /dev/null +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -0,0 +1,12 @@ +import { callAPI } from "$lib/hooks"; + +export const requestPubKeyRegistration = async (pubKeyBase64: string) => { + const res = await callAPI("/api/key/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ pubKey: pubKeyBase64 }), + }); + return res.ok; +}; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 65f7623..7a4351b 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -13,7 +13,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { pubKey: z.string().nonempty().optional(), }) .safeParse(await request.json()); - if (!zodRes.success) error(400, zodRes.error.message); + if (!zodRes.success) error(400, "Invalid request body"); const { email, password, pubKey } = zodRes.data; const { accessToken, refreshToken } = await login(email.trim(), password.trim(), pubKey?.trim()); diff --git a/src/routes/api/key/register/+server.ts b/src/routes/api/key/register/+server.ts new file mode 100644 index 0000000..1c98bb4 --- /dev/null +++ b/src/routes/api/key/register/+server.ts @@ -0,0 +1,22 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authenticate } from "$lib/server/modules/auth"; +import { registerPubKey } from "$lib/server/services/key"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request }) => { + const zodRes = z + .object({ + pubKey: z.string().base64().nonempty(), + }) + .safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + + const { userId, clientId } = authenticate(request); + if (clientId) { + error(403, "Forbidden"); + } + + await registerPubKey(userId, zodRes.data.pubKey); + return text("Public key registered"); +}; From 796e4a78315db8f8a8131a424237aad071182d17 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 13:05:59 +0900 Subject: [PATCH 019/115] =?UTF-8?q?Dockerfile=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 31 +++++++++++++++++++++++++++++++ Dockerfile | 29 +++++++++++++++++++++++++++++ src/lib/server/loadenv.ts | 5 ++++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4853a6e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +.git +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build +/drizzle + +# OS +.DS_Store +Thumbs.db + +# VSCode +/.vscode + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# SQLite +*.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d22f972 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Build Stage +FROM node:18-alpine AS build +WORKDIR /app + +RUN npm install -g pnpm@8 + +COPY pnpm-lock.yaml . +RUN pnpm fetch + +COPY . . +RUN pnpm install --offline +RUN pnpm build + +# Deploy Stage +FROM node:18-alpine +WORKDIR /app + +RUN npm install -g pnpm@8 + +COPY pnpm-lock.yaml . +RUN pnpm fetch --prod + +COPY package.json . +RUN pnpm install --offline --prod + +COPY --from=build /app/build ./build + +EXPOSE 3000 +CMD ["node", "./build/index.js"] diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts index 83f4514..8df1e19 100644 --- a/src/lib/server/loadenv.ts +++ b/src/lib/server/loadenv.ts @@ -1,6 +1,9 @@ +import { building } from "$app/environment"; import { env } from "$env/dynamic/private"; -if (!env.JWT_SECRET) throw new Error("JWT_SECRET is not set"); +if (!building) { + if (!env.JWT_SECRET) throw new Error("JWT_SECRET is not set"); +} export default { databaseUrl: env.DATABASE_URL || "local.db", From 1d0c3098789a11db4f84ef69b2350cf53ffe5c39 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 15:44:30 +0900 Subject: [PATCH 020/115] =?UTF-8?q?Refresh=20Token=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++ pnpm-lock.yaml | 49 ++++++++++++++++++++ src/hooks.server.ts | 9 ++++ src/lib/server/db/client.ts | 6 ++- src/lib/server/db/schema/index.ts | 1 + src/lib/server/db/schema/token.ts | 18 ++++++++ src/lib/server/db/schema/user.ts | 6 --- src/lib/server/db/token.ts | 75 +++++++++++++++++++++++++++++++ src/lib/server/db/user.ts | 21 +-------- src/lib/server/modules/auth.ts | 49 ++++++++++---------- src/lib/server/services/auth.ts | 75 ++++++++++++++++++++----------- 11 files changed, 233 insertions(+), 79 deletions(-) create mode 100644 src/hooks.server.ts create mode 100644 src/lib/server/db/schema/token.ts create mode 100644 src/lib/server/db/token.ts diff --git a/package.json b/package.json index 7ade218..b57af77 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/jsonwebtoken": "^9.0.7", "@types/ms": "^0.7.34", + "@types/node-schedule": "^2.1.7", "autoprefixer": "^10.4.20", "dexie": "^4.0.10", "drizzle-kit": "^0.22.0", @@ -50,6 +51,8 @@ "drizzle-orm": "^0.33.0", "jsonwebtoken": "^9.0.2", "ms": "^2.1.3", + "node-schedule": "^2.1.1", + "uuid": "^11.0.3", "zod": "^3.24.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb35a7d..df5d1cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ dependencies: ms: specifier: ^2.1.3 version: 2.1.3 + node-schedule: + specifier: ^2.1.1 + version: 2.1.1 + uuid: + specifier: ^11.0.3 + version: 11.0.3 zod: specifier: ^3.24.1 version: 3.24.1 @@ -49,6 +55,9 @@ devDependencies: '@types/ms': specifier: ^0.7.34 version: 0.7.34 + '@types/node-schedule': + specifier: ^2.1.7 + version: 2.1.7 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -1293,6 +1302,12 @@ packages: resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} dev: true + /@types/node-schedule@2.1.7: + resolution: {integrity: sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==} + dependencies: + '@types/node': 22.10.2 + dev: true + /@types/node@22.10.2: resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} dependencies: @@ -1694,6 +1709,13 @@ packages: engines: {node: '>= 0.6'} dev: true + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.5.0 + dev: false + /cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2621,10 +2643,19 @@ packages: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} dev: false + /long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + dev: false + /lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} dev: true + /luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + dev: false + /magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} dependencies: @@ -2740,6 +2771,15 @@ packages: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} dev: true + /node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3239,6 +3279,10 @@ packages: totalist: 3.0.1 dev: true + /sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + dev: false + /source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3591,6 +3635,11 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + dev: false + /vite@5.4.11: resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..c26afde --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,9 @@ +import type { ServerInit } from "@sveltejs/kit"; +import schedule from "node-schedule"; +import { cleanupExpiredRefreshTokens } from "$lib/server/db/token"; + +export const init: ServerInit = () => { + schedule.scheduleJob("0 * * * *", () => { + cleanupExpiredRefreshTokens(); + }); +}; diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index c3c64ad..006aa32 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -5,8 +5,10 @@ import { client, userClient } from "./schema"; export const createClient = async (pubKey: string, userId: number) => { await db.transaction(async (tx) => { const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id }); - const { id: clientId } = insertRes[0]!; - await tx.insert(userClient).values({ userId, clientId }); + await tx.insert(userClient).values({ + userId, + clientId: insertRes[0]!.id, + }); }); }; diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 6674d19..68981bb 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,2 +1,3 @@ export * from "./client"; +export * from "./token"; export * from "./user"; diff --git a/src/lib/server/db/schema/token.ts b/src/lib/server/db/schema/token.ts new file mode 100644 index 0000000..bbf6acf --- /dev/null +++ b/src/lib/server/db/schema/token.ts @@ -0,0 +1,18 @@ +import { sqliteTable, text, integer, unique } from "drizzle-orm/sqlite-core"; +import { client } from "./client"; +import { user } from "./user"; + +export const refreshToken = sqliteTable( + "refresh_token", + { + id: text("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => user.id), + clientId: integer("client_id").references(() => client.id), + expiresAt: integer("expires_at").notNull(), // Only used for cleanup + }, + (t) => ({ + unq: unique().on(t.userId, t.clientId), + }), +); diff --git a/src/lib/server/db/schema/user.ts b/src/lib/server/db/schema/user.ts index 474db82..2ad0e3c 100644 --- a/src/lib/server/db/schema/user.ts +++ b/src/lib/server/db/schema/user.ts @@ -5,9 +5,3 @@ export const user = sqliteTable("user", { email: text("email").notNull().unique(), password: text("password").notNull(), }); - -export const revokedToken = sqliteTable("revoked_token", { - id: integer("id").primaryKey(), - token: text("token").notNull().unique(), - revokedAt: integer("revoked_at").notNull(), -}); diff --git a/src/lib/server/db/token.ts b/src/lib/server/db/token.ts new file mode 100644 index 0000000..89e0deb --- /dev/null +++ b/src/lib/server/db/token.ts @@ -0,0 +1,75 @@ +import { SqliteError } from "better-sqlite3"; +import { eq, lte } from "drizzle-orm"; +import ms from "ms"; +import env from "$lib/server/loadenv"; +import db from "./drizzle"; +import { refreshToken } from "./schema"; + +const expiresIn = ms(env.jwt.refreshExp); +const expiresAt = () => Date.now() + expiresIn; + +export const registerRefreshToken = async ( + userId: number, + clientId: number | null, + tokenId: string, +) => { + try { + await db + .insert(refreshToken) + .values({ + id: tokenId, + userId, + clientId, + expiresAt: expiresAt(), + }) + .execute(); + return true; + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + return false; + } + throw e; + } +}; + +export const getRefreshToken = async (tokenId: string) => { + const tokens = await db.select().from(refreshToken).where(eq(refreshToken.id, tokenId)).execute(); + return tokens[0] ?? null; +}; + +export const rotateRefreshToken = async (oldTokenId: string, newTokenId: string) => { + const res = await db + .update(refreshToken) + .set({ + id: newTokenId, + expiresAt: expiresAt(), + }) + .where(eq(refreshToken.id, oldTokenId)) + .execute(); + return res.changes > 0; +}; + +export const upgradeRefreshToken = async ( + oldTokenId: string, + newTokenId: string, + clientId: number, +) => { + const res = await db + .update(refreshToken) + .set({ + id: newTokenId, + clientId, + expiresAt: expiresAt(), + }) + .where(eq(refreshToken.id, oldTokenId)) + .execute(); + return res.changes > 0; +}; + +export const revokeRefreshToken = async (tokenId: string) => { + await db.delete(refreshToken).where(eq(refreshToken.id, tokenId)).execute(); +}; + +export const cleanupExpiredRefreshTokens = async () => { + await db.delete(refreshToken).where(lte(refreshToken.expiresAt, Date.now())).execute(); +}; diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index f564680..38a53f0 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -1,27 +1,8 @@ import { eq } from "drizzle-orm"; import db from "./drizzle"; -import { user, revokedToken } from "./schema"; +import { user } from "./schema"; export const getUserByEmail = async (email: string) => { const users = await db.select().from(user).where(eq(user.email, email)).execute(); return users[0] ?? null; }; - -export const revokeToken = async (token: string) => { - await db - .insert(revokedToken) - .values({ - token, - revokedAt: Date.now(), - }) - .execute(); -}; - -export const isTokenRevoked = async (token: string) => { - const tokens = await db - .select() - .from(revokedToken) - .where(eq(revokedToken.token, token)) - .execute(); - return tokens.length > 0; -}; diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index 4d456b1..da8a2d8 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -2,34 +2,31 @@ import { error } from "@sveltejs/kit"; import jwt from "jsonwebtoken"; import env from "$lib/server/loadenv"; -interface TokenData { - type: "access" | "refresh"; - userId: number; - clientId?: number; -} +type TokenPayload = + | { + type: "access"; + userId: number; + clientId?: number; + } + | { + type: "refresh"; + jti: string; + }; export enum TokenError { EXPIRED, INVALID, } -export const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => { - return jwt.sign( - { - type, - userId, - clientId, - } satisfies TokenData, - env.jwt.secret, - { - expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, - }, - ); +export const issueToken = (payload: TokenPayload) => { + return jwt.sign(payload, env.jwt.secret, { + expiresIn: payload.type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, + }); }; export const verifyToken = (token: string) => { try { - return jwt.verify(token, env.jwt.secret) as TokenData; + return jwt.verify(token, env.jwt.secret) as TokenPayload; } catch (error) { if (error instanceof jwt.TokenExpiredError) { return TokenError.EXPIRED; @@ -41,18 +38,18 @@ export const verifyToken = (token: string) => { export const authenticate = (request: Request) => { const accessToken = request.headers.get("Authorization"); if (!accessToken?.startsWith("Bearer ")) { - error(401, "Token required"); + error(401, "Access token required"); } - const tokenData = verifyToken(accessToken.slice(7)); - if (tokenData === TokenError.EXPIRED) { - error(401, "Token expired"); - } else if (tokenData === TokenError.INVALID || tokenData.type !== "access") { - error(401, "Invalid token"); + const tokenPayload = verifyToken(accessToken.slice(7)); + if (tokenPayload === TokenError.EXPIRED) { + error(401, "Access token expired"); + } else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "access") { + error(401, "Invalid access token"); } return { - userId: tokenData.userId, - clientId: tokenData.clientId, + userId: tokenPayload.userId, + clientId: tokenPayload.clientId, }; }; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index af2a9ab..172fc4a 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -1,21 +1,37 @@ import { error } from "@sveltejs/kit"; import argon2 from "argon2"; +import { v4 as uuidv4 } from "uuid"; import { getClientByPubKey } from "$lib/server/db/client"; -import { getUserByEmail, revokeToken, isTokenRevoked } from "$lib/server/db/user"; +import { getUserByEmail } from "$lib/server/db/user"; +import { + getRefreshToken, + registerRefreshToken, + rotateRefreshToken, + revokeRefreshToken, +} from "$lib/server/db/token"; import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; const verifyPassword = async (hash: string, password: string) => { return await argon2.verify(hash, password); }; +const issueAccessToken = (userId: number, clientId?: number) => { + return issueToken({ type: "access", userId, clientId }); +}; + +const issueRefreshToken = async (userId: number, clientId?: number) => { + const jti = uuidv4(); + const token = issueToken({ type: "refresh", jti }); + + if (!(await registerRefreshToken(userId, clientId ?? null, jti))) { + error(403, "Already logged in"); + } + return token; +}; + export const login = async (email: string, password: string, pubKey?: string) => { const user = await getUserByEmail(email); - if (!user) { - error(401, "Invalid email or password"); - } - - const isValid = await verifyPassword(user.password, password); - if (!isValid) { + if (!user || !(await verifyPassword(user.password, password))) { error(401, "Invalid email or password"); } @@ -25,36 +41,45 @@ export const login = async (email: string, password: string, pubKey?: string) => } return { - accessToken: issueToken("access", user.id, client?.id), - refreshToken: issueToken("refresh", user.id, client?.id), + accessToken: issueAccessToken(user.id, client?.id), + refreshToken: await issueRefreshToken(user.id, client?.id), }; }; const verifyRefreshToken = async (refreshToken: string) => { - const tokenData = verifyToken(refreshToken); - if (tokenData === TokenError.EXPIRED) { - error(401, "Token expired"); - } else if ( - tokenData === TokenError.INVALID || - tokenData.type !== "refresh" || - (await isTokenRevoked(refreshToken)) - ) { - error(401, "Invalid token"); + const tokenPayload = verifyToken(refreshToken); + if (tokenPayload === TokenError.EXPIRED) { + error(401, "Refresh token expired"); + } else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "refresh") { + error(401, "Invalid refresh token"); } - return tokenData; + + const tokenData = await getRefreshToken(tokenPayload.jti); + if (!tokenData) { + error(500, "Refresh token not found"); + } + + return { + jti: tokenPayload.jti, + userId: tokenData.userId, + clientId: tokenData.clientId ?? undefined, + }; }; export const logout = async (refreshToken: string) => { - await verifyRefreshToken(refreshToken); - await revokeToken(refreshToken); + const { jti } = await verifyRefreshToken(refreshToken); + await revokeRefreshToken(jti); }; export const refreshToken = async (refreshToken: string) => { - const tokenData = await verifyRefreshToken(refreshToken); + const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); + const newJti = uuidv4(); - await revokeToken(refreshToken); + if (!(await rotateRefreshToken(oldJti, newJti))) { + error(500, "Refresh token not found"); + } return { - accessToken: issueToken("access", tokenData.userId, tokenData.clientId), - refreshToken: issueToken("refresh", tokenData.userId, tokenData.clientId), + accessToken: issueAccessToken(userId, clientId), + refreshToken: issueToken({ type: "refresh", jti: newJti }), }; }; From c09a0b4aa0d6e8c94548a6a3de473e36f76d959f Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 16:54:46 +0900 Subject: [PATCH 021/115] =?UTF-8?q?Access=20Token=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callAPI.ts | 53 ++++--------------- src/lib/server/modules/auth.ts | 12 ++--- src/lib/server/services/auth.ts | 2 +- src/lib/stores/auth.ts | 3 -- src/lib/stores/index.ts | 1 - src/routes/(fullscreen)/auth/login/service.ts | 12 +---- src/routes/api/auth/login/+server.ts | 13 +++-- src/routes/api/auth/logout/+server.ts | 7 ++- src/routes/api/auth/refreshToken/+server.ts | 20 +++---- src/routes/api/key/register/+server.ts | 4 +- 10 files changed, 44 insertions(+), 83 deletions(-) delete mode 100644 src/lib/stores/auth.ts diff --git a/src/lib/hooks/callAPI.ts b/src/lib/hooks/callAPI.ts index 73df085..aa369df 100644 --- a/src/lib/hooks/callAPI.ts +++ b/src/lib/hooks/callAPI.ts @@ -1,48 +1,15 @@ -import { get } from "svelte/store"; -import { accessTokenStore } from "$lib/stores"; - const refreshToken = async () => { - const res = await fetch("/api/auth/refreshToken", { - method: "POST", - credentials: "same-origin", - }); - if (!res.ok) { - accessTokenStore.set(null); - throw new Error("Failed to refresh token"); - } - - const data = await res.json(); - const token = data.accessToken as string; - - accessTokenStore.set(token); - return token; -}; - -const callAPIInternal = async ( - input: RequestInfo, - init: RequestInit | undefined, - token: string | null, - retryIfUnauthorized = true, -): Promise => { - if (!token) { - token = await refreshToken(); - retryIfUnauthorized = false; - } - - const res = await fetch(input, { - ...init, - headers: { - ...init?.headers, - Authorization: `Bearer ${token}`, - }, - }); - if (res.status === 401 && retryIfUnauthorized) { - return await callAPIInternal(input, init, null, false); - } - - return res; + return await fetch("/api/auth/refreshToken", { method: "POST" }); }; export const callAPI = async (input: RequestInfo, init?: RequestInit) => { - return await callAPIInternal(input, init, get(accessTokenStore)); + let res = await fetch(input, init); + if (res.status === 401) { + res = await refreshToken(); + if (!res.ok) { + return res; + } + res = await fetch(input, init); + } + return res; }; diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index da8a2d8..32bce3a 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -1,4 +1,4 @@ -import { error } from "@sveltejs/kit"; +import { error, type Cookies } from "@sveltejs/kit"; import jwt from "jsonwebtoken"; import env from "$lib/server/loadenv"; @@ -35,13 +35,13 @@ export const verifyToken = (token: string) => { } }; -export const authenticate = (request: Request) => { - const accessToken = request.headers.get("Authorization"); - if (!accessToken?.startsWith("Bearer ")) { - error(401, "Access token required"); +export const authenticate = (cookies: Cookies) => { + const accessToken = cookies.get("accessToken"); + if (!accessToken) { + error(401, "Access token not found"); } - const tokenPayload = verifyToken(accessToken.slice(7)); + const tokenPayload = verifyToken(accessToken); if (tokenPayload === TokenError.EXPIRED) { error(401, "Access token expired"); } else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "access") { diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index 172fc4a..0811802 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -71,7 +71,7 @@ export const logout = async (refreshToken: string) => { await revokeRefreshToken(jti); }; -export const refreshToken = async (refreshToken: string) => { +export const refreshTokens = async (refreshToken: string) => { const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); const newJti = uuidv4(); diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts deleted file mode 100644 index 93e276c..0000000 --- a/src/lib/stores/auth.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { writable } from "svelte/store"; - -export const accessTokenStore = writable(null); diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 86b0be9..668f46f 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -1,2 +1 @@ -export * from "./auth"; export * from "./key"; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 47f8f5b..dea5a25 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,5 +1,3 @@ -import { accessTokenStore } from "$lib/stores"; - export const requestLogin = async (email: string, password: string) => { const res = await fetch("/api/auth/login", { method: "POST", @@ -8,13 +6,5 @@ export const requestLogin = async (email: string, password: string) => { }, body: JSON.stringify({ email, password }), }); - if (!res.ok) { - return false; - } - - const data = await res.json(); - const token = data.accessToken as string; - - accessTokenStore.set(token); - return true; + return res.ok; }; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 7a4351b..ccd86f5 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -1,4 +1,4 @@ -import { error, json } from "@sveltejs/kit"; +import { error, text } from "@sveltejs/kit"; import ms from "ms"; import { z } from "zod"; import env from "$lib/server/loadenv"; @@ -10,7 +10,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { .object({ email: z.string().email().nonempty(), password: z.string().nonempty(), - pubKey: z.string().nonempty().optional(), + pubKey: z.string().base64().nonempty().optional(), }) .safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); @@ -18,12 +18,15 @@ export const POST: RequestHandler = async ({ request, cookies }) => { const { email, password, pubKey } = zodRes.data; const { accessToken, refreshToken } = await login(email.trim(), password.trim(), pubKey?.trim()); + cookies.set("accessToken", accessToken, { + path: "/", + maxAge: Math.floor(ms(env.jwt.accessExp) / 1000), + sameSite: "strict", + }); cookies.set("refreshToken", refreshToken, { path: "/api/auth", maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000), - httpOnly: true, - secure: true, sameSite: "strict", }); - return json({ accessToken }); + return text("Logged in", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts index 0499b87..a2750c9 100644 --- a/src/routes/api/auth/logout/+server.ts +++ b/src/routes/api/auth/logout/+server.ts @@ -4,8 +4,11 @@ import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ cookies }) => { const token = cookies.get("refreshToken"); - if (!token) error(401, "Token not found"); + if (!token) error(401, "Refresh token not found"); await logout(token.trim()); - return text("Logged out"); + + cookies.delete("accessToken", { path: "/" }); + cookies.delete("refreshToken", { path: "/api/auth" }); + return text("Logged out", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/auth/refreshToken/+server.ts b/src/routes/api/auth/refreshToken/+server.ts index 62f2a77..d05fc52 100644 --- a/src/routes/api/auth/refreshToken/+server.ts +++ b/src/routes/api/auth/refreshToken/+server.ts @@ -1,18 +1,20 @@ -import { error, json } from "@sveltejs/kit"; -import { refreshToken } from "$lib/server/services/auth"; +import { error, text } from "@sveltejs/kit"; +import { refreshTokens } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ cookies }) => { const token = cookies.get("refreshToken"); - if (!token) error(401, "Token not found"); + if (!token) error(401, "Refresh token not found"); - const { accessToken, refreshToken: newToken } = await refreshToken(token.trim()); + const { accessToken, refreshToken } = await refreshTokens(token.trim()); - cookies.set("refreshToken", newToken, { - path: "/api/auth", - httpOnly: true, - secure: true, + cookies.set("accessToken", accessToken, { + path: "/", sameSite: "strict", }); - return json({ accessToken }); + cookies.set("refreshToken", refreshToken, { + path: "/api/auth", + sameSite: "strict", + }); + return text("Token refreshed", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/key/register/+server.ts b/src/routes/api/key/register/+server.ts index 1c98bb4..5c95af8 100644 --- a/src/routes/api/key/register/+server.ts +++ b/src/routes/api/key/register/+server.ts @@ -4,7 +4,7 @@ import { authenticate } from "$lib/server/modules/auth"; import { registerPubKey } from "$lib/server/services/key"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request }) => { +export const POST: RequestHandler = async ({ request, cookies }) => { const zodRes = z .object({ pubKey: z.string().base64().nonempty(), @@ -12,7 +12,7 @@ export const POST: RequestHandler = async ({ request }) => { .safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); - const { userId, clientId } = authenticate(request); + const { userId, clientId } = authenticate(cookies); if (clientId) { error(403, "Forbidden"); } From 7267e319b4fc913e62e0b19378a1fed70cb6adca Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 17:35:24 +0900 Subject: [PATCH 022/115] =?UTF-8?q?Access=20Token=20=EC=9C=A0=EB=AC=B4?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9E=90=EB=8F=99=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks.server.ts | 16 +++++++++++++++- .../(fullscreen)/auth/login/+page.server.ts | 13 +++++++++++++ src/routes/(fullscreen)/auth/login/+page.svelte | 10 ++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/routes/(fullscreen)/auth/login/+page.server.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index c26afde..f9237c5 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,4 +1,4 @@ -import type { ServerInit } from "@sveltejs/kit"; +import { redirect, type ServerInit, type Handle } from "@sveltejs/kit"; import schedule from "node-schedule"; import { cleanupExpiredRefreshTokens } from "$lib/server/db/token"; @@ -7,3 +7,17 @@ export const init: ServerInit = () => { cleanupExpiredRefreshTokens(); }); }; + +export const handle: Handle = async ({ event, resolve }) => { + const path = event.url.pathname; + if (path.startsWith("/api") || path.startsWith("/auth")) { + return await resolve(event); + } + + const accessToken = event.cookies.get("accessToken"); + if (accessToken) { + return await resolve(event); + } else { + redirect(302, "/auth/login?redirect=" + encodeURIComponent(path)); + } +}; diff --git a/src/routes/(fullscreen)/auth/login/+page.server.ts b/src/routes/(fullscreen)/auth/login/+page.server.ts new file mode 100644 index 0000000..874e1be --- /dev/null +++ b/src/routes/(fullscreen)/auth/login/+page.server.ts @@ -0,0 +1,13 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const redirectPath = url.searchParams.get("redirect") || "/"; + + const accessToken = cookies.get("accessToken"); + if (accessToken) { + redirect(302, redirectPath); + } + + return { redirectPath }; +}; diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 0676741..5b13bab 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -1,9 +1,12 @@ From 173f4f5cfe44989060f96a38805fc8769cdf8719 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 18:33:30 +0900 Subject: [PATCH 023/115] =?UTF-8?q?pubKeyStore=EC=99=80=20privKeyStore?= =?UTF-8?q?=EB=A5=BC=20keyPairStore=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/indexedDB.ts | 15 +++++++++------ src/lib/stores/key.ts | 3 +-- src/routes/(fullscreen)/key/generate/service.ts | 13 +++++++------ src/routes/api/key/register/+server.ts | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/lib/indexedDB.ts b/src/lib/indexedDB.ts index 92553b0..ff0c29e 100644 --- a/src/lib/indexedDB.ts +++ b/src/lib/indexedDB.ts @@ -1,21 +1,21 @@ import { Dexie, type EntityTable } from "dexie"; -interface ClientKeyPair { +interface KeyPair { type: "publicKey" | "privateKey"; key: CryptoKey; } const keyStore = new Dexie("keyStore") as Dexie & { - clientKeyPairs: EntityTable; + keyPair: EntityTable; }; keyStore.version(1).stores({ - clientKeyPairs: "type", + keyPair: "type", }); export const getKeyPairFromIndexedDB = async () => { - const pubKey = await keyStore.clientKeyPairs.get("publicKey"); - const privKey = await keyStore.clientKeyPairs.get("privateKey"); + const pubKey = await keyStore.keyPair.get("publicKey"); + const privKey = await keyStore.keyPair.get("privateKey"); return { pubKey: pubKey?.key ?? null, privKey: privKey?.key ?? null, @@ -23,7 +23,10 @@ export const getKeyPairFromIndexedDB = async () => { }; export const storeKeyPairIntoIndexedDB = async (pubKey: CryptoKey, privKey: CryptoKey) => { - await keyStore.clientKeyPairs.bulkPut([ + if (!pubKey.extractable) throw new Error("Public key must be extractable"); + if (privKey.extractable) throw new Error("Private key must be non-extractable"); + + await keyStore.keyPair.bulkPut([ { type: "publicKey", key: pubKey }, { type: "privateKey", key: privKey }, ]); diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index 40458bb..f8dc6ac 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -1,4 +1,3 @@ import { writable } from "svelte/store"; -export const pubKeyStore = writable(null); -export const privKeyStore = writable(null); +export const keyPairStore = writable(null); diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 7b869d4..8c38abc 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,5 +1,5 @@ import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; -import { pubKeyStore, privKeyStore } from "$lib/stores"; +import { keyPairStore } from "$lib/stores"; type KeyType = "public" | "private"; @@ -42,12 +42,13 @@ const exportKeyToBase64 = async (key: CryptoKey, type: KeyType) => { export const generateKeyPair = async () => { const keyPair = await generateRSAKeyPair(); - const privKeySecure = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); + const privKeySecured = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); - pubKeyStore.set(keyPair.publicKey); - privKeyStore.set(privKeySecure); - - await storeKeyPairIntoIndexedDB(keyPair.publicKey, privKeySecure); + keyPairStore.set({ + publicKey: keyPair.publicKey, + privateKey: privKeySecured, + }); + await storeKeyPairIntoIndexedDB(keyPair.publicKey, privKeySecured); return { pubKeyBase64: await exportKeyToBase64(keyPair.publicKey, "public"), diff --git a/src/routes/api/key/register/+server.ts b/src/routes/api/key/register/+server.ts index 5c95af8..41888a8 100644 --- a/src/routes/api/key/register/+server.ts +++ b/src/routes/api/key/register/+server.ts @@ -18,5 +18,5 @@ export const POST: RequestHandler = async ({ request, cookies }) => { } await registerPubKey(userId, zodRes.data.pubKey); - return text("Public key registered"); + return text("Public key registered", { headers: { "Content-Type": "text/plain" } }); }; From dfb56b62b19e751f3b6196a633f9454ab3ac827f Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 18:55:20 +0900 Subject: [PATCH 024/115] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20?= =?UTF-8?q?=EC=9C=A0=EB=AC=B4=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks.server.ts | 8 +++++--- src/routes/(fullscreen)/auth/login/+page.svelte | 11 ++++++++--- src/routes/(fullscreen)/key/generate/service.ts | 2 -- src/routes/+layout.svelte | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index f9237c5..c915d9b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -9,8 +9,7 @@ export const init: ServerInit = () => { }; export const handle: Handle = async ({ event, resolve }) => { - const path = event.url.pathname; - if (path.startsWith("/api") || path.startsWith("/auth")) { + if (["/api", "/auth"].some((path) => event.url.pathname.startsWith(path))) { return await resolve(event); } @@ -18,6 +17,9 @@ export const handle: Handle = async ({ event, resolve }) => { if (accessToken) { return await resolve(event); } else { - redirect(302, "/auth/login?redirect=" + encodeURIComponent(path)); + redirect( + 302, + "/auth/login?redirect=" + encodeURIComponent(event.url.pathname + event.url.search), + ); } }; diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 5b13bab..ffac02d 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -1,8 +1,10 @@ {@render children()} From 52a61297c53200b5aea725ccfcc8496ffe5eba35 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 22:15:46 +0900 Subject: [PATCH 025/115] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20?= =?UTF-8?q?=EB=82=B4=EB=B3=B4=EB=82=B4=EA=B8=B0=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 14 ++++++ src/lib/components/BottomSheet.svelte | 31 ++++++++++++ src/lib/components/Modal.svelte | 15 ++---- src/lib/components/index.ts | 1 + src/lib/hooks/gotoStateful.ts | 1 + .../(fullscreen)/auth/login/+page.svelte | 3 +- .../(fullscreen)/key/export/+page.svelte | 50 +++++++++++++------ .../export/BeforeContinueBottomSheet.svelte | 35 +++++++++++++ .../key/export/BeforeContinueModal.svelte | 4 +- src/routes/(fullscreen)/key/export/service.ts | 17 +++++++ .../(fullscreen)/key/generate/+page.svelte | 20 +++++++- src/routes/(fullscreen)/key/generate/+page.ts | 6 +++ 13 files changed, 169 insertions(+), 30 deletions(-) create mode 100644 src/lib/components/BottomSheet.svelte create mode 100644 src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte create mode 100644 src/routes/(fullscreen)/key/generate/+page.ts diff --git a/package.json b/package.json index b57af77..ca5bc28 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/better-sqlite3": "^7.6.11", + "@types/file-saver": "^2.0.7", "@types/jsonwebtoken": "^9.0.7", "@types/ms": "^0.7.34", "@types/node-schedule": "^2.1.7", @@ -33,6 +34,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", "eslint-plugin-tailwindcss": "^3.17.5", + "file-saver": "^2.0.5", "globals": "^15.0.0", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df5d1cf..b791d45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ devDependencies: '@types/better-sqlite3': specifier: ^7.6.11 version: 7.6.12 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.7 @@ -79,6 +82,9 @@ devDependencies: eslint-plugin-tailwindcss: specifier: ^3.17.5 version: 3.17.5(tailwindcss@3.4.17) + file-saver: + specifier: ^2.0.5 + version: 2.0.5 globals: specifier: ^15.0.0 version: 15.14.0 @@ -1288,6 +1294,10 @@ packages: resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} dev: true + /@types/file-saver@2.0.7: + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true @@ -2269,6 +2279,10 @@ packages: flat-cache: 4.0.1 dev: true + /file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + dev: true + /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} dev: false diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte new file mode 100644 index 0000000..81e24e1 --- /dev/null +++ b/src/lib/components/BottomSheet.svelte @@ -0,0 +1,31 @@ + + +{#if isOpen} + + +
{ + isOpen = false; + }} + class="fixed inset-0 flex items-end justify-center" + > +
+
e.stopPropagation()} + class="z-10 flex max-h-[70vh] min-h-[30vh] w-full items-stretch rounded-t-2xl bg-white p-4" + transition:fly={{ y: 100, duration: 200 }} + > + {@render children?.()} +
+
+{/if} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 36c1171..37d7466 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -8,24 +8,19 @@ } let { children, isOpen = $bindable() }: Props = $props(); - - const closeModal = () => { - isOpen = false; - }; {#if isOpen}
{ + isOpen = false; + }} class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2" + transition:fade={{ duration: 100 }} > -
e.stopPropagation()} - class="max-w-full rounded-2xl bg-white p-4" - transition:fade={{ duration: 100 }} - > +
e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4"> {@render children?.()}
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 5ddd0fe..55979fb 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -1 +1,2 @@ +export { default as BottomSheet } from "./BottomSheet.svelte"; export { default as Modal } from "./Modal.svelte"; diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts index 1a05294..064be29 100644 --- a/src/lib/hooks/gotoStateful.ts +++ b/src/lib/hooks/gotoStateful.ts @@ -3,6 +3,7 @@ import { goto } from "$app/navigation"; type Path = "/key/export"; interface KeyExportState { + redirectPath: string; pubKeyBase64: string; privKeyBase64: string; } diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index ffac02d..46c03fc 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -1,5 +1,4 @@ @@ -64,7 +82,9 @@
- + diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte new file mode 100644 index 0000000..58aa9a5 --- /dev/null +++ b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte @@ -0,0 +1,35 @@ + + + +
+
+

암호 키 파일을 저장하셨나요?

+

+ 보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. 파일이 저장되었는지 + 다시 확인해 주세요. +

+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte index 984f553..3255026 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte @@ -23,8 +23,10 @@ color="gray" onclick={() => { isOpen = false; - }}>아니요 + 아니요 + diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 3cef2bc..c45ff50 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,4 +1,17 @@ import { callAPI } from "$lib/hooks"; +import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; + +export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: string) => { + const pubKeyFormatted = pubKeyBase64.match(/.{1,64}/g)?.join("\n"); + const privKeyFormatted = privKeyBase64.match(/.{1,64}/g)?.join("\n"); + if (!pubKeyFormatted || !privKeyFormatted) { + throw new Error("Failed to format key pair"); + } + + const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKeyFormatted}\n-----END PUBLIC KEY-----`; + const privKeyPem = `-----BEGIN PRIVATE KEY-----\n${privKeyFormatted}\n-----END PRIVATE KEY-----`; + return new Blob([`${pubKeyPem}\n${privKeyPem}\n`], { type: "text/plain" }); +}; export const requestPubKeyRegistration = async (pubKeyBase64: string) => { const res = await callAPI("/api/key/register", { @@ -10,3 +23,7 @@ export const requestPubKeyRegistration = async (pubKeyBase64: string) => { }); return res.ok; }; + +export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => { + await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey); +}; diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index af84aa8..3a751b6 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -1,12 +1,16 @@ diff --git a/src/routes/(fullscreen)/key/generate/+page.ts b/src/routes/(fullscreen)/key/generate/+page.ts new file mode 100644 index 0000000..626d2e0 --- /dev/null +++ b/src/routes/(fullscreen)/key/generate/+page.ts @@ -0,0 +1,6 @@ +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ url }) => { + const redirectPath = url.searchParams.get("redirect") || "/"; + return { redirectPath }; +}; From 928cb799d323ae52e9072091e655bfb4d271e498 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 28 Dec 2024 22:51:24 +0900 Subject: [PATCH 026/115] =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(fullscreen)/key/export/BeforeContinueBottomSheet.svelte | 2 +- src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte | 2 +- src/routes/(fullscreen)/key/export/service.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte index 58aa9a5..1a32b1b 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte @@ -18,7 +18,7 @@

암호 키 파일을 저장하셨나요?

보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. 파일이 저장되었는지 - 다시 확인해 주세요. + 다시 한 번 확인해 주세요.

diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte index 3255026..3fab3cd 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte @@ -27,7 +27,7 @@ > 아니요 - + diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index c45ff50..ac0319e 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -8,8 +8,8 @@ export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: throw new Error("Failed to format key pair"); } - const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKeyFormatted}\n-----END PUBLIC KEY-----`; - const privKeyPem = `-----BEGIN PRIVATE KEY-----\n${privKeyFormatted}\n-----END PRIVATE KEY-----`; + const pubKeyPem = `-----BEGIN RSA PUBLIC KEY-----\n${pubKeyFormatted}\n-----END RSA PUBLIC KEY-----`; + const privKeyPem = `-----BEGIN RSA PRIVATE KEY-----\n${privKeyFormatted}\n-----END RSA PRIVATE KEY-----`; return new Blob([`${pubKeyPem}\n${privKeyPem}\n`], { type: "text/plain" }); }; From 75ab5f5859c9edd310e54a9b1c10a3af41b184e3 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 29 Dec 2024 00:32:24 +0900 Subject: [PATCH 027/115] =?UTF-8?q?=EA=B3=B5=EA=B0=9C=20=ED=82=A4=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EC=8B=9C=20=EC=9D=B8=EC=A6=9D=20=EC=A0=88?= =?UTF-8?q?=EC=B0=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + src/hooks.server.ts | 2 + src/lib/server/db/client.ts | 69 +++++++++++++++++-- src/lib/server/db/schema/client.ts | 20 +++++- src/lib/server/loadenv.ts | 3 + src/lib/server/services/auth.ts | 6 +- src/lib/server/services/key.ts | 41 ++++++++++- .../(fullscreen)/key/export/+page.svelte | 4 +- src/routes/(fullscreen)/key/export/service.ts | 29 +++++++- src/routes/api/key/register/+server.ts | 8 +-- src/routes/api/key/verify/+server.ts | 22 ++++++ 11 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 src/routes/api/key/verify/+server.ts diff --git a/.env.example b/.env.example index e76f3cd..665a966 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,4 @@ JWT_SECRET= DATABASE_URL= JWT_ACCESS_TOKEN_EXPIRES= JWT_REFRESH_TOKEN_EXPIRES= +PUBKEY_CHALLENGE_EXPIRES= diff --git a/src/hooks.server.ts b/src/hooks.server.ts index c915d9b..1419eeb 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,9 +1,11 @@ import { redirect, type ServerInit, type Handle } from "@sveltejs/kit"; import schedule from "node-schedule"; +import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client"; import { cleanupExpiredRefreshTokens } from "$lib/server/db/token"; export const init: ServerInit = () => { schedule.scheduleJob("0 * * * *", () => { + cleanupExpiredUserClientChallenges(); cleanupExpiredRefreshTokens(); }); }; diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 006aa32..c6b4dfd 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -1,14 +1,14 @@ -import { and, eq } from "drizzle-orm"; +import { and, eq, gt, lte } from "drizzle-orm"; import db from "./drizzle"; -import { client, userClient } from "./schema"; +import { client, userClient, userClientChallenge, UserClientState } from "./schema"; export const createClient = async (pubKey: string, userId: number) => { - await db.transaction(async (tx) => { + return await db.transaction(async (tx) => { const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id }); - await tx.insert(userClient).values({ - userId, - clientId: insertRes[0]!.id, - }); + const { id: clientId } = insertRes[0]!; + await tx.insert(userClient).values({ userId, clientId }); + + return clientId; }); }; @@ -25,3 +25,58 @@ export const getUserClient = async (userId: number, clientId: number) => { .execute(); return userClients[0] ?? null; }; + +export const setUserClientStateToPending = async (userId: number, clientId: number) => { + await db + .update(userClient) + .set({ state: UserClientState.Pending }) + .where( + and( + eq(userClient.userId, userId), + eq(userClient.clientId, clientId), + eq(userClient.state, UserClientState.Challenging), + ), + ) + .execute(); +}; + +export const createUserClientChallenge = async ( + userId: number, + clientId: number, + challenge: string, + allowedIp: string, + expiresAt: number, +) => { + await db + .insert(userClientChallenge) + .values({ + userId, + clientId, + challenge, + allowedIp, + expiresAt, + }) + .execute(); +}; + +export const getUserClientChallenge = async (challenge: string, ip: string) => { + const challenges = await db + .select() + .from(userClientChallenge) + .where( + and( + eq(userClientChallenge.challenge, challenge), + eq(userClientChallenge.allowedIp, ip), + gt(userClientChallenge.expiresAt, Date.now()), + ), + ) + .execute(); + return challenges[0] ?? null; +}; + +export const cleanupExpiredUserClientChallenges = async () => { + await db + .delete(userClientChallenge) + .where(lte(userClientChallenge.expiresAt, Date.now())) + .execute(); +}; diff --git a/src/lib/server/db/schema/client.ts b/src/lib/server/db/schema/client.ts index ad5d678..efacf31 100644 --- a/src/lib/server/db/schema/client.ts +++ b/src/lib/server/db/schema/client.ts @@ -2,8 +2,9 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" import { user } from "./user"; export enum UserClientState { - PENDING = 0, - ACTIVE = 1, + Challenging = 0, + Pending = 1, + Active = 2, } export const client = sqliteTable("client", { @@ -20,10 +21,23 @@ export const userClient = sqliteTable( clientId: integer("client_id") .notNull() .references(() => client.id), - state: integer("state").notNull().default(0), + state: integer("state").notNull().default(UserClientState.Challenging), encKey: text("encrypted_key"), }, (t) => ({ pk: primaryKey({ columns: [t.userId, t.clientId] }), }), ); + +export const userClientChallenge = sqliteTable("user_client_challenge", { + id: integer("id").primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => user.id), + clientId: integer("client_id") + .notNull() + .references(() => client.id), + challenge: text("challenge").notNull().unique(), + allowedIp: text("allowed_ip").notNull(), + expiresAt: integer("expires_at").notNull(), +}); diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts index 8df1e19..c31fb07 100644 --- a/src/lib/server/loadenv.ts +++ b/src/lib/server/loadenv.ts @@ -12,4 +12,7 @@ export default { accessExp: env.JWT_ACCESS_TOKEN_EXPIRES || "5m", refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d", }, + challenge: { + pubKeyExp: env.PUBKEY_CHALLENGE_EXPIRES || "5m", + }, }; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index 0811802..e60c4be 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -1,7 +1,7 @@ import { error } from "@sveltejs/kit"; import argon2 from "argon2"; import { v4 as uuidv4 } from "uuid"; -import { getClientByPubKey } from "$lib/server/db/client"; +import { getClientByPubKey, getUserClient } from "$lib/server/db/client"; import { getUserByEmail } from "$lib/server/db/user"; import { getRefreshToken, @@ -9,6 +9,7 @@ import { rotateRefreshToken, revokeRefreshToken, } from "$lib/server/db/token"; +import { UserClientState } from "$lib/server/db/schema"; import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; const verifyPassword = async (hash: string, password: string) => { @@ -36,8 +37,11 @@ export const login = async (email: string, password: string, pubKey?: string) => } 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 === UserClientState.Challenging)) { + error(401, "Unregistered public key"); } return { diff --git a/src/lib/server/services/key.ts b/src/lib/server/services/key.ts index 4d57e08..f5dfa44 100644 --- a/src/lib/server/services/key.ts +++ b/src/lib/server/services/key.ts @@ -1,10 +1,45 @@ import { error } from "@sveltejs/kit"; -import { createClient, getClientByPubKey } from "$lib/server/db/client"; +import { randomBytes, publicEncrypt } from "crypto"; +import ms from "ms"; +import { promisify } from "util"; +import { + createClient, + getClientByPubKey, + createUserClientChallenge, + getUserClientChallenge, + setUserClientStateToPending, +} from "$lib/server/db/client"; +import env from "$lib/server/loadenv"; -export const registerPubKey = async (userId: number, pubKey: string) => { +const expiresIn = ms(env.challenge.pubKeyExp); +const expiresAt = () => Date.now() + expiresIn; + +const generateChallenge = async (userId: number, ip: string, clientId: number, pubKey: string) => { + const challenge = await promisify(randomBytes)(32); + const challengeBase64 = challenge.toString("base64"); + await createUserClientChallenge(userId, clientId, challengeBase64, ip, expiresAt()); + + const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; + const challengeEncrypted = publicEncrypt({ key: pubKeyPem, oaepHash: "sha256" }, challenge); + return challengeEncrypted.toString("base64"); +}; + +export const registerPubKey = async (userId: number, ip: string, pubKey: string) => { if (await getClientByPubKey(pubKey)) { error(409, "Public key already registered"); } - await createClient(pubKey, userId); + const clientId = await createClient(pubKey, userId); + return await generateChallenge(userId, ip, clientId, pubKey); +}; + +export const verifyPubKey = async (userId: number, ip: string, answer: string) => { + const challenge = await getUserClientChallenge(answer, ip); + if (!challenge) { + error(401, "Invalid challenge answer"); + } else if (challenge.userId !== userId) { + error(403, "Forbidden"); + } + + await setUserClientStateToPending(userId, challenge.clientId); }; diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index ba0283d..74bc801 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -38,11 +38,11 @@ isBeforeContinueModalOpen = false; isBeforeContinueBottomSheetOpen = false; - if (await requestPubKeyRegistration(data.pubKeyBase64)) { + if (await requestPubKeyRegistration(data.pubKeyBase64, $keyPairStore.privateKey)) { await storeKeyPairPersistently($keyPairStore); await goto(data.redirectPath); } else { - // TODO + // TODO: Error handling } }; diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index ac0319e..301c825 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -13,14 +13,39 @@ export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: return new Blob([`${pubKeyPem}\n${privKeyPem}\n`], { type: "text/plain" }); }; -export const requestPubKeyRegistration = async (pubKeyBase64: string) => { - const res = await callAPI("/api/key/register", { +const decryptChallenge = async (challenge: string, privateKey: CryptoKey) => { + const challengeBuffer = Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)); + const answer = await window.crypto.subtle.decrypt( + { + name: "RSA-OAEP", + } satisfies RsaOaepParams, + privateKey, + challengeBuffer, + ); + return btoa(String.fromCharCode(...new Uint8Array(answer))); +}; + +export const requestPubKeyRegistration = async (pubKeyBase64: string, privateKey: CryptoKey) => { + let res = await callAPI("/api/key/register", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ pubKey: pubKeyBase64 }), }); + if (!res.ok) return false; + + const data = await res.json(); + const challenge = data.challenge as string; + const answer = await decryptChallenge(challenge, privateKey); + + res = await callAPI("/api/key/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ answer }), + }); return res.ok; }; diff --git a/src/routes/api/key/register/+server.ts b/src/routes/api/key/register/+server.ts index 41888a8..766b13f 100644 --- a/src/routes/api/key/register/+server.ts +++ b/src/routes/api/key/register/+server.ts @@ -1,10 +1,10 @@ -import { error, text } from "@sveltejs/kit"; +import { error, json } from "@sveltejs/kit"; import { z } from "zod"; import { authenticate } from "$lib/server/modules/auth"; import { registerPubKey } from "$lib/server/services/key"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies }) => { +export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { const zodRes = z .object({ pubKey: z.string().base64().nonempty(), @@ -17,6 +17,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => { error(403, "Forbidden"); } - await registerPubKey(userId, zodRes.data.pubKey); - return text("Public key registered", { headers: { "Content-Type": "text/plain" } }); + const challenge = await registerPubKey(userId, getClientAddress(), zodRes.data.pubKey); + return json({ challenge }); }; diff --git a/src/routes/api/key/verify/+server.ts b/src/routes/api/key/verify/+server.ts new file mode 100644 index 0000000..bc59816 --- /dev/null +++ b/src/routes/api/key/verify/+server.ts @@ -0,0 +1,22 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authenticate } from "$lib/server/modules/auth"; +import { verifyPubKey } from "$lib/server/services/key"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { + const zodRes = z + .object({ + answer: z.string().base64().nonempty(), + }) + .safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + + const { userId, clientId } = authenticate(cookies); + if (clientId) { + error(403, "Forbidden"); + } + + await verifyPubKey(userId, getClientAddress(), zodRes.data.answer); + return text("Key verified", { headers: { "Content-Type": "text/plain" } }); +}; From f6432ff2906e71aefbb6c5f0ac8dfc757b59d15a Mon Sep 17 00:00:00 2001 From: static Date: Sun, 29 Dec 2024 00:36:13 +0900 Subject: [PATCH 028/115] =?UTF-8?q?/api/key/register=20Endpoint=EC=97=90?= =?UTF-8?q?=EC=84=9C,=20=EC=A0=9C=EA=B3=B5=EB=90=9C=20=EA=B3=B5=EA=B0=9C?= =?UTF-8?q?=20=ED=82=A4=EA=B0=80=20RSA=204096=EC=9D=98=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=20=ED=82=A4=EA=B0=80=20=EB=A7=9E=EB=8A=94=EC=A7=80=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/services/key.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/server/services/key.ts b/src/lib/server/services/key.ts index f5dfa44..7222036 100644 --- a/src/lib/server/services/key.ts +++ b/src/lib/server/services/key.ts @@ -1,5 +1,5 @@ import { error } from "@sveltejs/kit"; -import { randomBytes, publicEncrypt } from "crypto"; +import { randomBytes, publicEncrypt, createPublicKey } from "crypto"; import ms from "ms"; import { promisify } from "util"; import { @@ -29,6 +29,15 @@ export const registerPubKey = async (userId: number, ip: string, pubKey: string) error(409, "Public key already registered"); } + const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; + const pubKeyObject = createPublicKey(pubKeyPem); + if ( + pubKeyObject.asymmetricKeyType !== "rsa" || + pubKeyObject.asymmetricKeyDetails?.modulusLength !== 4096 + ) { + error(400, "Invalid public key"); + } + const clientId = await createClient(pubKey, userId); return await generateChallenge(userId, ip, clientId, pubKey); }; From 29b105a0693d46d51b5f73a3e94853b9c0310210 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 29 Dec 2024 00:53:49 +0900 Subject: [PATCH 029/115] =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(fullscreen)/key/export/BeforeContinueBottomSheet.svelte | 4 ++-- src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte index 1a32b1b..e89424c 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte @@ -17,8 +17,8 @@

암호 키 파일을 저장하셨나요?

- 보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. 파일이 저장되었는지 - 다시 한 번 확인해 주세요. + 암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요. 파일이 잘 저장되었는지 다시 + 한 번 확인해 주세요.

diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte index 3fab3cd..5e0e7c3 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte @@ -14,9 +14,7 @@

내보내지 않고 계속할까요?

-

- 보안상의 이유로 지금 시점 이후로는 암호 키를 파일로 내보낼 수 없어요. -

+

암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요.

+ {/each} +
+ +
diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte new file mode 100644 index 0000000..0f34ad6 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -0,0 +1,3 @@ +{#each Array(300) as _} +

Hello!

+{/each} From 91f4c85f7adf24f28c2d226066edd155bd59f278 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 01:13:47 +0900 Subject: [PATCH 067/115] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B9=A8=EC=A7=90=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/(fullscreen)/+layout.svelte | 4 ++-- src/routes/(fullscreen)/key/generate/+page.svelte | 2 +- src/routes/(main)/+layout.svelte | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/(fullscreen)/+layout.svelte b/src/routes/(fullscreen)/+layout.svelte index 39e4b10..4002fbe 100644 --- a/src/routes/(fullscreen)/+layout.svelte +++ b/src/routes/(fullscreen)/+layout.svelte @@ -4,8 +4,8 @@ let { children } = $props(); -
+
- {@render children?.()} + {@render children()}
diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 8af3b29..c37fd11 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -55,7 +55,7 @@ 암호 키 생성하기 -
+

암호 키 생성하기

diff --git a/src/routes/(main)/+layout.svelte b/src/routes/(main)/+layout.svelte index d51c84f..3610ec7 100644 --- a/src/routes/(main)/+layout.svelte +++ b/src/routes/(main)/+layout.svelte @@ -4,9 +4,9 @@ let { children } = $props(); -
+
- {@render children?.()} + {@render children()}
From 314b8cca5cc75b25478179f08d5ee7d35b52da16 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 01:17:09 +0900 Subject: [PATCH 068/115] =?UTF-8?q?DB=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=AC=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drizzle/0000_lazy_scarecrow.sql | 117 ++++++ drizzle/0000_spicy_morgan_stark.sql | 67 --- drizzle/0001_silly_vanisher.sql | 20 - drizzle/meta/0000_snapshot.json | 411 ++++++++++++++++++- drizzle/meta/0001_snapshot.json | 611 ---------------------------- drizzle/meta/_journal.json | 11 +- 6 files changed, 512 insertions(+), 725 deletions(-) create mode 100644 drizzle/0000_lazy_scarecrow.sql delete mode 100644 drizzle/0000_spicy_morgan_stark.sql delete mode 100644 drizzle/0001_silly_vanisher.sql delete mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/0000_lazy_scarecrow.sql b/drizzle/0000_lazy_scarecrow.sql new file mode 100644 index 0000000..89e8f99 --- /dev/null +++ b/drizzle/0000_lazy_scarecrow.sql @@ -0,0 +1,117 @@ +CREATE TABLE `client` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `encryption_public_key` text NOT NULL, + `signature_public_key` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `user_client` ( + `user_id` integer NOT NULL, + `client_id` integer NOT NULL, + `state` text DEFAULT 'challenging' NOT NULL, + PRIMARY KEY(`client_id`, `user_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`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 +CREATE TABLE `user_client_challenge` ( + `id` integer PRIMARY KEY NOT NULL, + `user_id` integer 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 (`user_id`) REFERENCES `user`(`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 +CREATE TABLE `directory` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `created_at` integer NOT NULL, + `parent_id` integer, + `user_id` integer NOT NULL, + `master_encryption_key_version` integer NOT NULL, + `encrypted_data_encryption_key` text NOT NULL, + `encrypted_at` integer NOT NULL, + `encrypted_name` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `file` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `path` text NOT NULL, + `parent_id` integer, + `created_at` integer NOT NULL, + `user_id` integer NOT NULL, + `master_encryption_key_version` integer NOT NULL, + `encrypted_data_encryption_key` text NOT NULL, + `encrypted_at` integer NOT NULL, + `encrypted_name` text NOT NULL, + FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `client_master_encryption_key` ( + `user_id` integer NOT NULL, + `client_id` integer NOT NULL, + `version` integer NOT NULL, + `encrypted_key` text NOT NULL, + `encrypted_key_signature` text NOT NULL, + PRIMARY KEY(`client_id`, `user_id`, `version`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `master_encryption_key` ( + `user_id` integer NOT NULL, + `version` integer NOT NULL, + `created_by` integer NOT NULL, + `created_at` integer NOT NULL, + `state` text NOT NULL, + `retired_at` integer, + PRIMARY KEY(`user_id`, `version`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`created_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `refresh_token` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `client_id` integer, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`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 +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 +CREATE TABLE `user` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `email` text NOT NULL, + `password` text NOT NULL +); +--> 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`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_client_challenge_challenge_unique` ON `user_client_challenge` (`challenge`);--> statement-breakpoint +CREATE UNIQUE INDEX `directory_encrypted_data_encryption_key_unique` ON `directory` (`encrypted_data_encryption_key`);--> statement-breakpoint +CREATE UNIQUE INDEX `file_path_unique` ON `file` (`path`);--> statement-breakpoint +CREATE UNIQUE INDEX `file_encrypted_data_encryption_key_unique` ON `file` (`encrypted_data_encryption_key`);--> statement-breakpoint +CREATE UNIQUE INDEX `refresh_token_user_id_client_id_unique` ON `refresh_token` (`user_id`,`client_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `token_upgrade_challenge_challenge_unique` ON `token_upgrade_challenge` (`challenge`);--> statement-breakpoint +CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/drizzle/0000_spicy_morgan_stark.sql b/drizzle/0000_spicy_morgan_stark.sql deleted file mode 100644 index 85ad972..0000000 --- a/drizzle/0000_spicy_morgan_stark.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE TABLE `client` ( - `id` integer PRIMARY KEY NOT NULL, - `public_key` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `user_client` ( - `user_id` integer NOT NULL, - `client_id` integer NOT NULL, - `state` text DEFAULT 'challenging' NOT NULL, - PRIMARY KEY(`client_id`, `user_id`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`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 -CREATE TABLE `user_client_challenge` ( - `id` integer PRIMARY KEY NOT NULL, - `user_id` integer NOT NULL, - `client_id` integer NOT NULL, - `challenge` text NOT NULL, - `allowed_ip` text NOT NULL, - `expires_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`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 -CREATE TABLE `client_master_encryption_key` ( - `user_id` integer NOT NULL, - `client_id` integer NOT NULL, - `master_encryption_key_version` integer NOT NULL, - `encrypted_master_encryption_key` text NOT NULL, - PRIMARY KEY(`client_id`, `master_encryption_key_version`, `user_id`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `master_encryption_key` ( - `user_id` integer NOT NULL, - `version` integer NOT NULL, - `created_by` integer NOT NULL, - `created_at` integer NOT NULL, - `state` text NOT NULL, - `retired_at` integer, - PRIMARY KEY(`user_id`, `version`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`created_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `refresh_token` ( - `id` text PRIMARY KEY NOT NULL, - `user_id` integer NOT NULL, - `client_id` integer, - `expires_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`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 -CREATE TABLE `user` ( - `id` integer PRIMARY KEY NOT NULL, - `email` text NOT NULL, - `password` text NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `client_public_key_unique` ON `client` (`public_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `user_client_challenge_challenge_unique` ON `user_client_challenge` (`challenge`);--> statement-breakpoint -CREATE UNIQUE INDEX `refresh_token_user_id_client_id_unique` ON `refresh_token` (`user_id`,`client_id`);--> statement-breakpoint -CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/drizzle/0001_silly_vanisher.sql b/drizzle/0001_silly_vanisher.sql deleted file mode 100644 index 32f51e0..0000000 --- a/drizzle/0001_silly_vanisher.sql +++ /dev/null @@ -1,20 +0,0 @@ -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`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index e01aaba..49d6d24 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "64e2c1ed-92bf-44d1-9094-7e3610b3224f", + "id": "901e84cd-f9eb-4329-a374-f71264675515", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "client": { @@ -12,10 +12,17 @@ "type": "integer", "primaryKey": true, "notNull": true, + "autoincrement": true + }, + "encryption_public_key": { + "name": "encryption_public_key", + "type": "text", + "primaryKey": false, + "notNull": true, "autoincrement": false }, - "public_key": { - "name": "public_key", + "signature_public_key": { + "name": "signature_public_key", "type": "text", "primaryKey": false, "notNull": true, @@ -23,10 +30,25 @@ } }, "indexes": { - "client_public_key_unique": { - "name": "client_public_key_unique", + "client_encryption_public_key_unique": { + "name": "client_encryption_public_key_unique", "columns": [ - "public_key" + "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 } @@ -145,6 +167,14 @@ "primaryKey": false, "notNull": true, "autoincrement": false + }, + "is_used": { + "name": "is_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false } }, "indexes": { @@ -187,6 +217,250 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, + "directory": { + "name": "directory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_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_data_encryption_key": { + "name": "encrypted_data_encryption_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_at": { + "name": "encrypted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "directory_encrypted_data_encryption_key_unique": { + "name": "directory_encrypted_data_encryption_key_unique", + "columns": [ + "encrypted_data_encryption_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "directory_user_id_user_id_fk": { + "name": "directory_user_id_user_id_fk", + "tableFrom": "directory", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "directory_parent_id_directory_id_fk": { + "name": "directory_parent_id_directory_id_fk", + "tableFrom": "directory", + "tableTo": "directory", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "directory_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { + "name": "directory_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", + "tableFrom": "directory", + "tableTo": "master_encryption_key", + "columnsFrom": [ + "user_id", + "master_encryption_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "file": { + "name": "file", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_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_data_encryption_key": { + "name": "encrypted_data_encryption_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_at": { + "name": "encrypted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "file_path_unique": { + "name": "file_path_unique", + "columns": [ + "path" + ], + "isUnique": true + }, + "file_encrypted_data_encryption_key_unique": { + "name": "file_encrypted_data_encryption_key_unique", + "columns": [ + "encrypted_data_encryption_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "file_parent_id_directory_id_fk": { + "name": "file_parent_id_directory_id_fk", + "tableFrom": "file", + "tableTo": "directory", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "file_user_id_user_id_fk": { + "name": "file_user_id_user_id_fk", + "tableFrom": "file", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "file_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { + "name": "file_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", + "tableFrom": "file", + "tableTo": "master_encryption_key", + "columnsFrom": [ + "user_id", + "master_encryption_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "client_master_encryption_key": { "name": "client_master_encryption_key", "columns": { @@ -204,15 +478,22 @@ "notNull": true, "autoincrement": false }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", + "version": { + "name": "version", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, - "encrypted_master_encryption_key": { - "name": "encrypted_master_encryption_key", + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_key_signature": { + "name": "encrypted_key_signature", "type": "text", "primaryKey": false, "notNull": true, @@ -247,13 +528,13 @@ "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", + "client_master_encryption_key_user_id_version_master_encryption_key_user_id_version_fk": { + "name": "client_master_encryption_key_user_id_version_master_encryption_key_user_id_version_fk", "tableFrom": "client_master_encryption_key", "tableTo": "master_encryption_key", "columnsFrom": [ "user_id", - "master_encryption_key_version" + "version" ], "columnsTo": [ "user_id", @@ -264,13 +545,13 @@ } }, "compositePrimaryKeys": { - "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk": { + "client_master_encryption_key_user_id_client_id_version_pk": { "columns": [ "client_id", - "master_encryption_key_version", - "user_id" + "user_id", + "version" ], - "name": "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk" + "name": "client_master_encryption_key_user_id_client_id_version_pk" } }, "uniqueConstraints": {} @@ -434,6 +715,100 @@ "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": { @@ -442,7 +817,7 @@ "type": "integer", "primaryKey": true, "notNull": true, - "autoincrement": false + "autoincrement": true }, "email": { "name": "email", diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json deleted file mode 100644 index c33f453..0000000 --- a/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,611 +0,0 @@ -{ - "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": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index dc91af8..7874a98 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,15 +5,8 @@ { "idx": 0, "version": "6", - "when": 1735525637133, - "tag": "0000_spicy_morgan_stark", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1735588850570, - "tag": "0001_silly_vanisher", + "when": 1735748192401, + "tag": "0000_lazy_scarecrow", "breakpoints": true } ] From 58eac9a6948a20242a16168df1b6388faa17df15 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 03:08:11 +0900 Subject: [PATCH 069/115] =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/BottomSheet.svelte | 17 ++++-- src/lib/components/Modal.svelte | 9 ++- src/lib/components/divs/AdaptiveDiv.svelte | 2 +- src/lib/components/divs/BottomDiv.svelte | 2 +- src/lib/components/divs/TitleDiv.svelte | 18 +++++- src/routes/(fullscreen)/+layout.svelte | 6 +- .../(fullscreen)/auth/login/+page.svelte | 38 ++++++------ .../(fullscreen)/client/pending/+page.svelte | 60 +++++++++---------- .../(fullscreen)/key/export/+page.svelte | 57 ++++++++---------- .../(fullscreen)/key/generate/+page.svelte | 52 ++++++++-------- src/routes/(main)/+layout.svelte | 7 ++- 11 files changed, 141 insertions(+), 127 deletions(-) diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte index 81e24e1..4accdf1 100644 --- a/src/lib/components/BottomSheet.svelte +++ b/src/lib/components/BottomSheet.svelte @@ -1,6 +1,7 @@ -
+
{@render children?.()}
diff --git a/src/lib/components/divs/BottomDiv.svelte b/src/lib/components/divs/BottomDiv.svelte index cfcfdd1..359c54a 100644 --- a/src/lib/components/divs/BottomDiv.svelte +++ b/src/lib/components/divs/BottomDiv.svelte @@ -2,6 +2,6 @@ let { children } = $props(); -
+
{@render children?.()}
diff --git a/src/lib/components/divs/TitleDiv.svelte b/src/lib/components/divs/TitleDiv.svelte index 5b225e6..fcf1141 100644 --- a/src/lib/components/divs/TitleDiv.svelte +++ b/src/lib/components/divs/TitleDiv.svelte @@ -1,7 +1,21 @@ -
+
+
+ {#if icon} + {@const Icon = icon} + + {/if} +
{@render children?.()}
diff --git a/src/routes/(fullscreen)/+layout.svelte b/src/routes/(fullscreen)/+layout.svelte index 4002fbe..cab9d42 100644 --- a/src/routes/(fullscreen)/+layout.svelte +++ b/src/routes/(fullscreen)/+layout.svelte @@ -4,8 +4,10 @@ let { children } = $props(); -
+
- {@render children()} +
+ {@render children()} +
diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 299a523..c86a483 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -55,23 +55,21 @@ 로그인 -
- -
-

환영합니다!

-

서비스를 이용하려면 로그인을 해야해요.

-
-
- - -
-
- -
- -
-
- 계정이 없어요 -
-
-
+ +
+

환영합니다!

+

서비스를 이용하려면 로그인을 해야해요.

+
+
+ + +
+
+ +
+ +
+
+ 계정이 없어요 +
+
diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index 3d000b2..c5cfd17 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -27,40 +27,38 @@ }); - + 승인을 기다리고 있어요. - + -
- -
-

승인을 기다리고 있어요.

-

- 회원님의 다른 디바이스에서 이 디바이스의 데이터 접근을 승인해야 서비스를 이용할 수 있어요. -

+ +
+

승인을 기다리고 있어요.

+

+ 회원님의 다른 디바이스에서 이 디바이스의 데이터 접근을 승인해야 서비스를 이용할 수 있어요. +

+
+
+
+ +

암호 키 지문

-
-
- -

암호 키 지문

-
-
-

- {#if !fingerprint} +

+

+ {#if !fingerprint} + 지문 생성하는 중... + {:else} + {#await fingerprint} 지문 생성하는 중... - {:else} - {#await fingerprint} - 지문 생성하는 중... - {:then fingerprint} - {fingerprint} - {/await} - {/if} -

-
-

- 암호 키 지문은 디바이스마다 다르게 생성돼요.
- 지문이 일치하는지 확인 후 승인해 주세요. + {:then fingerprint} + {fingerprint} + {/await} + {/if}

- -
+

+ 암호 키 지문은 디바이스마다 다르게 생성돼요.
+ 지문이 일치하는지 확인 후 승인해 주세요. +

+
+
diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index b45747c..6c767a4 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -2,7 +2,7 @@ import { saveAs } from "file-saver"; import { goto } from "$app/navigation"; import { Button, TextButton } from "$lib/components/buttons"; - import { BottomDiv } from "$lib/components/divs"; + import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { clientKeyStore } from "$lib/stores"; import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte"; import BeforeContinueModal from "./BeforeContinueModal.svelte"; @@ -84,40 +84,35 @@ }; - + 암호 키 생성하기 - + -
-
- -
-
-
-

암호 키를 파일로 내보낼까요?

-
-

- 모든 디바이스의 암호 키가 유실되면, 서버에 저장된 데이터를 영원히 복호화할 수 없게 돼요. -

-

만약의 상황을 위해 암호 키를 파일로 내보낼 수 있어요.

-
+ +
+

암호 키를 파일로 내보낼까요?

+
+

+ 모든 디바이스의 암호 키가 유실되면, 서버에 저장된 데이터를 영원히 복호화할 수 없게 돼요. +

+

만약의 상황을 위해 암호 키를 파일로 내보낼 수 있어요.

- -
- -
-
- { - isBeforeContinueModalOpen = true; - }} - > - 내보내지 않을래요 - -
-
-
+ + +
+ +
+
+ { + isBeforeContinueModalOpen = true; + }} + > + 내보내지 않을래요 + +
+
- + 암호 키 생성하기 - + -
- -
-

암호 키 생성하기

-

회원님의 디바이스 간의 안전한 데이터 동기화를 위해 암호 키를 생성해야 해요.

+ +
+

암호 키 생성하기

+

회원님의 디바이스 간의 안전한 데이터 동기화를 위해 암호 키를 생성해야 해요.

+
+
+
+ +

왜 암호 키가 필요한가요?

-
-
- -

왜 암호 키가 필요한가요?

-
-
- {#each orders as { title, description }, i} - - {/each} -
+
+ {#each orders as { title, description }, i} + + {/each}
- - -
- -
-
- 키를 갖고 있어요 -
-
-
+
+
+ +
+ +
+
+ 키를 갖고 있어요 +
+
diff --git a/src/routes/(main)/+layout.svelte b/src/routes/(main)/+layout.svelte index 3610ec7..0bebf88 100644 --- a/src/routes/(main)/+layout.svelte +++ b/src/routes/(main)/+layout.svelte @@ -1,12 +1,13 @@ -
-
+
+ {@render children()} -
+
From 45df24b416eb3259b0256f1ae02f3abae599902f Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 03:10:47 +0900 Subject: [PATCH 070/115] =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/(fullscreen)/+layout.svelte | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/routes/(fullscreen)/+layout.svelte b/src/routes/(fullscreen)/+layout.svelte index cab9d42..ab2b89f 100644 --- a/src/routes/(fullscreen)/+layout.svelte +++ b/src/routes/(fullscreen)/+layout.svelte @@ -4,10 +4,8 @@ let { children } = $props(); -
- -
- {@render children()} -
-
-
+ +
+ {@render children()} +
+
From b07d67b9580c53359e5fdb663b2493f742fb697c Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 04:44:02 +0900 Subject: [PATCH 071/115] =?UTF-8?q?/api/client/[id]/key=20Endpoint=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=EC=99=80=EC=9D=98=20Zod=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EA=B3=B5=EC=9C=A0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callAPI.ts | 15 ------ src/lib/hooks/callApi.ts | 41 ++++++++++++++++ src/lib/hooks/index.ts | 4 +- src/lib/server/schemas/auth.ts | 24 ++++++++++ src/lib/server/schemas/client.ts | 35 ++++++++++++++ src/lib/server/schemas/directory.ts | 27 +++++++++++ src/lib/server/schemas/index.ts | 4 ++ src/lib/server/schemas/mek.ts | 19 ++++++++ src/lib/server/services/client.ts | 10 ---- src/lib/server/services/mek.ts | 6 +-- src/lib/services/auth.ts | 11 +++-- src/lib/services/key.ts | 47 +++++++------------ .../(fullscreen)/auth/login/+page.svelte | 2 +- src/routes/(fullscreen)/auth/login/service.ts | 3 +- src/routes/(fullscreen)/key/export/service.ts | 24 ++++------ src/routes/api/auth/login/+server.ts | 9 +--- src/routes/api/auth/upgradeToken/+server.ts | 11 ++--- .../api/auth/upgradeToken/verify/+server.ts | 9 +--- src/routes/api/client/[id]/key/+server.ts | 20 -------- src/routes/api/client/list/+server.ts | 3 +- src/routes/api/client/register/+server.ts | 11 ++--- .../api/client/register/verify/+server.ts | 9 +--- src/routes/api/client/status/+server.ts | 3 +- src/routes/api/directory/[id]/+server.ts | 27 ++++++----- src/routes/api/directory/create/+server.ts | 11 +---- src/routes/api/mek/list/+server.ts | 16 +++++-- .../api/mek/register/initial/+server.ts | 9 ++-- 27 files changed, 241 insertions(+), 169 deletions(-) delete mode 100644 src/lib/hooks/callAPI.ts create mode 100644 src/lib/hooks/callApi.ts create mode 100644 src/lib/server/schemas/auth.ts create mode 100644 src/lib/server/schemas/client.ts create mode 100644 src/lib/server/schemas/directory.ts create mode 100644 src/lib/server/schemas/index.ts create mode 100644 src/lib/server/schemas/mek.ts delete mode 100644 src/routes/api/client/[id]/key/+server.ts diff --git a/src/lib/hooks/callAPI.ts b/src/lib/hooks/callAPI.ts deleted file mode 100644 index 9edc9e4..0000000 --- a/src/lib/hooks/callAPI.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const refreshToken = async () => { - return await fetch("/api/auth/refreshToken", { method: "POST" }); -}; - -export const callAPI = async (input: RequestInfo, init?: RequestInit) => { - let res = await fetch(input, init); - if (res.status === 401) { - res = await refreshToken(); - if (!res.ok) { - return res; - } - res = await fetch(input, init); - } - return res; -}; diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts new file mode 100644 index 0000000..7189735 --- /dev/null +++ b/src/lib/hooks/callApi.ts @@ -0,0 +1,41 @@ +import { signRequest } from "$lib/modules/crypto"; + +export const refreshToken = async () => { + return await fetch("/api/auth/refreshToken", { method: "POST" }); +}; + +const callApi = async (input: RequestInfo, init?: RequestInit) => { + let res = await fetch(input, init); + if (res.status === 401) { + res = await refreshToken(); + if (!res.ok) { + return res; + } + res = await fetch(input, init); + } + return res; +}; + +export const callGetApi = async (input: RequestInfo) => { + return await callApi(input); +}; + +export const callPostApi = async (input: RequestInfo, payload: T) => { + return await callApi(input, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); +}; + +export const callSignedPostApi = async (input: RequestInfo, payload: T, signKey: CryptoKey) => { + return await callApi(input, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: await signRequest(payload, signKey), + }); +}; diff --git a/src/lib/hooks/index.ts b/src/lib/hooks/index.ts index e2f0392..e3b8dde 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/hooks/index.ts @@ -1,2 +1,2 @@ -export { callAPI } from "./callAPI"; -export { gotoStateful } from "./gotoStateful"; +export * from "./callApi"; +export * from "./gotoStateful"; diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts new file mode 100644 index 0000000..220b029 --- /dev/null +++ b/src/lib/server/schemas/auth.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const loginRequest = z.object({ + email: z.string().email().nonempty(), + password: z.string().trim().nonempty(), +}); +export type LoginRequest = z.infer; + +export const tokenUpgradeRequest = z.object({ + encPubKey: z.string().base64().nonempty(), + sigPubKey: z.string().base64().nonempty(), +}); +export type TokenUpgradeRequest = z.infer; + +export const tokenUpgradeResponse = z.object({ + challenge: z.string().base64().nonempty(), +}); +export type TokenUpgradeResponse = z.infer; + +export const tokenUpgradeVerifyRequest = z.object({ + answer: z.string().base64().nonempty(), + sigAnswer: z.string().base64().nonempty(), +}); +export type TokenUpgradeVerifyRequest = z.infer; diff --git a/src/lib/server/schemas/client.ts b/src/lib/server/schemas/client.ts new file mode 100644 index 0000000..8bda49e --- /dev/null +++ b/src/lib/server/schemas/client.ts @@ -0,0 +1,35 @@ +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.infer; + +export const clientRegisterRequest = z.object({ + encPubKey: z.string().base64().nonempty(), + sigPubKey: z.string().base64().nonempty(), +}); +export type ClientRegisterRequest = z.infer; + +export const clientRegisterResponse = z.object({ + challenge: z.string().base64().nonempty(), +}); +export type ClientRegisterResponse = z.infer; + +export const clientRegisterVerifyRequest = z.object({ + answer: z.string().base64().nonempty(), + sigAnswer: z.string().base64().nonempty(), +}); +export type ClientRegisterVerifyRequest = z.infer; + +export const clientStatusResponse = z.object({ + id: z.number().int().positive(), + state: z.enum(["pending", "active"]), + isInitialMekNeeded: z.boolean(), +}); +export type ClientStatusResponse = z.infer; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts new file mode 100644 index 0000000..1c2f6c1 --- /dev/null +++ b/src/lib/server/schemas/directory.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +export const directroyEntriesResponse = z.object({ + metadata: z + .object({ + createdAt: z.date(), + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekIv: z.string().base64().nonempty(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }) + .optional(), + subDirectories: z.number().int().positive().array(), + files: z.number().int().positive().array(), +}); +export type DirectroyEntriesResponse = z.infer; + +export const directoryCreateRequest = z.object({ + parentId: z.union([z.enum(["root"]), z.number().int().positive()]), + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekIv: z.string().base64().nonempty(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), +}); +export type DirectoryCreateRequest = z.infer; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts new file mode 100644 index 0000000..0328467 --- /dev/null +++ b/src/lib/server/schemas/index.ts @@ -0,0 +1,4 @@ +export * from "./auth"; +export * from "./client"; +export * from "./directory"; +export * from "./mek"; diff --git a/src/lib/server/schemas/mek.ts b/src/lib/server/schemas/mek.ts new file mode 100644 index 0000000..e79f810 --- /dev/null +++ b/src/lib/server/schemas/mek.ts @@ -0,0 +1,19 @@ +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.infer; + +export const initialMasterKeyRegisterRequest = z.object({ + mek: z.string().base64().nonempty(), + mekSig: z.string().base64().nonempty(), +}); +export type InitialMasterKeyRegisterRequest = z.infer; diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index 8893973..004ef1a 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -8,7 +8,6 @@ import { createUserClient, getAllUserClients, getUserClient, - getUserClientWithDetails, setUserClientStateToPending, registerUserClientChallenge, getUserClientChallenge, @@ -18,15 +17,6 @@ import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/mo import { isInitialMekNeeded } from "$lib/server/modules/mek"; import env from "$lib/server/loadenv"; -export const getUserClientEncPubKey = async (userId: number, clientId: number) => { - const userClient = await getUserClientWithDetails(userId, clientId); - if (!userClient || userClient.user_client.state === "challenging") { - error(400, "Invalid client ID"); - } - - return { encPubKey: userClient.client.encPubKey }; -}; - export const getUserClientList = async (userId: number) => { const userClients = await getAllUserClients(userId); return { diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts index 9fc0452..94babfe 100644 --- a/src/lib/server/services/mek.ts +++ b/src/lib/server/services/mek.ts @@ -6,11 +6,11 @@ import { isInitialMekNeeded, verifyClientEncMekSig } from "$lib/server/modules/m export const getClientMekList = async (userId: number, clientId: number) => { const clientMeks = await getAllValidClientMeks(userId, clientId); return { - meks: clientMeks.map((clientMek) => ({ + encMeks: clientMeks.map((clientMek) => ({ version: clientMek.master_encryption_key.version, state: clientMek.master_encryption_key.state, - mek: clientMek.client_master_encryption_key.encMek, - mekSig: clientMek.client_master_encryption_key.encMekSig, + encMek: clientMek.client_master_encryption_key.encMek, + encMekSig: clientMek.client_master_encryption_key.encMekSig, })), }; }; diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 865b056..7a0936b 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -4,6 +4,11 @@ import { decryptRSACiphertext, signRSAMessage, } from "$lib/modules/crypto"; +import type { + TokenUpgradeRequest, + TokenUpgradeResponse, + TokenUpgradeVerifyRequest, +} from "$lib/server/schemas"; export const requestTokenUpgrade = async ( encryptKeyBase64: string, @@ -19,11 +24,11 @@ export const requestTokenUpgrade = async ( body: JSON.stringify({ encPubKey: encryptKeyBase64, sigPubKey: verifyKeyBase64, - }), + } satisfies TokenUpgradeRequest), }); if (!res.ok) return false; - const { challenge } = await res.json(); + const { challenge }: TokenUpgradeResponse = await res.json(); const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey); const sigAnswer = await signRSAMessage(answer, signKey); @@ -35,7 +40,7 @@ export const requestTokenUpgrade = async ( body: JSON.stringify({ answer: encodeToBase64(answer), sigAnswer: encodeToBase64(sigAnswer), - }), + } satisfies TokenUpgradeVerifyRequest), }); return res.ok; }; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 2eba920..2a38a5a 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -1,4 +1,4 @@ -import { callAPI } from "$lib/hooks"; +import { callGetApi, callPostApi } from "$lib/hooks"; import { storeMasterKeys } from "$lib/indexedDB"; import { encodeToBase64, @@ -9,6 +9,12 @@ import { unwrapAESKeyUsingRSA, verifyMasterKeyWrappedSig, } from "$lib/modules/crypto"; +import type { + ClientRegisterRequest, + ClientRegisterResponse, + ClientRegisterVerifyRequest, + MasterKeyListResponse, +} from "$lib/server/schemas"; import { masterKeyStore } from "$lib/stores"; export const requestClientRegistration = async ( @@ -17,49 +23,28 @@ export const requestClientRegistration = async ( 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, - }), + let res = await callPostApi("/api/client/register", { + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, }); if (!res.ok) return false; - const { challenge } = await res.json(); + const { challenge }: ClientRegisterResponse = await res.json(); const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey); const sigAnswer = await signRSAMessage(answer, signKey); - res = await callAPI("/api/client/register/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - answer: encodeToBase64(answer), - sigAnswer: encodeToBase64(sigAnswer), - }), + res = await callPostApi("/api/client/register/verify", { + answer: encodeToBase64(answer), + sigAnswer: encodeToBase64(sigAnswer), }); return res.ok; }; export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: CryptoKey) => { - const res = await callAPI("/api/mek/list", { method: "GET" }); + const res = await callGetApi("/api/mek/list"); if (!res.ok) return false; - const data = await res.json(); - const { meks: masterKeysWrapped } = data as { - meks: { - version: number; - state: "active" | "retired"; - mek: string; - mekSig: string; - }[]; - }; - + const { meks: masterKeysWrapped }: MasterKeyListResponse = await res.json(); const masterKeys = await Promise.all( masterKeysWrapped.map( async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => ({ diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index c86a483..9ab6c0c 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -4,7 +4,7 @@ import { Button, TextButton } from "$lib/components/buttons"; import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { TextInput } from "$lib/components/inputs"; - import { refreshToken } from "$lib/hooks/callAPI"; + import { refreshToken } from "$lib/hooks/callApi"; import { clientKeyStore, masterKeyStore } from "$lib/stores"; import { requestLogin, requestTokenUpgrade, requestMasterKeyDownload } from "./service"; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index e9002af..0227edc 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,4 +1,5 @@ import { exportRSAKeyToBase64 } from "$lib/modules/crypto"; +import type { LoginRequest } from "$lib/server/schemas"; import { requestTokenUpgrade as requestTokenUpgradeInternal } from "$lib/services/auth"; import { requestClientRegistration } from "$lib/services/key"; import type { ClientKeys } from "$lib/stores"; @@ -11,7 +12,7 @@ export const requestLogin = async (email: string, password: string) => { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ email, password }), + body: JSON.stringify({ email, password } satisfies LoginRequest), }); return res.ok; }; diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index cc8e7be..fd1fadd 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,6 +1,7 @@ -import { callAPI } from "$lib/hooks"; +import { callSignedPostApi } from "$lib/hooks"; import { storeClientKey } from "$lib/indexedDB"; -import { encodeToBase64, signRequest, signMasterKeyWrapped } from "$lib/modules/crypto"; +import { encodeToBase64, signMasterKeyWrapped } from "$lib/modules/crypto"; +import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; import type { ClientKeys } from "$lib/stores"; export { requestTokenUpgrade } from "$lib/services/auth"; @@ -45,18 +46,13 @@ export const requestInitialMasterKeyRegistration = async ( masterKeyWrapped: ArrayBuffer, signKey: CryptoKey, ) => { - const res = await callAPI("/api/mek/register/initial", { - method: "POST", - headers: { - "Content-Type": "application/json", + const res = await callSignedPostApi( + "/api/mek/register/initial", + { + mek: encodeToBase64(masterKeyWrapped), + mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), }, - body: await signRequest( - { - mek: encodeToBase64(masterKeyWrapped), - mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), - }, - signKey, - ), - }); + signKey, + ); return res.ok || res.status === 409; }; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index ec4f254..9f652df 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -1,17 +1,12 @@ import { error, text } from "@sveltejs/kit"; import ms from "ms"; -import { z } from "zod"; import env from "$lib/server/loadenv"; +import { loginRequest } from "$lib/server/schemas/auth"; import { login } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { - const zodRes = z - .object({ - email: z.string().email().nonempty(), - password: z.string().trim().nonempty(), - }) - .safeParse(await request.json()); + const zodRes = loginRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { email, password } = zodRes.data; diff --git a/src/routes/api/auth/upgradeToken/+server.ts b/src/routes/api/auth/upgradeToken/+server.ts index 90c5e60..99b987d 100644 --- a/src/routes/api/auth/upgradeToken/+server.ts +++ b/src/routes/api/auth/upgradeToken/+server.ts @@ -1,5 +1,5 @@ import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; +import { tokenUpgradeRequest, tokenUpgradeResponse } from "$lib/server/schemas/auth"; import { createTokenUpgradeChallenge } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; @@ -7,12 +7,7 @@ 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({ - encPubKey: z.string().base64().nonempty(), - sigPubKey: z.string().base64().nonempty(), - }) - .safeParse(await request.json()); + const zodRes = tokenUpgradeRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { encPubKey, sigPubKey } = zodRes.data; @@ -22,5 +17,5 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress encPubKey, sigPubKey, ); - return json({ challenge }); + return json(tokenUpgradeResponse.parse({ challenge })); }; diff --git a/src/routes/api/auth/upgradeToken/verify/+server.ts b/src/routes/api/auth/upgradeToken/verify/+server.ts index ca72695..84f8e82 100644 --- a/src/routes/api/auth/upgradeToken/verify/+server.ts +++ b/src/routes/api/auth/upgradeToken/verify/+server.ts @@ -1,5 +1,5 @@ import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; +import { tokenUpgradeVerifyRequest } from "$lib/server/schemas/auth"; import { upgradeToken } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; @@ -7,12 +7,7 @@ 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()); + const zodRes = tokenUpgradeVerifyRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { answer, sigAnswer } = zodRes.data; diff --git a/src/routes/api/client/[id]/key/+server.ts b/src/routes/api/client/[id]/key/+server.ts deleted file mode 100644 index 8e031f9..0000000 --- a/src/routes/api/client/[id]/key/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { getUserClientEncPubKey } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ cookies, params }) => { - const { userId } = await authorize(cookies, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { encPubKey } = await getUserClientEncPubKey(userId, id); - return json({ encPubKey }); -}; diff --git a/src/routes/api/client/list/+server.ts b/src/routes/api/client/list/+server.ts index 7769658..f16124c 100644 --- a/src/routes/api/client/list/+server.ts +++ b/src/routes/api/client/list/+server.ts @@ -1,10 +1,11 @@ import { json } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; +import { clientListResponse } from "$lib/server/schemas/client"; import { getUserClientList } from "$lib/server/services/client"; import type { RequestHandler } from "@sveltejs/kit"; export const GET: RequestHandler = async ({ cookies }) => { const { userId } = authenticate(cookies); const { userClients } = await getUserClientList(userId); - return json({ clients: userClients }); + return json(clientListResponse.parse({ clients: userClients })); }; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts index 361f38f..474995c 100644 --- a/src/routes/api/client/register/+server.ts +++ b/src/routes/api/client/register/+server.ts @@ -1,6 +1,6 @@ import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; import { authenticate } from "$lib/server/modules/auth"; +import { clientRegisterRequest, clientRegisterResponse } from "$lib/server/schemas/client"; import { registerUserClient } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; @@ -10,15 +10,10 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress error(403, "Forbidden"); } - const zodRes = z - .object({ - encPubKey: z.string().base64().nonempty(), - sigPubKey: z.string().base64().nonempty(), - }) - .safeParse(await request.json()); + const zodRes = clientRegisterRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { encPubKey, sigPubKey } = zodRes.data; const { challenge } = await registerUserClient(userId, getClientAddress(), encPubKey, sigPubKey); - return json({ challenge }); + return json(clientRegisterResponse.parse({ challenge })); }; diff --git a/src/routes/api/client/register/verify/+server.ts b/src/routes/api/client/register/verify/+server.ts index 9a34558..a2f2f9c 100644 --- a/src/routes/api/client/register/verify/+server.ts +++ b/src/routes/api/client/register/verify/+server.ts @@ -1,6 +1,6 @@ import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; import { authenticate } from "$lib/server/modules/auth"; +import { clientRegisterVerifyRequest } from "$lib/server/schemas/client"; import { verifyUserClient } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; @@ -10,12 +10,7 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress error(403, "Forbidden"); } - const zodRes = z - .object({ - answer: z.string().base64().nonempty(), - sigAnswer: z.string().base64().nonempty(), - }) - .safeParse(await request.json()); + const zodRes = clientRegisterVerifyRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); const { answer, sigAnswer } = zodRes.data; diff --git a/src/routes/api/client/status/+server.ts b/src/routes/api/client/status/+server.ts index dcd12a6..8bd7616 100644 --- a/src/routes/api/client/status/+server.ts +++ b/src/routes/api/client/status/+server.ts @@ -1,5 +1,6 @@ import { error, json } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; +import { clientStatusResponse } from "$lib/server/schemas/client"; import { getUserClientStatus } from "$lib/server/services/client"; import type { RequestHandler } from "@sveltejs/kit"; @@ -10,5 +11,5 @@ export const GET: RequestHandler = async ({ cookies }) => { } const { state, isInitialMekNeeded } = await getUserClientStatus(userId, clientId); - return json({ id: clientId, state, isInitialMekNeeded }); + return json(clientStatusResponse.parse({ id: clientId, state, isInitialMekNeeded })); }; diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts index 6decc3b..2a68763 100644 --- a/src/routes/api/directory/[id]/+server.ts +++ b/src/routes/api/directory/[id]/+server.ts @@ -1,6 +1,7 @@ import { error, json } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; +import { directroyEntriesResponse } from "$lib/server/schemas/directory"; import { getDirectroyInformation } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; @@ -16,16 +17,18 @@ export const GET: RequestHandler = async ({ cookies, params }) => { const { id } = zodRes.data; const { metadata, directories, files } = await getDirectroyInformation(userId, id); - return json({ - metadata: metadata && { - createdAt: metadata.createdAt, - mekVersion: metadata.mekVersion, - dek: metadata.encDek.ciphertext, - dekIv: metadata.encDek.iv, - name: metadata.encName.ciphertext, - nameIv: metadata.encName.iv, - }, - subDirectories: directories, - files, - }); + return json( + directroyEntriesResponse.parse({ + metadata: metadata && { + createdAt: metadata.createdAt, + mekVersion: metadata.mekVersion, + dek: metadata.encDek.ciphertext, + dekIv: metadata.encDek.iv, + name: metadata.encName.ciphertext, + nameIv: metadata.encName.iv, + }, + subDirectories: directories, + files, + }), + ); }; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index e51ec80..86db802 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -1,7 +1,7 @@ import { text } from "@sveltejs/kit"; -import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; import { parseSignedRequest } from "$lib/server/modules/crypto"; +import { directoryCreateRequest } from "$lib/server/schemas/directory"; import { createDirectory } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; @@ -10,14 +10,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { const { parentId, mekVersion, dek, dekIv, name, nameIv } = await parseSignedRequest( clientId, await request.json(), - z.object({ - parentId: z.union([z.enum(["root"]), z.number().int().positive()]), - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekIv: z.string().base64().nonempty(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - }), + directoryCreateRequest, ); await createDirectory({ diff --git a/src/routes/api/mek/list/+server.ts b/src/routes/api/mek/list/+server.ts index 4801df1..6fcc9f6 100644 --- a/src/routes/api/mek/list/+server.ts +++ b/src/routes/api/mek/list/+server.ts @@ -1,10 +1,20 @@ import { json } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; +import { masterKeyListResponse } from "$lib/server/schemas/mek"; import { getClientMekList } from "$lib/server/services/mek"; -import type { RequestHandler } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ cookies }) => { const { userId, clientId } = await authorize(cookies, "activeClient"); - const { meks } = await getClientMekList(userId, clientId); - return json({ meks }); + const { encMeks } = await getClientMekList(userId, clientId); + return json( + masterKeyListResponse.parse({ + meks: encMeks.map(({ version, state, encMek, encMekSig }) => ({ + version, + state, + mek: encMek, + mekSig: encMekSig, + })), + }), + ); }; diff --git a/src/routes/api/mek/register/initial/+server.ts b/src/routes/api/mek/register/initial/+server.ts index a5aa6d9..be1c8ee 100644 --- a/src/routes/api/mek/register/initial/+server.ts +++ b/src/routes/api/mek/register/initial/+server.ts @@ -1,9 +1,9 @@ import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; import { authenticate } from "$lib/server/modules/auth"; import { parseSignedRequest } from "$lib/server/modules/crypto"; +import { initialMasterKeyRegisterRequest } from "$lib/server/schemas/mek"; import { registerInitialActiveMek } from "$lib/server/services/mek"; -import type { RequestHandler } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { const { userId, clientId } = authenticate(cookies); @@ -14,10 +14,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { const { mek, mekSig } = await parseSignedRequest( clientId, await request.json(), - z.object({ - mek: z.string().base64().nonempty(), - mekSig: z.string().base64().nonempty(), - }), + initialMasterKeyRegisterRequest, ); await registerInitialActiveMek(userId, clientId, mek, mekSig); From baf48579b81f98fa8388156899e079d98cf66aa7 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 06:41:01 +0900 Subject: [PATCH 072/115] =?UTF-8?q?DEK=EB=A5=BC=20AES-256-KW=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=B4=20=EC=95=94=ED=98=B8=ED=99=94?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/file.ts | 3 +-- src/lib/server/db/schema/file.ts | 8 ++++---- src/lib/server/schemas/directory.ts | 6 ++---- src/lib/server/services/client.ts | 4 ++-- src/lib/server/services/mek.ts | 2 +- src/routes/api/auth/upgradeToken/+server.ts | 8 ++++++-- src/routes/api/client/list/+server.ts | 4 ++-- src/routes/api/client/register/+server.ts | 8 ++++++-- src/routes/api/client/status/+server.ts | 10 ++++++++-- src/routes/api/directory/[id]/+server.ts | 9 ++++----- src/routes/api/directory/create/+server.ts | 3 +-- src/routes/api/mek/list/+server.ts | 4 ++-- 12 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index a693f48..e5285dc 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -9,7 +9,6 @@ export interface NewDirectroyParams { parentId: DirectroyId; mekVersion: number; encDek: string; - encDekIv: string; encName: string; encNameIv: string; } @@ -30,7 +29,7 @@ export const registerNewDirectory = async (params: NewDirectroyParams) => { parentId: params.parentId === "root" ? null : params.parentId, userId: params.userId, mekVersion: params.mekVersion, - encDek: { ciphertext: params.encDek, iv: params.encDekIv }, + encDek: params.encDek, encryptedAt: now, encName: { ciphertext: params.encName, iv: params.encNameIv }, }); diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index f56e294..b5c41fd 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -4,8 +4,8 @@ import { user } from "./user"; const ciphertext = (name: string) => text(name, { mode: "json" }).$type<{ - ciphertext: string; - iv: string; + ciphertext: string; // Base64 + iv: string; // Base64 }>(); export const directory = sqliteTable( @@ -18,7 +18,7 @@ export const directory = sqliteTable( .notNull() .references(() => user.id), mekVersion: integer("master_encryption_key_version").notNull(), - encDek: ciphertext("encrypted_data_encryption_key").notNull().unique(), + encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 encryptedAt: integer("encrypted_at", { mode: "timestamp_ms" }).notNull(), encName: ciphertext("encrypted_name").notNull(), }, @@ -45,7 +45,7 @@ export const file = sqliteTable( .notNull() .references(() => user.id), mekVersion: integer("master_encryption_key_version").notNull(), - encDek: ciphertext("encrypted_data_encryption_key").notNull().unique(), + encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 encryptedAt: integer("encrypted_at", { mode: "timestamp_ms" }).notNull(), encName: ciphertext("encrypted_name").notNull(), }, diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index 1c2f6c1..b4594c9 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,12 +1,11 @@ import { z } from "zod"; -export const directroyEntriesResponse = z.object({ +export const directroyInfoResponse = z.object({ metadata: z .object({ createdAt: z.date(), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), - dekIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), }) @@ -14,13 +13,12 @@ export const directroyEntriesResponse = z.object({ subDirectories: z.number().int().positive().array(), files: z.number().int().positive().array(), }); -export type DirectroyEntriesResponse = z.infer; +export type DirectroyInfoResponse = z.infer; export const directoryCreateRequest = z.object({ parentId: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), - dekIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), }); diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index 004ef1a..9291d6b 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -22,7 +22,7 @@ export const getUserClientList = async (userId: number) => { return { userClients: userClients.map(({ clientId, state }) => ({ id: clientId, - state, + state: state as "pending" | "active", })), }; }; @@ -83,7 +83,7 @@ export const getUserClientStatus = async (userId: number, clientId: number) => { } return { - state: userClient.state, + state: userClient.state as "pending" | "active", isInitialMekNeeded: await isInitialMekNeeded(userId), }; }; diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts index 94babfe..95caef9 100644 --- a/src/lib/server/services/mek.ts +++ b/src/lib/server/services/mek.ts @@ -8,7 +8,7 @@ export const getClientMekList = async (userId: number, clientId: number) => { return { encMeks: clientMeks.map((clientMek) => ({ version: clientMek.master_encryption_key.version, - state: clientMek.master_encryption_key.state, + state: clientMek.master_encryption_key.state as "active" | "retired", encMek: clientMek.client_master_encryption_key.encMek, encMekSig: clientMek.client_master_encryption_key.encMekSig, })), diff --git a/src/routes/api/auth/upgradeToken/+server.ts b/src/routes/api/auth/upgradeToken/+server.ts index 99b987d..0436f22 100644 --- a/src/routes/api/auth/upgradeToken/+server.ts +++ b/src/routes/api/auth/upgradeToken/+server.ts @@ -1,5 +1,9 @@ import { error, json } from "@sveltejs/kit"; -import { tokenUpgradeRequest, tokenUpgradeResponse } from "$lib/server/schemas/auth"; +import { + tokenUpgradeRequest, + tokenUpgradeResponse, + type TokenUpgradeResponse, +} from "$lib/server/schemas/auth"; import { createTokenUpgradeChallenge } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; @@ -17,5 +21,5 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress encPubKey, sigPubKey, ); - return json(tokenUpgradeResponse.parse({ challenge })); + return json(tokenUpgradeResponse.parse({ challenge } satisfies TokenUpgradeResponse)); }; diff --git a/src/routes/api/client/list/+server.ts b/src/routes/api/client/list/+server.ts index f16124c..72f09d8 100644 --- a/src/routes/api/client/list/+server.ts +++ b/src/routes/api/client/list/+server.ts @@ -1,11 +1,11 @@ import { json } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; -import { clientListResponse } from "$lib/server/schemas/client"; +import { clientListResponse, type ClientListResponse } from "$lib/server/schemas/client"; import { getUserClientList } from "$lib/server/services/client"; import type { RequestHandler } from "@sveltejs/kit"; export const GET: RequestHandler = async ({ cookies }) => { const { userId } = authenticate(cookies); const { userClients } = await getUserClientList(userId); - return json(clientListResponse.parse({ clients: userClients })); + return json(clientListResponse.parse({ clients: userClients } satisfies ClientListResponse)); }; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts index 474995c..3a9f884 100644 --- a/src/routes/api/client/register/+server.ts +++ b/src/routes/api/client/register/+server.ts @@ -1,6 +1,10 @@ import { error, json } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; -import { clientRegisterRequest, clientRegisterResponse } from "$lib/server/schemas/client"; +import { + clientRegisterRequest, + clientRegisterResponse, + type ClientRegisterResponse, +} from "$lib/server/schemas/client"; import { registerUserClient } from "$lib/server/services/client"; import type { RequestHandler } from "./$types"; @@ -15,5 +19,5 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress const { encPubKey, sigPubKey } = zodRes.data; const { challenge } = await registerUserClient(userId, getClientAddress(), encPubKey, sigPubKey); - return json(clientRegisterResponse.parse({ challenge })); + return json(clientRegisterResponse.parse({ challenge } satisfies ClientRegisterResponse)); }; diff --git a/src/routes/api/client/status/+server.ts b/src/routes/api/client/status/+server.ts index 8bd7616..1eed893 100644 --- a/src/routes/api/client/status/+server.ts +++ b/src/routes/api/client/status/+server.ts @@ -1,6 +1,6 @@ import { error, json } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; -import { clientStatusResponse } from "$lib/server/schemas/client"; +import { clientStatusResponse, type ClientStatusResponse } from "$lib/server/schemas/client"; import { getUserClientStatus } from "$lib/server/services/client"; import type { RequestHandler } from "@sveltejs/kit"; @@ -11,5 +11,11 @@ export const GET: RequestHandler = async ({ cookies }) => { } const { state, isInitialMekNeeded } = await getUserClientStatus(userId, clientId); - return json(clientStatusResponse.parse({ id: clientId, state, isInitialMekNeeded })); + return json( + clientStatusResponse.parse({ + id: clientId, + state, + isInitialMekNeeded, + } satisfies ClientStatusResponse), + ); }; diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts index 2a68763..361d4dd 100644 --- a/src/routes/api/directory/[id]/+server.ts +++ b/src/routes/api/directory/[id]/+server.ts @@ -1,7 +1,7 @@ import { error, json } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; -import { directroyEntriesResponse } from "$lib/server/schemas/directory"; +import { directroyInfoResponse, type DirectroyInfoResponse } from "$lib/server/schemas/directory"; import { getDirectroyInformation } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; @@ -18,17 +18,16 @@ export const GET: RequestHandler = async ({ cookies, params }) => { const { metadata, directories, files } = await getDirectroyInformation(userId, id); return json( - directroyEntriesResponse.parse({ + directroyInfoResponse.parse({ metadata: metadata && { createdAt: metadata.createdAt, mekVersion: metadata.mekVersion, - dek: metadata.encDek.ciphertext, - dekIv: metadata.encDek.iv, + dek: metadata.encDek, name: metadata.encName.ciphertext, nameIv: metadata.encName.iv, }, subDirectories: directories, files, - }), + } satisfies DirectroyInfoResponse), ); }; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index 86db802..fd7ae7d 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -7,7 +7,7 @@ import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { const { userId, clientId } = await authorize(cookies, "activeClient"); - const { parentId, mekVersion, dek, dekIv, name, nameIv } = await parseSignedRequest( + const { parentId, mekVersion, dek, name, nameIv } = await parseSignedRequest( clientId, await request.json(), directoryCreateRequest, @@ -18,7 +18,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => { parentId, mekVersion, encDek: dek, - encDekIv: dekIv, encName: name, encNameIv: nameIv, }); diff --git a/src/routes/api/mek/list/+server.ts b/src/routes/api/mek/list/+server.ts index 6fcc9f6..3effea3 100644 --- a/src/routes/api/mek/list/+server.ts +++ b/src/routes/api/mek/list/+server.ts @@ -1,6 +1,6 @@ import { json } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; -import { masterKeyListResponse } from "$lib/server/schemas/mek"; +import { masterKeyListResponse, type MasterKeyListResponse } from "$lib/server/schemas/mek"; import { getClientMekList } from "$lib/server/services/mek"; import type { RequestHandler } from "./$types"; @@ -15,6 +15,6 @@ export const GET: RequestHandler = async ({ cookies }) => { mek: encMek, mekSig: encMekSig, })), - }), + } satisfies MasterKeyListResponse), ); }; From 31081e51910863ed7c64d47c0a947cb6d5678493 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 08:49:51 +0900 Subject: [PATCH 073/115] =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=84=B0=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/BottomSheet.svelte | 4 +- src/lib/components/Modal.svelte | 22 +++-- src/lib/components/buttons/Button.svelte | 4 +- src/lib/components/buttons/EntryButton.svelte | 30 ++++++ .../components/buttons/FloatingButton.svelte | 22 +++++ src/lib/components/buttons/index.ts | 2 + src/lib/components/divs/AdaptiveDiv.svelte | 2 +- src/lib/components/divs/TitleDiv.svelte | 5 +- src/lib/components/inputs/TextInput.svelte | 2 +- src/lib/modules/crypto.ts | 66 +++++++++++-- src/lib/services/key.ts | 11 ++- src/lib/stores/key.ts | 3 +- .../(fullscreen)/client/pending/+page.svelte | 2 +- .../(fullscreen)/key/generate/service.ts | 10 +- .../(main)/directory/[[id]]/+page.server.ts | 35 +++++++ .../(main)/directory/[[id]]/+page.svelte | 94 ++++++++++++++++++- .../directory/[[id]]/CreateBottomSheet.svelte | 32 +++++++ .../[[id]]/CreateDirectoryModal.svelte | 32 +++++++ .../directory/[[id]]/DirectoryEntry.svelte | 12 +++ src/routes/(main)/directory/[[id]]/service.ts | 49 ++++++++++ src/routes/services.ts | 2 +- 21 files changed, 403 insertions(+), 38 deletions(-) create mode 100644 src/lib/components/buttons/EntryButton.svelte create mode 100644 src/lib/components/buttons/FloatingButton.svelte create mode 100644 src/routes/(main)/directory/[[id]]/+page.server.ts create mode 100644 src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte create mode 100644 src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte create mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte create mode 100644 src/routes/(main)/directory/[[id]]/service.ts diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte index 4accdf1..2bf18c5 100644 --- a/src/lib/components/BottomSheet.svelte +++ b/src/lib/components/BottomSheet.svelte @@ -18,10 +18,10 @@ onclick={() => { isOpen = false; }} - class="fixed inset-0 flex items-end justify-center" + class="fixed inset-0 z-10 flex items-end justify-center" >
-
+
e.stopPropagation()} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 46c9e73..a83e4a1 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -5,25 +5,33 @@ interface Props { children: Snippet; + onClose?: () => void; isOpen: boolean; } - let { children, isOpen = $bindable() }: Props = $props(); + let { children, onClose, isOpen = $bindable() }: Props = $props(); + + const closeModal = $derived( + onClose || + (() => { + isOpen = false; + }), + ); {#if isOpen}
{ - isOpen = false; - }} - class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2" + onclick={closeModal} + class="fixed inset-0 z-10 bg-black bg-opacity-50 px-2" transition:fade={{ duration: 100 }} > -
e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4"> - {@render children?.()} +
+
e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4"> + {@render children?.()} +
diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte index 484a2ab..692d4a1 100644 --- a/src/lib/components/buttons/Button.svelte +++ b/src/lib/components/buttons/Button.svelte @@ -9,13 +9,13 @@ let { children, color = "primary", onclick }: Props = $props(); - let bgColorStyle = $derived( + const bgColorStyle = $derived( { primary: "bg-primary-600 active:bg-primary-500", gray: "bg-gray-300 active:bg-gray-400", }[color], ); - let fontColorStyle = $derived( + const fontColorStyle = $derived( { primary: "text-white", gray: "text-gray-800", diff --git a/src/lib/components/buttons/EntryButton.svelte b/src/lib/components/buttons/EntryButton.svelte new file mode 100644 index 0000000..b370252 --- /dev/null +++ b/src/lib/components/buttons/EntryButton.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/components/buttons/FloatingButton.svelte b/src/lib/components/buttons/FloatingButton.svelte new file mode 100644 index 0000000..823af8d --- /dev/null +++ b/src/lib/components/buttons/FloatingButton.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/buttons/index.ts b/src/lib/components/buttons/index.ts index 0b93571..1765400 100644 --- a/src/lib/components/buttons/index.ts +++ b/src/lib/components/buttons/index.ts @@ -1,2 +1,4 @@ export { default as Button } from "./Button.svelte"; +export { default as EntryButton } from "./EntryButton.svelte"; +export { default as FloatingButton } from "./FloatingButton.svelte"; export { default as TextButton } from "./TextButton.svelte"; diff --git a/src/lib/components/divs/AdaptiveDiv.svelte b/src/lib/components/divs/AdaptiveDiv.svelte index 4e15d6b..ee845cc 100644 --- a/src/lib/components/divs/AdaptiveDiv.svelte +++ b/src/lib/components/divs/AdaptiveDiv.svelte @@ -2,6 +2,6 @@ let { children } = $props(); -
+
{@render children?.()}
diff --git a/src/lib/components/divs/TitleDiv.svelte b/src/lib/components/divs/TitleDiv.svelte index fcf1141..3fba02e 100644 --- a/src/lib/components/divs/TitleDiv.svelte +++ b/src/lib/components/divs/TitleDiv.svelte @@ -7,13 +7,12 @@ children: Snippet; } - let { icon, children }: Props = $props(); + let { icon: Icon, children }: Props = $props();
- {#if icon} - {@const Icon = icon} + {#if Icon} {/if}
diff --git a/src/lib/components/inputs/TextInput.svelte b/src/lib/components/inputs/TextInput.svelte index 82b4182..77e3e95 100644 --- a/src/lib/components/inputs/TextInput.svelte +++ b/src/lib/components/inputs/TextInput.svelte @@ -8,7 +8,7 @@ let { placeholder, type = "text", value = $bindable("") }: Props = $props(); -
+
{ +export const generateAESMasterKey = async () => { + return await window.crypto.subtle.generateKey( + { + name: "AES-KW", + length: 256, + } satisfies AesKeyGenParams, + true, + ["wrapKey", "unwrapKey"], + ); +}; + +export const generateAESDataKey = async () => { return await window.crypto.subtle.generateKey( { name: "AES-GCM", @@ -117,13 +128,45 @@ export const exportAESKey = async (key: CryptoKey) => { return await window.crypto.subtle.exportKey("raw", key); }; -export const wrapAESKeyUsingRSA = async (aesKey: CryptoKey, rsaPublicKey: CryptoKey) => { +export const encryptAESPlaintext = async (plaintext: BufferSource, aesKey: CryptoKey) => { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + } satisfies AesGcmParams, + aesKey, + plaintext, + ); + return { ciphertext, iv }; +}; + +export const decryptAESCiphertext = async ( + ciphertext: BufferSource, + iv: BufferSource, + aesKey: CryptoKey, +) => { + return await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv, + } satisfies AesGcmParams, + aesKey, + ciphertext, + ); +}; + +export const wrapAESMasterKey = async (aesKey: CryptoKey, rsaPublicKey: CryptoKey) => { return await window.crypto.subtle.wrapKey("raw", aesKey, rsaPublicKey, { name: "RSA-OAEP", } satisfies RsaOaepParams); }; -export const unwrapAESKeyUsingRSA = async (wrappedKey: BufferSource, rsaPrivateKey: CryptoKey) => { +export const wrapAESDataKey = async (aesKey: CryptoKey, aesWrapKey: CryptoKey) => { + return await window.crypto.subtle.wrapKey("raw", aesKey, aesWrapKey, "AES-KW"); +}; + +export const unwrapAESMasterKey = async (wrappedKey: BufferSource, rsaPrivateKey: CryptoKey) => { return await window.crypto.subtle.unwrapKey( "raw", wrappedKey, @@ -131,11 +174,20 @@ export const unwrapAESKeyUsingRSA = async (wrappedKey: BufferSource, rsaPrivateK { name: "RSA-OAEP", } satisfies RsaOaepParams, - { - name: "AES-GCM", - length: 256, - } satisfies AesKeyGenParams, + "AES-KW", true, + ["wrapKey", "unwrapKey"], + ); +}; + +export const unwrapAESDataKey = async (wrappedKey: BufferSource, aesMasterKey: CryptoKey) => { + return await window.crypto.subtle.unwrapKey( + "raw", + wrappedKey, + aesMasterKey, + "AES-KW", + "AES-GCM", + false, ["encrypt", "decrypt"], ); }; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 2a38a5a..0d4b7a1 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -6,7 +6,7 @@ import { decryptRSACiphertext, signRSAMessage, makeAESKeyNonextractable, - unwrapAESKeyUsingRSA, + unwrapAESMasterKey, verifyMasterKeyWrappedSig, } from "$lib/modules/crypto"; import type { @@ -51,7 +51,7 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: version, state, masterKey: await makeAESKeyNonextractable( - await unwrapAESKeyUsingRSA(decodeFromBase64(masterKeyWrapped), decryptKey), + await unwrapAESMasterKey(decodeFromBase64(masterKeyWrapped), decryptKey), ), isValid: await verifyMasterKeyWrappedSig( version, @@ -68,7 +68,12 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: masterKeys.map(({ version, state, masterKey }) => ({ version, state, key: masterKey })), ); masterKeyStore.set( - new Map(masterKeys.map(({ version, state, masterKey }) => [version, { state, masterKey }])), + new Map( + masterKeys.map(({ version, state, masterKey }) => [ + version, + { version, state, key: masterKey }, + ]), + ), ); return true; diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index 7690bbf..d742634 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -8,8 +8,9 @@ export interface ClientKeys { } export interface MasterKey { + version: number; state: "active" | "retired" | "dead"; - masterKey: CryptoKey; + key: CryptoKey; } export const clientKeyStore = writable(null); diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index c5cfd17..ae5696d 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -8,7 +8,7 @@ let { data } = $props(); - let fingerprint = $derived( + const fingerprint = $derived( $clientKeyStore ? generateEncryptKeyFingerprint($clientKeyStore.encryptKey) : undefined, ); diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 77c3f6f..b8a4a9f 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -2,9 +2,8 @@ import { generateRSAKeyPair, makeRSAKeyNonextractable, exportRSAKeyToBase64, - generateAESKey, - makeAESKeyNonextractable, - wrapAESKeyUsingRSA, + generateAESMasterKey, + wrapAESMasterKey, } from "$lib/modules/crypto"; import { clientKeyStore } from "$lib/stores"; @@ -29,9 +28,8 @@ export const generateClientKeys = async () => { }; export const generateInitialMasterKey = async (encryptKey: CryptoKey) => { - const masterKey = await generateAESKey(); + const masterKey = await generateAESMasterKey(); return { - masterKey: await makeAESKeyNonextractable(masterKey), - masterKeyWrapped: await wrapAESKeyUsingRSA(masterKey, encryptKey), + masterKeyWrapped: await wrapAESMasterKey(masterKey, encryptKey), }; }; diff --git a/src/routes/(main)/directory/[[id]]/+page.server.ts b/src/routes/(main)/directory/[[id]]/+page.server.ts new file mode 100644 index 0000000..2da0522 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/+page.server.ts @@ -0,0 +1,35 @@ +import { error } from "@sveltejs/kit"; +import { z } from "zod"; +import type { DirectroyInfoResponse } from "$lib/server/schemas"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ params, fetch }) => { + const zodRes = z + .object({ + id: z.coerce.number().int().positive().optional(), + }) + .safeParse(params); + if (!zodRes.success) error(404, "Not found"); + const { id } = zodRes.data; + + const directoryId = id ? id : ("root" as const); + const res = await fetch(`/api/directory/${directoryId}`); + if (!res.ok) error(404, "Not found"); + + const directoryInfo: DirectroyInfoResponse = await res.json(); + const subDirectoryInfos = await Promise.all( + directoryInfo.subDirectories.map(async (subDirectoryId) => { + const res = await fetch(`/api/directory/${subDirectoryId}`); + if (!res.ok) error(500, "Internal server error"); + return (await res.json()) as DirectroyInfoResponse; + }), + ); + const fileInfos = directoryInfo.files; // TODO + + return { + id: directoryId, + metadata: directoryInfo.metadata, + subDirectories: subDirectoryInfos, + files: fileInfos, + }; +}; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 0f34ad6..518c8d2 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,3 +1,91 @@ -{#each Array(300) as _} -

Hello!

-{/each} + + + + 파일 + + +
+
+ {#if entries} + {#await entries then entries} + {#each entries as { name }} + + {/each} + {/await} + {/if} +
+ + { + isCreateBottomSheetOpen = true; + }} + /> +
+ + { + isCreateBottomSheetOpen = false; + isCreateDirectoryModalOpen = true; + }} + onFileUpload={() => { + isCreateBottomSheetOpen = false; + // TODO + }} +/> + diff --git a/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte new file mode 100644 index 0000000..9ea26ce --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte @@ -0,0 +1,32 @@ + + + +
+ +
+ +

폴더 만들기

+
+
+ +
+ +

파일 업로드

+
+
+
+
diff --git a/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte b/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte new file mode 100644 index 0000000..4828b87 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte @@ -0,0 +1,32 @@ + + + +
+

새 폴더

+
+ +
+
+ + +
+
+
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte new file mode 100644 index 0000000..ceb3460 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte @@ -0,0 +1,12 @@ + + +
+ +

{name}

+
diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts new file mode 100644 index 0000000..782197e --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -0,0 +1,49 @@ +import { callSignedPostApi } from "$lib/hooks"; +import { + encodeToBase64, + decodeFromBase64, + generateAESDataKey, + encryptAESPlaintext, + decryptAESCiphertext, + wrapAESDataKey, + unwrapAESDataKey, +} from "$lib/modules/crypto"; +import type { DirectroyInfoResponse, DirectoryCreateRequest } from "$lib/server/schemas"; +import type { MasterKey } from "$lib/stores"; + +export const decryptDirectroyMetadata = async ( + metadata: NonNullable, + masterKey: CryptoKey, +) => { + const dataDecryptKey = await unwrapAESDataKey(decodeFromBase64(metadata.dek), masterKey); + return { + name: new TextDecoder().decode( + await decryptAESCiphertext( + decodeFromBase64(metadata.name), + decodeFromBase64(metadata.nameIv), + dataDecryptKey, + ), + ), + }; +}; + +export const requestDirectroyCreation = async ( + name: string, + parentId: "root" | number, + masterKey: MasterKey, + signKey: CryptoKey, +) => { + const dataKey = await generateAESDataKey(); + const nameEncrypted = await encryptAESPlaintext(new TextEncoder().encode(name), dataKey); + return await callSignedPostApi( + "/api/directory/create", + { + parentId, + mekVersion: masterKey.version, + dek: encodeToBase64(await wrapAESDataKey(dataKey, masterKey.key)), + name: encodeToBase64(nameEncrypted.ciphertext), + nameIv: encodeToBase64(nameEncrypted.iv.buffer), + }, + signKey, + ); +}; diff --git a/src/routes/services.ts b/src/routes/services.ts index 1b5c854..de8f618 100644 --- a/src/routes/services.ts +++ b/src/routes/services.ts @@ -18,7 +18,7 @@ export const prepareMasterKeyStore = async () => { const masterKeys = await getMasterKeys(); if (masterKeys.length > 0) { masterKeyStore.set( - new Map(masterKeys.map(({ version, state, key }) => [version, { state, masterKey: key }])), + new Map(masterKeys.map(({ version, state, key }) => [version, { version, state, key }])), ); return true; } else { From afe672228aa3395703f6a40d10b3169086fba6d1 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 09:09:13 +0900 Subject: [PATCH 074/115] =?UTF-8?q?Token=20Upgrade/Refresh=20=ED=9B=84,=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=EC=9D=98=20=EC=9C=A0=ED=9A=A8=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=EC=9D=84=20=EC=84=A4=EC=A0=95=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/api/auth/refreshToken/+server.ts | 4 ++++ src/routes/api/auth/upgradeToken/verify/+server.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/routes/api/auth/refreshToken/+server.ts b/src/routes/api/auth/refreshToken/+server.ts index 7960348..5a01c85 100644 --- a/src/routes/api/auth/refreshToken/+server.ts +++ b/src/routes/api/auth/refreshToken/+server.ts @@ -1,4 +1,6 @@ import { error, text } from "@sveltejs/kit"; +import ms from "ms"; +import env from "$lib/server/loadenv"; import { refreshToken as doRefreshToken } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; @@ -9,10 +11,12 @@ export const POST: RequestHandler = async ({ cookies }) => { const { accessToken, refreshToken } = await doRefreshToken(token); cookies.set("accessToken", accessToken, { path: "/", + maxAge: ms(env.jwt.accessExp) / 1000, sameSite: "strict", }); cookies.set("refreshToken", refreshToken, { path: "/api/auth", + maxAge: ms(env.jwt.refreshExp) / 1000, sameSite: "strict", }); diff --git a/src/routes/api/auth/upgradeToken/verify/+server.ts b/src/routes/api/auth/upgradeToken/verify/+server.ts index 84f8e82..8abc130 100644 --- a/src/routes/api/auth/upgradeToken/verify/+server.ts +++ b/src/routes/api/auth/upgradeToken/verify/+server.ts @@ -1,4 +1,6 @@ import { error, text } from "@sveltejs/kit"; +import ms from "ms"; +import env from "$lib/server/loadenv"; import { tokenUpgradeVerifyRequest } from "$lib/server/schemas/auth"; import { upgradeToken } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; @@ -19,10 +21,12 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress ); cookies.set("accessToken", accessToken, { path: "/", + maxAge: ms(env.jwt.accessExp) / 1000, sameSite: "strict", }); cookies.set("refreshToken", refreshToken, { path: "/api/auth", + maxAge: ms(env.jwt.refreshExp) / 1000, sameSite: "strict", }); From aad5617d25bd0f4f9578aa0e3467934bb3ab9339 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 3 Jan 2025 12:21:53 +0900 Subject: [PATCH 075/115] =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=94=ED=98=B8=20=EB=AA=A8=EB=93=88=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callApi.ts | 4 +- src/lib/hooks/gotoStateful.ts | 2 +- src/lib/modules/crypto.ts | 227 ------------------ src/lib/modules/crypto/aes.ts | 83 +++++++ src/lib/modules/crypto/index.ts | 4 + src/lib/modules/crypto/rsa.ts | 159 ++++++++++++ src/lib/modules/crypto/sha.ts | 3 + src/lib/modules/crypto/util.ts | 19 ++ src/lib/services/auth.ts | 11 +- src/lib/services/key.ts | 39 ++- .../(fullscreen)/client/pending/+page.svelte | 4 +- .../(fullscreen)/client/pending/service.ts | 12 +- src/routes/(fullscreen)/key/export/service.ts | 6 +- .../(fullscreen)/key/generate/service.ts | 35 +-- src/routes/(main)/directory/[[id]]/service.ts | 26 +- 15 files changed, 336 insertions(+), 298 deletions(-) delete mode 100644 src/lib/modules/crypto.ts create mode 100644 src/lib/modules/crypto/aes.ts create mode 100644 src/lib/modules/crypto/index.ts create mode 100644 src/lib/modules/crypto/rsa.ts create mode 100644 src/lib/modules/crypto/sha.ts create mode 100644 src/lib/modules/crypto/util.ts diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts index 7189735..c36b59a 100644 --- a/src/lib/hooks/callApi.ts +++ b/src/lib/hooks/callApi.ts @@ -1,4 +1,4 @@ -import { signRequest } from "$lib/modules/crypto"; +import { signRequestBody } from "$lib/modules/crypto"; export const refreshToken = async () => { return await fetch("/api/auth/refreshToken", { method: "POST" }); @@ -36,6 +36,6 @@ export const callSignedPostApi = async (input: RequestInfo, payload: T, signK headers: { "Content-Type": "application/json", }, - body: await signRequest(payload, signKey), + body: await signRequestBody(payload, signKey), }); }; diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts index 2d168a7..fffc95f 100644 --- a/src/lib/hooks/gotoStateful.ts +++ b/src/lib/hooks/gotoStateful.ts @@ -10,7 +10,7 @@ interface KeyExportState { signKeyBase64: string; verifyKeyBase64: string; - masterKeyWrapped: ArrayBuffer; + masterKeyWrapped: string; } const useAutoNull = (value: T | null) => { diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts deleted file mode 100644 index 563ae49..0000000 --- a/src/lib/modules/crypto.ts +++ /dev/null @@ -1,227 +0,0 @@ -export type RSAKeyPurpose = "encryption" | "signature"; -export type RSAKeyType = "public" | "private"; - -export const encodeToBase64 = (data: ArrayBuffer) => { - return btoa(String.fromCharCode(...new Uint8Array(data))); -}; - -export const decodeFromBase64 = (data: string) => { - return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; -}; - -export const generateRSAKeyPair = async (purpose: RSAKeyPurpose) => { - return await window.crypto.subtle.generateKey( - { - name: purpose === "encryption" ? "RSA-OAEP" : "RSA-PSS", - modulusLength: 4096, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - } satisfies RsaHashedKeyGenParams, - true, - purpose === "encryption" ? ["encrypt", "decrypt", "wrapKey", "unwrapKey"] : ["sign", "verify"], - ); -}; - -export const makeRSAKeyNonextractable = async (key: CryptoKey) => { - const { format, key: exportedKey } = await exportRSAKey(key); - return await window.crypto.subtle.importKey( - format, - exportedKey, - key.algorithm, - false, - key.usages, - ); -}; - -export const exportRSAKey = async (key: CryptoKey) => { - const format = key.type === "public" ? ("spki" as const) : ("pkcs8" as const); - return { - format, - 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: BufferSource, publicKey: CryptoKey) => { - return await window.crypto.subtle.encrypt( - { - name: "RSA-OAEP", - } satisfies RsaOaepParams, - publicKey, - plaintext, - ); -}; - -export const decryptRSACiphertext = async (ciphertext: BufferSource, privateKey: CryptoKey) => { - return await window.crypto.subtle.decrypt( - { - name: "RSA-OAEP", - } satisfies RsaOaepParams, - privateKey, - ciphertext, - ); -}; - -export const signRSAMessage = async (message: BufferSource, privateKey: CryptoKey) => { - return await window.crypto.subtle.sign( - { - name: "RSA-PSS", - saltLength: 32, - } satisfies RsaPssParams, - privateKey, - message, - ); -}; - -export const verifyRSASignature = async ( - message: BufferSource, - signature: BufferSource, - publicKey: CryptoKey, -) => { - return await window.crypto.subtle.verify( - { - name: "RSA-PSS", - saltLength: 32, - } satisfies RsaPssParams, - publicKey, - signature, - message, - ); -}; - -export const generateAESMasterKey = async () => { - return await window.crypto.subtle.generateKey( - { - name: "AES-KW", - length: 256, - } satisfies AesKeyGenParams, - true, - ["wrapKey", "unwrapKey"], - ); -}; - -export const generateAESDataKey = async () => { - return await window.crypto.subtle.generateKey( - { - name: "AES-GCM", - length: 256, - } satisfies AesKeyGenParams, - true, - ["encrypt", "decrypt"], - ); -}; - -export const makeAESKeyNonextractable = async (key: CryptoKey) => { - return await window.crypto.subtle.importKey( - "raw", - await exportAESKey(key), - key.algorithm, - false, - key.usages, - ); -}; - -export const exportAESKey = async (key: CryptoKey) => { - return await window.crypto.subtle.exportKey("raw", key); -}; - -export const encryptAESPlaintext = async (plaintext: BufferSource, aesKey: CryptoKey) => { - const iv = window.crypto.getRandomValues(new Uint8Array(12)); - const ciphertext = await window.crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - } satisfies AesGcmParams, - aesKey, - plaintext, - ); - return { ciphertext, iv }; -}; - -export const decryptAESCiphertext = async ( - ciphertext: BufferSource, - iv: BufferSource, - aesKey: CryptoKey, -) => { - return await window.crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - } satisfies AesGcmParams, - aesKey, - ciphertext, - ); -}; - -export const wrapAESMasterKey = async (aesKey: CryptoKey, rsaPublicKey: CryptoKey) => { - return await window.crypto.subtle.wrapKey("raw", aesKey, rsaPublicKey, { - name: "RSA-OAEP", - } satisfies RsaOaepParams); -}; - -export const wrapAESDataKey = async (aesKey: CryptoKey, aesWrapKey: CryptoKey) => { - return await window.crypto.subtle.wrapKey("raw", aesKey, aesWrapKey, "AES-KW"); -}; - -export const unwrapAESMasterKey = async (wrappedKey: BufferSource, rsaPrivateKey: CryptoKey) => { - return await window.crypto.subtle.unwrapKey( - "raw", - wrappedKey, - rsaPrivateKey, - { - name: "RSA-OAEP", - } satisfies RsaOaepParams, - "AES-KW", - true, - ["wrapKey", "unwrapKey"], - ); -}; - -export const unwrapAESDataKey = async (wrappedKey: BufferSource, aesMasterKey: CryptoKey) => { - return await window.crypto.subtle.unwrapKey( - "raw", - wrappedKey, - aesMasterKey, - "AES-KW", - "AES-GCM", - false, - ["encrypt", "decrypt"], - ); -}; - -export const digestSHA256 = async (data: BufferSource) => { - return await window.crypto.subtle.digest("SHA-256", data); -}; - -export const signRequest = async (data: T, privateKey: CryptoKey) => { - const dataBuffer = new TextEncoder().encode(JSON.stringify(data)); - const signature = await signRSAMessage(dataBuffer, privateKey); - return JSON.stringify({ - data, - signature: encodeToBase64(signature), - }); -}; - -export const signMasterKeyWrapped = async ( - version: number, - masterKeyWrapped: ArrayBuffer, - privateKey: CryptoKey, -) => { - const data = JSON.stringify({ version, key: encodeToBase64(masterKeyWrapped) }); - const dataBuffer = new TextEncoder().encode(data); - return encodeToBase64(await signRSAMessage(dataBuffer, privateKey)); -}; - -export const verifyMasterKeyWrappedSig = async ( - version: number, - masterKeyWrappedBase64: string, - masterKeyWrappedSig: string, - publicKey: CryptoKey, -) => { - const data = JSON.stringify({ version, key: masterKeyWrappedBase64 }); - const dataBuffer = new TextEncoder().encode(data); - return await verifyRSASignature(dataBuffer, decodeFromBase64(masterKeyWrappedSig), publicKey); -}; diff --git a/src/lib/modules/crypto/aes.ts b/src/lib/modules/crypto/aes.ts new file mode 100644 index 0000000..e1e917a --- /dev/null +++ b/src/lib/modules/crypto/aes.ts @@ -0,0 +1,83 @@ +import { encodeToBase64, decodeFromBase64 } from "./util"; + +export const generateMasterKey = async () => { + return { + masterKey: await window.crypto.subtle.generateKey( + { + name: "AES-KW", + length: 256, + } satisfies AesKeyGenParams, + true, + ["wrapKey", "unwrapKey"], + ), + }; +}; + +export const generateDataKey = async () => { + return { + dataKey: await window.crypto.subtle.generateKey( + { + name: "AES-GCM", + length: 256, + } satisfies AesKeyGenParams, + true, + ["encrypt", "decrypt"], + ), + }; +}; + +const exportAESKey = async (key: CryptoKey) => { + return await window.crypto.subtle.exportKey("raw", key); +}; + +export const makeAESKeyNonextractable = async (key: CryptoKey) => { + return await window.crypto.subtle.importKey( + "raw", + await exportAESKey(key), + key.algorithm, + false, + key.usages, + ); +}; + +export const wrapDataKey = async (dataKey: CryptoKey, masterKey: CryptoKey) => { + return encodeToBase64(await window.crypto.subtle.wrapKey("raw", dataKey, masterKey, "AES-KW")); +}; + +export const unwrapDataKey = async (dataKeyWrapped: string, masterKey: CryptoKey) => { + return { + dataKey: await window.crypto.subtle.unwrapKey( + "raw", + decodeFromBase64(dataKeyWrapped), + masterKey, + "AES-KW", + "AES-GCM", + false, // Non-extractable + ["encrypt", "decrypt"], + ), + }; +}; + +export const encryptData = async (data: BufferSource, dataKey: CryptoKey) => { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + } satisfies AesGcmParams, + dataKey, + data, + ); + return { ciphertext, iv: encodeToBase64(iv.buffer) }; +}; + +export const decryptData = async (ciphertext: BufferSource, iv: string, dataKey: CryptoKey) => { + return await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: decodeFromBase64(iv), + } satisfies AesGcmParams, + dataKey, + ciphertext, + ); +}; diff --git a/src/lib/modules/crypto/index.ts b/src/lib/modules/crypto/index.ts new file mode 100644 index 0000000..e6972ba --- /dev/null +++ b/src/lib/modules/crypto/index.ts @@ -0,0 +1,4 @@ +export * from "./aes"; +export * from "./rsa"; +export * from "./sha"; +export * from "./util"; diff --git a/src/lib/modules/crypto/rsa.ts b/src/lib/modules/crypto/rsa.ts new file mode 100644 index 0000000..a82abee --- /dev/null +++ b/src/lib/modules/crypto/rsa.ts @@ -0,0 +1,159 @@ +import { encodeToBase64, decodeFromBase64 } from "./util"; + +export const generateEncryptionKeyPair = async () => { + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + } satisfies RsaHashedKeyGenParams, + true, + ["encrypt", "decrypt", "wrapKey", "unwrapKey"], + ); + return { + encryptKey: keyPair.publicKey, + decryptKey: keyPair.privateKey, + }; +}; + +export const generateSigningKeyPair = async () => { + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-PSS", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + } satisfies RsaHashedKeyGenParams, + true, + ["sign", "verify"], + ); + return { + signKey: keyPair.privateKey, + verifyKey: keyPair.publicKey, + }; +}; + +export const exportRSAKey = async (key: CryptoKey) => { + const format = key.type === "public" ? ("spki" as const) : ("pkcs8" as const); + return { + key: await window.crypto.subtle.exportKey(format, key), + format, + }; +}; + +export const exportRSAKeyToBase64 = async (key: CryptoKey) => { + return encodeToBase64((await exportRSAKey(key)).key); +}; + +export const makeRSAKeyNonextractable = async (key: CryptoKey) => { + const { key: exportedKey, format } = await exportRSAKey(key); + return await window.crypto.subtle.importKey( + format, + exportedKey, + key.algorithm, + false, + key.usages, + ); +}; + +export const decryptChallenge = async (challenge: string, decryptKey: CryptoKey) => { + return await window.crypto.subtle.decrypt( + { + name: "RSA-OAEP", + } satisfies RsaOaepParams, + decryptKey, + decodeFromBase64(challenge), + ); +}; + +export const wrapMasterKey = async (masterKey: CryptoKey, encryptKey: CryptoKey) => { + return encodeToBase64( + await window.crypto.subtle.wrapKey("raw", masterKey, encryptKey, { + name: "RSA-OAEP", + } satisfies RsaOaepParams), + ); +}; + +export const unwrapMasterKey = async ( + masterKeyWrapped: string, + decryptKey: CryptoKey, + extractable = false, +) => { + return { + masterKey: await window.crypto.subtle.unwrapKey( + "raw", + decodeFromBase64(masterKeyWrapped), + decryptKey, + { + name: "RSA-OAEP", + } satisfies RsaOaepParams, + "AES-KW", + extractable, + ["wrapKey", "unwrapKey"], + ), + }; +}; + +export const signMessage = async (message: BufferSource, signKey: CryptoKey) => { + return await window.crypto.subtle.sign( + { + name: "RSA-PSS", + saltLength: 32, // SHA-256 + } satisfies RsaPssParams, + signKey, + message, + ); +}; + +export const verifySignature = async ( + message: BufferSource, + signature: BufferSource, + verifyKey: CryptoKey, +) => { + return await window.crypto.subtle.verify( + { + name: "RSA-PSS", + saltLength: 32, // SHA-256 + } satisfies RsaPssParams, + verifyKey, + signature, + message, + ); +}; + +export const signRequestBody = async (requestBody: T, signKey: CryptoKey) => { + const dataBuffer = new TextEncoder().encode(JSON.stringify(requestBody)); + const signature = await signMessage(dataBuffer, signKey); + return JSON.stringify({ + data: requestBody, + signature: encodeToBase64(signature), + }); +}; + +export const signMasterKeyWrapped = async ( + masterKeyVersion: number, + masterKeyWrapped: string, + signKey: CryptoKey, +) => { + const serialized = JSON.stringify({ + version: masterKeyVersion, + key: masterKeyWrapped, + }); + const serializedBuffer = new TextEncoder().encode(serialized); + return encodeToBase64(await signMessage(serializedBuffer, signKey)); +}; + +export const verifyMasterKeyWrapped = async ( + masterKeyVersion: number, + masterKeyWrapped: string, + masterKeyWrappedSig: string, + verifyKey: CryptoKey, +) => { + const serialized = JSON.stringify({ + version: masterKeyVersion, + key: masterKeyWrapped, + }); + const serializedBuffer = new TextEncoder().encode(serialized); + return await verifySignature(serializedBuffer, decodeFromBase64(masterKeyWrappedSig), verifyKey); +}; diff --git a/src/lib/modules/crypto/sha.ts b/src/lib/modules/crypto/sha.ts new file mode 100644 index 0000000..e79f706 --- /dev/null +++ b/src/lib/modules/crypto/sha.ts @@ -0,0 +1,3 @@ +export const digestMessage = async (message: BufferSource) => { + return await window.crypto.subtle.digest("SHA-256", message); +}; diff --git a/src/lib/modules/crypto/util.ts b/src/lib/modules/crypto/util.ts new file mode 100644 index 0000000..9aeeb9d --- /dev/null +++ b/src/lib/modules/crypto/util.ts @@ -0,0 +1,19 @@ +export const encodeToBase64 = (data: ArrayBuffer) => { + return btoa(String.fromCharCode(...new Uint8Array(data))); +}; + +export const decodeFromBase64 = (data: string) => { + return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; +}; + +export const concatenateBuffers = (...buffers: ArrayBuffer[]) => { + const arrays = buffers.map((buffer) => new Uint8Array(buffer)); + const totalLength = arrays.reduce((acc, array) => acc + array.length, 0); + const result = new Uint8Array(totalLength); + + arrays.reduce((offset, array) => { + result.set(array, offset); + return offset + array.length; + }, 0); + return result; +}; diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 7a0936b..d796d28 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -1,9 +1,4 @@ -import { - encodeToBase64, - decodeFromBase64, - decryptRSACiphertext, - signRSAMessage, -} from "$lib/modules/crypto"; +import { encodeToBase64, decryptChallenge, signMessage } from "$lib/modules/crypto"; import type { TokenUpgradeRequest, TokenUpgradeResponse, @@ -29,8 +24,8 @@ export const requestTokenUpgrade = async ( if (!res.ok) return false; const { challenge }: TokenUpgradeResponse = await res.json(); - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey); - const sigAnswer = await signRSAMessage(answer, signKey); + const answer = await decryptChallenge(challenge, decryptKey); + const sigAnswer = await signMessage(answer, signKey); res = await fetch("/api/auth/upgradeToken/verify", { method: "POST", diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 0d4b7a1..a430eb1 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -2,12 +2,10 @@ import { callGetApi, callPostApi } from "$lib/hooks"; import { storeMasterKeys } from "$lib/indexedDB"; import { encodeToBase64, - decodeFromBase64, - decryptRSACiphertext, - signRSAMessage, - makeAESKeyNonextractable, - unwrapAESMasterKey, - verifyMasterKeyWrappedSig, + decryptChallenge, + signMessage, + unwrapMasterKey, + verifyMasterKeyWrapped, } from "$lib/modules/crypto"; import type { ClientRegisterRequest, @@ -30,8 +28,8 @@ export const requestClientRegistration = async ( if (!res.ok) return false; const { challenge }: ClientRegisterResponse = await res.json(); - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey); - const sigAnswer = await signRSAMessage(answer, signKey); + const answer = await decryptChallenge(challenge, decryptKey); + const sigAnswer = await signMessage(answer, signKey); res = await callPostApi("/api/client/register/verify", { answer: encodeToBase64(answer), @@ -47,19 +45,20 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: const { meks: masterKeysWrapped }: MasterKeyListResponse = await res.json(); const masterKeys = await Promise.all( masterKeysWrapped.map( - async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => ({ - version, - state, - masterKey: await makeAESKeyNonextractable( - await unwrapAESMasterKey(decodeFromBase64(masterKeyWrapped), decryptKey), - ), - isValid: await verifyMasterKeyWrappedSig( + async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => { + const { masterKey } = await unwrapMasterKey(masterKeyWrapped, decryptKey); + return { version, - masterKeyWrapped, - masterKeyWrappedSig, - verfiyKey, - ), - }), + state, + masterKey, + isValid: await verifyMasterKeyWrapped( + version, + masterKeyWrapped, + masterKeyWrappedSig, + verfiyKey, + ), + }; + }, ), ); if (!masterKeys.every(({ isValid }) => isValid)) return false; diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index ae5696d..e009529 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -9,7 +9,9 @@ let { data } = $props(); const fingerprint = $derived( - $clientKeyStore ? generateEncryptKeyFingerprint($clientKeyStore.encryptKey) : undefined, + $clientKeyStore + ? generateEncryptKeyFingerprint($clientKeyStore.encryptKey, $clientKeyStore.verifyKey) + : undefined, ); $effect(() => { diff --git a/src/routes/(fullscreen)/client/pending/service.ts b/src/routes/(fullscreen)/client/pending/service.ts index 5a1a06d..df634d9 100644 --- a/src/routes/(fullscreen)/client/pending/service.ts +++ b/src/routes/(fullscreen)/client/pending/service.ts @@ -1,10 +1,14 @@ -import { exportRSAKey, digestSHA256 } from "$lib/modules/crypto"; +import { concatenateBuffers, exportRSAKey, digestMessage } from "$lib/modules/crypto"; export { requestMasterKeyDownload } from "$lib/services/key"; -export const generateEncryptKeyFingerprint = async (encryptKey: CryptoKey) => { - const { key } = await exportRSAKey(encryptKey); - const digest = await digestSHA256(key); +export const generateEncryptKeyFingerprint = async ( + encryptKey: CryptoKey, + verifyKey: CryptoKey, +) => { + const { key: encryptKeyBuffer } = await exportRSAKey(encryptKey); + const { key: verifyKeyBuffer } = await exportRSAKey(verifyKey); + const digest = await digestMessage(concatenateBuffers(encryptKeyBuffer, verifyKeyBuffer)); return Array.from(new Uint8Array(digest)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index fd1fadd..415eb77 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,6 +1,6 @@ import { callSignedPostApi } from "$lib/hooks"; import { storeClientKey } from "$lib/indexedDB"; -import { encodeToBase64, signMasterKeyWrapped } from "$lib/modules/crypto"; +import { signMasterKeyWrapped } from "$lib/modules/crypto"; import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; import type { ClientKeys } from "$lib/stores"; @@ -43,13 +43,13 @@ export const storeClientKeys = async (clientKeys: ClientKeys) => { }; export const requestInitialMasterKeyRegistration = async ( - masterKeyWrapped: ArrayBuffer, + masterKeyWrapped: string, signKey: CryptoKey, ) => { const res = await callSignedPostApi( "/api/mek/register/initial", { - mek: encodeToBase64(masterKeyWrapped), + mek: masterKeyWrapped, mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), }, signKey, diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index b8a4a9f..b63da21 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,35 +1,36 @@ import { - generateRSAKeyPair, - makeRSAKeyNonextractable, + generateEncryptionKeyPair, + generateSigningKeyPair, exportRSAKeyToBase64, - generateAESMasterKey, - wrapAESMasterKey, + makeRSAKeyNonextractable, + generateMasterKey, + wrapMasterKey, } from "$lib/modules/crypto"; import { clientKeyStore } from "$lib/stores"; export const generateClientKeys = async () => { - const encKeyPair = await generateRSAKeyPair("encryption"); - const sigKeyPair = await generateRSAKeyPair("signature"); + const { encryptKey, decryptKey } = await generateEncryptionKeyPair(); + const { signKey, verifyKey } = await generateSigningKeyPair(); clientKeyStore.set({ - encryptKey: encKeyPair.publicKey, - decryptKey: await makeRSAKeyNonextractable(encKeyPair.privateKey), - signKey: await makeRSAKeyNonextractable(sigKeyPair.privateKey), - verifyKey: sigKeyPair.publicKey, + encryptKey, + decryptKey: await makeRSAKeyNonextractable(decryptKey), + signKey: await makeRSAKeyNonextractable(signKey), + verifyKey, }); return { - encryptKey: encKeyPair.publicKey, - encryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey), - decryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey), - signKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey), - verifyKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey), + encryptKey, + encryptKeyBase64: await exportRSAKeyToBase64(encryptKey), + decryptKeyBase64: await exportRSAKeyToBase64(decryptKey), + signKeyBase64: await exportRSAKeyToBase64(signKey), + verifyKeyBase64: await exportRSAKeyToBase64(verifyKey), }; }; export const generateInitialMasterKey = async (encryptKey: CryptoKey) => { - const masterKey = await generateAESMasterKey(); + const { masterKey } = await generateMasterKey(); return { - masterKeyWrapped: await wrapAESMasterKey(masterKey, encryptKey), + masterKeyWrapped: await wrapMasterKey(masterKey, encryptKey), }; }; diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index 782197e..f90477f 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -2,11 +2,11 @@ import { callSignedPostApi } from "$lib/hooks"; import { encodeToBase64, decodeFromBase64, - generateAESDataKey, - encryptAESPlaintext, - decryptAESCiphertext, - wrapAESDataKey, - unwrapAESDataKey, + generateDataKey, + wrapDataKey, + unwrapDataKey, + encryptData, + decryptData, } from "$lib/modules/crypto"; import type { DirectroyInfoResponse, DirectoryCreateRequest } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; @@ -15,14 +15,10 @@ export const decryptDirectroyMetadata = async ( metadata: NonNullable, masterKey: CryptoKey, ) => { - const dataDecryptKey = await unwrapAESDataKey(decodeFromBase64(metadata.dek), masterKey); + const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); return { name: new TextDecoder().decode( - await decryptAESCiphertext( - decodeFromBase64(metadata.name), - decodeFromBase64(metadata.nameIv), - dataDecryptKey, - ), + await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey), ), }; }; @@ -33,16 +29,16 @@ export const requestDirectroyCreation = async ( masterKey: MasterKey, signKey: CryptoKey, ) => { - const dataKey = await generateAESDataKey(); - const nameEncrypted = await encryptAESPlaintext(new TextEncoder().encode(name), dataKey); + const { dataKey } = await generateDataKey(); + const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey); return await callSignedPostApi( "/api/directory/create", { parentId, mekVersion: masterKey.version, - dek: encodeToBase64(await wrapAESDataKey(dataKey, masterKey.key)), + dek: await wrapDataKey(dataKey, masterKey.key), name: encodeToBase64(nameEncrypted.ciphertext), - nameIv: encodeToBase64(nameEncrypted.iv.buffer), + nameIv: nameEncrypted.iv, }, signKey, ); From da18e6856a50b7ec12534e47485ea68248c23d8d Mon Sep 17 00:00:00 2001 From: static Date: Sat, 4 Jan 2025 00:00:55 +0900 Subject: [PATCH 076/115] =?UTF-8?q?Store=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EB=A5=BC=20hooks.client.ts=EC=97=90=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks.client.ts | 26 ++++++++++++++++++ .../(fullscreen)/client/pending/+page.svelte | 19 ++++++------- .../(fullscreen)/key/generate/+page.svelte | 5 ++-- src/routes/+layout.svelte | 19 ++++++------- src/routes/services.ts | 27 ------------------- 5 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 src/hooks.client.ts delete mode 100644 src/routes/services.ts diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..217d7ea --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,26 @@ +import type { ClientInit } from "@sveltejs/kit"; +import { getClientKey, getMasterKeys } from "$lib/indexedDB"; +import { clientKeyStore, masterKeyStore } from "$lib/stores"; + +const prepareClientKeyStore = async () => { + const [encryptKey, decryptKey, signKey, verifyKey] = await Promise.all([ + getClientKey("encrypt"), + getClientKey("decrypt"), + getClientKey("sign"), + getClientKey("verify"), + ]); + if (encryptKey && decryptKey && signKey && verifyKey) { + clientKeyStore.set({ encryptKey, decryptKey, signKey, verifyKey }); + } +}; + +const prepareMasterKeyStore = async () => { + const masterKeys = await getMasterKeys(); + if (masterKeys.length > 0) { + masterKeyStore.set(new Map(masterKeys.map((masterKey) => [masterKey.version, masterKey]))); + } +}; + +export const init: ClientInit = async () => { + await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore()]); +}; diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index e009529..8eacc09 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 91f1fab..5812750 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -1,4 +1,5 @@ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 066a4e0..d682821 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,24 +1,25 @@ diff --git a/src/routes/services.ts b/src/routes/services.ts deleted file mode 100644 index de8f618..0000000 --- a/src/routes/services.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getClientKey, getMasterKeys } from "$lib/indexedDB"; -import { clientKeyStore, masterKeyStore } from "$lib/stores"; - -export const prepareClientKeyStore = async () => { - const encryptKey = await getClientKey("encrypt"); - const decryptKey = await getClientKey("decrypt"); - const signKey = await getClientKey("sign"); - const verifyKey = await getClientKey("verify"); - if (encryptKey && decryptKey && signKey && verifyKey) { - clientKeyStore.set({ encryptKey, decryptKey, signKey, verifyKey }); - return true; - } else { - return false; - } -}; - -export const prepareMasterKeyStore = async () => { - const masterKeys = await getMasterKeys(); - if (masterKeys.length > 0) { - masterKeyStore.set( - new Map(masterKeys.map(({ version, state, key }) => [version, { version, state, key }])), - ); - return true; - } else { - return false; - } -}; From 5115217153d332ea765968cc958d0a6375f0ab46 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 4 Jan 2025 02:46:19 +0900 Subject: [PATCH 077/115] =?UTF-8?q?DirectroyEntry=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/Modal.svelte | 6 +-- src/lib/components/TopBar.svelte | 29 +++++++++++ src/lib/components/index.ts | 1 + .../(fullscreen)/auth/login/+page.svelte | 2 +- .../(fullscreen)/client/pending/+page.svelte | 2 +- .../(fullscreen)/key/generate/+page.svelte | 2 +- .../(main)/directory/[[id]]/+page.server.ts | 5 +- .../(main)/directory/[[id]]/+page.svelte | 49 ++++++++++++------- .../[[id]]/CreateDirectoryModal.svelte | 2 +- .../directory/[[id]]/DirectoryEntry.svelte | 42 ++++++++++++++-- 10 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 src/lib/components/TopBar.svelte diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index a83e4a1..ccb1431 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -5,14 +5,14 @@ interface Props { children: Snippet; - onClose?: () => void; + onclose?: () => void; isOpen: boolean; } - let { children, onClose, isOpen = $bindable() }: Props = $props(); + let { children, onclose, isOpen = $bindable() }: Props = $props(); const closeModal = $derived( - onClose || + onclose || (() => { isOpen = false; }), diff --git a/src/lib/components/TopBar.svelte b/src/lib/components/TopBar.svelte new file mode 100644 index 0000000..de2346e --- /dev/null +++ b/src/lib/components/TopBar.svelte @@ -0,0 +1,29 @@ + + +
+ + {#if title} +

{title}

+ {/if} + {#if children} + {@render children?.()} + {/if} +
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 55979fb..536f01f 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -1,2 +1,3 @@ export { default as BottomSheet } from "./BottomSheet.svelte"; export { default as Modal } from "./Modal.svelte"; +export { default as TopBar } from "./TopBar.svelte"; diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 9ab6c0c..04f215f 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -46,7 +46,7 @@ onMount(async () => { const res = await refreshToken(); if (res.ok) { - await goto(data.redirectPath); + await goto(data.redirectPath, { replaceState: true }); } }); diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index 8eacc09..6369981 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -21,7 +21,7 @@ ($clientKeyStore && (await requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey))) ) { - await goto(data.redirectPath); + await goto(data.redirectPath, { replaceState: true }); } }); diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 5812750..17def3c 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -47,7 +47,7 @@ onMount(async () => { if ($clientKeyStore) { - await goto(data.redirectPath); + await goto(data.redirectPath, { replaceState: true }); } }); diff --git a/src/routes/(main)/directory/[[id]]/+page.server.ts b/src/routes/(main)/directory/[[id]]/+page.server.ts index 2da0522..58d64d4 100644 --- a/src/routes/(main)/directory/[[id]]/+page.server.ts +++ b/src/routes/(main)/directory/[[id]]/+page.server.ts @@ -21,7 +21,10 @@ export const load: PageServerLoad = async ({ params, fetch }) => { directoryInfo.subDirectories.map(async (subDirectoryId) => { const res = await fetch(`/api/directory/${subDirectoryId}`); if (!res.ok) error(500, "Internal server error"); - return (await res.json()) as DirectroyInfoResponse; + return { + ...((await res.json()) as DirectroyInfoResponse), + id: subDirectoryId, + }; }), ); const fileInfos = directoryInfo.files; // TODO diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 518c8d2..6e1e17d 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,4 +1,5 @@ - +

새 폴더

diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte index ceb3460..87c739b 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte @@ -1,12 +1,46 @@ -
- -

{name}

+ + +
+
+
+ +

{name}

+
+ +
+ + From 663a0f08b36ce6e3c7b2463e5d57a5623c64a0b4 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 4 Jan 2025 04:15:46 +0900 Subject: [PATCH 078/115] =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C,=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=EC=9D=B4=20=EB=A7=8E=EC=9D=84=20=EB=95=8C=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=EC=9D=B4=20=EA=B9=A8=EC=A7=80?= =?UTF-8?q?=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/TopBar.svelte | 2 +- .../components/buttons/FloatingButton.svelte | 32 +++++++++++++------ src/routes/(main)/+layout.svelte | 8 +++-- src/routes/(main)/BottomBar.svelte | 10 +++--- .../(main)/directory/[[id]]/+page.svelte | 19 ++++++----- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/lib/components/TopBar.svelte b/src/lib/components/TopBar.svelte index de2346e..38dc550 100644 --- a/src/lib/components/TopBar.svelte +++ b/src/lib/components/TopBar.svelte @@ -16,7 +16,7 @@ }); -
+
diff --git a/src/lib/components/buttons/FloatingButton.svelte b/src/lib/components/buttons/FloatingButton.svelte index 823af8d..4d5ee0f 100644 --- a/src/lib/components/buttons/FloatingButton.svelte +++ b/src/lib/components/buttons/FloatingButton.svelte @@ -1,22 +1,36 @@ + let { bottom = "bottom-20", icon: Icon, onclick }: Props = $props(); - + }; + + +
+
+ +
+
+ +
+
+
+
+
diff --git a/src/routes/(main)/+layout.svelte b/src/routes/(main)/+layout.svelte index 0bebf88..f726970 100644 --- a/src/routes/(main)/+layout.svelte +++ b/src/routes/(main)/+layout.svelte @@ -6,8 +6,10 @@
- - {@render children()} - +
+ + {@render children()} + +
diff --git a/src/routes/(main)/BottomBar.svelte b/src/routes/(main)/BottomBar.svelte index f0c14db..3360a75 100644 --- a/src/routes/(main)/BottomBar.svelte +++ b/src/routes/(main)/BottomBar.svelte @@ -19,14 +19,14 @@ // TODO: Navigation -
+
{#each pages as { path, label, icon: Icon }} - {@const isCurrent = page.url.pathname.startsWith(path)} - {#if title} -

{title}

+

{title}

{/if} {#if children} {@render children?.()} diff --git a/src/lib/components/divs/BottomDiv.svelte b/src/lib/components/divs/BottomDiv.svelte index 359c54a..96edaf5 100644 --- a/src/lib/components/divs/BottomDiv.svelte +++ b/src/lib/components/divs/BottomDiv.svelte @@ -2,6 +2,6 @@ let { children } = $props(); -
+
{@render children?.()}
diff --git a/src/lib/components/divs/TitleDiv.svelte b/src/lib/components/divs/TitleDiv.svelte index 3fba02e..e622e6e 100644 --- a/src/lib/components/divs/TitleDiv.svelte +++ b/src/lib/components/divs/TitleDiv.svelte @@ -11,7 +11,7 @@
-
+
{#if Icon} {/if} diff --git a/src/routes/(fullscreen)/+layout.svelte b/src/routes/(fullscreen)/+layout.svelte index ab2b89f..8ee7a9e 100644 --- a/src/routes/(fullscreen)/+layout.svelte +++ b/src/routes/(fullscreen)/+layout.svelte @@ -5,7 +5,7 @@ -
+
{@render children()}
diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte index e89424c..075af30 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueBottomSheet.svelte @@ -13,7 +13,7 @@ -
+

암호 키 파일을 저장하셨나요?

diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte index 5e0e7c3..a35d102 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte @@ -11,7 +11,7 @@ -

+

내보내지 않고 계속할까요?

암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요.

diff --git a/src/routes/(main)/BottomBar.svelte b/src/routes/(main)/BottomBar.svelte index 3360a75..611649b 100644 --- a/src/routes/(main)/BottomBar.svelte +++ b/src/routes/(main)/BottomBar.svelte @@ -23,7 +23,7 @@ class="sticky bottom-0 h-20 w-full flex-shrink-0 rounded-t-2xl border-t border-gray-300 bg-white" > -
+
{#each pages as { path, label, icon: Icon }} {@const textColor = !page.url.pathname.startsWith(path) ? "text-gray-600" : ""} {#if title} -

{title}

- {/if} - {#if children} - {@render children?.()} +

{title}

{/if} +
+ {#if children} + {@render children?.()} + {/if} +
diff --git a/src/routes/(main)/directory/[[id]]/+page.server.ts b/src/routes/(main)/directory/[[id]]/+page.server.ts index 58d64d4..d71982e 100644 --- a/src/routes/(main)/directory/[[id]]/+page.server.ts +++ b/src/routes/(main)/directory/[[id]]/+page.server.ts @@ -1,6 +1,6 @@ import { error } from "@sveltejs/kit"; import { z } from "zod"; -import type { DirectroyInfoResponse } from "$lib/server/schemas"; +import type { DirectroyInfoResponse, FileInfoResponse } from "$lib/server/schemas"; import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ params, fetch }) => { @@ -27,7 +27,16 @@ export const load: PageServerLoad = async ({ params, fetch }) => { }; }), ); - const fileInfos = directoryInfo.files; // TODO + const fileInfos = await Promise.all( + directoryInfo.files.map(async (fileId) => { + const res = await fetch(`/api/file/${fileId}`); + if (!res.ok) error(500, "Internal server error"); + return { + ...((await res.json()) as FileInfoResponse), + id: fileId, + }; + }), + ); return { id: directoryId, diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 53f4270..ae0c95d 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -5,7 +5,12 @@ import CreateBottomSheet from "./CreateBottomSheet.svelte"; import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; import DirectoryEntry from "./DirectoryEntry.svelte"; - import { decryptDirectroyMetadata, requestDirectroyCreation, requestFileUpload } from "./service"; + import { + decryptDirectroyMetadata, + decryptFileMetadata, + requestDirectroyCreation, + requestFileUpload, + } from "./service"; import IconAdd from "~icons/material-symbols/add"; @@ -43,6 +48,20 @@ }); } }); + const files = $derived.by(() => { + const { files } = data; + if ($masterKeyStore) { + return Promise.all( + files.map(async (file) => ({ + ...(await decryptFileMetadata(file!, $masterKeyStore.get(file.mekVersion)!.key)), + id: file.id, + })), + ).then((files) => { + files.sort((a, b) => a.name.localeCompare(b.name)); + return files; + }); + } + }); const createDirectory = async (name: string) => { await requestDirectroyCreation( @@ -84,7 +103,14 @@ {#if subDirectories} {#await subDirectories then subDirectories} {#each subDirectories as { id, name }} - + + {/each} + {/await} + {/if} + {#if files} + {#await files then files} + {#each files as { id, name }} + {/each} {/await} {/if} diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte index 87c739b..69a589d 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte @@ -2,34 +2,42 @@ import { goto } from "$app/navigation"; import IconFolder from "~icons/material-symbols/folder"; + import IconDraft from "~icons/material-symbols/draft"; import IconMoreVert from "~icons/material-symbols/more-vert"; interface Props { id: number; name: string; + type: "directory" | "file"; } - let { id, name }: Props = $props(); + let { id, name, type }: Props = $props(); - const openDirectory = () => { + const open = () => { setTimeout(() => { - goto(`/directory/${id}`); + goto(`/${type}/${id}`); }, 100); }; -
-
-
- -

{name}

+
+
+
+ {#if type === "directory"} + + {:else if type === "file"} + + {/if}
+

+ {name} +

diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index 8baead9..b093e61 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -14,6 +14,7 @@ import type { DirectroyInfoResponse, DirectoryCreateRequest, FileUploadRequest, + FileInfoResponse, } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; @@ -29,6 +30,15 @@ export const decryptDirectroyMetadata = async ( }; }; +export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => { + const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); + return { + name: new TextDecoder().decode( + await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey), + ), + }; +}; + export const requestDirectroyCreation = async ( name: string, parentId: "root" | number, From 9ca6444bc96622c61cc1d5e198a5ed858c73da00 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 5 Jan 2025 18:23:34 +0900 Subject: [PATCH 083/115] =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=ED=83=90=EC=83=89=20=EC=A4=91=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=B4=20=EB=A7=8C=EB=A3=8C=EB=90=90?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=EC=85=98=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callApi.ts | 55 ++++++++++++------- .../[[id]]/{+page.server.ts => +page.ts} | 11 ++-- 2 files changed, 40 insertions(+), 26 deletions(-) rename src/routes/(main)/directory/[[id]]/{+page.server.ts => +page.ts} (76%) diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts index c36b59a..933bbb0 100644 --- a/src/lib/hooks/callApi.ts +++ b/src/lib/hooks/callApi.ts @@ -1,41 +1,54 @@ import { signRequestBody } from "$lib/modules/crypto"; -export const refreshToken = async () => { - return await fetch("/api/auth/refreshToken", { method: "POST" }); +export const refreshToken = async (fetchInternal = fetch) => { + return await fetchInternal("/api/auth/refreshToken", { method: "POST" }); }; -const callApi = async (input: RequestInfo, init?: RequestInit) => { - let res = await fetch(input, init); +const callApi = async (input: RequestInfo, init?: RequestInit, fetchInternal = fetch) => { + let res = await fetchInternal(input, init); if (res.status === 401) { res = await refreshToken(); if (!res.ok) { return res; } - res = await fetch(input, init); + res = await fetchInternal(input, init); } return res; }; -export const callGetApi = async (input: RequestInfo) => { - return await callApi(input); +export const callGetApi = async (input: RequestInfo, fetchInternal?: typeof fetch) => { + return await callApi(input, undefined, fetchInternal); }; -export const callPostApi = async (input: RequestInfo, payload: T) => { - return await callApi(input, { - method: "POST", - headers: { - "Content-Type": "application/json", +export const callPostApi = async ( + input: RequestInfo, + payload: T, + fetchInternal?: typeof fetch, +) => { + return await callApi( + input, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), }, - body: JSON.stringify(payload), - }); + fetchInternal, + ); }; -export const callSignedPostApi = async (input: RequestInfo, payload: T, signKey: CryptoKey) => { - return await callApi(input, { - method: "POST", - headers: { - "Content-Type": "application/json", +export const callSignedPostApi = async ( + input: RequestInfo, + payload: T, + signKey: CryptoKey, + fetchInternal?: typeof fetch, +) => { + return await callApi( + input, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: await signRequestBody(payload, signKey), }, - body: await signRequestBody(payload, signKey), - }); + fetchInternal, + ); }; diff --git a/src/routes/(main)/directory/[[id]]/+page.server.ts b/src/routes/(main)/directory/[[id]]/+page.ts similarity index 76% rename from src/routes/(main)/directory/[[id]]/+page.server.ts rename to src/routes/(main)/directory/[[id]]/+page.ts index d71982e..d80e3e9 100644 --- a/src/routes/(main)/directory/[[id]]/+page.server.ts +++ b/src/routes/(main)/directory/[[id]]/+page.ts @@ -1,9 +1,10 @@ import { error } from "@sveltejs/kit"; import { z } from "zod"; +import { callGetApi } from "$lib/hooks"; import type { DirectroyInfoResponse, FileInfoResponse } from "$lib/server/schemas"; -import type { PageServerLoad } from "./$types"; +import type { PageLoad } from "./$types"; -export const load: PageServerLoad = async ({ params, fetch }) => { +export const load: PageLoad = async ({ params, fetch }) => { const zodRes = z .object({ id: z.coerce.number().int().positive().optional(), @@ -13,13 +14,13 @@ export const load: PageServerLoad = async ({ params, fetch }) => { const { id } = zodRes.data; const directoryId = id ? id : ("root" as const); - const res = await fetch(`/api/directory/${directoryId}`); + const res = await callGetApi(`/api/directory/${directoryId}`, fetch); if (!res.ok) error(404, "Not found"); const directoryInfo: DirectroyInfoResponse = await res.json(); const subDirectoryInfos = await Promise.all( directoryInfo.subDirectories.map(async (subDirectoryId) => { - const res = await fetch(`/api/directory/${subDirectoryId}`); + const res = await callGetApi(`/api/directory/${subDirectoryId}`, fetch); if (!res.ok) error(500, "Internal server error"); return { ...((await res.json()) as DirectroyInfoResponse), @@ -29,7 +30,7 @@ export const load: PageServerLoad = async ({ params, fetch }) => { ); const fileInfos = await Promise.all( directoryInfo.files.map(async (fileId) => { - const res = await fetch(`/api/file/${fileId}`); + const res = await callGetApi(`/api/file/${fileId}`, fetch); if (!res.ok) error(500, "Internal server error"); return { ...((await res.json()) as FileInfoResponse), From c580556740f0e8ed3ea38599e8b3346a61b05b20 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 5 Jan 2025 20:45:31 +0900 Subject: [PATCH 084/115] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=9E=84=EC=8B=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/crypto/aes.ts | 11 ++++- src/lib/modules/crypto/util.ts | 11 +++++ src/lib/services/file.ts | 10 +++++ .../(fullscreen)/file/[id]/+page.svelte | 42 +++++++++++++++++++ src/routes/(fullscreen)/file/[id]/+page.ts | 24 +++++++++++ src/routes/(fullscreen)/file/[id]/service.ts | 33 +++++++++++++++ src/routes/(main)/directory/[[id]]/service.ts | 24 ++++------- 7 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 src/lib/services/file.ts create mode 100644 src/routes/(fullscreen)/file/[id]/+page.svelte create mode 100644 src/routes/(fullscreen)/file/[id]/+page.ts create mode 100644 src/routes/(fullscreen)/file/[id]/service.ts diff --git a/src/lib/modules/crypto/aes.ts b/src/lib/modules/crypto/aes.ts index e1e917a..7301373 100644 --- a/src/lib/modules/crypto/aes.ts +++ b/src/lib/modules/crypto/aes.ts @@ -1,4 +1,4 @@ -import { encodeToBase64, decodeFromBase64 } from "./util"; +import { encodeString, decodeString, encodeToBase64, decodeFromBase64 } from "./util"; export const generateMasterKey = async () => { return { @@ -81,3 +81,12 @@ export const decryptData = async (ciphertext: BufferSource, iv: string, dataKey: ciphertext, ); }; + +export const encryptString = async (plaintext: string, dataKey: CryptoKey) => { + const { ciphertext, iv } = await encryptData(encodeString(plaintext), dataKey); + return { ciphertext: encodeToBase64(ciphertext), iv }; +}; + +export const decryptString = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { + return decodeString(await decryptData(decodeFromBase64(ciphertext), iv, dataKey)); +}; diff --git a/src/lib/modules/crypto/util.ts b/src/lib/modules/crypto/util.ts index 9aeeb9d..a3e3bc0 100644 --- a/src/lib/modules/crypto/util.ts +++ b/src/lib/modules/crypto/util.ts @@ -1,3 +1,14 @@ +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +export const encodeString = (data: string) => { + return textEncoder.encode(data); +}; + +export const decodeString = (data: ArrayBuffer) => { + return textDecoder.decode(data); +}; + export const encodeToBase64 = (data: ArrayBuffer) => { return btoa(String.fromCharCode(...new Uint8Array(data))); }; diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts new file mode 100644 index 0000000..adaa524 --- /dev/null +++ b/src/lib/services/file.ts @@ -0,0 +1,10 @@ +import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; +import type { FileInfoResponse } from "$lib/server/schemas"; + +export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => { + const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); + return { + dataKey, + name: await decryptString(metadata.name, metadata.nameIv, dataKey), + }; +}; diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte new file mode 100644 index 0000000..380dfb2 --- /dev/null +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -0,0 +1,42 @@ + + + + 파일 + + +{#if metadata} + +{:else} + +{/if} diff --git a/src/routes/(fullscreen)/file/[id]/+page.ts b/src/routes/(fullscreen)/file/[id]/+page.ts new file mode 100644 index 0000000..6ecfe77 --- /dev/null +++ b/src/routes/(fullscreen)/file/[id]/+page.ts @@ -0,0 +1,24 @@ +import { error } from "@sveltejs/kit"; +import { z } from "zod"; +import { callGetApi } from "$lib/hooks"; +import type { FileInfoResponse } from "$lib/server/schemas"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ params, fetch }) => { + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(404, "Not found"); + const { id } = zodRes.data; + + const res = await callGetApi(`/api/file/${id}`, fetch); + if (!res.ok) error(404, "Not found"); + + const fileInfo: FileInfoResponse = await res.json(); + return { + id, + metadata: fileInfo, + }; +}; diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts new file mode 100644 index 0000000..16aba8b --- /dev/null +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -0,0 +1,33 @@ +import { decryptData } from "$lib/modules/crypto"; + +export { decryptFileMetadata } from "$lib/services/file"; + +export const requestFileDownload = ( + fileId: number, + fileEncryptedIv: string, + dataKey: CryptoKey, +) => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.responseType = "arraybuffer"; + + xhr.addEventListener("load", async () => { + if (xhr.status !== 200) { + reject(new Error("Failed to download file")); + return; + } + + const fileDecrypted = await decryptData( + xhr.response as ArrayBuffer, + fileEncryptedIv, + dataKey, + ); + resolve(fileDecrypted); + }); + + // TODO: Progress, ... + + xhr.open("GET", `/api/file/${fileId}/download`); + xhr.send(); + }); +}; diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index b093e61..d7cae06 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -1,12 +1,12 @@ import { callSignedPostApi } from "$lib/hooks"; import { encodeToBase64, - decodeFromBase64, generateDataKey, wrapDataKey, unwrapDataKey, encryptData, - decryptData, + encryptString, + decryptString, digestMessage, signRequestBody, } from "$lib/modules/crypto"; @@ -14,28 +14,18 @@ import type { DirectroyInfoResponse, DirectoryCreateRequest, FileUploadRequest, - FileInfoResponse, } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; +export { decryptFileMetadata } from "$lib/services/file"; + export const decryptDirectroyMetadata = async ( metadata: NonNullable, masterKey: CryptoKey, ) => { const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); return { - name: new TextDecoder().decode( - await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey), - ), - }; -}; - -export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => { - const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); - return { - name: new TextDecoder().decode( - await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey), - ), + name: await decryptString(metadata.name, metadata.nameIv, dataKey), }; }; @@ -69,7 +59,7 @@ export const requestFileUpload = async ( const { dataKey } = await generateDataKey(); const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey); const fileEncryptedHash = await digestMessage(fileEncrypted.ciphertext); - const nameEncrypted = await encryptData(new TextEncoder().encode(file.name), dataKey); + const nameEncrypted = await encryptString(file.name, dataKey); const form = new FormData(); form.set( @@ -81,7 +71,7 @@ export const requestFileUpload = async ( dek: await wrapDataKey(dataKey, masterKey.key), contentHash: encodeToBase64(fileEncryptedHash), contentIv: fileEncrypted.iv, - name: encodeToBase64(nameEncrypted.ciphertext), + name: nameEncrypted.ciphertext, nameIv: nameEncrypted.iv, }, signKey, From 14d1adc416c457449ce748225a139a99cd1f1ff5 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 5 Jan 2025 22:59:11 +0900 Subject: [PATCH 085/115] =?UTF-8?q?=ED=8C=8C=EC=9D=BC/=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/BottomSheet.svelte | 17 ++--- .../(main)/directory/[[id]]/+page.svelte | 56 +++++++++++++++-- .../directory/[[id]]/CreateBottomSheet.svelte | 10 +-- .../[[id]]/DeleteDirectoryEntryModal.svelte | 60 ++++++++++++++++++ .../directory/[[id]]/DirectoryEntry.svelte | 16 ++--- .../DirectoryEntryMenuBottomSheet.svelte | 62 +++++++++++++++++++ .../[[id]]/RenameDirectoryEntryModal.svelte | 46 ++++++++++++++ 7 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte create mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte create mode 100644 src/routes/(main)/directory/[[id]]/RenameDirectoryEntryModal.svelte diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte index 471643a..e92c842 100644 --- a/src/lib/components/BottomSheet.svelte +++ b/src/lib/components/BottomSheet.svelte @@ -5,21 +5,24 @@ interface Props { children: Snippet; + onclose?: () => void; isOpen: boolean; } - let { children, isOpen = $bindable() }: Props = $props(); + let { children, onclose, isOpen = $bindable() }: Props = $props(); + + const closeBottomSheet = $derived( + onclose || + (() => { + isOpen = false; + }), + ); {#if isOpen} -
{ - isOpen = false; - }} - class="fixed inset-0 z-10 flex items-end justify-center" - > +
diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index ae0c95d..51bab14 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,10 +1,22 @@ + +
- +

폴더 만들기

- +

파일 업로드

diff --git a/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte b/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte new file mode 100644 index 0000000..8904f83 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte @@ -0,0 +1,60 @@ + + + + {#if selectedEntry} + {@const { type, name } = selectedEntry} + {@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name} +
+
+

+ {#if type === "directory"} + '{nameShort}' 폴더를 삭제할까요? + {:else} + '{nameShort}' 파일을 삭제할까요? + {/if} +

+

+ {#if type === "directory"} + 삭제한 폴더는 복구할 수 없어요.
+ 폴더 안의 모든 파일과 폴더도 함께 삭제돼요. + {:else} + 삭제한 파일은 복구할 수 없어요. + {/if} +

+
+
+ + +
+
+ {/if} +
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte index 69a589d..29f11d0 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte @@ -1,28 +1,28 @@ -
+
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl">
{#if type === "directory"} @@ -36,7 +36,7 @@

+ +
+
+ From 9fad26d53888de0450cae21a68f47f6965af9fe5 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 03:05:31 +0900 Subject: [PATCH 086/115] =?UTF-8?q?/api/file/[id]/delete,=20/api/file/[id]?= =?UTF-8?q?/rename,=20/api/directory/[id]/delete,=20/api/directory/[id]/re?= =?UTF-8?q?name=20Endpoint=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/file.ts | 63 +++++++++++++++++ src/lib/server/schemas/directory.ts | 6 ++ src/lib/server/schemas/file.ts | 6 ++ src/lib/server/services/directory.ts | 68 +++++++++++++++++++ src/lib/server/services/file.ts | 55 +++++++-------- src/routes/api/directory/[id]/+server.ts | 2 +- .../api/directory/[id]/delete/+server.ts | 20 ++++++ .../api/directory/[id]/rename/+server.ts | 27 ++++++++ src/routes/api/directory/create/+server.ts | 2 +- src/routes/api/file/[id]/delete/+server.ts | 20 ++++++ src/routes/api/file/[id]/rename/+server.ts | 27 ++++++++ 11 files changed, 263 insertions(+), 33 deletions(-) create mode 100644 src/lib/server/services/directory.ts create mode 100644 src/routes/api/directory/[id]/delete/+server.ts create mode 100644 src/routes/api/directory/[id]/rename/+server.ts create mode 100644 src/routes/api/file/[id]/delete/+server.ts create mode 100644 src/routes/api/file/[id]/rename/+server.ts diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index a85dcab..0b1d5bd 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -69,6 +69,47 @@ export const getDirectory = async (userId: number, directoryId: number) => { return res[0] ?? null; }; +export const setDirectoryEncName = async ( + userId: number, + directoryId: number, + encName: string, + encNameIv: string, +) => { + await db + .update(directory) + .set({ encName: { ciphertext: encName, iv: encNameIv } }) + .where(and(eq(directory.userId, userId), eq(directory.id, directoryId))) + .execute(); +}; + +export const unregisterDirectory = async (userId: number, directoryId: number) => { + return await db.transaction(async (tx) => { + const getFilePaths = async (parentId: number) => { + const files = await tx + .select({ path: file.path }) + .from(file) + .where(and(eq(file.userId, userId), eq(file.parentId, parentId))); + return files.map(({ path }) => path); + }; + const unregisterSubDirectoriesRecursively = async (directoryId: number): Promise => { + const subDirectories = await tx + .select({ id: directory.id }) + .from(directory) + .where(and(eq(directory.userId, userId), eq(directory.parentId, directoryId))); + const subDirectoryFilePaths = await Promise.all( + subDirectories.map(async ({ id }) => await unregisterSubDirectoriesRecursively(id)), + ); + const filePaths = await getFilePaths(directoryId); + + await tx.delete(file).where(eq(file.parentId, directoryId)); + await tx.delete(directory).where(eq(directory.id, directoryId)); + + return filePaths.concat(...subDirectoryFilePaths); + }; + return await unregisterSubDirectoriesRecursively(directoryId); + }); +}; + export const registerNewFile = async (params: NewFileParams) => { await db.transaction(async (tx) => { const meks = await tx @@ -115,3 +156,25 @@ export const getFile = async (userId: number, fileId: number) => { .execute(); return res[0] ?? null; }; + +export const setFileEncName = async ( + userId: number, + fileId: number, + encName: string, + encNameIv: string, +) => { + await db + .update(file) + .set({ encName: { ciphertext: encName, iv: encNameIv } }) + .where(and(eq(file.userId, userId), eq(file.id, fileId))) + .execute(); +}; + +export const unregisterFile = async (userId: number, fileId: number) => { + const res = await db + .delete(file) + .where(and(eq(file.userId, userId), eq(file.id, fileId))) + .returning({ path: file.path }) + .execute(); + return res[0]?.path ?? null; +}; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index b4594c9..cad0f0c 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +export const directoryRenameRequest = z.object({ + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), +}); +export type DirectoryRenameRequest = z.infer; + export const directroyInfoResponse = z.object({ metadata: z .object({ diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index a9ba7e4..b753517 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +export const fileRenameRequest = z.object({ + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), +}); +export type FileRenameRequest = z.infer; + export const fileInfoResponse = z.object({ createdAt: z.date(), mekVersion: z.number().int().positive(), diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts new file mode 100644 index 0000000..ede995b --- /dev/null +++ b/src/lib/server/services/directory.ts @@ -0,0 +1,68 @@ +import { error } from "@sveltejs/kit"; +import { unlink } from "fs/promises"; +import { + getAllDirectoriesByParent, + registerNewDirectory, + getDirectory, + setDirectoryEncName, + unregisterDirectory, + getAllFilesByParent, + type NewDirectroyParams, +} from "$lib/server/db/file"; +import { getActiveMekVersion } from "$lib/server/db/mek"; + +export const deleteDirectory = async (userId: number, directoryId: number) => { + const directory = await getDirectory(userId, directoryId); + if (!directory) { + error(404, "Invalid directory id"); + } + + const filePaths = await unregisterDirectory(userId, directoryId); + filePaths.map((path) => unlink(path)); // Intended +}; + +export const renameDirectory = async ( + userId: number, + directoryId: number, + newEncName: string, + newEncNameIv: string, +) => { + const directory = await getDirectory(userId, directoryId); + if (!directory) { + error(404, "Invalid directory id"); + } + + await setDirectoryEncName(userId, directoryId, newEncName, newEncNameIv); +}; + +export const getDirectroyInformation = async (userId: number, directroyId: "root" | number) => { + const directory = directroyId !== "root" ? await getDirectory(userId, directroyId) : undefined; + if (directory === null) { + error(404, "Invalid directory id"); + } + + const directories = await getAllDirectoriesByParent(userId, directroyId); + const files = await getAllFilesByParent(userId, directroyId); + + return { + metadata: directory && { + createdAt: directory.createdAt, + mekVersion: directory.mekVersion, + encDek: directory.encDek, + encName: directory.encName, + }, + directories: directories.map(({ id }) => id), + files: files.map(({ id }) => id), + }; +}; + +export const createDirectory = async (params: NewDirectroyParams) => { + const activeMekVersion = await getActiveMekVersion(params.userId); + if (activeMekVersion === null) { + error(500, "Invalid MEK version"); + } else if (activeMekVersion !== params.mekVersion) { + error(400, "Invalid MEK version"); + } + + await registerNewDirectory(params); +}; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index fda36a6..e98a642 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -5,48 +5,27 @@ import { mkdir, stat, unlink } from "fs/promises"; import { dirname } from "path"; import { v4 as uuidv4 } from "uuid"; import { - getAllDirectoriesByParent, - registerNewDirectory, - getDirectory, registerNewFile, - getAllFilesByParent, getFile, - type NewDirectroyParams, + setFileEncName, + unregisterFile, type NewFileParams, } from "$lib/server/db/file"; import { getActiveMekVersion } from "$lib/server/db/mek"; import env from "$lib/server/loadenv"; -export const getDirectroyInformation = async (userId: number, directroyId: "root" | number) => { - const directory = directroyId !== "root" ? await getDirectory(userId, directroyId) : undefined; - if (directory === null) { - error(404, "Invalid directory id"); +export const deleteFile = async (userId: number, fileId: number) => { + const file = await getFile(userId, fileId); + if (!file) { + error(404, "Invalid file id"); } - const directories = await getAllDirectoriesByParent(userId, directroyId); - const files = await getAllFilesByParent(userId, directroyId); - - return { - metadata: directory && { - createdAt: directory.createdAt, - mekVersion: directory.mekVersion, - encDek: directory.encDek, - encName: directory.encName, - }, - directories: directories.map(({ id }) => id), - files: files.map(({ id }) => id), - }; -}; - -export const createDirectory = async (params: NewDirectroyParams) => { - const activeMekVersion = await getActiveMekVersion(params.userId); - if (activeMekVersion === null) { - error(500, "Invalid MEK version"); - } else if (activeMekVersion !== params.mekVersion) { - error(400, "Invalid MEK version"); + const path = await unregisterFile(userId, fileId); + if (!path) { + error(500, "Invalid file id"); } - await registerNewDirectory(params); + unlink(path); // Intended }; const convertToReadableStream = (readStream: ReadStream) => { @@ -75,6 +54,20 @@ export const getFileStream = async (userId: number, fileId: number) => { }; }; +export const renameFile = async ( + userId: number, + fileId: number, + newEncName: string, + newEncNameIv: string, +) => { + const file = await getFile(userId, fileId); + if (!file) { + error(404, "Invalid file id"); + } + + await setFileEncName(userId, fileId, newEncName, newEncNameIv); +}; + export const getFileInformation = async (userId: number, fileId: number) => { const file = await getFile(userId, fileId); if (!file) { diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts index 108a3b1..806ea4d 100644 --- a/src/routes/api/directory/[id]/+server.ts +++ b/src/routes/api/directory/[id]/+server.ts @@ -2,7 +2,7 @@ import { error, json } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; import { directroyInfoResponse, type DirectroyInfoResponse } from "$lib/server/schemas"; -import { getDirectroyInformation } from "$lib/server/services/file"; +import { getDirectroyInformation } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ cookies, params }) => { diff --git a/src/routes/api/directory/[id]/delete/+server.ts b/src/routes/api/directory/[id]/delete/+server.ts new file mode 100644 index 0000000..c7777df --- /dev/null +++ b/src/routes/api/directory/[id]/delete/+server.ts @@ -0,0 +1,20 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { deleteDirectory } from "$lib/server/services/directory"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ cookies, params }) => { + const { userId } = await authorize(cookies, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + await deleteDirectory(userId, id); + return text("Directory deleted", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts new file mode 100644 index 0000000..89fa98d --- /dev/null +++ b/src/routes/api/directory/[id]/rename/+server.ts @@ -0,0 +1,27 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { parseSignedRequest } from "$lib/server/modules/crypto"; +import { directoryRenameRequest } from "$lib/server/schemas"; +import { renameDirectory } from "$lib/server/services/directory"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, cookies, params }) => { + const { userId, clientId } = await authorize(cookies, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + const { name, nameIv } = await parseSignedRequest( + clientId, + await request.json(), + directoryRenameRequest, + ); + + await renameDirectory(userId, id, name, nameIv); + return text("Directory renamed", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index ca13705..559d34b 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -2,7 +2,7 @@ import { text } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; import { parseSignedRequest } from "$lib/server/modules/crypto"; import { directoryCreateRequest } from "$lib/server/schemas"; -import { createDirectory } from "$lib/server/services/file"; +import { createDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { diff --git a/src/routes/api/file/[id]/delete/+server.ts b/src/routes/api/file/[id]/delete/+server.ts new file mode 100644 index 0000000..4cbf733 --- /dev/null +++ b/src/routes/api/file/[id]/delete/+server.ts @@ -0,0 +1,20 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { deleteFile } from "$lib/server/services/file"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ cookies, params }) => { + const { userId } = await authorize(cookies, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + await deleteFile(userId, id); + return text("File deleted", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts new file mode 100644 index 0000000..5c816a8 --- /dev/null +++ b/src/routes/api/file/[id]/rename/+server.ts @@ -0,0 +1,27 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { parseSignedRequest } from "$lib/server/modules/crypto"; +import { fileRenameRequest } from "$lib/server/schemas"; +import { renameFile } from "$lib/server/services/file"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, cookies, params }) => { + const { userId, clientId } = await authorize(cookies, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + const { name, nameIv } = await parseSignedRequest( + clientId, + await request.json(), + fileRenameRequest, + ); + + await renameFile(userId, id, name, nameIv); + return text("File renamed", { headers: { "Content-Type": "text/plain" } }); +}; From 6bf40e4ab46c52e4e9df75927e7408b1e4203354 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 03:47:33 +0900 Subject: [PATCH 087/115] =?UTF-8?q?Request=20=EC=84=9C=EB=AA=85=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보안에 큰 도움이 되지 않는다고 판단하여 삭제하였습니다. 판단 근거는 다음과 같습니다. 1. Web Crypto API는 HTTPS 환경에서만 사용할 수 있음 2. 프론트엔드와 백엔드가 하나의 서버에서 제공되므로, 리버스 프록시에 의한 중간자 공격을 받지 않는가에 대한 직관적인 검증이 불가능함 3. 신뢰할 수 없는 리버스 프록시는 애초에 사용하지 않는 것이 맞음 다만 MEK에 대한 서명 등은 그대로 유지됩니다. --- src/lib/hooks/callApi.ts | 19 -------- src/lib/modules/crypto/rsa.ts | 9 ---- src/lib/server/modules/crypto.ts | 31 ------------- src/lib/server/schemas/file.ts | 1 - src/lib/server/services/file.ts | 26 +---------- src/routes/(fullscreen)/key/export/service.ts | 14 +++--- .../(main)/directory/[[id]]/+page.svelte | 11 ++--- src/routes/(main)/directory/[[id]]/service.ts | 45 +++++++------------ .../api/directory/[id]/rename/+server.ts | 18 ++++---- src/routes/api/directory/create/+server.ts | 14 +++--- src/routes/api/file/[id]/rename/+server.ts | 18 ++++---- src/routes/api/file/upload/+server.ts | 17 +++---- .../api/mek/register/initial/+server.ts | 9 ++-- 13 files changed, 57 insertions(+), 175 deletions(-) diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts index 933bbb0..d2987a9 100644 --- a/src/lib/hooks/callApi.ts +++ b/src/lib/hooks/callApi.ts @@ -1,5 +1,3 @@ -import { signRequestBody } from "$lib/modules/crypto"; - export const refreshToken = async (fetchInternal = fetch) => { return await fetchInternal("/api/auth/refreshToken", { method: "POST" }); }; @@ -35,20 +33,3 @@ export const callPostApi = async ( fetchInternal, ); }; - -export const callSignedPostApi = async ( - input: RequestInfo, - payload: T, - signKey: CryptoKey, - fetchInternal?: typeof fetch, -) => { - return await callApi( - input, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: await signRequestBody(payload, signKey), - }, - fetchInternal, - ); -}; diff --git a/src/lib/modules/crypto/rsa.ts b/src/lib/modules/crypto/rsa.ts index a82abee..c4a7be5 100644 --- a/src/lib/modules/crypto/rsa.ts +++ b/src/lib/modules/crypto/rsa.ts @@ -122,15 +122,6 @@ export const verifySignature = async ( ); }; -export const signRequestBody = async (requestBody: T, signKey: CryptoKey) => { - const dataBuffer = new TextEncoder().encode(JSON.stringify(requestBody)); - const signature = await signMessage(dataBuffer, signKey); - return JSON.stringify({ - data: requestBody, - signature: encodeToBase64(signature), - }); -}; - export const signMasterKeyWrapped = async ( masterKeyVersion: number, masterKeyWrapped: string, diff --git a/src/lib/server/modules/crypto.ts b/src/lib/server/modules/crypto.ts index 38efc7f..de3dbf4 100644 --- a/src/lib/server/modules/crypto.ts +++ b/src/lib/server/modules/crypto.ts @@ -1,8 +1,5 @@ -import { error } from "@sveltejs/kit"; import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto"; import { promisify } from "util"; -import { z } from "zod"; -import { getClient } from "$lib/server/db/client"; const makePubKeyToPem = (pubKey: string) => `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; @@ -37,31 +34,3 @@ export const generateChallenge = async (length: number, encPubKey: string) => { const challenge = encryptAsymmetric(answer, encPubKey); return { answer, challenge }; }; - -export const parseSignedRequest = async ( - clientId: number, - data: unknown, - schema: T, -) => { - const zodRes = z - .object({ - data: schema, - signature: z.string().base64().nonempty(), - }) - .safeParse(data); - if (!zodRes.success) error(400, "Invalid request body"); - - const { data: parsedData, signature } = zodRes.data; - if (!parsedData) error(500, "Invalid request body"); - - const client = await getClient(clientId); - if (!client) { - error(500, "Invalid access token"); - } else if ( - !verifySignature(Buffer.from(JSON.stringify(parsedData)), signature, client.sigPubKey) - ) { - error(400, "Invalid signature"); - } - - return parsedData; -}; diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index b753517..5c43f00 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -20,7 +20,6 @@ export const fileUploadRequest = z.object({ parentId: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), - contentHash: z.string().base64().nonempty(), contentIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index e98a642..a884dac 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -1,5 +1,4 @@ import { error } from "@sveltejs/kit"; -import { createHash } from "crypto"; import { createReadStream, createWriteStream, ReadStream, WriteStream } from "fs"; import { mkdir, stat, unlink } from "fs/promises"; import { dirname } from "path"; @@ -106,7 +105,6 @@ const safeUnlink = async (path: string) => { export const uploadFile = async ( params: Omit, encContentStream: ReadableStream, - encContentHash: string, ) => { const activeMekVersion = await getActiveMekVersion(params.userId); if (activeMekVersion === null) { @@ -116,32 +114,12 @@ export const uploadFile = async ( } const path = `${env.libraryPath}/${params.userId}/${uuidv4()}`; - const hash = createHash("sha256"); - await mkdir(dirname(path), { recursive: true }); try { - const hashStream = new TransformStream({ - transform: (chunk, controller) => { - hash.update(chunk); - controller.enqueue(chunk); - }, - }); - const fileStream = convertToWritableStream( - createWriteStream(path, { flags: "wx", mode: 0o600 }), + await encContentStream.pipeTo( + convertToWritableStream(createWriteStream(path, { flags: "wx", mode: 0o600 })), ); - await encContentStream.pipeThrough(hashStream).pipeTo(fileStream); - } catch (e) { - await safeUnlink(path); - throw e; - } - - if (hash.digest("base64") !== encContentHash) { - await safeUnlink(path); - error(400, "Invalid content hash"); - } - - try { await registerNewFile({ ...params, path, diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 415eb77..a96b4be 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,4 +1,4 @@ -import { callSignedPostApi } from "$lib/hooks"; +import { callPostApi } from "$lib/hooks"; import { storeClientKey } from "$lib/indexedDB"; import { signMasterKeyWrapped } from "$lib/modules/crypto"; import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; @@ -46,13 +46,9 @@ export const requestInitialMasterKeyRegistration = async ( masterKeyWrapped: string, signKey: CryptoKey, ) => { - const res = await callSignedPostApi( - "/api/mek/register/initial", - { - mek: masterKeyWrapped, - mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), - }, - signKey, - ); + const res = await callPostApi("/api/mek/register/initial", { + mek: masterKeyWrapped, + mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), + }); return res.ok || res.status === 409; }; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 51bab14..449c47b 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -10,7 +10,7 @@ import { goto } from "$app/navigation"; import { TopBar } from "$lib/components"; import { FloatingButton } from "$lib/components/buttons"; - import { clientKeyStore, masterKeyStore } from "$lib/stores"; + import { masterKeyStore } from "$lib/stores"; import CreateBottomSheet from "./CreateBottomSheet.svelte"; import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte"; @@ -81,12 +81,7 @@ }); const createDirectory = async (name: string) => { - await requestDirectroyCreation( - name, - data.id, - $masterKeyStore?.get(1)!, - $clientKeyStore?.signKey!, - ); + await requestDirectroyCreation(name, data.id, $masterKeyStore?.get(1)!); isCreateDirectoryModalOpen = false; }; @@ -94,7 +89,7 @@ const file = fileInput?.files?.[0]; if (!file) return; - requestFileUpload(file, data.id, $masterKeyStore?.get(1)!, $clientKeyStore?.signKey!); + requestFileUpload(file, data.id, $masterKeyStore?.get(1)!); }; diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index d7cae06..4c17228 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -1,4 +1,4 @@ -import { callSignedPostApi } from "$lib/hooks"; +import { callPostApi } from "$lib/hooks"; import { encodeToBase64, generateDataKey, @@ -7,8 +7,6 @@ import { encryptData, encryptString, decryptString, - digestMessage, - signRequestBody, } from "$lib/modules/crypto"; import type { DirectroyInfoResponse, @@ -33,49 +31,38 @@ export const requestDirectroyCreation = async ( name: string, parentId: "root" | number, masterKey: MasterKey, - signKey: CryptoKey, ) => { const { dataKey } = await generateDataKey(); const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey); - return await callSignedPostApi( - "/api/directory/create", - { - parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - name: encodeToBase64(nameEncrypted.ciphertext), - nameIv: nameEncrypted.iv, - }, - signKey, - ); + return await callPostApi("/api/directory/create", { + parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + name: encodeToBase64(nameEncrypted.ciphertext), + nameIv: nameEncrypted.iv, + }); }; export const requestFileUpload = async ( file: File, parentId: "root" | number, masterKey: MasterKey, - signKey: CryptoKey, ) => { const { dataKey } = await generateDataKey(); const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey); - const fileEncryptedHash = await digestMessage(fileEncrypted.ciphertext); const nameEncrypted = await encryptString(file.name, dataKey); const form = new FormData(); form.set( "metadata", - await signRequestBody( - { - parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - contentHash: encodeToBase64(fileEncryptedHash), - contentIv: fileEncrypted.iv, - name: nameEncrypted.ciphertext, - nameIv: nameEncrypted.iv, - }, - signKey, - ), + JSON.stringify({ + parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + contentIv: fileEncrypted.iv, + name: nameEncrypted.ciphertext, + nameIv: nameEncrypted.iv, + } satisfies FileUploadRequest), ); form.set("content", new Blob([fileEncrypted.ciphertext])); diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts index 89fa98d..ee52ac5 100644 --- a/src/routes/api/directory/[id]/rename/+server.ts +++ b/src/routes/api/directory/[id]/rename/+server.ts @@ -1,26 +1,24 @@ import { error, text } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { directoryRenameRequest } from "$lib/server/schemas"; import { renameDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies, params }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); + const { userId } = await authorize(cookies, "activeClient"); - const zodRes = z + const paramsZodRes = z .object({ id: z.coerce.number().int().positive(), }) .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - const { name, nameIv } = await parseSignedRequest( - clientId, - await request.json(), - directoryRenameRequest, - ); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = directoryRenameRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { name, nameIv } = bodyZodRes.data; await renameDirectory(userId, id, name, nameIv); return text("Directory renamed", { headers: { "Content-Type": "text/plain" } }); diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index 559d34b..0f97117 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -1,17 +1,15 @@ -import { text } from "@sveltejs/kit"; +import { error, text } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { directoryCreateRequest } from "$lib/server/schemas"; import { createDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); - const { parentId, mekVersion, dek, name, nameIv } = await parseSignedRequest( - clientId, - await request.json(), - directoryCreateRequest, - ); + const { userId } = await authorize(cookies, "activeClient"); + + const zodRes = directoryCreateRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { parentId, mekVersion, dek, name, nameIv } = zodRes.data; await createDirectory({ userId, diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts index 5c816a8..d9bcd60 100644 --- a/src/routes/api/file/[id]/rename/+server.ts +++ b/src/routes/api/file/[id]/rename/+server.ts @@ -1,26 +1,24 @@ import { error, text } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { fileRenameRequest } from "$lib/server/schemas"; import { renameFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies, params }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); + const { userId } = await authorize(cookies, "activeClient"); - const zodRes = z + const paramsZodRes = z .object({ id: z.coerce.number().int().positive(), }) .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - const { name, nameIv } = await parseSignedRequest( - clientId, - await request.json(), - fileRenameRequest, - ); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = fileRenameRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { name, nameIv } = bodyZodRes.data; await renameFile(userId, id, name, nameIv); return text("File renamed", { headers: { "Content-Type": "text/plain" } }); diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index 1cf9e87..0cd1cab 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -1,27 +1,23 @@ import { error, text } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { fileUploadRequest } from "$lib/server/schemas"; import { uploadFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); + const { userId } = await authorize(cookies, "activeClient"); const form = await request.formData(); - const metadata = form.get("metadata"); - if (!metadata || typeof metadata !== "string") { - error(400, "Invalid request body"); - } - const { parentId, mekVersion, dek, contentHash, contentIv, name, nameIv } = - await parseSignedRequest(clientId, JSON.parse(metadata), fileUploadRequest); - const content = form.get("content"); - if (!content || !(content instanceof File)) { + if (typeof metadata !== "string" || !(content instanceof File)) { error(400, "Invalid request body"); } + const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata)); + if (!zodRes.success) error(400, "Invalid request body"); + const { parentId, mekVersion, dek, contentIv, name, nameIv } = zodRes.data; + await uploadFile( { userId, @@ -33,7 +29,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => { encNameIv: nameIv, }, content.stream(), - contentHash, ); return text("File uploaded", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/mek/register/initial/+server.ts b/src/routes/api/mek/register/initial/+server.ts index c39ef37..ba959fb 100644 --- a/src/routes/api/mek/register/initial/+server.ts +++ b/src/routes/api/mek/register/initial/+server.ts @@ -1,6 +1,5 @@ import { error, text } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { initialMasterKeyRegisterRequest } from "$lib/server/schemas"; import { registerInitialActiveMek } from "$lib/server/services/mek"; import type { RequestHandler } from "./$types"; @@ -11,11 +10,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => { error(403, "Forbidden"); } - const { mek, mekSig } = await parseSignedRequest( - clientId, - await request.json(), - initialMasterKeyRegisterRequest, - ); + 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" } }); From bd0dd3343af6bdc122f2835be0bcc3eb71d3838b Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 14:12:23 +0900 Subject: [PATCH 088/115] =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/file.ts | 14 +++++++------- src/lib/server/schemas/directory.ts | 4 ++-- src/lib/server/services/directory.ts | 12 ++++++------ src/lib/services/key.ts | 4 ++-- src/routes/(fullscreen)/key/export/+page.svelte | 2 +- src/routes/(main)/directory/[[id]]/+page.svelte | 14 +++++++------- src/routes/(main)/directory/[[id]]/+page.ts | 6 +++--- .../[[id]]/DeleteDirectoryEntryModal.svelte | 4 ++-- .../[[id]]/DirectoryEntryMenuBottomSheet.svelte | 4 ++-- .../[[id]]/RenameDirectoryEntryModal.svelte | 4 ++-- src/routes/(main)/directory/[[id]]/service.ts | 8 ++++---- src/routes/api/directory/[id]/+server.ts | 10 +++++----- 12 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 0b1d5bd..0fa7772 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -2,11 +2,11 @@ import { and, eq, isNull } from "drizzle-orm"; import db from "./drizzle"; import { directory, file, mek } from "./schema"; -type DirectroyId = "root" | number; +type DirectoryId = "root" | number; -export interface NewDirectroyParams { +export interface NewDirectoryParams { userId: number; - parentId: DirectroyId; + parentId: DirectoryId; mekVersion: number; encDek: string; encName: string; @@ -15,7 +15,7 @@ export interface NewDirectroyParams { export interface NewFileParams { path: string; - parentId: DirectroyId; + parentId: DirectoryId; userId: number; mekVersion: number; encDek: string; @@ -24,7 +24,7 @@ export interface NewFileParams { encNameIv: string; } -export const registerNewDirectory = async (params: NewDirectroyParams) => { +export const registerNewDirectory = async (params: NewDirectoryParams) => { return await db.transaction(async (tx) => { const meks = await tx .select() @@ -47,7 +47,7 @@ export const registerNewDirectory = async (params: NewDirectroyParams) => { }); }; -export const getAllDirectoriesByParent = async (userId: number, directoryId: DirectroyId) => { +export const getAllDirectoriesByParent = async (userId: number, directoryId: DirectoryId) => { return await db .select() .from(directory) @@ -135,7 +135,7 @@ export const registerNewFile = async (params: NewFileParams) => { }); }; -export const getAllFilesByParent = async (userId: number, parentId: DirectroyId) => { +export const getAllFilesByParent = async (userId: number, parentId: DirectoryId) => { return await db .select() .from(file) diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index cad0f0c..ae5ca9a 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -6,7 +6,7 @@ export const directoryRenameRequest = z.object({ }); export type DirectoryRenameRequest = z.infer; -export const directroyInfoResponse = z.object({ +export const directoryInfoResponse = z.object({ metadata: z .object({ createdAt: z.date(), @@ -19,7 +19,7 @@ export const directroyInfoResponse = z.object({ subDirectories: z.number().int().positive().array(), files: z.number().int().positive().array(), }); -export type DirectroyInfoResponse = z.infer; +export type DirectoryInfoResponse = z.infer; export const directoryCreateRequest = z.object({ parentId: z.union([z.enum(["root"]), z.number().int().positive()]), diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts index ede995b..173a246 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -7,7 +7,7 @@ import { setDirectoryEncName, unregisterDirectory, getAllFilesByParent, - type NewDirectroyParams, + type NewDirectoryParams, } from "$lib/server/db/file"; import { getActiveMekVersion } from "$lib/server/db/mek"; @@ -35,14 +35,14 @@ export const renameDirectory = async ( await setDirectoryEncName(userId, directoryId, newEncName, newEncNameIv); }; -export const getDirectroyInformation = async (userId: number, directroyId: "root" | number) => { - const directory = directroyId !== "root" ? await getDirectory(userId, directroyId) : undefined; +export const getDirectoryInformation = async (userId: number, directoryId: "root" | number) => { + const directory = directoryId !== "root" ? await getDirectory(userId, directoryId) : undefined; if (directory === null) { error(404, "Invalid directory id"); } - const directories = await getAllDirectoriesByParent(userId, directroyId); - const files = await getAllFilesByParent(userId, directroyId); + const directories = await getAllDirectoriesByParent(userId, directoryId); + const files = await getAllFilesByParent(userId, directoryId); return { metadata: directory && { @@ -56,7 +56,7 @@ export const getDirectroyInformation = async (userId: number, directroyId: "root }; }; -export const createDirectory = async (params: NewDirectroyParams) => { +export const createDirectory = async (params: NewDirectoryParams) => { const activeMekVersion = await getActiveMekVersion(params.userId); if (activeMekVersion === null) { error(500, "Invalid MEK version"); diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index a430eb1..335df73 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -38,7 +38,7 @@ export const requestClientRegistration = async ( return res.ok; }; -export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: CryptoKey) => { +export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => { const res = await callGetApi("/api/mek/list"); if (!res.ok) return false; @@ -55,7 +55,7 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: version, masterKeyWrapped, masterKeyWrappedSig, - verfiyKey, + verifyKey, ), }; }, diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index 6c767a4..215f0e7 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -31,7 +31,7 @@ const clientKeysBlob = new Blob([JSON.stringify(clientKeysExported)], { type: "application/json", }); - saveAs(clientKeysBlob, "arkvalut-clientkey.json"); + saveAs(clientKeysBlob, "arkvault-clientkey.json"); if (!isBeforeContinueBottomSheetOpen) { setTimeout(() => { diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 449c47b..7e4eafa 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,5 +1,5 @@ - diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte index 5e01939..182f494 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte @@ -1,7 +1,7 @@ @@ -35,8 +30,4 @@ 파일 -{#if metadata} - -{:else} - -{/if} + diff --git a/src/routes/(fullscreen)/file/[id]/+page.ts b/src/routes/(fullscreen)/file/[id]/+page.ts index 6ecfe77..0521107 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.ts +++ b/src/routes/(fullscreen)/file/[id]/+page.ts @@ -1,10 +1,8 @@ import { error } from "@sveltejs/kit"; import { z } from "zod"; -import { callGetApi } from "$lib/hooks"; -import type { FileInfoResponse } from "$lib/server/schemas"; import type { PageLoad } from "./$types"; -export const load: PageLoad = async ({ params, fetch }) => { +export const load: PageLoad = async ({ params }) => { const zodRes = z .object({ id: z.coerce.number().int().positive(), @@ -13,12 +11,5 @@ export const load: PageLoad = async ({ params, fetch }) => { if (!zodRes.success) error(404, "Not found"); const { id } = zodRes.data; - const res = await callGetApi(`/api/file/${id}`, fetch); - if (!res.ok) error(404, "Not found"); - - const fileInfo: FileInfoResponse = await res.json(); - return { - id, - metadata: fileInfo, - }; + return { id }; }; diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index 16aba8b..fc97c3e 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,7 +1,5 @@ import { decryptData } from "$lib/modules/crypto"; -export { decryptFileMetadata } from "$lib/services/file"; - export const requestFileDownload = ( fileId: number, fileEncryptedIv: string, diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 89c6c64..38cd8e0 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,17 +1,18 @@ @@ -94,50 +58,42 @@ -
- {#if data.id !== "root"} - {#if !metadata} - - {:else} - {#await metadata} - - {:then metadata} - - {/await} - {/if} - {/if} -
- {#if subDirectories} - {#await subDirectories then subDirectories} - {#each subDirectories as { id, dataKey, dataKeyVersion, name }} - goto(`/directory/${id}`)} - onOpenMenuClick={() => { - selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name }; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - type="directory" - /> - {/each} - {/await} - {/if} - {#if files} - {#await files then files} - {#each files as { id, dataKey, dataKeyVersion, name }} - goto(`/file/${id}`)} - onOpenMenuClick={() => { - selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name }; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - type="file" - /> - {/each} - {/await} +
+
+ {#if data.id !== "root"} + {/if}
+ {#if $info && $info.subDirectoryIds.length + $info.fileIds.length > 0} +
+ {#each $info.subDirectoryIds as subDirectoryId} + {@const subDirectoryInfo = getDirectoryInfo(subDirectoryId, $masterKeyStore?.get(1)?.key!)} + goto(`/directory/${subDirectoryId}`)} + onOpenMenuClick={({ id, dataKey, dataKeyVersion, name }) => { + selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name }; + isDirectoryEntryMenuBottomSheetOpen = true; + }} + /> + {/each} + {#each $info.fileIds as fileId} + {@const fileInfo = getFileInfo(fileId, $masterKeyStore?.get(1)?.key!)} + goto(`/file/${fileId}`)} + onOpenMenuClick={({ dataKey, id, dataKeyVersion, name }) => { + selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name }; + isDirectoryEntryMenuBottomSheetOpen = true; + }} + /> + {/each} +
+ {:else} +
+

폴더가 비어있어요.

+
+ {/if}
{ +export const load: PageLoad = async ({ params }) => { const zodRes = z .object({ id: z.coerce.number().int().positive().optional(), @@ -13,36 +11,7 @@ export const load: PageLoad = async ({ params, fetch }) => { if (!zodRes.success) error(404, "Not found"); const { id } = zodRes.data; - const directoryId = id ? id : ("root" as const); - const res = await callGetApi(`/api/directory/${directoryId}`, fetch); - if (!res.ok) error(404, "Not found"); - - const directoryInfo: DirectoryInfoResponse = await res.json(); - const subDirectoryInfos = await Promise.all( - directoryInfo.subDirectories.map(async (subDirectoryId) => { - const res = await callGetApi(`/api/directory/${subDirectoryId}`, fetch); - if (!res.ok) error(500, "Internal server error"); - return { - ...((await res.json()) as DirectoryInfoResponse), - id: subDirectoryId, - }; - }), - ); - const fileInfos = await Promise.all( - directoryInfo.files.map(async (fileId) => { - const res = await callGetApi(`/api/file/${fileId}`, fetch); - if (!res.ok) error(500, "Internal server error"); - return { - ...((await res.json()) as FileInfoResponse), - id: fileId, - }; - }), - ); - return { - id: directoryId, - metadata: directoryInfo.metadata, - subDirectories: subDirectoryInfos, - files: fileInfos, + id: id ? id : ("root" as const), }; }; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte deleted file mode 100644 index 29f11d0..0000000 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - - -
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> -
-
- {#if type === "directory"} - - {:else if type === "file"} - - {/if} -
-

- {name} -

- -
-
- - diff --git a/src/routes/(main)/directory/[[id]]/File.svelte b/src/routes/(main)/directory/[[id]]/File.svelte new file mode 100644 index 0000000..da9b843 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/File.svelte @@ -0,0 +1,53 @@ + + +{#if $info} + + +
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> +
+
+ +
+

+ {$info.name} +

+ +
+
+{/if} + + diff --git a/src/routes/(main)/directory/[[id]]/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/SubDirectory.svelte new file mode 100644 index 0000000..6bf29b0 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/SubDirectory.svelte @@ -0,0 +1,55 @@ + + +{#if $info} + + +
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> +
+
+ +
+

+ {$info.name} +

+ +
+
+{/if} + + diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index ce17472..79fe1b9 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -3,22 +3,17 @@ import { encodeToBase64, generateDataKey, wrapDataKey, - unwrapDataKey, encryptData, encryptString, - decryptString, } from "$lib/modules/crypto"; import type { DirectoryRenameRequest, - DirectoryInfoResponse, DirectoryCreateRequest, FileRenameRequest, FileUploadRequest, } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; -export { decryptFileMetadata } from "$lib/services/file"; - export interface SelectedDirectoryEntry { type: "directory" | "file"; id: number; @@ -27,18 +22,6 @@ export interface SelectedDirectoryEntry { name: string; } -export const decryptDirectoryMetadata = async ( - metadata: NonNullable, - masterKey: CryptoKey, -) => { - const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); - return { - dataKey, - dataKeyVersion: metadata.dekVersion, - name: await decryptString(metadata.name, metadata.nameIv, dataKey), - }; -}; - export const requestDirectoryCreation = async ( name: string, parentId: "root" | number, From 183a3590a9c1548e66c90588b08f294d68695c86 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 21:15:23 +0900 Subject: [PATCH 092/115] =?UTF-8?q?=ED=8C=8C=EC=9D=BC/=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=84=B0=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=9E=AC=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/directory/[[id]]/+page.svelte | 43 ++++---------- .../DirectoryEntries/DirectoryEntries.svelte | 57 +++++++++++++++++++ .../[[id]]/{ => DirectoryEntries}/File.svelte | 18 ++++-- .../SubDirectory.svelte | 18 ++++-- .../[[id]]/DirectoryEntries/index.ts | 2 + .../[[id]]/DirectoryEntries/service.ts | 30 ++++++++++ 6 files changed, 128 insertions(+), 40 deletions(-) create mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte rename src/routes/(main)/directory/[[id]]/{ => DirectoryEntries}/File.svelte (68%) rename src/routes/(main)/directory/[[id]]/{ => DirectoryEntries}/SubDirectory.svelte (67%) create mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts create mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 38cd8e0..25df773 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -3,15 +3,14 @@ import { goto } from "$app/navigation"; import { TopBar } from "$lib/components"; import { FloatingButton } from "$lib/components/buttons"; - import { getDirectoryInfo, getFileInfo } from "$lib/modules/file"; + import { getDirectoryInfo } from "$lib/modules/file"; import { masterKeyStore, type DirectoryInfo } from "$lib/stores"; import CreateBottomSheet from "./CreateBottomSheet.svelte"; import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte"; + import DirectoryEntries from "./DirectoryEntries"; import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte"; - import File from "./File.svelte"; import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte"; - import SubDirectory from "./SubDirectory.svelte"; import { requestDirectoryCreation, requestFileUpload, @@ -64,35 +63,15 @@ {/if}
- {#if $info && $info.subDirectoryIds.length + $info.fileIds.length > 0} -
- {#each $info.subDirectoryIds as subDirectoryId} - {@const subDirectoryInfo = getDirectoryInfo(subDirectoryId, $masterKeyStore?.get(1)?.key!)} - goto(`/directory/${subDirectoryId}`)} - onOpenMenuClick={({ id, dataKey, dataKeyVersion, name }) => { - selectedEntry = { type: "directory", id, dataKey, dataKeyVersion, name }; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - /> - {/each} - {#each $info.fileIds as fileId} - {@const fileInfo = getFileInfo(fileId, $masterKeyStore?.get(1)?.key!)} - goto(`/file/${fileId}`)} - onOpenMenuClick={({ dataKey, id, dataKeyVersion, name }) => { - selectedEntry = { type: "file", id, dataKey, dataKeyVersion, name }; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - /> - {/each} -
- {:else} -
-

폴더가 비어있어요.

-
+ {#if $info} + goto(`/${type}/${id}`)} + onEntryMenuClick={(entry) => { + selectedEntry = entry; + isDirectoryEntryMenuBottomSheetOpen = true; + }} + /> {/if}
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte new file mode 100644 index 0000000..f852afb --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte @@ -0,0 +1,57 @@ + + +{#if info.subDirectoryIds.length + info.fileIds.length > 0} +
+ {#each subDirectoryInfos as subDirectory} + + {/each} + {#each fileInfos as file} + + {/each} +
+{:else} +
+

폴더가 비어있어요.

+
+{/if} diff --git a/src/routes/(main)/directory/[[id]]/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte similarity index 68% rename from src/routes/(main)/directory/[[id]]/File.svelte rename to src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index da9b843..55c9c27 100644 --- a/src/routes/(main)/directory/[[id]]/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -1,22 +1,32 @@ @@ -24,7 +34,7 @@ {#if $info} -
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> +
diff --git a/src/routes/(main)/directory/[[id]]/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte similarity index 67% rename from src/routes/(main)/directory/[[id]]/SubDirectory.svelte rename to src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte index 6bf29b0..b0d231e 100644 --- a/src/routes/(main)/directory/[[id]]/SubDirectory.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte @@ -1,6 +1,7 @@ @@ -26,7 +36,7 @@ {#if $info} -
setTimeout(onclick, 100)} class="h-12 w-full rounded-xl"> +
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts new file mode 100644 index 0000000..72ab278 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts @@ -0,0 +1,2 @@ +export { default } from "./DirectoryEntries.svelte"; +export * from "./service"; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts new file mode 100644 index 0000000..2ad5941 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts @@ -0,0 +1,30 @@ +import { get, type Writable } from "svelte/store"; +import type { DirectoryInfo, FileInfo } from "$lib/stores"; + +export enum SortBy { + NAME_ASC, + NAME_DESC, +} + +type SortFunc = (a: DirectoryInfo | FileInfo | null, b: DirectoryInfo | FileInfo | null) => number; + +const sortByNameAsc: SortFunc = (a, b) => { + if (a && b) return a.name!.localeCompare(b.name!); + return 0; +}; + +const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); + +export const sortEntries = ( + entries: Writable[], + sortBy: SortBy = SortBy.NAME_ASC, +) => { + let sortFunc: SortFunc; + if (sortBy === SortBy.NAME_ASC) { + sortFunc = sortByNameAsc; + } else { + sortFunc = sortByNameDesc; + } + + entries.sort((a, b) => sortFunc(get(a), get(b))); +}; From 3168c441b909ea4ea8810361ab5dbc2a8b753ddb Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 21:20:00 +0900 Subject: [PATCH 093/115] =?UTF-8?q?=ED=95=98=EC=9C=84=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=84=B0=EB=A6=AC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=EC=8B=9C=20Dir?= =?UTF-8?q?ectoryEntries=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EA=B0=80?= =?UTF-8?q?=20=EC=9E=AC=EC=82=AC=EC=9A=A9=EB=90=98=EB=A9=B4=EC=84=9C=20?= =?UTF-8?q?=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=EC=9D=B4=20?= =?UTF-8?q?=EC=A6=89=EC=8B=9C=20=EC=82=AC=EB=9D=BC=EC=A7=80=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/directory/[[id]]/+page.svelte | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 25df773..43c4755 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -64,14 +64,16 @@ {/if}
{#if $info} - goto(`/${type}/${id}`)} - onEntryMenuClick={(entry) => { - selectedEntry = entry; - isDirectoryEntryMenuBottomSheetOpen = true; - }} - /> + {#key $info} + goto(`/${type}/${id}`)} + onEntryMenuClick={(entry) => { + selectedEntry = entry; + isDirectoryEntryMenuBottomSheetOpen = true; + }} + /> + {/key} {/if}
From 1c06a604c5e224f9184f5003ff3caaf8d34c0e27 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 22:55:11 +0900 Subject: [PATCH 094/115] =?UTF-8?q?DB=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=AC=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B0=84?= =?UTF-8?q?=EB=8B=A8=ED=95=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80/=EB=B9=84?= =?UTF-8?q?=EB=94=94=EC=98=A4=20=EB=B7=B0=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...crow.sql => 0000_handy_captain_marvel.sql} | 6 +- drizzle/meta/0000_snapshot.json | 24 ++++++-- drizzle/meta/_journal.json | 4 +- package.json | 1 + pnpm-lock.yaml | 9 +++ src/lib/modules/file.ts | 1 + src/lib/server/db/file.ts | 2 + src/lib/server/db/schema/file.ts | 1 + src/lib/server/schemas/file.ts | 9 +++ src/lib/server/services/file.ts | 1 + src/lib/stores/file.ts | 1 + .../(fullscreen)/file/[id]/+page.svelte | 60 +++++++++++++++++-- src/routes/(main)/directory/[[id]]/service.ts | 1 + src/routes/api/file/[id]/+server.ts | 3 +- src/routes/api/file/upload/+server.ts | 4 +- 15 files changed, 111 insertions(+), 16 deletions(-) rename drizzle/{0000_lazy_scarecrow.sql => 0000_handy_captain_marvel.sql} (97%) diff --git a/drizzle/0000_lazy_scarecrow.sql b/drizzle/0000_handy_captain_marvel.sql similarity index 97% rename from drizzle/0000_lazy_scarecrow.sql rename to drizzle/0000_handy_captain_marvel.sql index 89e8f99..05d5e02 100644 --- a/drizzle/0000_lazy_scarecrow.sql +++ b/drizzle/0000_handy_captain_marvel.sql @@ -32,7 +32,7 @@ CREATE TABLE `directory` ( `user_id` integer NOT NULL, `master_encryption_key_version` integer NOT NULL, `encrypted_data_encryption_key` text NOT NULL, - `encrypted_at` integer NOT NULL, + `data_encryption_key_version` integer NOT NULL, `encrypted_name` text NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, @@ -47,7 +47,9 @@ CREATE TABLE `file` ( `user_id` integer NOT NULL, `master_encryption_key_version` integer NOT NULL, `encrypted_data_encryption_key` text NOT NULL, - `encrypted_at` integer NOT NULL, + `data_encryption_key_version` integer NOT NULL, + `content_type` text NOT NULL, + `encrypted_content_iv` text NOT NULL, `encrypted_name` text NOT NULL, FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 49d6d24..d8c1013 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "901e84cd-f9eb-4329-a374-f71264675515", + "id": "929c6bca-d0c0-4899-afc6-a0a498226f28", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "client": { @@ -262,8 +262,8 @@ "notNull": true, "autoincrement": false }, - "encrypted_at": { - "name": "encrypted_at", + "data_encryption_key_version": { + "name": "data_encryption_key_version", "type": "integer", "primaryKey": false, "notNull": true, @@ -384,13 +384,27 @@ "notNull": true, "autoincrement": false }, - "encrypted_at": { - "name": "encrypted_at", + "data_encryption_key_version": { + "name": "data_encryption_key_version", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_content_iv": { + "name": "encrypted_content_iv", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "encrypted_name": { "name": "encrypted_name", "type": "text", diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 7874a98..b2615a0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1735748192401, - "tag": "0000_lazy_scarecrow", + "when": 1736170919561, + "tag": "0000_handy_captain_marvel", "breakpoints": true } ] diff --git a/package.json b/package.json index ca5bc28..e1c05db 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "eslint-plugin-tailwindcss": "^3.17.5", "file-saver": "^2.0.5", "globals": "^15.0.0", + "mime": "^4.0.6", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.6", "prettier-plugin-tailwindcss": "^0.6.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b791d45..9b4997e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,9 @@ devDependencies: globals: specifier: ^15.0.0 version: 15.14.0 + mime: + specifier: ^4.0.6 + version: 4.0.6 prettier: specifier: ^3.3.2 version: 3.4.2 @@ -2689,6 +2692,12 @@ packages: picomatch: 2.3.1 dev: true + /mime@4.0.6: + resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==} + engines: {node: '>=16'} + hasBin: true + dev: true + /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} diff --git a/src/lib/modules/file.ts b/src/lib/modules/file.ts index 25e301e..9f5f725 100644 --- a/src/lib/modules/file.ts +++ b/src/lib/modules/file.ts @@ -64,6 +64,7 @@ const fetchFileInfo = async (fileId: number, masterKey: CryptoKey) => { id: fileId, dataKey, dataKeyVersion: metadata.dekVersion, + contentType: metadata.contentType, contentIv: metadata.contentIv, name: await decryptString(metadata.name, metadata.nameIv, dataKey), }; diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index f7aefdf..2fe4b53 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -21,6 +21,7 @@ export interface NewFileParams { mekVersion: number; encDek: string; dekVersion: Date; + contentType: string; encContentIv: string; encName: string; encNameIv: string; @@ -137,6 +138,7 @@ export const registerNewFile = async (params: NewFileParams) => { createdAt: now, userId: params.userId, mekVersion: params.mekVersion, + contentType: params.contentType, encDek: params.encDek, dekVersion: params.dekVersion, encContentIv: params.encContentIv, diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index c2ef676..dbaf944 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -47,6 +47,7 @@ export const file = sqliteTable( mekVersion: integer("master_encryption_key_version").notNull(), encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), + contentType: text("content_type").notNull(), encContentIv: text("encrypted_content_iv").notNull(), // Base64 encName: ciphertext("encrypted_name").notNull(), }, diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 13649e7..0df09df 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -1,3 +1,4 @@ +import mime from "mime"; import { z } from "zod"; export const fileRenameRequest = z.object({ @@ -12,6 +13,10 @@ export const fileInfoResponse = z.object({ mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.date(), + contentType: z + .string() + .nonempty() + .refine((value) => mime.getExtension(value) !== null), // MIME type contentIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), @@ -23,6 +28,10 @@ export const fileUploadRequest = z.object({ mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.coerce.date(), + contentType: z + .string() + .nonempty() + .refine((value) => mime.getExtension(value) !== null), // MIME type contentIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index 11fa536..7bf9b72 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -83,6 +83,7 @@ export const getFileInformation = async (userId: number, fileId: number) => { mekVersion: file.mekVersion, encDek: file.encDek, dekVersion: file.dekVersion, + contentType: file.contentType, encContentIv: file.encContentIv, encName: file.encName, }; diff --git a/src/lib/stores/file.ts b/src/lib/stores/file.ts index 78e7691..24997da 100644 --- a/src/lib/stores/file.ts +++ b/src/lib/stores/file.ts @@ -22,6 +22,7 @@ export interface FileInfo { id: number; dataKey: CryptoKey; dataKeyVersion: Date; + contentType: string; contentIv: string; name: string; } diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index ea9a886..a6d2f72 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -1,26 +1,48 @@ diff --git a/src/routes/(fullscreen)/file/[id]/+page.ts b/src/routes/(fullscreen)/file/[id]/+page.ts index 0521107..45c696e 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.ts +++ b/src/routes/(fullscreen)/file/[id]/+page.ts @@ -2,6 +2,8 @@ import { error } from "@sveltejs/kit"; import { z } from "zod"; import type { PageLoad } from "./$types"; +export const ssr = false; // Because of heic2any + export const load: PageLoad = async ({ params }) => { const zodRes = z .object({ From 6c4bd590f056520fd4dc6d0e8e083e857a6f6034 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 23:50:03 +0900 Subject: [PATCH 097/115] =?UTF-8?q?=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20HEIF=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=ED=91=9C=ED=98=84?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8D=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(fullscreen)/file/[id]/+page.svelte | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 51d7e21..c5e822e 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -32,24 +32,22 @@ untrack(() => { isDownloaded = true; + if ($info.contentType.startsWith("image")) { + contentType = "image"; + } else if ($info.contentType.startsWith("video")) { + contentType = "video"; + } + requestFileDownload(data.id, $info.contentIv, $info.dataKey).then(async (res) => { content = new Blob([res], { type: $info.contentType }); if (content.type === "image/heic" || content.type === "image/heif") { contentUrl = URL.createObjectURL( (await heic2any({ blob: content, toType: "image/jpeg" })) as Blob, ); - } else { + } else if (contentType) { contentUrl = URL.createObjectURL(content); - } - - if (content.type.startsWith("image")) { - contentType = "image"; - } else if (content.type.startsWith("video")) { - contentType = "video"; - } - - if (!contentType) { - FileSaver.saveAs(new Blob([res], { type: $info.contentType }), $info.name); + } else { + FileSaver.saveAs(content, $info.name); } }); }); @@ -80,18 +78,16 @@
{/snippet} - {#if contentType === "image"} - {#if $info && content} - {@const src = URL.createObjectURL(new Blob([content], { type: $info.contentType }))} - {$info.name} + {#if $info && contentType === "image"} + {#if contentUrl} + {$info.name} {:else} {@render viewerLoading("이미지를 불러오고 있어요.")} {/if} {:else if contentType === "video"} - {#if $info && content} - {@const src = URL.createObjectURL(new Blob([content], { type: $info.contentType }))} + {#if contentUrl} - + {:else} {@render viewerLoading("비디오를 불러오고 있어요.")} {/if} From 1cabc5f7b3d7fcd388bb66008feb8e9eaa46c3cd Mon Sep 17 00:00:00 2001 From: static Date: Tue, 7 Jan 2025 00:09:32 +0900 Subject: [PATCH 098/115] =?UTF-8?q?=EB=B9=84=ED=9A=A8=EC=9C=A8=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9E=90=EB=8F=99=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/(main)/directory/[[id]]/+page.svelte | 7 ++++++- src/routes/(main)/directory/[[id]]/service.ts | 2 +- svelte.config.js | 3 +++ vite.config.ts | 3 +++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 43c4755..95d05db 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -37,13 +37,16 @@ const createDirectory = async (name: string) => { await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!); isCreateDirectoryModalOpen = false; + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME }; const uploadFile = () => { const file = fileInput?.files?.[0]; if (!file) return; - requestFileUpload(file, data.id, $masterKeyStore?.get(1)!); + requestFileUpload(file, data.id, $masterKeyStore?.get(1)!).then(() => { + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + }); }; $effect(() => { @@ -114,6 +117,7 @@ bind:selectedEntry onRenameClick={async (newName) => { await requestDirectoryEntryRename(selectedEntry!, newName); + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME return true; }} /> @@ -122,6 +126,7 @@ bind:selectedEntry onDeleteClick={async () => { await requestDirectoryEntryDeletion(selectedEntry!); + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME return true; }} /> diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index 5a85c93..7fdb1df 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -29,7 +29,7 @@ export const requestDirectoryCreation = async ( ) => { const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey); - return await callPostApi("/api/directory/create", { + await callPostApi("/api/directory/create", { parentId, mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key), diff --git a/svelte.config.js b/svelte.config.js index bbef2bb..27d6c18 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -12,6 +12,9 @@ const config = { // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), + csrf: { + checkOrigin: false, + }, }, }; diff --git a/vite.config.ts b/vite.config.ts index 1e576b9..cf2e3fa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,4 +9,7 @@ export default defineConfig({ compiler: "svelte", }), ], + server: { + host: true, + }, }); From 7a1bf80a1274ecde1027613234599f6c1332eec0 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 7 Jan 2025 01:07:31 +0900 Subject: [PATCH 099/115] =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=EB=90=9C=20=EA=B0=9C=EB=B0=9C=EC=9A=A9=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- svelte.config.js | 3 --- vite.config.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/svelte.config.js b/svelte.config.js index 27d6c18..bbef2bb 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -12,9 +12,6 @@ const config = { // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), - csrf: { - checkOrigin: false, - }, }, }; diff --git a/vite.config.ts b/vite.config.ts index cf2e3fa..1e576b9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,4 @@ export default defineConfig({ compiler: "svelte", }), ], - server: { - host: true, - }, }); From 661e7a33de2a867d6391230afdf424727e51d8a2 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 7 Jan 2025 01:25:14 +0900 Subject: [PATCH 100/115] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EA=B0=80=20=EC=99=84=EB=A3=8C=EB=90=9C=20=ED=9B=84?= =?UTF-8?q?=EC=97=90=20=ED=8C=8C=EC=9D=BC/=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=EC=9D=B4=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B5=AC=ED=98=84=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=83=AD=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(fullscreen)/auth/login/+page.server.ts | 2 +- .../(fullscreen)/client/pending/+page.ts | 2 +- src/routes/(fullscreen)/key/generate/+page.ts | 2 +- src/routes/(main)/BottomBar.svelte | 6 +- src/routes/(main)/category/+page.svelte | 3 + .../DirectoryEntries/DirectoryEntries.svelte | 2 +- src/routes/(main)/directory/[[id]]/service.ts | 61 +++++++++++-------- src/routes/(main)/favorite/+page.svelte | 3 + src/routes/(main)/home/+page.svelte | 3 + src/routes/(main)/menu/+page.svelte | 3 + src/routes/+page.svelte | 2 - src/routes/+server.ts | 6 ++ 12 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 src/routes/(main)/category/+page.svelte create mode 100644 src/routes/(main)/favorite/+page.svelte create mode 100644 src/routes/(main)/home/+page.svelte create mode 100644 src/routes/(main)/menu/+page.svelte delete mode 100644 src/routes/+page.svelte create mode 100644 src/routes/+server.ts diff --git a/src/routes/(fullscreen)/auth/login/+page.server.ts b/src/routes/(fullscreen)/auth/login/+page.server.ts index 874e1be..da7da9c 100644 --- a/src/routes/(fullscreen)/auth/login/+page.server.ts +++ b/src/routes/(fullscreen)/auth/login/+page.server.ts @@ -2,7 +2,7 @@ import { redirect } from "@sveltejs/kit"; import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ url, cookies }) => { - const redirectPath = url.searchParams.get("redirect") || "/"; + const redirectPath = url.searchParams.get("redirect") || "/home"; const accessToken = cookies.get("accessToken"); if (accessToken) { diff --git a/src/routes/(fullscreen)/client/pending/+page.ts b/src/routes/(fullscreen)/client/pending/+page.ts index 626d2e0..455f322 100644 --- a/src/routes/(fullscreen)/client/pending/+page.ts +++ b/src/routes/(fullscreen)/client/pending/+page.ts @@ -1,6 +1,6 @@ import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ url }) => { - const redirectPath = url.searchParams.get("redirect") || "/"; + const redirectPath = url.searchParams.get("redirect") || "/home"; return { redirectPath }; }; diff --git a/src/routes/(fullscreen)/key/generate/+page.ts b/src/routes/(fullscreen)/key/generate/+page.ts index 626d2e0..455f322 100644 --- a/src/routes/(fullscreen)/key/generate/+page.ts +++ b/src/routes/(fullscreen)/key/generate/+page.ts @@ -1,6 +1,6 @@ import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ url }) => { - const redirectPath = url.searchParams.get("redirect") || "/"; + const redirectPath = url.searchParams.get("redirect") || "/home"; return { redirectPath }; }; diff --git a/src/routes/(main)/BottomBar.svelte b/src/routes/(main)/BottomBar.svelte index 611649b..0e4cbda 100644 --- a/src/routes/(main)/BottomBar.svelte +++ b/src/routes/(main)/BottomBar.svelte @@ -1,4 +1,5 @@ -
+
{@render children?.()}
diff --git a/src/lib/components/inputs/TextInput.svelte b/src/lib/components/inputs/TextInput.svelte index 77e3e95..61f42ad 100644 --- a/src/lib/components/inputs/TextInput.svelte +++ b/src/lib/components/inputs/TextInput.svelte @@ -12,8 +12,8 @@