From f914026922e3e6ba40b781c95fc2c6887cc01a1f Mon Sep 17 00:00:00 2001 From: static Date: Mon, 13 Jan 2025 07:06:31 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=81=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EB=A7=88?= =?UTF-8?q?=EC=A7=80=EB=A7=89=20=EC=88=98=EC=A0=95=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=EC=9D=84=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=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 파일 마지막 수정 시각은 반드시 지정되어야 하며, 파일 시스템에서 읽어옵니다. 파일 생성 시각은 선택적으로 지정될 수 있으며, 이미지일 경우 EXIF에서 추출을 시도합니다. 두 값 모두 클라이언트에서 암호화되어 서버에 저장됩니다. --- package.json | 1 + pnpm-lock.yaml | 17 ++++++++ src/lib/modules/file.ts | 9 ++++ src/lib/server/db/file.ts | 19 ++++++++- src/lib/server/db/schema/file.ts | 2 + src/lib/server/schemas/file.ts | 8 ++++ src/lib/server/services/file.ts | 2 + src/lib/stores/file.ts | 3 ++ .../[[id]]/DirectoryEntries/File.svelte | 12 ++++-- .../DirectoryEntries/SubDirectory.svelte | 2 +- .../[[id]]/DirectoryEntries/service.ts | 12 ++++++ src/routes/(main)/directory/[[id]]/service.ts | 41 ++++++++++++++++++- src/routes/api/file/[id]/+server.ts | 16 +++++++- src/routes/api/file/upload/+server.ts | 10 +++++ 14 files changed, 145 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 996e9f8..dfa988e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.46.1", "eslint-plugin-tailwindcss": "^3.17.5", + "exifreader": "^4.26.0", "file-saver": "^2.0.5", "globals": "^15.14.0", "heic2any": "^0.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6e96c5..dccbc50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: eslint-plugin-tailwindcss: specifier: ^3.17.5 version: 3.17.5(tailwindcss@3.4.17) + exifreader: + specifier: ^4.26.0 + version: 4.26.0 file-saver: specifier: ^2.0.5 version: 2.0.5 @@ -907,6 +910,10 @@ packages: resolution: {integrity: sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@xmldom/xmldom@0.9.6': + resolution: {integrity: sha512-Su4xcxR0CPGwlDHNmVP09fqET9YxbyDXHaSob6JlBH7L6reTYaeim6zbk9o08UarO0L5GTRo3uzl0D+9lSxmvw==} + engines: {node: '>=14.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1343,6 +1350,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + exifreader@4.26.0: + resolution: {integrity: sha512-nNN9B0oaXTOpArdnIdJBAro2Sa620m7wMjMA5Xy1rcua0EYHVjzGKM5syBOWDqIG2Qay6Pes/5FOdj65hvZ9Vw==} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -2852,6 +2862,9 @@ snapshots: '@typescript-eslint/types': 8.19.1 eslint-visitor-keys: 4.2.0 + '@xmldom/xmldom@0.9.6': + optional: true + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -3275,6 +3288,10 @@ snapshots: esutils@2.0.3: {} + exifreader@4.26.0: + optionalDependencies: + '@xmldom/xmldom': 0.9.6 + expand-template@2.0.3: {} fast-deep-equal@3.1.3: {} diff --git a/src/lib/modules/file.ts b/src/lib/modules/file.ts index 2e25399..82d136e 100644 --- a/src/lib/modules/file.ts +++ b/src/lib/modules/file.ts @@ -53,6 +53,10 @@ export const getDirectoryInfo = (directoryId: "root" | number, masterKey: Crypto return info; }; +const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { + return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); +}; + const fetchFileInfo = async ( fileId: number, masterKey: CryptoKey, @@ -70,6 +74,11 @@ const fetchFileInfo = async ( contentType: metadata.contentType, contentIv: metadata.contentIv, name: await decryptString(metadata.name, metadata.nameIv, dataKey), + createdAt: + metadata.createdAt && metadata.createdAtIv + ? await decryptDate(metadata.createdAt, metadata.createdAtIv, dataKey) + : undefined, + lastModifiedAt: await decryptDate(metadata.lastModifiedAt, metadata.lastModifiedAtIv, dataKey), }; infoStore.update(() => newInfo); diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index db6b881..b99bd38 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -28,6 +28,10 @@ export interface NewFileParams { encContentIv: string; encName: string; encNameIv: string; + encCreatedAt: string | null; + encCreatedAtIv: string | null; + encLastModifiedAt: string; + encLastModifiedAtIv: string; } export const registerDirectory = async (params: NewDirectoryParams) => { @@ -154,7 +158,12 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = }; export const registerFile = async (params: NewFileParams) => { - if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) { + if ( + (params.hskVersion && !params.contentHmac) || + (!params.hskVersion && params.contentHmac) || + (params.encCreatedAt && !params.encCreatedAtIv) || + (!params.encCreatedAt && params.encCreatedAtIv) + ) { throw new Error("Invalid arguments"); } @@ -194,6 +203,14 @@ export const registerFile = async (params: NewFileParams) => { dekVersion: params.dekVersion, encContentIv: params.encContentIv, encName: { ciphertext: params.encName, iv: params.encNameIv }, + encCreatedAt: + params.encCreatedAt && params.encCreatedAtIv + ? { ciphertext: params.encCreatedAt, iv: params.encCreatedAtIv } + : null, + encLastModifiedAt: { + ciphertext: params.encLastModifiedAt, + iv: params.encLastModifiedAtIv, + }, }) .returning({ id: file.id }); const { id: fileId } = newFiles[0]!; diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index 7ac0b77..65c5471 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -61,6 +61,8 @@ export const file = sqliteTable( contentType: text("content_type").notNull(), encContentIv: text("encrypted_content_iv").notNull(), // Base64 encName: ciphertext("encrypted_name").notNull(), + encCreatedAt: ciphertext("encrypted_created_at"), + encLastModifiedAt: ciphertext("encrypted_last_modified_at").notNull(), }, (t) => ({ ref1: foreignKey({ diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index f73b299..f6ac315 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -12,6 +12,10 @@ export const fileInfoResponse = z.object({ contentIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), + createdAt: z.string().base64().nonempty().optional(), + createdAtIv: z.string().base64().nonempty().optional(), + lastModifiedAt: z.string().base64().nonempty(), + lastModifiedAtIv: z.string().base64().nonempty(), }); export type FileInfoResponse = z.infer; @@ -47,5 +51,9 @@ export const fileUploadRequest = z.object({ contentIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), + createdAt: z.string().base64().nonempty().optional(), + createdAtIv: z.string().base64().nonempty().optional(), + lastModifiedAt: z.string().base64().nonempty(), + lastModifiedAtIv: z.string().base64().nonempty(), }); export type FileUploadRequest = z.infer; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index 7599939..cb5559b 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -28,6 +28,8 @@ export const getFileInformation = async (userId: number, fileId: number) => { contentType: file.contentType, encContentIv: file.encContentIv, encName: file.encName, + encCreatedAt: file.encCreatedAt, + encLastModifiedAt: file.encLastModifiedAt, }; }; diff --git a/src/lib/stores/file.ts b/src/lib/stores/file.ts index 24997da..2debd57 100644 --- a/src/lib/stores/file.ts +++ b/src/lib/stores/file.ts @@ -25,7 +25,10 @@ export interface FileInfo { contentType: string; contentIv: string; name: string; + createdAt?: Date; + lastModifiedAt: Date; } export const directoryInfoStore = new Map<"root" | number, Writable>(); + export const fileInfoStore = new Map>(); diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index c9ef1d8..c95ba6d 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -1,6 +1,7 @@