Merge pull request #4 from kmc7468/add-hmac-secret-key

파일 중복 검사 시스템 도입
This commit is contained in:
static
2025-01-13 01:19:32 +09:00
committed by GitHub
35 changed files with 1045 additions and 96 deletions

View File

@@ -27,7 +27,6 @@ CREATE TABLE `user_client_challenge` (
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `directory` ( CREATE TABLE `directory` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`created_at` integer NOT NULL,
`parent_id` integer, `parent_id` integer,
`user_id` integer NOT NULL, `user_id` integer NOT NULL,
`master_encryption_key_version` integer NOT NULL, `master_encryption_key_version` integer NOT NULL,
@@ -39,23 +38,66 @@ CREATE TABLE `directory` (
FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) 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 --> statement-breakpoint
CREATE TABLE `directory_log` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`directory_id` integer NOT NULL,
`timestamp` integer NOT NULL,
`action` text NOT NULL,
`new_name` text,
FOREIGN KEY (`directory_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `file` ( CREATE TABLE `file` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`path` text NOT NULL,
`parent_id` integer, `parent_id` integer,
`created_at` integer NOT NULL,
`user_id` integer NOT NULL, `user_id` integer NOT NULL,
`path` text NOT NULL,
`master_encryption_key_version` integer NOT NULL, `master_encryption_key_version` integer NOT NULL,
`encrypted_data_encryption_key` text NOT NULL, `encrypted_data_encryption_key` text NOT NULL,
`data_encryption_key_version` integer NOT NULL, `data_encryption_key_version` integer NOT NULL,
`hmac_secret_key_version` integer,
`content_hmac` text,
`content_type` text NOT NULL, `content_type` text NOT NULL,
`encrypted_content_iv` text NOT NULL, `encrypted_content_iv` text NOT NULL,
`encrypted_name` 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 (`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`) 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,
FOREIGN KEY (`user_id`,`hmac_secret_key_version`) REFERENCES `hmac_secret_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `file_log` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`file_id` integer NOT NULL,
`timestamp` integer NOT NULL,
`action` text NOT NULL,
`new_name` text,
FOREIGN KEY (`file_id`) REFERENCES `file`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `hmac_secret_key` (
`user_id` integer NOT NULL,
`version` integer NOT NULL,
`state` text NOT NULL,
`master_encryption_key_version` integer NOT NULL,
`encrypted_key` text NOT NULL,
PRIMARY KEY(`user_id`, `version`),
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 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 --> statement-breakpoint
CREATE TABLE `hmac_secret_key_log` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`hmac_secret_key_version` integer NOT NULL,
`timestamp` integer NOT NULL,
`action` text NOT NULL,
`action_by` integer,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`action_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`user_id`,`hmac_secret_key_version`) REFERENCES `hmac_secret_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `client_master_encryption_key` ( CREATE TABLE `client_master_encryption_key` (
`user_id` integer NOT NULL, `user_id` integer NOT NULL,
`client_id` integer NOT NULL, `client_id` integer NOT NULL,
@@ -71,13 +113,22 @@ CREATE TABLE `client_master_encryption_key` (
CREATE TABLE `master_encryption_key` ( CREATE TABLE `master_encryption_key` (
`user_id` integer NOT NULL, `user_id` integer NOT NULL,
`version` integer NOT NULL, `version` integer NOT NULL,
`created_by` integer NOT NULL,
`created_at` integer NOT NULL,
`state` text NOT NULL, `state` text NOT NULL,
`retired_at` integer, `retired_at` integer,
PRIMARY KEY(`user_id`, `version`), PRIMARY KEY(`user_id`, `version`),
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `master_encryption_key_log` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`master_encryption_key_version` integer NOT NULL,
`timestamp` integer NOT NULL,
`action` text NOT NULL,
`action_by` integer,
FOREIGN KEY (`user_id`) REFERENCES `user`(`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 (`created_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action FOREIGN KEY (`action_by`) 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 --> statement-breakpoint
CREATE TABLE `session` ( CREATE TABLE `session` (
@@ -116,6 +167,7 @@ CREATE UNIQUE INDEX `user_client_challenge_answer_unique` ON `user_client_challe
CREATE UNIQUE INDEX `directory_encrypted_data_encryption_key_unique` ON `directory` (`encrypted_data_encryption_key`);--> 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_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 `file_encrypted_data_encryption_key_unique` ON `file` (`encrypted_data_encryption_key`);--> statement-breakpoint
CREATE UNIQUE INDEX `hmac_secret_key_encrypted_key_unique` ON `hmac_secret_key` (`encrypted_key`);--> statement-breakpoint
CREATE UNIQUE INDEX `session_user_id_client_id_unique` ON `session` (`user_id`,`client_id`);--> statement-breakpoint CREATE UNIQUE INDEX `session_user_id_client_id_unique` ON `session` (`user_id`,`client_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `session_upgrade_challenge_session_id_unique` ON `session_upgrade_challenge` (`session_id`);--> statement-breakpoint CREATE UNIQUE INDEX `session_upgrade_challenge_session_id_unique` ON `session_upgrade_challenge` (`session_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `session_upgrade_challenge_answer_unique` ON `session_upgrade_challenge` (`answer`);--> statement-breakpoint CREATE UNIQUE INDEX `session_upgrade_challenge_answer_unique` ON `session_upgrade_challenge` (`answer`);--> statement-breakpoint

View File

@@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "c518e1b4-38f8-4c8e-bdc9-64152ab456d8", "id": "f2fbe45c-1f1d-4dd8-92ab-dd057c0e668b",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"client": { "client": {
@@ -234,13 +234,6 @@
"notNull": true, "notNull": true,
"autoincrement": true "autoincrement": true
}, },
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"parent_id": { "parent_id": {
"name": "parent_id", "name": "parent_id",
"type": "integer", "type": "integer",
@@ -339,6 +332,64 @@
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"directory_log": {
"name": "directory_log",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"directory_id": {
"name": "directory_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"new_name": {
"name": "new_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"directory_log_directory_id_directory_id_fk": {
"name": "directory_log_directory_id_directory_id_fk",
"tableFrom": "directory_log",
"tableTo": "directory",
"columnsFrom": [
"directory_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"file": { "file": {
"name": "file", "name": "file",
"columns": { "columns": {
@@ -349,13 +400,6 @@
"notNull": true, "notNull": true,
"autoincrement": true "autoincrement": true
}, },
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"parent_id": { "parent_id": {
"name": "parent_id", "name": "parent_id",
"type": "integer", "type": "integer",
@@ -363,16 +407,16 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"created_at": { "user_id": {
"name": "created_at", "name": "user_id",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"user_id": { "path": {
"name": "user_id", "name": "path",
"type": "integer", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
@@ -398,6 +442,20 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"hmac_secret_key_version": {
"name": "hmac_secret_key_version",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content_hmac": {
"name": "content_hmac",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content_type": { "content_type": {
"name": "content_type", "name": "content_type",
"type": "text", "type": "text",
@@ -477,6 +535,261 @@
], ],
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "no action" "onUpdate": "no action"
},
"file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": {
"name": "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk",
"tableFrom": "file",
"tableTo": "hmac_secret_key",
"columnsFrom": [
"user_id",
"hmac_secret_key_version"
],
"columnsTo": [
"user_id",
"version"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"file_log": {
"name": "file_log",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"file_id": {
"name": "file_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"new_name": {
"name": "new_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"file_log_file_id_file_id_fk": {
"name": "file_log_file_id_file_id_fk",
"tableFrom": "file_log",
"tableTo": "file",
"columnsFrom": [
"file_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"hmac_secret_key": {
"name": "hmac_secret_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
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"master_encryption_key_version": {
"name": "master_encryption_key_version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"encrypted_key": {
"name": "encrypted_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"hmac_secret_key_encrypted_key_unique": {
"name": "hmac_secret_key_encrypted_key_unique",
"columns": [
"encrypted_key"
],
"isUnique": true
}
},
"foreignKeys": {
"hmac_secret_key_user_id_user_id_fk": {
"name": "hmac_secret_key_user_id_user_id_fk",
"tableFrom": "hmac_secret_key",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": {
"name": "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk",
"tableFrom": "hmac_secret_key",
"tableTo": "master_encryption_key",
"columnsFrom": [
"user_id",
"master_encryption_key_version"
],
"columnsTo": [
"user_id",
"version"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"hmac_secret_key_user_id_version_pk": {
"columns": [
"user_id",
"version"
],
"name": "hmac_secret_key_user_id_version_pk"
}
},
"uniqueConstraints": {}
},
"hmac_secret_key_log": {
"name": "hmac_secret_key_log",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hmac_secret_key_version": {
"name": "hmac_secret_key_version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"action_by": {
"name": "action_by",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"hmac_secret_key_log_user_id_user_id_fk": {
"name": "hmac_secret_key_log_user_id_user_id_fk",
"tableFrom": "hmac_secret_key_log",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"hmac_secret_key_log_action_by_user_id_fk": {
"name": "hmac_secret_key_log_action_by_user_id_fk",
"tableFrom": "hmac_secret_key_log",
"tableTo": "user",
"columnsFrom": [
"action_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": {
"name": "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk",
"tableFrom": "hmac_secret_key_log",
"tableTo": "hmac_secret_key",
"columnsFrom": [
"user_id",
"hmac_secret_key_version"
],
"columnsTo": [
"user_id",
"version"
],
"onDelete": "no action",
"onUpdate": "no action"
} }
}, },
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
@@ -594,20 +907,6 @@
"notNull": true, "notNull": true,
"autoincrement": false "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": { "state": {
"name": "state", "name": "state",
"type": "text", "type": "text",
@@ -637,19 +936,6 @@
], ],
"onDelete": "no action", "onDelete": "no action",
"onUpdate": "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": { "compositePrimaryKeys": {
@@ -663,6 +949,99 @@
}, },
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"master_encryption_key_log": {
"name": "master_encryption_key_log",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"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
},
"timestamp": {
"name": "timestamp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"action_by": {
"name": "action_by",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"master_encryption_key_log_user_id_user_id_fk": {
"name": "master_encryption_key_log_user_id_user_id_fk",
"tableFrom": "master_encryption_key_log",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"master_encryption_key_log_action_by_client_id_fk": {
"name": "master_encryption_key_log_action_by_client_id_fk",
"tableFrom": "master_encryption_key_log",
"tableTo": "client",
"columnsFrom": [
"action_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": {
"name": "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk",
"tableFrom": "master_encryption_key_log",
"tableTo": "master_encryption_key",
"columnsFrom": [
"user_id",
"master_encryption_key_version"
],
"columnsTo": [
"user_id",
"version"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": { "session": {
"name": "session", "name": "session",
"columns": { "columns": {

View File

@@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1736637983139, "when": 1736696839327,
"tag": "0000_spooky_lady_bullseye", "tag": "0000_lush_black_bolt",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -1,6 +1,6 @@
import type { ClientInit } from "@sveltejs/kit"; import type { ClientInit } from "@sveltejs/kit";
import { getClientKey, getMasterKeys } from "$lib/indexedDB"; import { getClientKey, getMasterKeys, getHmacSecrets } from "$lib/indexedDB";
import { clientKeyStore, masterKeyStore } from "$lib/stores"; import { clientKeyStore, masterKeyStore, hmacSecretStore } from "$lib/stores";
const prepareClientKeyStore = async () => { const prepareClientKeyStore = async () => {
const [encryptKey, decryptKey, signKey, verifyKey] = await Promise.all([ const [encryptKey, decryptKey, signKey, verifyKey] = await Promise.all([
@@ -21,6 +21,13 @@ const prepareMasterKeyStore = async () => {
} }
}; };
export const init: ClientInit = async () => { const prepareHmacSecretStore = async () => {
await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore()]); const hmacSecrets = await getHmacSecrets();
if (hmacSecrets.length > 0) {
hmacSecretStore.set(new Map(hmacSecrets.map((hmacSecret) => [hmacSecret.version, hmacSecret])));
}
};
export const init: ClientInit = async () => {
await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore(), prepareHmacSecretStore()]);
}; };

View File

@@ -29,7 +29,7 @@
onclick?.(); onclick?.();
}, 100); }, 100);
}} }}
class="{bgColorStyle} {fontColorStyle} h-12 w-full rounded-xl font-medium" class="{bgColorStyle} {fontColorStyle} h-12 w-full min-w-fit rounded-xl font-medium"
> >
<div class="h-full w-full p-3 transition active:scale-95"> <div class="h-full w-full p-3 transition active:scale-95">
{@render children?.()} {@render children?.()}

View File

@@ -11,6 +11,7 @@ interface KeyExportState {
verifyKeyBase64: string; verifyKeyBase64: string;
masterKeyWrapped: string; masterKeyWrapped: string;
hmacSecretWrapped: string;
} }
const useAutoNull = <T>(value: T | null) => { const useAutoNull = <T>(value: T | null) => {

View File

@@ -7,22 +7,28 @@ interface ClientKey {
key: CryptoKey; key: CryptoKey;
} }
type MasterKeyState = "active" | "retired";
interface MasterKey { interface MasterKey {
version: number; version: number;
state: MasterKeyState; state: "active" | "retired";
key: CryptoKey; key: CryptoKey;
} }
interface HmacSecret {
version: number;
state: "active";
secret: CryptoKey;
}
const keyStore = new Dexie("keyStore") as Dexie & { const keyStore = new Dexie("keyStore") as Dexie & {
clientKey: EntityTable<ClientKey, "usage">; clientKey: EntityTable<ClientKey, "usage">;
masterKey: EntityTable<MasterKey, "version">; masterKey: EntityTable<MasterKey, "version">;
hmacSecret: EntityTable<HmacSecret, "version">;
}; };
keyStore.version(1).stores({ keyStore.version(1).stores({
clientKey: "usage", clientKey: "usage",
masterKey: "version", masterKey: "version",
hmacSecret: "version",
}); });
export const getClientKey = async (usage: ClientKeyUsage) => { export const getClientKey = async (usage: ClientKeyUsage) => {
@@ -62,3 +68,14 @@ export const storeMasterKeys = async (keys: MasterKey[]) => {
} }
await keyStore.masterKey.bulkPut(keys); await keyStore.masterKey.bulkPut(keys);
}; };
export const getHmacSecrets = async () => {
return await keyStore.hmacSecret.toArray();
};
export const storeHmacSecrets = async (secrets: HmacSecret[]) => {
if (secrets.some(({ secret }) => secret.extractable)) {
throw new Error("Hmac secrets must be nonextractable");
}
await keyStore.hmacSecret.bulkPut(secrets);
};

View File

@@ -55,6 +55,27 @@ export const unwrapDataKey = async (dataKeyWrapped: string, masterKey: CryptoKey
}; };
}; };
export const wrapHmacSecret = async (hmacSecret: CryptoKey, masterKey: CryptoKey) => {
return encodeToBase64(await window.crypto.subtle.wrapKey("raw", hmacSecret, masterKey, "AES-KW"));
};
export const unwrapHmacSecret = async (hmacSecretWrapped: string, masterKey: CryptoKey) => {
return {
hmacSecret: await window.crypto.subtle.unwrapKey(
"raw",
decodeFromBase64(hmacSecretWrapped),
masterKey,
"AES-KW",
{
name: "HMAC",
hash: "SHA-256",
} satisfies HmacImportParams,
false, // Nonextractable
["sign", "verify"],
),
};
};
export const encryptData = async (data: BufferSource, dataKey: CryptoKey) => { export const encryptData = async (data: BufferSource, dataKey: CryptoKey) => {
const iv = window.crypto.getRandomValues(new Uint8Array(12)); const iv = window.crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await window.crypto.subtle.encrypt( const ciphertext = await window.crypto.subtle.encrypt(

View File

@@ -95,7 +95,7 @@ export const unwrapMasterKey = async (
}; };
}; };
export const signMessage = async (message: BufferSource, signKey: CryptoKey) => { export const signMessageRSA = async (message: BufferSource, signKey: CryptoKey) => {
return await window.crypto.subtle.sign( return await window.crypto.subtle.sign(
{ {
name: "RSA-PSS", name: "RSA-PSS",
@@ -106,7 +106,7 @@ export const signMessage = async (message: BufferSource, signKey: CryptoKey) =>
); );
}; };
export const verifySignature = async ( export const verifySignatureRSA = async (
message: BufferSource, message: BufferSource,
signature: BufferSource, signature: BufferSource,
verifyKey: CryptoKey, verifyKey: CryptoKey,
@@ -131,7 +131,7 @@ export const signMasterKeyWrapped = async (
version: masterKeyVersion, version: masterKeyVersion,
key: masterKeyWrapped, key: masterKeyWrapped,
}); });
return encodeToBase64(await signMessage(encodeString(serialized), signKey)); return encodeToBase64(await signMessageRSA(encodeString(serialized), signKey));
}; };
export const verifyMasterKeyWrapped = async ( export const verifyMasterKeyWrapped = async (
@@ -144,7 +144,7 @@ export const verifyMasterKeyWrapped = async (
version: masterKeyVersion, version: masterKeyVersion,
key: masterKeyWrapped, key: masterKeyWrapped,
}); });
return await verifySignature( return await verifySignatureRSA(
encodeString(serialized), encodeString(serialized),
decodeFromBase64(masterKeyWrappedSig), decodeFromBase64(masterKeyWrappedSig),
verifyKey, verifyKey,

View File

@@ -1,3 +1,20 @@
export const digestMessage = async (message: BufferSource) => { export const digestMessage = async (message: BufferSource) => {
return await window.crypto.subtle.digest("SHA-256", message); return await window.crypto.subtle.digest("SHA-256", message);
}; };
export const generateHmacSecret = async () => {
return {
hmacSecret: await window.crypto.subtle.generateKey(
{
name: "HMAC",
hash: "SHA-256",
} satisfies HmacKeyGenParams,
true,
["sign", "verify"],
),
};
};
export const signMessageHmac = async (message: BufferSource, hmacSecret: CryptoKey) => {
return await window.crypto.subtle.sign("HMAC", hmacSecret, message);
};

View File

@@ -8,6 +8,9 @@ type IntegrityErrorMessages =
| "Directory not found" | "Directory not found"
| "File not found" | "File not found"
| "Invalid DEK version" | "Invalid DEK version"
// HSK
| "HSK already registered"
| "Inactive HSK version"
// MEK // MEK
| "MEK already registered" | "MEK already registered"
| "Inactive MEK version" | "Inactive MEK version"

View File

@@ -1,7 +1,7 @@
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import db from "./drizzle"; import db from "./drizzle";
import { IntegrityError } from "./error"; import { IntegrityError } from "./error";
import { directory, directoryLog, file, fileLog, mek } from "./schema"; import { directory, directoryLog, file, fileLog, hsk, mek } from "./schema";
type DirectoryId = "root" | number; type DirectoryId = "root" | number;
@@ -22,6 +22,8 @@ export interface NewFileParams {
mekVersion: number; mekVersion: number;
encDek: string; encDek: string;
dekVersion: Date; dekVersion: Date;
hskVersion: number | null;
contentHmac: string | null;
contentType: string; contentType: string;
encContentIv: string; encContentIv: string;
encName: string; encName: string;
@@ -152,6 +154,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
}; };
export const registerFile = async (params: NewFileParams) => { export const registerFile = async (params: NewFileParams) => {
if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) {
throw new Error("Invalid arguments");
}
await db.transaction( await db.transaction(
async (tx) => { async (tx) => {
const meks = await tx const meks = await tx
@@ -163,6 +169,17 @@ export const registerFile = async (params: NewFileParams) => {
throw new IntegrityError("Inactive MEK version"); throw new IntegrityError("Inactive MEK version");
} }
if (params.hskVersion) {
const hsks = await tx
.select({ version: hsk.version })
.from(hsk)
.where(and(eq(hsk.userId, params.userId), eq(hsk.state, "active")))
.limit(1);
if (hsks[0]?.version !== params.hskVersion) {
throw new IntegrityError("Inactive HSK version");
}
}
const newFiles = await tx const newFiles = await tx
.insert(file) .insert(file)
.values({ .values({
@@ -170,6 +187,8 @@ export const registerFile = async (params: NewFileParams) => {
parentId: params.parentId === "root" ? null : params.parentId, parentId: params.parentId === "root" ? null : params.parentId,
userId: params.userId, userId: params.userId,
mekVersion: params.mekVersion, mekVersion: params.mekVersion,
hskVersion: params.hskVersion,
contentHmac: params.contentHmac,
contentType: params.contentType, contentType: params.contentType,
encDek: params.encDek, encDek: params.encDek,
dekVersion: params.dekVersion, dekVersion: params.dekVersion,
@@ -201,6 +220,23 @@ export const getAllFilesByParent = async (userId: number, parentId: DirectoryId)
); );
}; };
export const getAllFileIdsByContentHmac = async (
userId: number,
hskVersion: number,
contentHmac: string,
) => {
return await db
.select({ id: file.id })
.from(file)
.where(
and(
eq(file.userId, userId),
eq(file.hskVersion, hskVersion),
eq(file.contentHmac, contentHmac),
),
);
};
export const getFile = async (userId: number, fileId: number) => { export const getFile = async (userId: number, fileId: number) => {
const res = await db const res = await db
.select() .select()

45
src/lib/server/db/hsk.ts Normal file
View File

@@ -0,0 +1,45 @@
import { SqliteError } from "better-sqlite3";
import { and, eq } from "drizzle-orm";
import db from "./drizzle";
import { IntegrityError } from "./error";
import { hsk, hskLog } from "./schema";
export const registerInitialHsk = async (
userId: number,
createdBy: number,
mekVersion: number,
encHsk: string,
) => {
await db.transaction(
async (tx) => {
try {
await tx.insert(hsk).values({
userId,
version: 1,
state: "active",
mekVersion,
encHsk,
});
await tx.insert(hskLog).values({
userId,
hskVersion: 1,
timestamp: new Date(),
action: "create",
actionBy: createdBy,
});
} catch (e) {
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
throw new IntegrityError("HSK already registered");
}
}
},
{ behavior: "exclusive" },
);
};
export const getAllValidHsks = async (userId: number) => {
return await db
.select()
.from(hsk)
.where(and(eq(hsk.userId, userId), eq(hsk.state, "active")));
};

View File

@@ -1,4 +1,5 @@
import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core";
import { hsk } from "./hsk";
import { mek } from "./mek"; import { mek } from "./mek";
import { user } from "./user"; import { user } from "./user";
@@ -55,15 +56,21 @@ export const file = sqliteTable(
mekVersion: integer("master_encryption_key_version").notNull(), mekVersion: integer("master_encryption_key_version").notNull(),
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(),
hskVersion: integer("hmac_secret_key_version"),
contentHmac: text("content_hmac"), // Base64
contentType: text("content_type").notNull(), contentType: text("content_type").notNull(),
encContentIv: text("encrypted_content_iv").notNull(), // Base64 encContentIv: text("encrypted_content_iv").notNull(), // Base64
encName: ciphertext("encrypted_name").notNull(), encName: ciphertext("encrypted_name").notNull(),
}, },
(t) => ({ (t) => ({
ref: foreignKey({ ref1: foreignKey({
columns: [t.userId, t.mekVersion], columns: [t.userId, t.mekVersion],
foreignColumns: [mek.userId, mek.version], foreignColumns: [mek.userId, mek.version],
}), }),
ref2: foreignKey({
columns: [t.userId, t.hskVersion],
foreignColumns: [hsk.userId, hsk.version],
}),
}), }),
); );

View File

@@ -0,0 +1,43 @@
import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core";
import { mek } from "./mek";
import { user } from "./user";
export const hsk = sqliteTable(
"hmac_secret_key",
{
userId: integer("user_id")
.notNull()
.references(() => user.id),
version: integer("version").notNull(),
state: text("state", { enum: ["active"] }).notNull(),
mekVersion: integer("master_encryption_key_version").notNull(),
encHsk: text("encrypted_key").notNull().unique(), // Base64
},
(t) => ({
pk: primaryKey({ columns: [t.userId, t.version] }),
ref: foreignKey({
columns: [t.userId, t.mekVersion],
foreignColumns: [mek.userId, mek.version],
}),
}),
);
export const hskLog = sqliteTable(
"hmac_secret_key_log",
{
id: integer("id").primaryKey({ autoIncrement: true }),
userId: integer("user_id")
.notNull()
.references(() => user.id),
hskVersion: integer("hmac_secret_key_version").notNull(),
timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(),
action: text("action", { enum: ["create"] }).notNull(),
actionBy: integer("action_by").references(() => user.id),
},
(t) => ({
ref: foreignKey({
columns: [t.userId, t.hskVersion],
foreignColumns: [hsk.userId, hsk.version],
}),
}),
);

View File

@@ -1,5 +1,6 @@
export * from "./client"; export * from "./client";
export * from "./file"; export * from "./file";
export * from "./hsk";
export * from "./mek"; export * from "./mek";
export * from "./session"; export * from "./session";
export * from "./user"; export * from "./user";

View File

@@ -22,11 +22,24 @@ export const fileRenameRequest = z.object({
}); });
export type FileRenameRequest = z.infer<typeof fileRenameRequest>; export type FileRenameRequest = z.infer<typeof fileRenameRequest>;
export const duplicateFileScanRequest = z.object({
hskVersion: z.number().int().positive(),
contentHmac: z.string().base64().nonempty(),
});
export type DuplicateFileScanRequest = z.infer<typeof duplicateFileScanRequest>;
export const duplicateFileScanResponse = z.object({
files: z.number().int().positive().array(),
});
export type DuplicateFileScanResponse = z.infer<typeof duplicateFileScanResponse>;
export const fileUploadRequest = z.object({ export const fileUploadRequest = z.object({
parentId: z.union([z.enum(["root"]), z.number().int().positive()]), parentId: z.union([z.enum(["root"]), z.number().int().positive()]),
mekVersion: z.number().int().positive(), mekVersion: z.number().int().positive(),
dek: z.string().base64().nonempty(), dek: z.string().base64().nonempty(),
dekVersion: z.string().datetime(), dekVersion: z.string().datetime(),
hskVersion: z.number().int().positive(),
contentHmac: z.string().base64().nonempty(),
contentType: z contentType: z
.string() .string()
.nonempty() .nonempty()

View File

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

View File

@@ -2,4 +2,5 @@ export * from "./auth";
export * from "./client"; export * from "./client";
export * from "./directory"; export * from "./directory";
export * from "./file"; export * from "./file";
export * from "./hsk";
export * from "./mek"; export * from "./mek";

View File

@@ -7,6 +7,7 @@ import { v4 as uuidv4 } from "uuid";
import { IntegrityError } from "$lib/server/db/error"; import { IntegrityError } from "$lib/server/db/error";
import { import {
registerFile, registerFile,
getAllFileIdsByContentHmac,
getFile, getFile,
setFileEncName, setFileEncName,
unregisterFile, unregisterFile,
@@ -76,6 +77,15 @@ export const renameFile = async (
} }
}; };
export const scanDuplicateFiles = async (
userId: number,
hskVersion: number,
contentHmac: string,
) => {
const fileIds = await getAllFileIdsByContentHmac(userId, hskVersion, contentHmac);
return { files: fileIds.map(({ id }) => id) };
};
const safeUnlink = async (path: string) => { const safeUnlink = async (path: string) => {
await unlink(path).catch(console.error); await unlink(path).catch(console.error);
}; };

View File

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

View File

@@ -1,5 +1,5 @@
import { callPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import { encodeToBase64, decryptChallenge, signMessage } from "$lib/modules/crypto"; import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto";
import type { import type {
SessionUpgradeRequest, SessionUpgradeRequest,
SessionUpgradeResponse, SessionUpgradeResponse,
@@ -20,7 +20,7 @@ export const requestSessionUpgrade = async (
const { challenge }: SessionUpgradeResponse = await res.json(); const { challenge }: SessionUpgradeResponse = await res.json();
const answer = await decryptChallenge(challenge, decryptKey); const answer = await decryptChallenge(challenge, decryptKey);
const answerSig = await signMessage(answer, signKey); const answerSig = await signMessageRSA(answer, signKey);
res = await callPostApi<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", { res = await callPostApi<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
answer: encodeToBase64(answer), answer: encodeToBase64(answer),

View File

@@ -3,7 +3,7 @@ import { storeMasterKeys } from "$lib/indexedDB";
import { import {
encodeToBase64, encodeToBase64,
decryptChallenge, decryptChallenge,
signMessage, signMessageRSA,
unwrapMasterKey, unwrapMasterKey,
verifyMasterKeyWrapped, verifyMasterKeyWrapped,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
@@ -29,7 +29,7 @@ export const requestClientRegistration = async (
const { challenge }: ClientRegisterResponse = await res.json(); const { challenge }: ClientRegisterResponse = await res.json();
const answer = await decryptChallenge(challenge, decryptKey); const answer = await decryptChallenge(challenge, decryptKey);
const answerSig = await signMessage(answer, signKey); const answerSig = await signMessageRSA(answer, signKey);
res = await callPostApi<ClientRegisterVerifyRequest>("/api/client/register/verify", { res = await callPostApi<ClientRegisterVerifyRequest>("/api/client/register/verify", {
answer: encodeToBase64(answer), answer: encodeToBase64(answer),

View File

@@ -13,6 +13,14 @@ export interface MasterKey {
key: CryptoKey; key: CryptoKey;
} }
export interface HmacSecret {
version: number;
state: "active";
secret: CryptoKey;
}
export const clientKeyStore = writable<ClientKeys | null>(null); export const clientKeyStore = writable<ClientKeys | null>(null);
export const masterKeyStore = writable<Map<number, MasterKey> | null>(null); export const masterKeyStore = writable<Map<number, MasterKey> | null>(null);
export const hmacSecretStore = writable<Map<number, HmacSecret> | null>(null);

View File

@@ -11,7 +11,7 @@
requestClientRegistration, requestClientRegistration,
storeClientKeys, storeClientKeys,
requestSessionUpgrade, requestSessionUpgrade,
requestInitialMasterKeyRegistration, requestInitialMasterKeyAndHmacSecretRegistration,
} from "./service"; } from "./service";
import IconKey from "~icons/material-symbols/key"; import IconKey from "~icons/material-symbols/key";
@@ -69,9 +69,13 @@
throw new Error("Failed to upgrade session"); throw new Error("Failed to upgrade session");
if ( if (
!(await requestInitialMasterKeyRegistration(data.masterKeyWrapped, $clientKeyStore.signKey)) !(await requestInitialMasterKeyAndHmacSecretRegistration(
data.masterKeyWrapped,
data.hmacSecretWrapped,
$clientKeyStore.signKey,
))
) )
throw new Error("Failed to register initial MEK"); throw new Error("Failed to register initial MEK and HSK");
await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath)); await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath));
} catch (e) { } catch (e) {

View File

@@ -1,7 +1,10 @@
import { callPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import { storeClientKey } from "$lib/indexedDB"; import { storeClientKey } from "$lib/indexedDB";
import { signMasterKeyWrapped } from "$lib/modules/crypto"; import { signMasterKeyWrapped } from "$lib/modules/crypto";
import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; import type {
InitialMasterKeyRegisterRequest,
InitialHmacSecretRegisterRequest,
} from "$lib/server/schemas";
import type { ClientKeys } from "$lib/stores"; import type { ClientKeys } from "$lib/stores";
export { requestSessionUpgrade } from "$lib/services/auth"; export { requestSessionUpgrade } from "$lib/services/auth";
@@ -44,13 +47,22 @@ export const storeClientKeys = async (clientKeys: ClientKeys) => {
]); ]);
}; };
export const requestInitialMasterKeyRegistration = async ( export const requestInitialMasterKeyAndHmacSecretRegistration = async (
masterKeyWrapped: string, masterKeyWrapped: string,
hmacSecretWrapped: string,
signKey: CryptoKey, signKey: CryptoKey,
) => { ) => {
const res = await callPostApi<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", { let res = await callPostApi<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", {
mek: masterKeyWrapped, mek: masterKeyWrapped,
mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey),
}); });
return res.ok || res.status === 409; if (!res.ok) {
return res.status === 409;
}
res = await callPostApi<InitialHmacSecretRegisterRequest>("/api/hsk/register/initial", {
mekVersion: 1,
hsk: hmacSecretWrapped,
});
return res.ok;
}; };

View File

@@ -6,7 +6,11 @@
import { gotoStateful } from "$lib/hooks"; import { gotoStateful } from "$lib/hooks";
import { clientKeyStore } from "$lib/stores"; import { clientKeyStore } from "$lib/stores";
import Order from "./Order.svelte"; import Order from "./Order.svelte";
import { generateClientKeys, generateInitialMasterKey } from "./service"; import {
generateClientKeys,
generateInitialMasterKey,
generateInitialHmacSecret,
} from "./service";
import IconKey from "~icons/material-symbols/key"; import IconKey from "~icons/material-symbols/key";
@@ -36,12 +40,14 @@
// TODO: Loading indicator // TODO: Loading indicator
const { encryptKey, ...clientKeys } = await generateClientKeys(); const { encryptKey, ...clientKeys } = await generateClientKeys();
const { masterKeyWrapped } = await generateInitialMasterKey(encryptKey); const { masterKey, masterKeyWrapped } = await generateInitialMasterKey(encryptKey);
const { hmacSecretWrapped } = await generateInitialHmacSecret(masterKey);
await gotoStateful("/key/export", { await gotoStateful("/key/export", {
...clientKeys, ...clientKeys,
redirectPath: data.redirectPath, redirectPath: data.redirectPath,
masterKeyWrapped, masterKeyWrapped,
hmacSecretWrapped,
}); });
}; };

View File

@@ -3,8 +3,11 @@ import {
generateSigningKeyPair, generateSigningKeyPair,
exportRSAKeyToBase64, exportRSAKeyToBase64,
makeRSAKeyNonextractable, makeRSAKeyNonextractable,
generateMasterKey,
wrapMasterKey, wrapMasterKey,
generateMasterKey,
makeAESKeyNonextractable,
wrapHmacSecret,
generateHmacSecret,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import { clientKeyStore } from "$lib/stores"; import { clientKeyStore } from "$lib/stores";
@@ -31,6 +34,14 @@ export const generateClientKeys = async () => {
export const generateInitialMasterKey = async (encryptKey: CryptoKey) => { export const generateInitialMasterKey = async (encryptKey: CryptoKey) => {
const { masterKey } = await generateMasterKey(); const { masterKey } = await generateMasterKey();
return { return {
masterKey: await makeAESKeyNonextractable(masterKey),
masterKeyWrapped: await wrapMasterKey(masterKey, encryptKey), masterKeyWrapped: await wrapMasterKey(masterKey, encryptKey),
}; };
}; };
export const generateInitialHmacSecret = async (masterKey: CryptoKey) => {
const { hmacSecret } = await generateHmacSecret();
return {
hmacSecretWrapped: await wrapHmacSecret(hmacSecret, masterKey),
};
};

View File

@@ -1,18 +1,22 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { TopBar } from "$lib/components"; import { TopBar } from "$lib/components";
import { FloatingButton } from "$lib/components/buttons"; import { FloatingButton } from "$lib/components/buttons";
import { getDirectoryInfo } from "$lib/modules/file"; import { getDirectoryInfo } from "$lib/modules/file";
import { masterKeyStore, type DirectoryInfo } from "$lib/stores"; import { masterKeyStore, hmacSecretStore, type DirectoryInfo } from "$lib/stores";
import CreateBottomSheet from "./CreateBottomSheet.svelte"; import CreateBottomSheet from "./CreateBottomSheet.svelte";
import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; import CreateDirectoryModal from "./CreateDirectoryModal.svelte";
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte"; import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
import DirectoryEntries from "./DirectoryEntries"; import DirectoryEntries from "./DirectoryEntries";
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte"; import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
import DuplicateFileModal from "./DuplicateFileModal.svelte";
import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte"; import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte";
import { import {
requestHmacSecretDownload,
requestDirectoryCreation, requestDirectoryCreation,
requestDuplicateFileScan,
requestFileUpload, requestFileUpload,
requestDirectoryEntryRename, requestDirectoryEntryRename,
requestDirectoryEntryDeletion, requestDirectoryEntryDeletion,
@@ -21,14 +25,22 @@
import IconAdd from "~icons/material-symbols/add"; import IconAdd from "~icons/material-symbols/add";
interface LoadedFile {
file: File;
fileBuffer: ArrayBuffer;
fileSigned: string;
}
let { data } = $props(); let { data } = $props();
let info: Writable<DirectoryInfo | null> | undefined = $state(); let info: Writable<DirectoryInfo | null> | undefined = $state();
let fileInput: HTMLInputElement | undefined = $state(); let fileInput: HTMLInputElement | undefined = $state();
let loadedFile: LoadedFile | undefined = $state();
let selectedEntry: SelectedDirectoryEntry | undefined = $state(); let selectedEntry: SelectedDirectoryEntry | undefined = $state();
let isCreateBottomSheetOpen = $state(false); let isCreateBottomSheetOpen = $state(false);
let isCreateDirectoryModalOpen = $state(false); let isCreateDirectoryModalOpen = $state(false);
let isDuplicateFileModalOpen = $state(false);
let isDirectoryEntryMenuBottomSheetOpen = $state(false); let isDirectoryEntryMenuBottomSheetOpen = $state(false);
let isRenameDirectoryEntryModalOpen = $state(false); let isRenameDirectoryEntryModalOpen = $state(false);
@@ -40,15 +52,42 @@
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
}; };
const uploadFile = () => { const uploadFile = (loadedFile: LoadedFile) => {
const file = fileInput?.files?.[0]; requestFileUpload(
if (!file) return; loadedFile.file,
loadedFile.fileBuffer,
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!).then(() => { loadedFile.fileSigned,
data.id,
$masterKeyStore?.get(1)!,
$hmacSecretStore?.get(1)!,
).then(() => {
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
}); });
}; };
const loadAndUploadFile = async () => {
const file = fileInput?.files?.[0];
if (!file) return;
fileInput!.value = "";
const scanRes = await requestDuplicateFileScan(file, $hmacSecretStore?.get(1)!);
if (scanRes === null) {
throw new Error("Failed to scan duplicate files");
} else if (scanRes.isDuplicate) {
loadedFile = { ...scanRes, file };
isDuplicateFileModalOpen = true;
} else {
uploadFile({ ...scanRes, file });
}
};
onMount(async () => {
if (!$hmacSecretStore && !(await requestHmacSecretDownload($masterKeyStore?.get(1)?.key!))) {
throw new Error("Failed to download hmac secrets");
}
});
$effect(() => { $effect(() => {
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
}); });
@@ -58,7 +97,7 @@
<title>파일</title> <title>파일</title>
</svelte:head> </svelte:head>
<input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" /> <input bind:this={fileInput} onchange={loadAndUploadFile} type="file" class="hidden" />
<div class="flex min-h-full flex-col px-4"> <div class="flex min-h-full flex-col px-4">
{#if data.id !== "root"} {#if data.id !== "root"}
@@ -99,6 +138,18 @@
}} }}
/> />
<CreateDirectoryModal bind:isOpen={isCreateDirectoryModalOpen} onCreateClick={createDirectory} /> <CreateDirectoryModal bind:isOpen={isCreateDirectoryModalOpen} onCreateClick={createDirectory} />
<DuplicateFileModal
bind:isOpen={isDuplicateFileModalOpen}
onclose={() => {
isDuplicateFileModalOpen = false;
loadedFile = undefined;
}}
onDuplicateClick={() => {
uploadFile(loadedFile!);
isDuplicateFileModalOpen = false;
loadedFile = undefined;
}}
/>
<DirectoryEntryMenuBottomSheet <DirectoryEntryMenuBottomSheet
bind:isOpen={isDirectoryEntryMenuBottomSheetOpen} bind:isOpen={isDirectoryEntryMenuBottomSheetOpen}

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import { Modal } from "$lib/components";
import { Button } from "$lib/components/buttons";
interface Props {
onclose: () => void;
onDuplicateClick: () => void;
isOpen: boolean;
}
let { onclose, onDuplicateClick, isOpen = $bindable() }: Props = $props();
</script>
<Modal bind:isOpen {onclose}>
<div class="space-y-4">
<div class="space-y-2 break-keep">
<p class="text-xl font-bold">이미 업로드된 파일이에요.</p>
<p>그래도 업로드할까요?</p>
</div>
<div class="flex gap-2">
<Button
color="gray"
onclick={() => {
isOpen = false;
}}
>
아니요
</Button>
<Button onclick={onDuplicateClick}>업로드할게요</Button>
</div>
</div>
</Modal>

View File

@@ -1,12 +1,24 @@
import { callPostApi } from "$lib/hooks"; import { callGetApi, callPostApi } from "$lib/hooks";
import { generateDataKey, wrapDataKey, encryptData, encryptString } from "$lib/modules/crypto"; import { storeHmacSecrets } from "$lib/indexedDB";
import {
encodeToBase64,
generateDataKey,
wrapDataKey,
unwrapHmacSecret,
encryptData,
encryptString,
signMessageHmac,
} from "$lib/modules/crypto";
import type { import type {
DirectoryRenameRequest, DirectoryRenameRequest,
DirectoryCreateRequest, DirectoryCreateRequest,
FileRenameRequest, FileRenameRequest,
FileUploadRequest, FileUploadRequest,
HmacSecretListResponse,
DuplicateFileScanRequest,
DuplicateFileScanResponse,
} from "$lib/server/schemas"; } from "$lib/server/schemas";
import type { MasterKey } from "$lib/stores"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
export interface SelectedDirectoryEntry { export interface SelectedDirectoryEntry {
type: "directory" | "file"; type: "directory" | "file";
@@ -16,6 +28,26 @@ export interface SelectedDirectoryEntry {
name: string; name: string;
} }
export const requestHmacSecretDownload = async (masterKey: CryptoKey) => {
// TODO: MEK rotation
const res = await callGetApi("/api/hsk/list");
if (!res.ok) return false;
const { hsks: hmacSecretsWrapped }: HmacSecretListResponse = await res.json();
const hmacSecrets = await Promise.all(
hmacSecretsWrapped.map(async ({ version, state, hsk: hmacSecretWrapped }) => {
const { hmacSecret } = await unwrapHmacSecret(hmacSecretWrapped, masterKey);
return { version, state, secret: hmacSecret };
}),
);
await storeHmacSecrets(hmacSecrets);
hmacSecretStore.set(new Map(hmacSecrets.map((hmacSecret) => [hmacSecret.version, hmacSecret])));
return true;
};
export const requestDirectoryCreation = async ( export const requestDirectoryCreation = async (
name: string, name: string,
parentId: "root" | number, parentId: "root" | number,
@@ -33,14 +65,34 @@ export const requestDirectoryCreation = async (
}); });
}; };
export const requestDuplicateFileScan = async (file: File, hmacSecret: HmacSecret) => {
const fileBuffer = await file.arrayBuffer();
const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret));
const res = await callPostApi<DuplicateFileScanRequest>("/api/file/scanDuplicates", {
hskVersion: hmacSecret.version,
contentHmac: fileSigned,
});
if (!res.ok) return null;
const { files }: DuplicateFileScanResponse = await res.json();
return {
fileBuffer,
fileSigned,
isDuplicate: files.length > 0,
};
};
export const requestFileUpload = async ( export const requestFileUpload = async (
file: File, file: File,
fileBuffer: ArrayBuffer,
fileSigned: string,
parentId: "root" | number, parentId: "root" | number,
masterKey: MasterKey, masterKey: MasterKey,
hmacSecret: HmacSecret,
) => { ) => {
const { dataKey, dataKeyVersion } = await generateDataKey(); const { dataKey, dataKeyVersion } = await generateDataKey();
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey);
const nameEncrypted = await encryptString(file.name, dataKey); const nameEncrypted = await encryptString(file.name, dataKey);
const fileEncrypted = await encryptData(fileBuffer, dataKey);
const form = new FormData(); const form = new FormData();
form.set( form.set(
@@ -50,6 +102,8 @@ export const requestFileUpload = async (
mekVersion: masterKey.version, mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key), dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion.toISOString(), dekVersion: dataKeyVersion.toISOString(),
hskVersion: hmacSecret.version,
contentHmac: fileSigned,
contentType: file.type, contentType: file.type,
contentIv: fileEncrypted.iv, contentIv: fileEncrypted.iv,
name: nameEncrypted.ciphertext, name: nameEncrypted.ciphertext,

View File

@@ -0,0 +1,20 @@
import { error, json } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth";
import {
duplicateFileScanRequest,
duplicateFileScanResponse,
type DuplicateFileScanResponse,
} from "$lib/server/schemas";
import { scanDuplicateFiles } from "$lib/server/services/file";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "activeClient");
const zodRes = duplicateFileScanRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { hskVersion, contentHmac } = zodRes.data;
const { files } = await scanDuplicateFiles(userId, hskVersion, contentHmac);
return json(duplicateFileScanResponse.parse({ files } satisfies DuplicateFileScanResponse));
};

View File

@@ -16,8 +16,18 @@ export const POST: RequestHandler = async ({ locals, request }) => {
const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata)); const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata));
if (!zodRes.success) error(400, "Invalid request body"); if (!zodRes.success) error(400, "Invalid request body");
const { parentId, mekVersion, dek, dekVersion, contentType, contentIv, name, nameIv } = const {
zodRes.data; parentId,
mekVersion,
dek,
dekVersion,
hskVersion,
contentHmac,
contentType,
contentIv,
name,
nameIv,
} = zodRes.data;
await uploadFile( await uploadFile(
{ {
@@ -26,6 +36,8 @@ export const POST: RequestHandler = async ({ locals, request }) => {
mekVersion, mekVersion,
encDek: dek, encDek: dek,
dekVersion: new Date(dekVersion), dekVersion: new Date(dekVersion),
hskVersion,
contentHmac,
contentType, contentType,
encContentIv: contentIv, encContentIv: contentIv,
encName: name, encName: name,

View File

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

View File

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