mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
@@ -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
|
||||
@@ -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": {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1736637983139,
|
||||
"tag": "0000_spooky_lady_bullseye",
|
||||
"when": 1736696839327,
|
||||
"tag": "0000_lush_black_bolt",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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()]);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<div class="h-full w-full p-3 transition active:scale-95">
|
||||
{@render children?.()}
|
||||
|
||||
@@ -11,6 +11,7 @@ interface KeyExportState {
|
||||
verifyKeyBase64: string;
|
||||
|
||||
masterKeyWrapped: string;
|
||||
hmacSecretWrapped: string;
|
||||
}
|
||||
|
||||
const useAutoNull = <T>(value: T | null) => {
|
||||
|
||||
@@ -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<ClientKey, "usage">;
|
||||
masterKey: EntityTable<MasterKey, "version">;
|
||||
hmacSecret: EntityTable<HmacSecret, "version">;
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
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 { 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],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
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 "./file";
|
||||
export * from "./hsk";
|
||||
export * from "./mek";
|
||||
export * from "./session";
|
||||
export * from "./user";
|
||||
|
||||
@@ -22,11 +22,24 @@ export const fileRenameRequest = z.object({
|
||||
});
|
||||
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({
|
||||
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()
|
||||
|
||||
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 "./directory";
|
||||
export * from "./file";
|
||||
export * from "./hsk";
|
||||
export * from "./mek";
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
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 { 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<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
|
||||
answer: encodeToBase64(answer),
|
||||
|
||||
@@ -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<ClientRegisterVerifyRequest>("/api/client/register/verify", {
|
||||
answer: encodeToBase64(answer),
|
||||
|
||||
@@ -13,6 +13,14 @@ export interface MasterKey {
|
||||
key: CryptoKey;
|
||||
}
|
||||
|
||||
export interface HmacSecret {
|
||||
version: number;
|
||||
state: "active";
|
||||
secret: CryptoKey;
|
||||
}
|
||||
|
||||
export const clientKeyStore = writable<ClientKeys | 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,
|
||||
storeClientKeys,
|
||||
requestSessionUpgrade,
|
||||
requestInitialMasterKeyRegistration,
|
||||
requestInitialMasterKeyAndHmacSecretRegistration,
|
||||
} from "./service";
|
||||
|
||||
import IconKey from "~icons/material-symbols/key";
|
||||
@@ -69,9 +69,13 @@
|
||||
throw new Error("Failed to upgrade session");
|
||||
|
||||
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));
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { callPostApi } from "$lib/hooks";
|
||||
import { storeClientKey } from "$lib/indexedDB";
|
||||
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";
|
||||
|
||||
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,
|
||||
hmacSecretWrapped: string,
|
||||
signKey: CryptoKey,
|
||||
) => {
|
||||
const res = await callPostApi<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", {
|
||||
let res = await callPostApi<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", {
|
||||
mek: masterKeyWrapped,
|
||||
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 { clientKeyStore } from "$lib/stores";
|
||||
import Order from "./Order.svelte";
|
||||
import { generateClientKeys, generateInitialMasterKey } from "./service";
|
||||
import {
|
||||
generateClientKeys,
|
||||
generateInitialMasterKey,
|
||||
generateInitialHmacSecret,
|
||||
} from "./service";
|
||||
|
||||
import IconKey from "~icons/material-symbols/key";
|
||||
|
||||
@@ -36,12 +40,14 @@
|
||||
// TODO: Loading indicator
|
||||
|
||||
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", {
|
||||
...clientKeys,
|
||||
redirectPath: data.redirectPath,
|
||||
masterKeyWrapped,
|
||||
hmacSecretWrapped,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,11 @@ import {
|
||||
generateSigningKeyPair,
|
||||
exportRSAKeyToBase64,
|
||||
makeRSAKeyNonextractable,
|
||||
generateMasterKey,
|
||||
wrapMasterKey,
|
||||
generateMasterKey,
|
||||
makeAESKeyNonextractable,
|
||||
wrapHmacSecret,
|
||||
generateHmacSecret,
|
||||
} from "$lib/modules/crypto";
|
||||
import { clientKeyStore } from "$lib/stores";
|
||||
|
||||
@@ -31,6 +34,14 @@ export const generateClientKeys = async () => {
|
||||
export const generateInitialMasterKey = async (encryptKey: CryptoKey) => {
|
||||
const { masterKey } = await generateMasterKey();
|
||||
return {
|
||||
masterKey: await makeAESKeyNonextractable(masterKey),
|
||||
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">
|
||||
import { onMount } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { goto } from "$app/navigation";
|
||||
import { TopBar } from "$lib/components";
|
||||
import { FloatingButton } from "$lib/components/buttons";
|
||||
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 CreateDirectoryModal from "./CreateDirectoryModal.svelte";
|
||||
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
|
||||
import DirectoryEntries from "./DirectoryEntries";
|
||||
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
|
||||
import DuplicateFileModal from "./DuplicateFileModal.svelte";
|
||||
import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte";
|
||||
import {
|
||||
requestHmacSecretDownload,
|
||||
requestDirectoryCreation,
|
||||
requestDuplicateFileScan,
|
||||
requestFileUpload,
|
||||
requestDirectoryEntryRename,
|
||||
requestDirectoryEntryDeletion,
|
||||
@@ -21,14 +25,22 @@
|
||||
|
||||
import IconAdd from "~icons/material-symbols/add";
|
||||
|
||||
interface LoadedFile {
|
||||
file: File;
|
||||
fileBuffer: ArrayBuffer;
|
||||
fileSigned: string;
|
||||
}
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
||||
let fileInput: HTMLInputElement | undefined = $state();
|
||||
let loadedFile: LoadedFile | undefined = $state();
|
||||
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
||||
|
||||
let isCreateBottomSheetOpen = $state(false);
|
||||
let isCreateDirectoryModalOpen = $state(false);
|
||||
let isDuplicateFileModalOpen = $state(false);
|
||||
|
||||
let isDirectoryEntryMenuBottomSheetOpen = $state(false);
|
||||
let isRenameDirectoryEntryModalOpen = $state(false);
|
||||
@@ -40,15 +52,42 @@
|
||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||
};
|
||||
|
||||
const uploadFile = () => {
|
||||
const file = fileInput?.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!).then(() => {
|
||||
const uploadFile = (loadedFile: LoadedFile) => {
|
||||
requestFileUpload(
|
||||
loadedFile.file,
|
||||
loadedFile.fileBuffer,
|
||||
loadedFile.fileSigned,
|
||||
data.id,
|
||||
$masterKeyStore?.get(1)!,
|
||||
$hmacSecretStore?.get(1)!,
|
||||
).then(() => {
|
||||
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(() => {
|
||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||
});
|
||||
@@ -58,7 +97,7 @@
|
||||
<title>파일</title>
|
||||
</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">
|
||||
{#if data.id !== "root"}
|
||||
@@ -99,6 +138,18 @@
|
||||
}}
|
||||
/>
|
||||
<CreateDirectoryModal bind:isOpen={isCreateDirectoryModalOpen} onCreateClick={createDirectory} />
|
||||
<DuplicateFileModal
|
||||
bind:isOpen={isDuplicateFileModalOpen}
|
||||
onclose={() => {
|
||||
isDuplicateFileModalOpen = false;
|
||||
loadedFile = undefined;
|
||||
}}
|
||||
onDuplicateClick={() => {
|
||||
uploadFile(loadedFile!);
|
||||
isDuplicateFileModalOpen = false;
|
||||
loadedFile = undefined;
|
||||
}}
|
||||
/>
|
||||
|
||||
<DirectoryEntryMenuBottomSheet
|
||||
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 { generateDataKey, wrapDataKey, encryptData, encryptString } from "$lib/modules/crypto";
|
||||
import { callGetApi, callPostApi } from "$lib/hooks";
|
||||
import { storeHmacSecrets } from "$lib/indexedDB";
|
||||
import {
|
||||
encodeToBase64,
|
||||
generateDataKey,
|
||||
wrapDataKey,
|
||||
unwrapHmacSecret,
|
||||
encryptData,
|
||||
encryptString,
|
||||
signMessageHmac,
|
||||
} from "$lib/modules/crypto";
|
||||
import type {
|
||||
DirectoryRenameRequest,
|
||||
DirectoryCreateRequest,
|
||||
FileRenameRequest,
|
||||
FileUploadRequest,
|
||||
HmacSecretListResponse,
|
||||
DuplicateFileScanRequest,
|
||||
DuplicateFileScanResponse,
|
||||
} from "$lib/server/schemas";
|
||||
import type { MasterKey } from "$lib/stores";
|
||||
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
|
||||
|
||||
export interface SelectedDirectoryEntry {
|
||||
type: "directory" | "file";
|
||||
@@ -16,6 +28,26 @@ export interface SelectedDirectoryEntry {
|
||||
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 (
|
||||
name: string,
|
||||
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 (
|
||||
file: File,
|
||||
fileBuffer: ArrayBuffer,
|
||||
fileSigned: string,
|
||||
parentId: "root" | number,
|
||||
masterKey: MasterKey,
|
||||
hmacSecret: HmacSecret,
|
||||
) => {
|
||||
const { dataKey, dataKeyVersion } = await generateDataKey();
|
||||
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey);
|
||||
const nameEncrypted = await encryptString(file.name, dataKey);
|
||||
const fileEncrypted = await encryptData(fileBuffer, dataKey);
|
||||
|
||||
const form = new FormData();
|
||||
form.set(
|
||||
@@ -50,6 +102,8 @@ export const requestFileUpload = async (
|
||||
mekVersion: masterKey.version,
|
||||
dek: await wrapDataKey(dataKey, masterKey.key),
|
||||
dekVersion: dataKeyVersion.toISOString(),
|
||||
hskVersion: hmacSecret.version,
|
||||
contentHmac: fileSigned,
|
||||
contentType: file.type,
|
||||
contentIv: fileEncrypted.iv,
|
||||
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));
|
||||
if (!zodRes.success) error(400, "Invalid request body");
|
||||
const { parentId, mekVersion, dek, dekVersion, contentType, contentIv, name, nameIv } =
|
||||
zodRes.data;
|
||||
const {
|
||||
parentId,
|
||||
mekVersion,
|
||||
dek,
|
||||
dekVersion,
|
||||
hskVersion,
|
||||
contentHmac,
|
||||
contentType,
|
||||
contentIv,
|
||||
name,
|
||||
nameIv,
|
||||
} = zodRes.data;
|
||||
|
||||
await uploadFile(
|
||||
{
|
||||
@@ -26,6 +36,8 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
mekVersion,
|
||||
encDek: dek,
|
||||
dekVersion: new Date(dekVersion),
|
||||
hskVersion,
|
||||
contentHmac,
|
||||
contentType,
|
||||
encContentIv: contentIv,
|
||||
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