diff --git a/drizzle/0000_spooky_lady_bullseye.sql b/drizzle/0000_lush_black_bolt.sql
similarity index 68%
rename from drizzle/0000_spooky_lady_bullseye.sql
rename to drizzle/0000_lush_black_bolt.sql
index d9b520c..4012e91 100644
--- a/drizzle/0000_spooky_lady_bullseye.sql
+++ b/drizzle/0000_lush_black_bolt.sql
@@ -27,7 +27,6 @@ CREATE TABLE `user_client_challenge` (
--> 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,
@@ -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
);
--> 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` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
- `path` text NOT NULL,
`parent_id` integer,
- `created_at` integer NOT NULL,
`user_id` integer NOT NULL,
+ `path` text NOT NULL,
`master_encryption_key_version` integer NOT NULL,
`encrypted_data_encryption_key` text NOT NULL,
`data_encryption_key_version` integer NOT NULL,
+ `hmac_secret_key_version` integer,
+ `content_hmac` text,
`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,
+ 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
);
--> 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` (
`user_id` integer NOT NULL,
`client_id` integer NOT NULL,
@@ -71,13 +113,22 @@ CREATE TABLE `client_master_encryption_key` (
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
+);
+--> 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 (`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
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 `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 `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_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
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
index 57c4a6a..4905c11 100644
--- a/drizzle/meta/0000_snapshot.json
+++ b/drizzle/meta/0000_snapshot.json
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
- "id": "c518e1b4-38f8-4c8e-bdc9-64152ab456d8",
+ "id": "f2fbe45c-1f1d-4dd8-92ab-dd057c0e668b",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"client": {
@@ -234,13 +234,6 @@
"notNull": true,
"autoincrement": true
},
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
"parent_id": {
"name": "parent_id",
"type": "integer",
@@ -339,6 +332,64 @@
"compositePrimaryKeys": {},
"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": {
"name": "file",
"columns": {
@@ -349,13 +400,6 @@
"notNull": true,
"autoincrement": true
},
- "path": {
- "name": "path",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
"parent_id": {
"name": "parent_id",
"type": "integer",
@@ -363,16 +407,16 @@
"notNull": false,
"autoincrement": false
},
- "created_at": {
- "name": "created_at",
+ "user_id": {
+ "name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
- "user_id": {
- "name": "user_id",
- "type": "integer",
+ "path": {
+ "name": "path",
+ "type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
@@ -398,6 +442,20 @@
"notNull": true,
"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": {
"name": "content_type",
"type": "text",
@@ -477,6 +535,261 @@
],
"onDelete": "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": {},
@@ -594,20 +907,6 @@
"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",
@@ -637,19 +936,6 @@
],
"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": {
@@ -663,6 +949,99 @@
},
"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": {
"name": "session",
"columns": {
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 62c9f38..723ede5 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
- "when": 1736637983139,
- "tag": "0000_spooky_lady_bullseye",
+ "when": 1736696839327,
+ "tag": "0000_lush_black_bolt",
"breakpoints": true
}
]
diff --git a/src/hooks.client.ts b/src/hooks.client.ts
index 217d7ea..3f0ccfb 100644
--- a/src/hooks.client.ts
+++ b/src/hooks.client.ts
@@ -1,6 +1,6 @@
import type { ClientInit } from "@sveltejs/kit";
-import { getClientKey, getMasterKeys } from "$lib/indexedDB";
-import { clientKeyStore, masterKeyStore } from "$lib/stores";
+import { getClientKey, getMasterKeys, getHmacSecrets } from "$lib/indexedDB";
+import { clientKeyStore, masterKeyStore, hmacSecretStore } from "$lib/stores";
const prepareClientKeyStore = async () => {
const [encryptKey, decryptKey, signKey, verifyKey] = await Promise.all([
@@ -21,6 +21,13 @@ const prepareMasterKeyStore = async () => {
}
};
-export const init: ClientInit = async () => {
- await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore()]);
+const prepareHmacSecretStore = async () => {
+ 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()]);
};
diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte
index 692d4a1..65f80ab 100644
--- a/src/lib/components/buttons/Button.svelte
+++ b/src/lib/components/buttons/Button.svelte
@@ -29,7 +29,7 @@
onclick?.();
}, 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"
>
{@render children?.()}
diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts
index fffc95f..4bcbebf 100644
--- a/src/lib/hooks/gotoStateful.ts
+++ b/src/lib/hooks/gotoStateful.ts
@@ -11,6 +11,7 @@ interface KeyExportState {
verifyKeyBase64: string;
masterKeyWrapped: string;
+ hmacSecretWrapped: string;
}
const useAutoNull =
(value: T | null) => {
diff --git a/src/lib/indexedDB.ts b/src/lib/indexedDB.ts
index b2fbd22..7a4c89e 100644
--- a/src/lib/indexedDB.ts
+++ b/src/lib/indexedDB.ts
@@ -7,22 +7,28 @@ interface ClientKey {
key: CryptoKey;
}
-type MasterKeyState = "active" | "retired";
-
interface MasterKey {
version: number;
- state: MasterKeyState;
+ state: "active" | "retired";
key: CryptoKey;
}
+interface HmacSecret {
+ version: number;
+ state: "active";
+ secret: CryptoKey;
+}
+
const keyStore = new Dexie("keyStore") as Dexie & {
clientKey: EntityTable;
masterKey: EntityTable;
+ hmacSecret: EntityTable;
};
keyStore.version(1).stores({
clientKey: "usage",
masterKey: "version",
+ hmacSecret: "version",
});
export const getClientKey = async (usage: ClientKeyUsage) => {
@@ -62,3 +68,14 @@ export const storeMasterKeys = async (keys: MasterKey[]) => {
}
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);
+};
diff --git a/src/lib/modules/crypto/aes.ts b/src/lib/modules/crypto/aes.ts
index df04851..3c096ba 100644
--- a/src/lib/modules/crypto/aes.ts
+++ b/src/lib/modules/crypto/aes.ts
@@ -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) => {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await window.crypto.subtle.encrypt(
diff --git a/src/lib/modules/crypto/rsa.ts b/src/lib/modules/crypto/rsa.ts
index 9eb81c0..4df8f9e 100644
--- a/src/lib/modules/crypto/rsa.ts
+++ b/src/lib/modules/crypto/rsa.ts
@@ -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(
{
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,
signature: BufferSource,
verifyKey: CryptoKey,
@@ -131,7 +131,7 @@ export const signMasterKeyWrapped = async (
version: masterKeyVersion,
key: masterKeyWrapped,
});
- return encodeToBase64(await signMessage(encodeString(serialized), signKey));
+ return encodeToBase64(await signMessageRSA(encodeString(serialized), signKey));
};
export const verifyMasterKeyWrapped = async (
@@ -144,7 +144,7 @@ export const verifyMasterKeyWrapped = async (
version: masterKeyVersion,
key: masterKeyWrapped,
});
- return await verifySignature(
+ return await verifySignatureRSA(
encodeString(serialized),
decodeFromBase64(masterKeyWrappedSig),
verifyKey,
diff --git a/src/lib/modules/crypto/sha.ts b/src/lib/modules/crypto/sha.ts
index e79f706..3acb258 100644
--- a/src/lib/modules/crypto/sha.ts
+++ b/src/lib/modules/crypto/sha.ts
@@ -1,3 +1,20 @@
export const digestMessage = async (message: BufferSource) => {
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);
+};
diff --git a/src/lib/server/db/error.ts b/src/lib/server/db/error.ts
index beadb6f..547cc6c 100644
--- a/src/lib/server/db/error.ts
+++ b/src/lib/server/db/error.ts
@@ -8,6 +8,9 @@ type IntegrityErrorMessages =
| "Directory not found"
| "File not found"
| "Invalid DEK version"
+ // HSK
+ | "HSK already registered"
+ | "Inactive HSK version"
// MEK
| "MEK already registered"
| "Inactive MEK version"
diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts
index edf0249..db6b881 100644
--- a/src/lib/server/db/file.ts
+++ b/src/lib/server/db/file.ts
@@ -1,7 +1,7 @@
import { and, eq, isNull } from "drizzle-orm";
import db from "./drizzle";
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;
@@ -22,6 +22,8 @@ export interface NewFileParams {
mekVersion: number;
encDek: string;
dekVersion: Date;
+ hskVersion: number | null;
+ contentHmac: string | null;
contentType: string;
encContentIv: string;
encName: string;
@@ -152,6 +154,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
};
export const registerFile = async (params: NewFileParams) => {
+ if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) {
+ throw new Error("Invalid arguments");
+ }
+
await db.transaction(
async (tx) => {
const meks = await tx
@@ -163,6 +169,17 @@ export const registerFile = async (params: NewFileParams) => {
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
.insert(file)
.values({
@@ -170,6 +187,8 @@ export const registerFile = async (params: NewFileParams) => {
parentId: params.parentId === "root" ? null : params.parentId,
userId: params.userId,
mekVersion: params.mekVersion,
+ hskVersion: params.hskVersion,
+ contentHmac: params.contentHmac,
contentType: params.contentType,
encDek: params.encDek,
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) => {
const res = await db
.select()
diff --git a/src/lib/server/db/hsk.ts b/src/lib/server/db/hsk.ts
new file mode 100644
index 0000000..809ed7b
--- /dev/null
+++ b/src/lib/server/db/hsk.ts
@@ -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")));
+};
diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts
index 0bae91a..7ac0b77 100644
--- a/src/lib/server/db/schema/file.ts
+++ b/src/lib/server/db/schema/file.ts
@@ -1,4 +1,5 @@
import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core";
+import { hsk } from "./hsk";
import { mek } from "./mek";
import { user } from "./user";
@@ -55,15 +56,21 @@ 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(),
+ hskVersion: integer("hmac_secret_key_version"),
+ contentHmac: text("content_hmac"), // Base64
contentType: text("content_type").notNull(),
encContentIv: text("encrypted_content_iv").notNull(), // Base64
encName: ciphertext("encrypted_name").notNull(),
},
(t) => ({
- ref: foreignKey({
+ ref1: foreignKey({
columns: [t.userId, t.mekVersion],
foreignColumns: [mek.userId, mek.version],
}),
+ ref2: foreignKey({
+ columns: [t.userId, t.hskVersion],
+ foreignColumns: [hsk.userId, hsk.version],
+ }),
}),
);
diff --git a/src/lib/server/db/schema/hsk.ts b/src/lib/server/db/schema/hsk.ts
new file mode 100644
index 0000000..b78c512
--- /dev/null
+++ b/src/lib/server/db/schema/hsk.ts
@@ -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],
+ }),
+ }),
+);
diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts
index 41fd4fe..40cb9be 100644
--- a/src/lib/server/db/schema/index.ts
+++ b/src/lib/server/db/schema/index.ts
@@ -1,5 +1,6 @@
export * from "./client";
export * from "./file";
+export * from "./hsk";
export * from "./mek";
export * from "./session";
export * from "./user";
diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts
index 0592835..f73b299 100644
--- a/src/lib/server/schemas/file.ts
+++ b/src/lib/server/schemas/file.ts
@@ -22,11 +22,24 @@ export const fileRenameRequest = z.object({
});
export type FileRenameRequest = z.infer;
+export const duplicateFileScanRequest = z.object({
+ hskVersion: z.number().int().positive(),
+ contentHmac: z.string().base64().nonempty(),
+});
+export type DuplicateFileScanRequest = z.infer;
+
+export const duplicateFileScanResponse = z.object({
+ files: z.number().int().positive().array(),
+});
+export type DuplicateFileScanResponse = z.infer;
+
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(),
dekVersion: z.string().datetime(),
+ hskVersion: z.number().int().positive(),
+ contentHmac: z.string().base64().nonempty(),
contentType: z
.string()
.nonempty()
diff --git a/src/lib/server/schemas/hsk.ts b/src/lib/server/schemas/hsk.ts
new file mode 100644
index 0000000..bcea3cd
--- /dev/null
+++ b/src/lib/server/schemas/hsk.ts
@@ -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;
+
+export const initialHmacSecretRegisterRequest = z.object({
+ mekVersion: z.number().int().positive(),
+ hsk: z.string().base64().nonempty(),
+});
+export type InitialHmacSecretRegisterRequest = z.infer;
diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts
index cd2a366..615e2bc 100644
--- a/src/lib/server/schemas/index.ts
+++ b/src/lib/server/schemas/index.ts
@@ -2,4 +2,5 @@ export * from "./auth";
export * from "./client";
export * from "./directory";
export * from "./file";
+export * from "./hsk";
export * from "./mek";
diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts
index b342b90..7599939 100644
--- a/src/lib/server/services/file.ts
+++ b/src/lib/server/services/file.ts
@@ -7,6 +7,7 @@ import { v4 as uuidv4 } from "uuid";
import { IntegrityError } from "$lib/server/db/error";
import {
registerFile,
+ getAllFileIdsByContentHmac,
getFile,
setFileEncName,
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) => {
await unlink(path).catch(console.error);
};
diff --git a/src/lib/server/services/hsk.ts b/src/lib/server/services/hsk.ts
new file mode 100644
index 0000000..c381c51
--- /dev/null
+++ b/src/lib/server/services/hsk.ts
@@ -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;
+ }
+};
diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts
index 03d445a..498c794 100644
--- a/src/lib/services/auth.ts
+++ b/src/lib/services/auth.ts
@@ -1,5 +1,5 @@
import { callPostApi } from "$lib/hooks";
-import { encodeToBase64, decryptChallenge, signMessage } from "$lib/modules/crypto";
+import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto";
import type {
SessionUpgradeRequest,
SessionUpgradeResponse,
@@ -20,7 +20,7 @@ export const requestSessionUpgrade = async (
const { challenge }: SessionUpgradeResponse = await res.json();
const answer = await decryptChallenge(challenge, decryptKey);
- const answerSig = await signMessage(answer, signKey);
+ const answerSig = await signMessageRSA(answer, signKey);
res = await callPostApi("/api/auth/upgradeSession/verify", {
answer: encodeToBase64(answer),
diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts
index 79a4390..fb368dd 100644
--- a/src/lib/services/key.ts
+++ b/src/lib/services/key.ts
@@ -3,7 +3,7 @@ import { storeMasterKeys } from "$lib/indexedDB";
import {
encodeToBase64,
decryptChallenge,
- signMessage,
+ signMessageRSA,
unwrapMasterKey,
verifyMasterKeyWrapped,
} from "$lib/modules/crypto";
@@ -29,7 +29,7 @@ export const requestClientRegistration = async (
const { challenge }: ClientRegisterResponse = await res.json();
const answer = await decryptChallenge(challenge, decryptKey);
- const answerSig = await signMessage(answer, signKey);
+ const answerSig = await signMessageRSA(answer, signKey);
res = await callPostApi("/api/client/register/verify", {
answer: encodeToBase64(answer),
diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts
index d742634..77d1268 100644
--- a/src/lib/stores/key.ts
+++ b/src/lib/stores/key.ts
@@ -13,6 +13,14 @@ export interface MasterKey {
key: CryptoKey;
}
+export interface HmacSecret {
+ version: number;
+ state: "active";
+ secret: CryptoKey;
+}
+
export const clientKeyStore = writable(null);
export const masterKeyStore = writable