mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-16 06:58:46 +00:00
@@ -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
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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()]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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?.()}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
45
src/lib/server/db/hsk.ts
Normal 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")));
|
||||||
|
};
|
||||||
@@ -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],
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
43
src/lib/server/db/schema/hsk.ts
Normal file
43
src/lib/server/db/schema/hsk.ts
Normal 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],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
19
src/lib/server/schemas/hsk.ts
Normal file
19
src/lib/server/schemas/hsk.ts
Normal 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>;
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
31
src/lib/server/services/hsk.ts
Normal file
31
src/lib/server/services/hsk.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
32
src/routes/(main)/directory/[[id]]/DuplicateFileModal.svelte
Normal file
32
src/routes/(main)/directory/[[id]]/DuplicateFileModal.svelte
Normal 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>
|
||||||
@@ -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,
|
||||||
|
|||||||
20
src/routes/api/file/scanDuplicates/+server.ts
Normal file
20
src/routes/api/file/scanDuplicates/+server.ts
Normal 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));
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
20
src/routes/api/hsk/list/+server.ts
Normal file
20
src/routes/api/hsk/list/+server.ts
Normal 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),
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/routes/api/hsk/register/initial/+server.ts
Normal file
16
src/routes/api/hsk/register/initial/+server.ts
Normal 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" } });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user