mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[ENG-355] Keychain integration (and some typesafety) (#558)
* update crypto MSRV * rename `keychain` to `keyring` * make a start on the keymanager unlock refactor/keychain integration * update routes * update bindings * add const identifiers * add UI/front-end support for unlocking KM with OS keychains * remove SK from lib creation dialog * update query name * add keyring functions * attempt to update `change_master_password()` to use the keychain * cleanup, fix master password change ui, better secret key in keyring detection * cleanup TS a little * add route for getting secret key from keyring * update bindings * update var names + show secret key in keys settings * add `react-qr-code` and option to view the secret key (if it's in the OS keyring) * allow copying SK to clipboard * add `key_type` so we're not reliant on specific UUIDs for root/verification key handling * clippy * fix mobile typecheck * fix typecheck, fix typo and tweak balloon hash parameters * minor cleanup + typo fix * use newtype structs * WIP type refactoring (major readability boost!) * update `use` * add tokio `sync` feature * too many structs? idk * more cleanup * add `generate` and `Nonce` * `Nonce` and `Key` typesafety (beautiful) * clippy + cleanup * update code & examples * fix bug & remove `ProtectedVec` as it looked out of place * use `Key` * add a query invalidation to make the UI extremely responsive * ci pls work * remove `keyringHasSk` route
This commit is contained in:
parent
d99adcf7ca
commit
d1b6263ae7
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5542,6 +5542,7 @@ dependencies = [
|
|||
"blake3",
|
||||
"chacha20poly1305",
|
||||
"dashmap",
|
||||
"hex",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rspc",
|
||||
|
|
|
@ -47,9 +47,9 @@ fn print_crypto_details(header: &FileHeader, aad: &[u8]) {
|
|||
version = k.version,
|
||||
algorithm = k.algorithm,
|
||||
hashing_algorithm = k.hashing_algorithm,
|
||||
salt = hex::encode(k.salt),
|
||||
master = hex::encode(k.master_key),
|
||||
nonce = hex::encode(k.nonce.clone())
|
||||
salt = hex::encode(&*k.salt),
|
||||
master = hex::encode(&*k.master_key),
|
||||
nonce = hex::encode(k.nonce)
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -64,7 +64,7 @@ fn print_crypto_details(header: &FileHeader, aad: &[u8]) {
|
|||
version = m.version,
|
||||
algorithm = m.algorithm,
|
||||
size = m.metadata.len(),
|
||||
nonce = hex::encode(m.metadata_nonce.clone())
|
||||
nonce = hex::encode(m.metadata_nonce)
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -79,7 +79,7 @@ fn print_crypto_details(header: &FileHeader, aad: &[u8]) {
|
|||
version = p.version,
|
||||
algorithm = p.algorithm,
|
||||
size = p.media.len(),
|
||||
nonce = hex::encode(p.media_nonce.clone())
|
||||
nonce = hex::encode(p.media_nonce)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
|
|||
name: libName,
|
||||
// TODO: Support password and secret on mobile
|
||||
password: '',
|
||||
secret_key: '',
|
||||
algorithm: 'XChaCha20Poly1305',
|
||||
hashing_algorithm: { name: 'Argon2id', params: 'Standard' }
|
||||
})
|
||||
|
|
74
core/prisma/migrations/20230202133507_keytype/migration.sql
Normal file
74
core/prisma/migrations/20230202133507_keytype/migration.sql
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `sync_event` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the column `cas_id` on the `object` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `integrity_checksum` on the `object` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[integrity_checksum]` on the table `file_path` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `key_type` to the `key` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `pub_id` to the `object` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "file_path" ADD COLUMN "cas_id" TEXT;
|
||||
ALTER TABLE "file_path" ADD COLUMN "integrity_checksum" TEXT;
|
||||
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "sync_event";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_key" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"uuid" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"key_type" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"default" BOOLEAN NOT NULL DEFAULT false,
|
||||
"date_created" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
"algorithm" TEXT NOT NULL,
|
||||
"hashing_algorithm" TEXT NOT NULL,
|
||||
"content_salt" BLOB NOT NULL,
|
||||
"master_key" BLOB NOT NULL,
|
||||
"master_key_nonce" BLOB NOT NULL,
|
||||
"key_nonce" BLOB NOT NULL,
|
||||
"key" BLOB NOT NULL,
|
||||
"salt" BLOB NOT NULL,
|
||||
"automount" BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
INSERT INTO "new_key" ("algorithm", "automount", "content_salt", "date_created", "default", "hashing_algorithm", "id", "key", "key_nonce", "master_key", "master_key_nonce", "name", "salt", "uuid", "version") SELECT "algorithm", "automount", "content_salt", "date_created", "default", "hashing_algorithm", "id", "key", "key_nonce", "master_key", "master_key_nonce", "name", "salt", "uuid", "version" FROM "key";
|
||||
DROP TABLE "key";
|
||||
ALTER TABLE "new_key" RENAME TO "key";
|
||||
CREATE UNIQUE INDEX "key_uuid_key" ON "key"("uuid");
|
||||
CREATE TABLE "new_object" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"pub_id" BLOB NOT NULL,
|
||||
"name" TEXT,
|
||||
"extension" TEXT,
|
||||
"kind" INTEGER NOT NULL DEFAULT 0,
|
||||
"size_in_bytes" TEXT NOT NULL DEFAULT '0',
|
||||
"key_id" INTEGER,
|
||||
"hidden" BOOLEAN NOT NULL DEFAULT false,
|
||||
"favorite" BOOLEAN NOT NULL DEFAULT false,
|
||||
"important" BOOLEAN NOT NULL DEFAULT false,
|
||||
"has_thumbnail" BOOLEAN NOT NULL DEFAULT false,
|
||||
"has_thumbstrip" BOOLEAN NOT NULL DEFAULT false,
|
||||
"has_video_preview" BOOLEAN NOT NULL DEFAULT false,
|
||||
"ipfs_id" TEXT,
|
||||
"note" TEXT,
|
||||
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "object_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "key" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_object" ("date_created", "date_indexed", "date_modified", "extension", "favorite", "has_thumbnail", "has_thumbstrip", "has_video_preview", "hidden", "id", "important", "ipfs_id", "key_id", "kind", "name", "note", "size_in_bytes") SELECT "date_created", "date_indexed", "date_modified", "extension", "favorite", "has_thumbnail", "has_thumbstrip", "has_video_preview", "hidden", "id", "important", "ipfs_id", "key_id", "kind", "name", "note", "size_in_bytes" FROM "object";
|
||||
DROP TABLE "object";
|
||||
ALTER TABLE "new_object" RENAME TO "object";
|
||||
CREATE UNIQUE INDEX "object_pub_id_key" ON "object"("pub_id");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "file_path_integrity_checksum_key" ON "file_path"("integrity_checksum");
|
|
@ -216,6 +216,7 @@ model Key {
|
|||
// uuid to identify the key
|
||||
uuid String @unique
|
||||
version String
|
||||
key_type String
|
||||
// the name that the user sets
|
||||
name String?
|
||||
// is this key the default for encryption?
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use sd_crypto::primitives::types::{Password, SecretKeyString};
|
||||
use sd_crypto::primitives::SECRET_KEY_IDENTIFIER;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
use tokio::fs::File;
|
||||
|
||||
use sd_crypto::keys::keymanager::StoredKey;
|
||||
use sd_crypto::keys::keymanager::{StoredKey, StoredKeyType};
|
||||
use sd_crypto::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Error, Protected};
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
|
@ -17,28 +19,27 @@ use super::{utils::LibraryRequest, RouterBuilder};
|
|||
pub struct KeyAddArgs {
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
key: String,
|
||||
key: Protected<String>,
|
||||
library_sync: bool,
|
||||
automount: bool,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct UnlockKeyManagerArgs {
|
||||
password: String,
|
||||
secret_key: Option<String>,
|
||||
password: Protected<String>,
|
||||
secret_key: Protected<String>,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct RestoreBackupArgs {
|
||||
password: String,
|
||||
secret_key: Option<String>,
|
||||
password: Protected<String>,
|
||||
secret_key: Protected<String>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Type, Deserialize)]
|
||||
pub struct MasterPasswordChangeArgs {
|
||||
password: String,
|
||||
secret_key: Option<String>,
|
||||
password: Protected<String>,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
}
|
||||
|
@ -55,10 +56,8 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
t(|_, _: (), library| async move { Ok(library.key_manager.dump_keystore()) })
|
||||
})
|
||||
// do not unlock the key manager until this route returns true
|
||||
.library_query("hasMasterPassword", |t| {
|
||||
t(
|
||||
|_, _: (), library| async move { Ok(library.key_manager.has_master_password().await?) },
|
||||
)
|
||||
.library_query("isUnlocked", |t| {
|
||||
t(|_, _: (), library| async move { Ok(library.key_manager.is_unlocked().await?) })
|
||||
})
|
||||
// this is so we can show the key as mounted in the UI
|
||||
.library_query("listMounted", |t| {
|
||||
|
@ -66,9 +65,12 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
})
|
||||
.library_query("getKey", |t| {
|
||||
t(|_, key_uuid: Uuid, library| async move {
|
||||
let key = library.key_manager.get_key(key_uuid).await?;
|
||||
|
||||
Ok(String::from_utf8(key.into_inner()).map_err(Error::StringParse)?)
|
||||
Ok(library
|
||||
.key_manager
|
||||
.get_key(key_uuid)
|
||||
.await?
|
||||
.expose()
|
||||
.clone())
|
||||
})
|
||||
})
|
||||
.library_mutation("mount", |t| {
|
||||
|
@ -79,6 +81,27 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
Ok(())
|
||||
})
|
||||
})
|
||||
.library_query("getSecretKey", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
if library
|
||||
.key_manager
|
||||
.keyring_contains_valid_secret_key(library.id)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(Some(
|
||||
library
|
||||
.key_manager
|
||||
.keyring_retrieve(library.id, SECRET_KEY_IDENTIFIER.to_string())
|
||||
.await?
|
||||
.expose()
|
||||
.clone(),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
})
|
||||
.library_mutation("unmount", |t| {
|
||||
t(|_, key_uuid: Uuid, library| async move {
|
||||
library.key_manager.unmount(key_uuid)?;
|
||||
|
@ -92,7 +115,7 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
// This technically clears the root key, but it means the same thing to the frontend
|
||||
library.key_manager.clear_root_key().await?;
|
||||
|
||||
invalidate_query!(library, "keys.hasMasterPassword");
|
||||
invalidate_query!(library, "keys.isUnlocked");
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
|
@ -152,17 +175,19 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
})
|
||||
.library_mutation("unlockKeyManager", |t| {
|
||||
t(|_, args: UnlockKeyManagerArgs, library| async move {
|
||||
// if this returns an error, the user MUST re-enter the correct password
|
||||
let secret_key = (!args.secret_key.expose().is_empty()).then_some(args.secret_key);
|
||||
|
||||
library
|
||||
.key_manager
|
||||
.unlock(
|
||||
Protected::new(args.password),
|
||||
args.secret_key.map(Protected::new),
|
||||
Password(args.password),
|
||||
secret_key.map(SecretKeyString),
|
||||
library.id,
|
||||
|| invalidate_query!(library, "keys.isKeyManagerUnlocking"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
invalidate_query!(library, "keys.hasMasterPassword");
|
||||
invalidate_query!(library, "keys.isUnlocked");
|
||||
|
||||
let automount = library
|
||||
.db
|
||||
|
@ -215,7 +240,7 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
t(|_, _: (), library| async move { library.key_manager.get_default().await.ok() })
|
||||
})
|
||||
.library_query("isKeyManagerUnlocking", |t| {
|
||||
t(|_, _: (), library| async move { library.key_manager.is_queued(Uuid::nil()) })
|
||||
t(|_, _: (), library| async move { library.key_manager.is_unlocking().await.ok() })
|
||||
})
|
||||
.library_mutation("unmountAll", |t| {
|
||||
t(|_, _: (), library| async move {
|
||||
|
@ -231,7 +256,7 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
let uuid = library
|
||||
.key_manager
|
||||
.add_to_keystore(
|
||||
Protected::new(args.key.as_bytes().to_vec()),
|
||||
Password(args.key),
|
||||
args.algorithm,
|
||||
args.hashing_algorithm,
|
||||
!args.library_sync,
|
||||
|
@ -297,11 +322,13 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
let stored_keys: Vec<StoredKey> =
|
||||
serde_json::from_slice(&backup).map_err(|_| Error::Serialization)?;
|
||||
|
||||
let secret_key = args.secret_key.map(Protected::new);
|
||||
|
||||
let updated_keys = library
|
||||
.key_manager
|
||||
.import_keystore_backup(Protected::new(args.password), secret_key, &stored_keys)
|
||||
.import_keystore_backup(
|
||||
args.password,
|
||||
SecretKeyString(args.secret_key),
|
||||
&stored_keys,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for key in &updated_keys {
|
||||
|
@ -316,23 +343,25 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
})
|
||||
.library_mutation("changeMasterPassword", |t| {
|
||||
t(|_, args: MasterPasswordChangeArgs, library| async move {
|
||||
let secret_key = args.secret_key.map(Protected::new);
|
||||
|
||||
let verification_key = library
|
||||
.key_manager
|
||||
.change_master_password(
|
||||
Protected::new(args.password),
|
||||
args.password,
|
||||
args.algorithm,
|
||||
args.hashing_algorithm,
|
||||
secret_key,
|
||||
library.id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// remove old nil-id keys if they were set
|
||||
invalidate_query!(library, "keys.getSecretKey");
|
||||
|
||||
// remove old root key if present
|
||||
library
|
||||
.db
|
||||
.key()
|
||||
.delete_many(vec![key::uuid::equals(Uuid::nil().to_string())])
|
||||
.delete_many(vec![key::key_type::equals(
|
||||
serde_json::to_string(&StoredKeyType::Root).unwrap(),
|
||||
)])
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ use chrono::Utc;
|
|||
use fs_extra::dir::get_size; // TODO: Remove this dependency as it is sync instead of async
|
||||
use rspc::Type;
|
||||
use sd_crypto::{
|
||||
crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, primitives::OnboardingConfig,
|
||||
Protected,
|
||||
crypto::stream::Algorithm, keys::hashing::HashingAlgorithm,
|
||||
primitives::types::OnboardingConfig, Protected,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
|
@ -81,7 +81,6 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
pub struct CreateLibraryArgs {
|
||||
name: String,
|
||||
password: Protected<String>,
|
||||
secret_key: Option<Protected<String>>,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
}
|
||||
|
@ -96,7 +95,6 @@ pub(crate) fn mount() -> RouterBuilder {
|
|||
},
|
||||
OnboardingConfig {
|
||||
password: args.password,
|
||||
secret_key: args.secret_key,
|
||||
algorithm: args.algorithm,
|
||||
hashing_algorithm: args.hashing_algorithm,
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
|
||||
use sd_crypto::{
|
||||
keys::keymanager::{KeyManager, StoredKey},
|
||||
primitives::{to_array, OnboardingConfig},
|
||||
primitives::types::{EncryptedKey, Nonce, OnboardingConfig, Salt},
|
||||
};
|
||||
use std::{
|
||||
env, fs, io,
|
||||
|
@ -93,16 +93,18 @@ pub async fn seed_keymanager(
|
|||
uuid,
|
||||
version: serde_json::from_str(&key.version)
|
||||
.map_err(|_| sd_crypto::Error::Serialization)?,
|
||||
key_type: serde_json::from_str(&key.key_type)
|
||||
.map_err(|_| sd_crypto::Error::Serialization)?,
|
||||
algorithm: serde_json::from_str(&key.algorithm)
|
||||
.map_err(|_| sd_crypto::Error::Serialization)?,
|
||||
content_salt: to_array(key.content_salt)?,
|
||||
master_key: to_array(key.master_key)?,
|
||||
master_key_nonce: key.master_key_nonce,
|
||||
key_nonce: key.key_nonce,
|
||||
content_salt: Salt::try_from(key.content_salt)?,
|
||||
master_key: EncryptedKey::try_from(key.master_key)?,
|
||||
master_key_nonce: Nonce::try_from(key.master_key_nonce)?,
|
||||
key_nonce: Nonce::try_from(key.key_nonce)?,
|
||||
key: key.key,
|
||||
hashing_algorithm: serde_json::from_str(&key.hashing_algorithm)
|
||||
.map_err(|_| sd_crypto::Error::Serialization)?,
|
||||
salt: to_array(key.salt)?,
|
||||
salt: Salt::try_from(key.salt)?,
|
||||
memory_only: false,
|
||||
automount: key.automount,
|
||||
})
|
||||
|
@ -197,7 +199,7 @@ impl LibraryManager {
|
|||
indexer_rules_seeder(&library.db).await?;
|
||||
|
||||
// setup master password
|
||||
let verification_key = KeyManager::onboarding(km_config).await?;
|
||||
let verification_key = KeyManager::onboarding(km_config, library.id).await?;
|
||||
|
||||
write_storedkey_to_db(&library.db, &verification_key).await?;
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use sd_crypto::{crypto::stream::StreamDecryption, header::file::FileHeader, Protected};
|
||||
use sd_crypto::{
|
||||
crypto::stream::StreamDecryption, header::file::FileHeader, primitives::types::Password,
|
||||
Protected,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{collections::VecDeque, path::PathBuf};
|
||||
|
@ -88,17 +91,17 @@ impl StatefulJob for FileDecryptorJob {
|
|||
|
||||
let master_key = if let Some(password) = state.init.password.clone() {
|
||||
if let Some(save_to_library) = state.init.save_to_library {
|
||||
let password = Protected::new(password.into_bytes());
|
||||
|
||||
// we can do this first, as `find_key_index` requires a successful decryption (just like `decrypt_master_key`)
|
||||
let password_bytes = Protected::new(password.as_bytes().to_vec());
|
||||
|
||||
if save_to_library {
|
||||
let index = header.find_key_index(password.clone()).await?;
|
||||
let index = header.find_key_index(password_bytes.clone()).await?;
|
||||
|
||||
// inherit the encryption algorithm from the keyslot
|
||||
ctx.library_ctx
|
||||
.key_manager
|
||||
.add_to_keystore(
|
||||
password.clone(),
|
||||
Password::new(password),
|
||||
header.algorithm,
|
||||
header.keyslots[index].hashing_algorithm,
|
||||
false,
|
||||
|
@ -108,7 +111,7 @@ impl StatefulJob for FileDecryptorJob {
|
|||
.await?;
|
||||
}
|
||||
|
||||
header.decrypt_master_key(password).await?
|
||||
header.decrypt_master_key(password_bytes).await?
|
||||
} else {
|
||||
return Err(JobError::JobDataNotFound(String::from(
|
||||
"Password decryption selected, but save to library boolean was not included",
|
||||
|
@ -120,7 +123,7 @@ impl StatefulJob for FileDecryptorJob {
|
|||
header.decrypt_master_key_from_prehashed(keys).await?
|
||||
};
|
||||
|
||||
let decryptor = StreamDecryption::new(master_key, &header.nonce, header.algorithm)?;
|
||||
let decryptor = StreamDecryption::new(master_key, header.nonce, header.algorithm)?;
|
||||
|
||||
decryptor
|
||||
.decrypt_streams(&mut reader, &mut writer, &aad)
|
||||
|
|
|
@ -7,8 +7,7 @@ use sd_crypto::{
|
|||
crypto::stream::{Algorithm, StreamEncryption},
|
||||
header::{file::FileHeader, keyslot::Keyslot},
|
||||
primitives::{
|
||||
generate_master_key, LATEST_FILE_HEADER, LATEST_KEYSLOT, LATEST_METADATA,
|
||||
LATEST_PREVIEW_MEDIA,
|
||||
types::Key, LATEST_FILE_HEADER, LATEST_KEYSLOT, LATEST_METADATA, LATEST_PREVIEW_MEDIA,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -130,7 +129,7 @@ impl StatefulJob for FileEncryptorJob {
|
|||
let mut reader = File::open(&info.fs_path).await?;
|
||||
let mut writer = File::create(output_path).await?;
|
||||
|
||||
let master_key = generate_master_key();
|
||||
let master_key = Key::generate();
|
||||
|
||||
let mut header = FileHeader::new(
|
||||
LATEST_FILE_HEADER,
|
||||
|
@ -146,7 +145,7 @@ impl StatefulJob for FileEncryptorJob {
|
|||
)
|
||||
.await?,
|
||||
],
|
||||
);
|
||||
)?;
|
||||
|
||||
if state.init.metadata || state.init.preview_media {
|
||||
// if any are requested, we can make the query as it'll be used at least once
|
||||
|
@ -208,7 +207,7 @@ impl StatefulJob for FileEncryptorJob {
|
|||
|
||||
header.write(&mut writer).await?;
|
||||
|
||||
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm)?;
|
||||
let encryptor = StreamEncryption::new(master_key, header.nonce, header.algorithm)?;
|
||||
|
||||
encryptor
|
||||
.encrypt_streams(&mut reader, &mut writer, &header.generate_aad())
|
||||
|
|
|
@ -54,9 +54,10 @@ pub async fn write_storedkey_to_db(
|
|||
.create(
|
||||
key.uuid.to_string(),
|
||||
serde_json::to_string(&key.version)?,
|
||||
serde_json::to_string(&key.key_type)?,
|
||||
serde_json::to_string(&key.algorithm)?,
|
||||
serde_json::to_string(&key.hashing_algorithm)?,
|
||||
key.content_salt.to_vec(),
|
||||
key.content_salt.0.to_vec(),
|
||||
key.master_key.to_vec(),
|
||||
key.master_key_nonce.to_vec(),
|
||||
key.key_nonce.to_vec(),
|
||||
|
|
|
@ -5,7 +5,7 @@ authors = ["Jake Robinson <jake@spacedrive.com>"]
|
|||
readme = "README.md"
|
||||
description = "A library to handle cryptographic functions within Spacedrive"
|
||||
edition = "2021"
|
||||
rust-version = "1.64.0"
|
||||
rust-version = "1.67.0"
|
||||
|
||||
[dependencies]
|
||||
# rng
|
||||
|
@ -44,13 +44,15 @@ rspc = { workspace = true, features = ["uuid"], optional = true }
|
|||
specta = { workspace = true, optional = true }
|
||||
|
||||
# for asynchronous crypto
|
||||
tokio = { workspace = true, features = ["io-util", "rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "sync"] }
|
||||
|
||||
# linux OS keychain
|
||||
hex = "0.4.3"
|
||||
|
||||
# linux OS keyring
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
secret-service = "2.0.2"
|
||||
|
||||
# macos/ios OS keychain
|
||||
# macos/ios OS keyring
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
security-framework = "2.8.1"
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@ use sd_crypto::{
|
|||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
header::{file::FileHeader, keyslot::Keyslot},
|
||||
keys::hashing::{HashingAlgorithm, Params},
|
||||
primitives::{generate_master_key, generate_salt, LATEST_FILE_HEADER, LATEST_KEYSLOT},
|
||||
primitives::{
|
||||
types::{Key, Salt},
|
||||
LATEST_FILE_HEADER, LATEST_KEYSLOT,
|
||||
},
|
||||
Protected,
|
||||
};
|
||||
|
||||
|
@ -19,10 +22,10 @@ async fn encrypt() {
|
|||
let mut writer = File::create("test.encrypted").await.unwrap();
|
||||
|
||||
// This needs to be generated here, otherwise we won't have access to it for encryption
|
||||
let master_key = generate_master_key();
|
||||
let master_key = Key::generate();
|
||||
|
||||
// These should ideally be done by a key management system
|
||||
let content_salt = generate_salt();
|
||||
let content_salt = Salt::generate();
|
||||
let hashed_password = HASHING_ALGORITHM
|
||||
.hash(password, content_salt, None)
|
||||
.unwrap();
|
||||
|
@ -40,13 +43,13 @@ async fn encrypt() {
|
|||
.unwrap()];
|
||||
|
||||
// Create the header for the encrypted file
|
||||
let header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots);
|
||||
let header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots).unwrap();
|
||||
|
||||
// Write the header to the file
|
||||
header.write(&mut writer).await.unwrap();
|
||||
|
||||
// Use the nonce created by the header to initialize a stream encryption object
|
||||
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap();
|
||||
let encryptor = StreamEncryption::new(master_key, header.nonce, header.algorithm).unwrap();
|
||||
|
||||
// Encrypt the data from the reader, and write it to the writer
|
||||
// Use AAD so the header can be authenticated against every block of data
|
||||
|
@ -70,7 +73,7 @@ async fn decrypt() {
|
|||
let master_key = header.decrypt_master_key(password).await.unwrap();
|
||||
|
||||
// Initialize a stream decryption object using data provided by the header
|
||||
let decryptor = StreamDecryption::new(master_key, &header.nonce, header.algorithm).unwrap();
|
||||
let decryptor = StreamDecryption::new(master_key, header.nonce, header.algorithm).unwrap();
|
||||
|
||||
// Decrypt data the from the writer, and write it to the writer
|
||||
decryptor
|
||||
|
|
|
@ -4,7 +4,10 @@ use sd_crypto::{
|
|||
crypto::stream::{Algorithm, StreamEncryption},
|
||||
header::{file::FileHeader, keyslot::Keyslot, metadata::MetadataVersion},
|
||||
keys::hashing::{HashingAlgorithm, Params},
|
||||
primitives::{generate_master_key, generate_salt, LATEST_FILE_HEADER, LATEST_KEYSLOT},
|
||||
primitives::{
|
||||
types::{Key, Salt},
|
||||
LATEST_FILE_HEADER, LATEST_KEYSLOT,
|
||||
},
|
||||
Protected,
|
||||
};
|
||||
use tokio::fs::File;
|
||||
|
@ -28,10 +31,10 @@ async fn encrypt() {
|
|||
let mut writer = File::create("test.encrypted").await.unwrap();
|
||||
|
||||
// This needs to be generated here, otherwise we won't have access to it for encryption
|
||||
let master_key = generate_master_key();
|
||||
let master_key = Key::generate();
|
||||
|
||||
// These should ideally be done by a key management system
|
||||
let content_salt = generate_salt();
|
||||
let content_salt = Salt::generate();
|
||||
let hashed_password = HASHING_ALGORITHM
|
||||
.hash(password, content_salt, None)
|
||||
.unwrap();
|
||||
|
@ -49,7 +52,7 @@ async fn encrypt() {
|
|||
.unwrap()];
|
||||
|
||||
// Create the header for the encrypted file (and include our metadata)
|
||||
let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots);
|
||||
let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots).unwrap();
|
||||
|
||||
header
|
||||
.add_metadata(
|
||||
|
@ -65,7 +68,7 @@ async fn encrypt() {
|
|||
header.write(&mut writer).await.unwrap();
|
||||
|
||||
// Use the nonce created by the header to initialise a stream encryption object
|
||||
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap();
|
||||
let encryptor = StreamEncryption::new(master_key, header.nonce, header.algorithm).unwrap();
|
||||
|
||||
// Encrypt the data from the reader, and write it to the writer
|
||||
// Use AAD so the header can be authenticated against every block of data
|
||||
|
|
|
@ -4,7 +4,10 @@ use sd_crypto::{
|
|||
crypto::stream::{Algorithm, StreamEncryption},
|
||||
header::{file::FileHeader, keyslot::Keyslot, preview_media::PreviewMediaVersion},
|
||||
keys::hashing::{HashingAlgorithm, Params},
|
||||
primitives::{generate_master_key, generate_salt, LATEST_FILE_HEADER, LATEST_KEYSLOT},
|
||||
primitives::{
|
||||
types::{Key, Salt},
|
||||
LATEST_FILE_HEADER, LATEST_KEYSLOT,
|
||||
},
|
||||
Protected,
|
||||
};
|
||||
|
||||
|
@ -19,10 +22,10 @@ async fn encrypt() {
|
|||
let mut writer = File::create("test.encrypted").await.unwrap();
|
||||
|
||||
// This needs to be generated here, otherwise we won't have access to it for encryption
|
||||
let master_key = generate_master_key();
|
||||
let master_key = Key::generate();
|
||||
|
||||
// These should ideally be done by a key management system
|
||||
let content_salt = generate_salt();
|
||||
let content_salt = Salt::generate();
|
||||
let hashed_password = HASHING_ALGORITHM
|
||||
.hash(password, content_salt, None)
|
||||
.unwrap();
|
||||
|
@ -42,7 +45,7 @@ async fn encrypt() {
|
|||
let pvm_media = b"a nice mountain".to_vec();
|
||||
|
||||
// Create the header for the encrypted file (and include our preview media)
|
||||
let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots);
|
||||
let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots).unwrap();
|
||||
|
||||
header
|
||||
.add_preview_media(
|
||||
|
@ -58,7 +61,7 @@ async fn encrypt() {
|
|||
header.write(&mut writer).await.unwrap();
|
||||
|
||||
// Use the nonce created by the header to initialise a stream encryption object
|
||||
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap();
|
||||
let encryptor = StreamEncryption::new(master_key, header.nonce, header.algorithm).unwrap();
|
||||
|
||||
// Encrypt the data from the reader, and write it to the writer
|
||||
// Use AAD so the header can be authenticated against every block of data
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
use std::io::Cursor;
|
||||
|
||||
use crate::{
|
||||
primitives::{Key, AEAD_TAG_SIZE, BLOCK_SIZE},
|
||||
protected::ProtectedVec,
|
||||
primitives::{
|
||||
types::{Key, Nonce},
|
||||
AEAD_TAG_SIZE, BLOCK_SIZE,
|
||||
},
|
||||
Error, Protected, Result,
|
||||
};
|
||||
use aead::{
|
||||
|
@ -55,7 +57,7 @@ impl StreamEncryption {
|
|||
///
|
||||
/// The master key, a suitable nonce, and a specific algorithm should be provided.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(key: Protected<Key>, nonce: &[u8], algorithm: Algorithm) -> Result<Self> {
|
||||
pub fn new(key: Key, nonce: Nonce, algorithm: Algorithm) -> Result<Self> {
|
||||
if nonce.len() != algorithm.nonce_len() {
|
||||
return Err(Error::NonceLengthMismatch);
|
||||
}
|
||||
|
@ -65,14 +67,14 @@ impl StreamEncryption {
|
|||
let cipher = XChaCha20Poly1305::new_from_slice(key.expose())
|
||||
.map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = EncryptorLE31::from_aead(cipher, nonce.into());
|
||||
let stream = EncryptorLE31::from_aead(cipher, (&*nonce).into());
|
||||
Self::XChaCha20Poly1305(Box::new(stream))
|
||||
}
|
||||
Algorithm::Aes256Gcm => {
|
||||
let cipher =
|
||||
Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = EncryptorLE31::from_aead(cipher, nonce.into());
|
||||
let stream = EncryptorLE31::from_aead(cipher, (&*nonce).into());
|
||||
Self::Aes256Gcm(Box::new(stream))
|
||||
}
|
||||
};
|
||||
|
@ -137,7 +139,6 @@ impl StreamEncryption {
|
|||
};
|
||||
|
||||
let encrypted_data = self.encrypt_next(payload).map_err(|_| Error::Encrypt)?;
|
||||
|
||||
writer.write_all(&encrypted_data).await?;
|
||||
} else {
|
||||
// we use `..read_count` in order to only use the read data, and not zeroes also
|
||||
|
@ -148,7 +149,6 @@ impl StreamEncryption {
|
|||
|
||||
let encrypted_data = self.encrypt_last(payload).map_err(|_| Error::Encrypt)?;
|
||||
writer.write_all(&encrypted_data).await?;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -163,8 +163,8 @@ impl StreamEncryption {
|
|||
/// It is just a thin wrapper around `encrypt_streams()`, but reduces the amount of code needed elsewhere.
|
||||
#[allow(unused_mut)]
|
||||
pub async fn encrypt_bytes(
|
||||
key: Protected<Key>,
|
||||
nonce: &[u8],
|
||||
key: Key,
|
||||
nonce: Nonce,
|
||||
algorithm: Algorithm,
|
||||
bytes: &[u8],
|
||||
aad: &[u8],
|
||||
|
@ -184,7 +184,7 @@ impl StreamDecryption {
|
|||
///
|
||||
/// The master key, nonce and algorithm that were used for encryption should be provided.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(key: Protected<Key>, nonce: &[u8], algorithm: Algorithm) -> Result<Self> {
|
||||
pub fn new(key: Key, nonce: Nonce, algorithm: Algorithm) -> Result<Self> {
|
||||
if nonce.len() != algorithm.nonce_len() {
|
||||
return Err(Error::NonceLengthMismatch);
|
||||
}
|
||||
|
@ -194,14 +194,14 @@ impl StreamDecryption {
|
|||
let cipher = XChaCha20Poly1305::new_from_slice(key.expose())
|
||||
.map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = DecryptorLE31::from_aead(cipher, nonce.into());
|
||||
let stream = DecryptorLE31::from_aead(cipher, (&*nonce).into());
|
||||
Self::XChaCha20Poly1305(Box::new(stream))
|
||||
}
|
||||
Algorithm::Aes256Gcm => {
|
||||
let cipher =
|
||||
Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = DecryptorLE31::from_aead(cipher, nonce.into());
|
||||
let stream = DecryptorLE31::from_aead(cipher, (&*nonce).into());
|
||||
Self::Aes256Gcm(Box::new(stream))
|
||||
}
|
||||
};
|
||||
|
@ -266,7 +266,6 @@ impl StreamDecryption {
|
|||
};
|
||||
|
||||
let decrypted_data = self.decrypt_next(payload).map_err(|_| Error::Decrypt)?;
|
||||
|
||||
writer.write_all(&decrypted_data).await?;
|
||||
} else {
|
||||
let payload = Payload {
|
||||
|
@ -276,7 +275,6 @@ impl StreamDecryption {
|
|||
|
||||
let decrypted_data = self.decrypt_last(payload).map_err(|_| Error::Decrypt)?;
|
||||
writer.write_all(&decrypted_data).await?;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -291,12 +289,12 @@ impl StreamDecryption {
|
|||
/// It is just a thin wrapper around `decrypt_streams()`, but reduces the amount of code needed elsewhere.
|
||||
#[allow(unused_mut)]
|
||||
pub async fn decrypt_bytes(
|
||||
key: Protected<Key>,
|
||||
nonce: &[u8],
|
||||
key: Key,
|
||||
nonce: Nonce,
|
||||
algorithm: Algorithm,
|
||||
bytes: &[u8],
|
||||
aad: &[u8],
|
||||
) -> Result<ProtectedVec<u8>> {
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
let mut writer = Cursor::new(Vec::<u8>::new());
|
||||
let decryptor = Self::new(key, nonce, algorithm)?;
|
||||
|
||||
|
|
|
@ -35,8 +35,7 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
|||
|
||||
use crate::{
|
||||
crypto::stream::Algorithm,
|
||||
primitives::{generate_nonce, to_array, Key, KEY_LEN},
|
||||
protected::ProtectedVec,
|
||||
primitives::types::{Key, Nonce},
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
|
@ -61,7 +60,7 @@ pub const MAGIC_BYTES: [u8; 7] = [0x62, 0x61, 0x6C, 0x6C, 0x61, 0x70, 0x70];
|
|||
pub struct FileHeader {
|
||||
pub version: FileHeaderVersion,
|
||||
pub algorithm: Algorithm,
|
||||
pub nonce: Vec<u8>,
|
||||
pub nonce: Nonce,
|
||||
pub keyslots: Vec<Keyslot>,
|
||||
pub metadata: Option<Metadata>,
|
||||
pub preview_media: Option<PreviewMedia>,
|
||||
|
@ -75,16 +74,21 @@ pub enum FileHeaderVersion {
|
|||
|
||||
impl FileHeader {
|
||||
/// This function is used for creating a file header.
|
||||
#[must_use]
|
||||
pub fn new(version: FileHeaderVersion, algorithm: Algorithm, keyslots: Vec<Keyslot>) -> Self {
|
||||
Self {
|
||||
pub fn new(
|
||||
version: FileHeaderVersion,
|
||||
algorithm: Algorithm,
|
||||
keyslots: Vec<Keyslot>,
|
||||
) -> Result<Self> {
|
||||
let f = Self {
|
||||
version,
|
||||
algorithm,
|
||||
nonce: generate_nonce(algorithm),
|
||||
nonce: Nonce::generate(algorithm)?,
|
||||
keyslots,
|
||||
metadata: None,
|
||||
preview_media: None,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// This includes the magic bytes at the start of the file, and remainder of the header itself (excluding keyslots, metadata, and preview media as these can all change)
|
||||
|
@ -101,18 +105,13 @@ impl FileHeader {
|
|||
///
|
||||
/// You receive an error if the password doesn't match or if there are no keyslots.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn decrypt_master_key(&self, password: ProtectedVec<u8>) -> Result<Protected<Key>> {
|
||||
pub async fn decrypt_master_key(&self, password: Protected<Vec<u8>>) -> Result<Key> {
|
||||
if self.keyslots.is_empty() {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
for v in &self.keyslots {
|
||||
if let Some(key) = v
|
||||
.decrypt_master_key(password.clone())
|
||||
.await
|
||||
.ok()
|
||||
.map(|v| Protected::new(to_array::<KEY_LEN>(v.into_inner()).unwrap()))
|
||||
{
|
||||
if let Ok(key) = v.decrypt_master_key(password.clone()).await {
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +119,31 @@ impl FileHeader {
|
|||
Err(Error::IncorrectPassword)
|
||||
}
|
||||
|
||||
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
|
||||
///
|
||||
/// It takes in a Vec of pre-hashed keys, which is what the key manager returns
|
||||
///
|
||||
/// You receive an error if the password doesn't match or if there are no keyslots.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn decrypt_master_key_from_prehashed(&self, hashed_keys: Vec<Key>) -> Result<Key> {
|
||||
if self.keyslots.is_empty() {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
for hashed_key in hashed_keys {
|
||||
for v in &self.keyslots {
|
||||
if let Ok(key) = v
|
||||
.decrypt_master_key_from_prehashed(hashed_key.clone())
|
||||
.await
|
||||
{
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::IncorrectPassword)
|
||||
}
|
||||
|
||||
/// This is a helper function to serialize and write a header to a file.
|
||||
pub async fn write<W>(&self, writer: &mut W) -> Result<()>
|
||||
where
|
||||
|
@ -129,41 +153,11 @@ impl FileHeader {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
|
||||
///
|
||||
/// It takes in a Vec of pre-hashed keys, which is what the key manager returns
|
||||
///
|
||||
/// You receive an error if the password doesn't match or if there are no keyslots.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn decrypt_master_key_from_prehashed(
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<Key>>,
|
||||
) -> Result<Protected<Key>> {
|
||||
if self.keyslots.is_empty() {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
for hashed_key in hashed_keys {
|
||||
for v in &self.keyslots {
|
||||
if let Some(key) = v
|
||||
.decrypt_master_key_from_prehashed(hashed_key.clone())
|
||||
.await
|
||||
.ok()
|
||||
.map(|v| Protected::new(to_array::<KEY_LEN>(v.into_inner()).unwrap()))
|
||||
{
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::IncorrectPassword)
|
||||
}
|
||||
|
||||
/// This is a helper function to find which keyslot a key belongs to.
|
||||
///
|
||||
/// You receive an error if the password doesn't match or if there are no keyslots.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn find_key_index(&self, password: ProtectedVec<u8>) -> Result<usize> {
|
||||
pub async fn find_key_index(&self, password: Protected<Vec<u8>>) -> Result<usize> {
|
||||
if self.keyslots.is_empty() {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
@ -185,9 +179,9 @@ impl FileHeader {
|
|||
match self.version {
|
||||
FileHeaderVersion::V1 => [
|
||||
MAGIC_BYTES.as_ref(),
|
||||
self.version.to_bytes().as_ref(),
|
||||
self.algorithm.to_bytes().as_ref(),
|
||||
self.nonce.as_ref(),
|
||||
&self.version.to_bytes(),
|
||||
&self.algorithm.to_bytes(),
|
||||
&self.nonce,
|
||||
&vec![0u8; 25 - self.nonce.len()],
|
||||
]
|
||||
.into_iter()
|
||||
|
@ -291,6 +285,7 @@ impl FileHeader {
|
|||
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut nonce).await?;
|
||||
let nonce = Nonce::try_from(nonce)?;
|
||||
|
||||
// read and discard the padding
|
||||
reader.read_exact(&mut vec![0u8; 25 - nonce.len()]).await?;
|
||||
|
|
|
@ -27,10 +27,9 @@ use crate::{
|
|||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
keys::hashing::HashingAlgorithm,
|
||||
primitives::{
|
||||
derive_key, generate_nonce, generate_salt, to_array, EncryptedKey, Key, Salt,
|
||||
types::{EncryptedKey, Key, Nonce, Salt},
|
||||
ENCRYPTED_KEY_LEN, FILE_KEY_CONTEXT, SALT_LEN,
|
||||
},
|
||||
protected::ProtectedVec,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
|
@ -45,7 +44,7 @@ pub struct Keyslot {
|
|||
pub salt: Salt, // the salt used for deriving a KEK from a (key/content salt) hash
|
||||
pub content_salt: Salt,
|
||||
pub master_key: EncryptedKey, // this is encrypted so we can store it
|
||||
pub nonce: Vec<u8>,
|
||||
pub nonce: Nonce,
|
||||
}
|
||||
|
||||
pub const KEYSLOT_SIZE: usize = 112;
|
||||
|
@ -70,17 +69,17 @@ impl Keyslot {
|
|||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
content_salt: Salt,
|
||||
hashed_key: Protected<Key>,
|
||||
master_key: Protected<Key>,
|
||||
hashed_key: Key,
|
||||
master_key: Key,
|
||||
) -> Result<Self> {
|
||||
let nonce = generate_nonce(algorithm);
|
||||
let nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let salt = generate_salt();
|
||||
let salt = Salt::generate();
|
||||
|
||||
let encrypted_master_key = to_array::<ENCRYPTED_KEY_LEN>(
|
||||
let encrypted_master_key = EncryptedKey::try_from(
|
||||
StreamEncryption::encrypt_bytes(
|
||||
derive_key(hashed_key, salt, FILE_KEY_CONTEXT),
|
||||
&nonce,
|
||||
Key::derive(hashed_key, salt, FILE_KEY_CONTEXT),
|
||||
nonce,
|
||||
algorithm,
|
||||
master_key.expose(),
|
||||
&[],
|
||||
|
@ -105,20 +104,22 @@ impl Keyslot {
|
|||
///
|
||||
/// An error will be returned on failure.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn decrypt_master_key(&self, password: ProtectedVec<u8>) -> Result<ProtectedVec<u8>> {
|
||||
pub async fn decrypt_master_key(&self, password: Protected<Vec<u8>>) -> Result<Key> {
|
||||
let key = self
|
||||
.hashing_algorithm
|
||||
.hash(password, self.content_salt, None)
|
||||
.map_err(|_| Error::PasswordHash)?;
|
||||
|
||||
StreamDecryption::decrypt_bytes(
|
||||
derive_key(key, self.salt, FILE_KEY_CONTEXT),
|
||||
&self.nonce,
|
||||
self.algorithm,
|
||||
&self.master_key,
|
||||
&[],
|
||||
Key::try_from(
|
||||
StreamDecryption::decrypt_bytes(
|
||||
Key::derive(key, self.salt, FILE_KEY_CONTEXT),
|
||||
self.nonce,
|
||||
self.algorithm,
|
||||
&self.master_key,
|
||||
&[],
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// This function should not be used directly, use `header.decrypt_master_key()` instead
|
||||
|
@ -128,18 +129,17 @@ impl Keyslot {
|
|||
/// No hashing is done internally.
|
||||
///
|
||||
/// An error will be returned on failure.
|
||||
pub async fn decrypt_master_key_from_prehashed(
|
||||
&self,
|
||||
key: Protected<Key>,
|
||||
) -> Result<ProtectedVec<u8>> {
|
||||
StreamDecryption::decrypt_bytes(
|
||||
derive_key(key, self.salt, FILE_KEY_CONTEXT),
|
||||
&self.nonce,
|
||||
self.algorithm,
|
||||
&self.master_key,
|
||||
&[],
|
||||
pub async fn decrypt_master_key_from_prehashed(&self, key: Key) -> Result<Key> {
|
||||
Key::try_from(
|
||||
StreamDecryption::decrypt_bytes(
|
||||
Key::derive(key, self.salt, FILE_KEY_CONTEXT),
|
||||
self.nonce,
|
||||
self.algorithm,
|
||||
&self.master_key,
|
||||
&[],
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// This function is used to serialize a keyslot into bytes
|
||||
|
@ -197,6 +197,7 @@ impl Keyslot {
|
|||
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut nonce)?;
|
||||
let nonce = Nonce::try_from(nonce)?;
|
||||
|
||||
reader.read_exact(&mut vec![0u8; 26 - nonce.len()])?;
|
||||
|
||||
|
@ -204,9 +205,9 @@ impl Keyslot {
|
|||
version,
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
salt,
|
||||
content_salt,
|
||||
master_key,
|
||||
salt: Salt(salt),
|
||||
content_salt: Salt(content_salt),
|
||||
master_key: EncryptedKey(master_key),
|
||||
nonce,
|
||||
};
|
||||
|
||||
|
|
|
@ -31,13 +31,13 @@
|
|||
#[cfg(feature = "serde")]
|
||||
use crate::{
|
||||
crypto::stream::{StreamDecryption, StreamEncryption},
|
||||
primitives::{generate_nonce, Key},
|
||||
Protected, ProtectedVec,
|
||||
primitives::types::Key,
|
||||
Protected,
|
||||
};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::{crypto::stream::Algorithm, Error, Result};
|
||||
use crate::{crypto::stream::Algorithm, primitives::types::Nonce, Error, Result};
|
||||
|
||||
use super::file::FileHeader;
|
||||
|
||||
|
@ -50,7 +50,7 @@ use super::file::FileHeader;
|
|||
pub struct Metadata {
|
||||
pub version: MetadataVersion,
|
||||
pub algorithm: Algorithm, // encryption algorithm
|
||||
pub metadata_nonce: Vec<u8>,
|
||||
pub metadata_nonce: Nonce,
|
||||
pub metadata: Vec<u8>,
|
||||
}
|
||||
|
||||
|
@ -73,17 +73,17 @@ impl FileHeader {
|
|||
&mut self,
|
||||
version: MetadataVersion,
|
||||
algorithm: Algorithm,
|
||||
master_key: Protected<Key>,
|
||||
master_key: Key,
|
||||
metadata: &T,
|
||||
) -> Result<()>
|
||||
where
|
||||
T: ?Sized + serde::Serialize + Sync + Send,
|
||||
{
|
||||
let metadata_nonce = generate_nonce(algorithm);
|
||||
let metadata_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let encrypted_metadata = StreamEncryption::encrypt_bytes(
|
||||
master_key,
|
||||
&metadata_nonce,
|
||||
metadata_nonce,
|
||||
algorithm,
|
||||
&serde_json::to_vec(metadata).map_err(|_| Error::Serialization)?,
|
||||
&[],
|
||||
|
@ -100,44 +100,13 @@ impl FileHeader {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// This function should be used to retrieve the metadata for a file
|
||||
///
|
||||
/// All it requires is pre-hashed keys returned from the key manager
|
||||
///
|
||||
/// A deserialized data type will be returned from this function
|
||||
#[cfg(feature = "serde")]
|
||||
pub async fn decrypt_metadata_from_prehashed<T>(
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<Key>>,
|
||||
) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys).await?;
|
||||
|
||||
if let Some(metadata) = self.metadata.as_ref() {
|
||||
let metadata = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
&metadata.metadata_nonce,
|
||||
metadata.algorithm,
|
||||
&metadata.metadata,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::Serialization)
|
||||
} else {
|
||||
Err(Error::NoMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
/// This function should be used to retrieve the metadata for a file
|
||||
///
|
||||
/// All it requires is a password. Hashing is handled for you.
|
||||
///
|
||||
/// A deserialized data type will be returned from this function
|
||||
#[cfg(feature = "serde")]
|
||||
pub async fn decrypt_metadata<T>(&self, password: ProtectedVec<u8>) -> Result<T>
|
||||
pub async fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
|
@ -146,14 +115,42 @@ impl FileHeader {
|
|||
if let Some(metadata) = self.metadata.as_ref() {
|
||||
let metadata = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
&metadata.metadata_nonce,
|
||||
metadata.metadata_nonce,
|
||||
metadata.algorithm,
|
||||
&metadata.metadata,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::Serialization)
|
||||
serde_json::from_slice::<T>(metadata.expose()).map_err(|_| Error::Serialization)
|
||||
} else {
|
||||
Err(Error::NoMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
/// This function should be used to retrieve the metadata for a file
|
||||
///
|
||||
/// All it requires is pre-hashed keys returned from the key manager
|
||||
///
|
||||
/// A deserialized data type will be returned from this function
|
||||
#[cfg(feature = "serde")]
|
||||
pub async fn decrypt_metadata_from_prehashed<T>(&self, hashed_keys: Vec<Key>) -> Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys).await?;
|
||||
|
||||
if let Some(metadata) = self.metadata.as_ref() {
|
||||
let metadata = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
metadata.metadata_nonce,
|
||||
metadata.algorithm,
|
||||
&metadata.metadata,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
serde_json::from_slice::<T>(metadata.expose()).map_err(|_| Error::Serialization)
|
||||
} else {
|
||||
Err(Error::NoMetadata)
|
||||
}
|
||||
|
@ -208,6 +205,7 @@ impl Metadata {
|
|||
|
||||
let mut metadata_nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut metadata_nonce).await?;
|
||||
let metadata_nonce = Nonce::try_from(metadata_nonce)?;
|
||||
|
||||
reader
|
||||
.read_exact(&mut vec![0u8; 24 - metadata_nonce.len()])
|
||||
|
|
|
@ -24,8 +24,8 @@ use tokio::io::AsyncReadExt;
|
|||
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
primitives::{generate_nonce, Key},
|
||||
Error, Protected, ProtectedVec, Result,
|
||||
primitives::types::{Key, Nonce},
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
use super::file::FileHeader;
|
||||
|
@ -39,7 +39,7 @@ use super::file::FileHeader;
|
|||
pub struct PreviewMedia {
|
||||
pub version: PreviewMediaVersion,
|
||||
pub algorithm: Algorithm, // encryption algorithm
|
||||
pub media_nonce: Vec<u8>,
|
||||
pub media_nonce: Nonce,
|
||||
pub media: Vec<u8>,
|
||||
}
|
||||
|
||||
|
@ -61,14 +61,13 @@ impl FileHeader {
|
|||
&mut self,
|
||||
version: PreviewMediaVersion,
|
||||
algorithm: Algorithm,
|
||||
master_key: Protected<Key>,
|
||||
master_key: Key,
|
||||
media: &[u8],
|
||||
) -> Result<()> {
|
||||
let media_nonce = generate_nonce(algorithm);
|
||||
let media_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let encrypted_media =
|
||||
StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])
|
||||
.await?;
|
||||
StreamEncryption::encrypt_bytes(master_key, media_nonce, algorithm, media, &[]).await?;
|
||||
|
||||
self.preview_media = Some(PreviewMedia {
|
||||
version,
|
||||
|
@ -82,19 +81,19 @@ impl FileHeader {
|
|||
|
||||
/// This function is what you'll want to use to get the preview media for a file
|
||||
///
|
||||
/// All it requires is pre-hashed keys returned from the key manager
|
||||
/// All it requires is the user's password. Hashing is handled for you.
|
||||
///
|
||||
/// Once provided, a `Vec<u8>` is returned that contains the preview media
|
||||
pub async fn decrypt_preview_media_from_prehashed(
|
||||
pub async fn decrypt_preview_media(
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<Key>>,
|
||||
) -> Result<ProtectedVec<u8>> {
|
||||
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys).await?;
|
||||
password: Protected<Vec<u8>>,
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
let master_key = self.decrypt_master_key(password).await?;
|
||||
|
||||
if let Some(pvm) = self.preview_media.as_ref() {
|
||||
let pvm = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
&pvm.media_nonce,
|
||||
pvm.media_nonce,
|
||||
pvm.algorithm,
|
||||
&pvm.media,
|
||||
&[],
|
||||
|
@ -109,19 +108,19 @@ impl FileHeader {
|
|||
|
||||
/// This function is what you'll want to use to get the preview media for a file
|
||||
///
|
||||
/// All it requires is the user's password. Hashing is handled for you.
|
||||
/// All it requires is pre-hashed keys returned from the key manager
|
||||
///
|
||||
/// Once provided, a `Vec<u8>` is returned that contains the preview media
|
||||
pub async fn decrypt_preview_media(
|
||||
pub async fn decrypt_preview_media_from_prehashed(
|
||||
&self,
|
||||
password: ProtectedVec<u8>,
|
||||
) -> Result<ProtectedVec<u8>> {
|
||||
let master_key = self.decrypt_master_key(password).await?;
|
||||
hashed_keys: Vec<Key>,
|
||||
) -> Result<Protected<Vec<u8>>> {
|
||||
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys).await?;
|
||||
|
||||
if let Some(pvm) = self.preview_media.as_ref() {
|
||||
let pvm = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
&pvm.media_nonce,
|
||||
pvm.media_nonce,
|
||||
pvm.algorithm,
|
||||
&pvm.media,
|
||||
&[],
|
||||
|
@ -184,6 +183,7 @@ impl PreviewMedia {
|
|||
|
||||
let mut media_nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut media_nonce).await?;
|
||||
let media_nonce = Nonce::try_from(media_nonce)?;
|
||||
|
||||
reader
|
||||
.read_exact(&mut vec![0u8; 24 - media_nonce.len()])
|
||||
|
|
|
@ -12,8 +12,11 @@
|
|||
//! ```
|
||||
|
||||
use crate::{
|
||||
primitives::{Key, Salt, KEY_LEN},
|
||||
Error, Protected, ProtectedVec, Result,
|
||||
primitives::{
|
||||
types::{Key, Salt, SecretKey},
|
||||
KEY_LEN,
|
||||
},
|
||||
Error, Protected, Result,
|
||||
};
|
||||
use argon2::Argon2;
|
||||
use balloon_hash::Balloon;
|
||||
|
@ -53,10 +56,10 @@ impl HashingAlgorithm {
|
|||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn hash(
|
||||
&self,
|
||||
password: ProtectedVec<u8>,
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: Salt,
|
||||
secret: Option<ProtectedVec<u8>>,
|
||||
) -> Result<Protected<Key>> {
|
||||
secret: Option<SecretKey>,
|
||||
) -> Result<Key> {
|
||||
match self {
|
||||
Self::Argon2id(params) => PasswordHasher::argon2id(password, salt, secret, *params),
|
||||
Self::BalloonBlake3(params) => {
|
||||
|
@ -79,8 +82,8 @@ impl Params {
|
|||
// Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine
|
||||
// It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks)
|
||||
Self::Standard => argon2::Params::new(131_072, 8, 4, None).unwrap(),
|
||||
Self::Paranoid => argon2::Params::new(262_144, 8, 4, None).unwrap(),
|
||||
Self::Hardened => argon2::Params::new(524_288, 8, 4, None).unwrap(),
|
||||
Self::Hardened => argon2::Params::new(262_144, 8, 4, None).unwrap(),
|
||||
Self::Paranoid => argon2::Params::new(524_288, 8, 4, None).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,9 +98,9 @@ impl Params {
|
|||
// It's very hardware dependant but we should aim for at least 64MB of RAM usage on standard
|
||||
// Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine
|
||||
// It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks)
|
||||
Self::Standard => balloon_hash::Params::new(131_072, 1, 1).unwrap(),
|
||||
Self::Paranoid => balloon_hash::Params::new(262_144, 1, 1).unwrap(),
|
||||
Self::Hardened => balloon_hash::Params::new(524_288, 1, 1).unwrap(),
|
||||
Self::Standard => balloon_hash::Params::new(131_072, 2, 1).unwrap(),
|
||||
Self::Hardened => balloon_hash::Params::new(262_144, 2, 1).unwrap(),
|
||||
Self::Paranoid => balloon_hash::Params::new(524_288, 2, 1).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,12 +110,14 @@ struct PasswordHasher;
|
|||
impl PasswordHasher {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn argon2id(
|
||||
password: ProtectedVec<u8>,
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: Salt,
|
||||
secret: Option<ProtectedVec<u8>>,
|
||||
secret: Option<SecretKey>,
|
||||
params: Params,
|
||||
) -> Result<Protected<Key>> {
|
||||
let secret = secret.map_or(Protected::new(vec![]), |k| k);
|
||||
) -> Result<Key> {
|
||||
let secret = secret.map_or(Protected::new(vec![]), |k| {
|
||||
Protected::new(k.expose().to_vec())
|
||||
});
|
||||
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
let argon2 = Argon2::new_with_secret(
|
||||
|
@ -125,17 +130,19 @@ impl PasswordHasher {
|
|||
|
||||
argon2
|
||||
.hash_password_into(password.expose(), &salt, &mut key)
|
||||
.map_or(Err(Error::PasswordHash), |_| Ok(Protected::new(key)))
|
||||
.map_or(Err(Error::PasswordHash), |_| Ok(Key::new(key)))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn balloon_blake3(
|
||||
password: ProtectedVec<u8>,
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: Salt,
|
||||
secret: Option<ProtectedVec<u8>>,
|
||||
secret: Option<SecretKey>,
|
||||
params: Params,
|
||||
) -> Result<Protected<Key>> {
|
||||
let secret = secret.map_or(Protected::new(vec![]), |k| k);
|
||||
) -> Result<Key> {
|
||||
let secret = secret.map_or(Protected::new(vec![]), |k| {
|
||||
Protected::new(k.expose().to_vec())
|
||||
});
|
||||
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
|
||||
|
@ -147,6 +154,6 @@ impl PasswordHasher {
|
|||
|
||||
balloon
|
||||
.hash_into(password.expose(), &salt, &mut key)
|
||||
.map_or(Err(Error::PasswordHash), |_| Ok(Protected::new(key)))
|
||||
.map_or(Err(Error::PasswordHash), |_| Ok(Key::new(key)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,50 +35,58 @@
|
|||
//! let keys = key_manager.enumerate_hashed_keys();
|
||||
//! ```
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
// use crate::primitives::{
|
||||
// derive_key, generate_master_key, generate_nonce, generate_salt, to_array, EncryptedKey, Key,
|
||||
// OnboardingConfig, Salt, KEY_LEN, LATEST_STORED_KEY, MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT,
|
||||
// };
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
primitives::{
|
||||
derive_key, generate_master_key, generate_nonce, generate_salt, to_array, EncryptedKey,
|
||||
Key, OnboardingConfig, Salt, ENCRYPTED_KEY_LEN, KEY_LEN, LATEST_STORED_KEY,
|
||||
MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT,
|
||||
types::{
|
||||
EncryptedKey, Key, Nonce, OnboardingConfig, Password, Salt, SecretKey, SecretKeyString,
|
||||
},
|
||||
APP_IDENTIFIER, LATEST_STORED_KEY, MASTER_PASSWORD_CONTEXT, ROOT_KEY_CONTEXT,
|
||||
SECRET_KEY_IDENTIFIER,
|
||||
},
|
||||
Error, Protected, ProtectedVec, Result,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde_big_array::BigArray;
|
||||
|
||||
use super::hashing::HashingAlgorithm;
|
||||
use super::{
|
||||
hashing::HashingAlgorithm,
|
||||
keyring::{Identifier, KeyringInterface},
|
||||
};
|
||||
|
||||
/// This is a stored key, and can be freely written to Prisma/another database.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub struct StoredKey {
|
||||
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys
|
||||
pub uuid: Uuid, // uuid for identification. shared with mounted keys
|
||||
pub version: StoredKeyVersion,
|
||||
pub key_type: StoredKeyType,
|
||||
pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though)
|
||||
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm used for hashing the key with the content salt
|
||||
pub content_salt: Salt,
|
||||
#[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data
|
||||
pub master_key: EncryptedKey, // this is for encrypting the `key`
|
||||
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
|
||||
pub key_nonce: Vec<u8>, // nonce used for encrypting the main key
|
||||
pub key: Vec<u8>, // encrypted. the key stored in spacedrive (e.g. generated 64 char key)
|
||||
pub master_key_nonce: Nonce, // nonce for encrypting the master key
|
||||
pub key_nonce: Nonce, // nonce used for encrypting the main key
|
||||
pub key: Vec<u8>, // encrypted. the password stored in spacedrive (e.g. generated 64 char key)
|
||||
pub salt: Salt,
|
||||
pub memory_only: bool,
|
||||
pub automount: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub enum StoredKeyType {
|
||||
User,
|
||||
Root,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
|
@ -91,8 +99,8 @@ pub enum StoredKeyVersion {
|
|||
/// This contains the plaintext key, and the same key hashed with the content salt.
|
||||
#[derive(Clone)]
|
||||
pub struct MountedKey {
|
||||
pub uuid: Uuid, // used for identification. shared with stored keys
|
||||
pub hashed_key: Protected<Key>, // this is hashed with the content salt, for instant access
|
||||
pub uuid: Uuid, // used for identification. shared with stored keys
|
||||
pub hashed_key: Key, // this is hashed with the content salt, for instant access
|
||||
}
|
||||
|
||||
/// This is the key manager itself.
|
||||
|
@ -101,18 +109,23 @@ pub struct MountedKey {
|
|||
///
|
||||
/// Use the associated functions to interact with it.
|
||||
pub struct KeyManager {
|
||||
root_key: Mutex<Option<Protected<Key>>>, // the root key for the vault
|
||||
root_key: Mutex<Option<Key>>, // the root key for the vault
|
||||
verification_key: Mutex<Option<StoredKey>>,
|
||||
keystore: DashMap<Uuid, StoredKey>,
|
||||
keymount: DashMap<Uuid, MountedKey>,
|
||||
default: Mutex<Option<Uuid>>,
|
||||
mounting_queue: DashSet<Uuid>,
|
||||
keyring: Option<Arc<Mutex<KeyringInterface>>>,
|
||||
}
|
||||
|
||||
/// The `KeyManager` functions should be used for all key-related management.
|
||||
impl KeyManager {
|
||||
/// Initialize the Key Manager with `StoredKeys` retrieved from Prisma
|
||||
pub async fn new(stored_keys: Vec<StoredKey>) -> Result<Self> {
|
||||
let keyring = KeyringInterface::new()
|
||||
.map(|k| Arc::new(Mutex::new(k)))
|
||||
.ok();
|
||||
|
||||
let keymanager = Self {
|
||||
root_key: Mutex::new(None),
|
||||
verification_key: Mutex::new(None),
|
||||
|
@ -120,6 +133,7 @@ impl KeyManager {
|
|||
keymount: DashMap::new(),
|
||||
default: Mutex::new(None),
|
||||
mounting_queue: DashSet::new(),
|
||||
keyring,
|
||||
};
|
||||
|
||||
keymanager.populate_keystore(stored_keys).await?;
|
||||
|
@ -127,15 +141,89 @@ impl KeyManager {
|
|||
Ok(keymanager)
|
||||
}
|
||||
|
||||
// A returned error here should be treated as `false`
|
||||
pub async fn keyring_contains(&self, library_uuid: Uuid, usage: String) -> Result<()> {
|
||||
self.get_keyring()?.lock().await.retrieve(Identifier {
|
||||
application: APP_IDENTIFIER,
|
||||
library_uuid: &library_uuid.to_string(),
|
||||
usage: &usage,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn keyring_retrieve(
|
||||
&self,
|
||||
library_uuid: Uuid,
|
||||
usage: String,
|
||||
) -> Result<Protected<String>> {
|
||||
let value = self.get_keyring()?.lock().await.retrieve(Identifier {
|
||||
application: APP_IDENTIFIER,
|
||||
library_uuid: &library_uuid.to_string(),
|
||||
usage: &usage,
|
||||
})?;
|
||||
|
||||
Ok(Protected::new(String::from_utf8(value.expose().clone())?))
|
||||
}
|
||||
|
||||
/// This checks to see if the keyring is active, and if the keyring has a valid secret key.
|
||||
///
|
||||
/// For a secret key to be considered valid, it must be 18 bytes encoded in hex. It can be separated with `-`.
|
||||
///
|
||||
/// We can use this to detect if a secret key is technically present in the keyring, but not valid/has been tampered with.
|
||||
pub async fn keyring_contains_valid_secret_key(&self, library_uuid: Uuid) -> Result<()> {
|
||||
let secret_key = self
|
||||
.keyring_retrieve(library_uuid, SECRET_KEY_IDENTIFIER.to_string())
|
||||
.await?;
|
||||
|
||||
let mut secret_key_sanitized = secret_key.expose().clone();
|
||||
secret_key_sanitized.retain(|c| c != '-' && !c.is_whitespace());
|
||||
|
||||
if hex::decode(secret_key_sanitized)
|
||||
.map_err(|_| Error::IncorrectPassword)?
|
||||
.len() != 18
|
||||
{
|
||||
return Err(Error::IncorrectPassword);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn keyring_insert(
|
||||
&self,
|
||||
library_uuid: Uuid,
|
||||
usage: String,
|
||||
value: SecretKeyString,
|
||||
) -> Result<()> {
|
||||
self.get_keyring()?.lock().await.insert(
|
||||
Identifier {
|
||||
application: APP_IDENTIFIER,
|
||||
library_uuid: &library_uuid.to_string(),
|
||||
usage: &usage,
|
||||
},
|
||||
value,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_keyring(&self) -> Result<Arc<Mutex<KeyringInterface>>> {
|
||||
self.keyring
|
||||
.as_ref()
|
||||
.map_or(Err(Error::KeyringNotSupported), |k| Ok(k.clone()))
|
||||
}
|
||||
|
||||
/// This should be used to generate everything for the user during onboarding.
|
||||
///
|
||||
/// This will create a master password (a 7-word diceware passphrase), and a secret key (16 bytes, hex encoded)
|
||||
///
|
||||
/// It will also generate a verification key, which should be written to the database.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn onboarding(config: OnboardingConfig) -> Result<StoredKey> {
|
||||
let content_salt = generate_salt();
|
||||
let secret_key = config.secret_key.map(Self::convert_secret_key_string);
|
||||
pub async fn onboarding(config: OnboardingConfig, library_uuid: Uuid) -> Result<StoredKey> {
|
||||
let content_salt = Salt::generate();
|
||||
let secret_key = SecretKey::generate();
|
||||
|
||||
dbg!(SecretKeyString::from(secret_key.clone()).expose());
|
||||
|
||||
let algorithm = config.algorithm;
|
||||
let hashing_algorithm = config.hashing_algorithm;
|
||||
|
@ -144,24 +232,23 @@ impl KeyManager {
|
|||
let hashed_password = hashing_algorithm.hash(
|
||||
Protected::new(config.password.expose().as_bytes().to_vec()),
|
||||
content_salt,
|
||||
secret_key,
|
||||
Some(secret_key.clone()),
|
||||
)?;
|
||||
|
||||
let salt = generate_salt();
|
||||
let uuid = uuid::Uuid::nil();
|
||||
let salt = Salt::generate();
|
||||
|
||||
// Generate items we'll need for encryption
|
||||
let master_key = generate_master_key();
|
||||
let master_key_nonce = generate_nonce(algorithm);
|
||||
let master_key = Key::generate();
|
||||
let master_key_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let root_key = generate_master_key();
|
||||
let root_key_nonce = generate_nonce(algorithm);
|
||||
let root_key = Key::generate();
|
||||
let root_key_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
// Encrypt the master key with the hashed master password
|
||||
let encrypted_master_key = to_array::<ENCRYPTED_KEY_LEN>(
|
||||
let encrypted_master_key = EncryptedKey::try_from(
|
||||
StreamEncryption::encrypt_bytes(
|
||||
derive_key(hashed_password, salt, MASTER_PASSWORD_CONTEXT),
|
||||
&master_key_nonce,
|
||||
Key::derive(hashed_password, salt, MASTER_PASSWORD_CONTEXT),
|
||||
master_key_nonce,
|
||||
algorithm,
|
||||
master_key.expose(),
|
||||
&[],
|
||||
|
@ -171,16 +258,29 @@ impl KeyManager {
|
|||
|
||||
let encrypted_root_key = StreamEncryption::encrypt_bytes(
|
||||
master_key,
|
||||
&root_key_nonce,
|
||||
root_key_nonce,
|
||||
algorithm,
|
||||
root_key.expose(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// attempt to insert into the OS keyring
|
||||
// can ignore false here as we want to silently error
|
||||
if let Ok(keyring) = KeyringInterface::new() {
|
||||
let identifier = Identifier {
|
||||
application: APP_IDENTIFIER,
|
||||
library_uuid: &library_uuid.to_string(),
|
||||
usage: SECRET_KEY_IDENTIFIER,
|
||||
};
|
||||
|
||||
keyring.insert(identifier, secret_key.into()).ok();
|
||||
}
|
||||
|
||||
let verification_key = StoredKey {
|
||||
uuid,
|
||||
uuid: Uuid::new_v4(),
|
||||
version: LATEST_STORED_KEY,
|
||||
key_type: StoredKeyType::Root,
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
content_salt, // salt used for hashing
|
||||
|
@ -207,7 +307,7 @@ impl KeyManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
if key.uuid.is_nil() {
|
||||
if key.key_type == StoredKeyType::Root {
|
||||
*self.verification_key.lock().await = Some(key);
|
||||
} else {
|
||||
self.keystore.insert(key.uuid, key);
|
||||
|
@ -246,33 +346,33 @@ impl KeyManager {
|
|||
master_password: Protected<String>,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
secret_key: Option<Protected<String>>,
|
||||
library_uuid: Uuid,
|
||||
) -> Result<StoredKey> {
|
||||
let secret_key = secret_key.map(Self::convert_secret_key_string);
|
||||
let content_salt = generate_salt();
|
||||
let secret_key = SecretKey::generate();
|
||||
let content_salt = Salt::generate();
|
||||
|
||||
dbg!(SecretKeyString::from(secret_key.clone()).expose());
|
||||
|
||||
let hashed_password = hashing_algorithm.hash(
|
||||
Protected::new(master_password.expose().as_bytes().to_vec()),
|
||||
content_salt,
|
||||
secret_key,
|
||||
Some(secret_key.clone()),
|
||||
)?;
|
||||
|
||||
let uuid = uuid::Uuid::nil();
|
||||
|
||||
// Generate items we'll need for encryption
|
||||
let master_key = generate_master_key();
|
||||
let master_key_nonce = generate_nonce(algorithm);
|
||||
let master_key = Key::generate();
|
||||
let master_key_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let root_key = self.get_root_key().await?;
|
||||
let root_key_nonce = generate_nonce(algorithm);
|
||||
let root_key_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let salt = generate_salt();
|
||||
let salt = Salt::generate();
|
||||
|
||||
// Encrypt the master key with the hashed master password
|
||||
let encrypted_master_key = to_array::<ENCRYPTED_KEY_LEN>(
|
||||
let encrypted_master_key = EncryptedKey::try_from(
|
||||
StreamEncryption::encrypt_bytes(
|
||||
derive_key(hashed_password, salt, MASTER_PASSWORD_CONTEXT),
|
||||
&master_key_nonce,
|
||||
Key::derive(hashed_password, salt, MASTER_PASSWORD_CONTEXT),
|
||||
master_key_nonce,
|
||||
algorithm,
|
||||
master_key.expose(),
|
||||
&[],
|
||||
|
@ -282,16 +382,26 @@ impl KeyManager {
|
|||
|
||||
let encrypted_root_key = StreamEncryption::encrypt_bytes(
|
||||
master_key,
|
||||
&root_key_nonce,
|
||||
root_key_nonce,
|
||||
algorithm,
|
||||
root_key.expose(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// will update if it's already present
|
||||
self.keyring_insert(
|
||||
library_uuid,
|
||||
SECRET_KEY_IDENTIFIER.to_string(),
|
||||
secret_key.into(),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let verification_key = StoredKey {
|
||||
uuid,
|
||||
uuid: Uuid::new_v4(),
|
||||
version: LATEST_STORED_KEY,
|
||||
key_type: StoredKeyType::Root,
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
content_salt,
|
||||
|
@ -315,19 +425,19 @@ impl KeyManager {
|
|||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn import_keystore_backup(
|
||||
&self,
|
||||
master_password: Protected<String>, // at the time of the backup
|
||||
secret_key: Option<Protected<String>>, // at the time of the backup
|
||||
stored_keys: &[StoredKey], // from the backup
|
||||
master_password: Protected<String>, // at the time of the backup
|
||||
secret_key: SecretKeyString, // at the time of the backup
|
||||
stored_keys: &[StoredKey], // from the backup
|
||||
) -> Result<Vec<StoredKey>> {
|
||||
// this backup should contain a verification key, which will tell us the algorithm+hashing algorithm
|
||||
let secret_key = secret_key.map(Self::convert_secret_key_string);
|
||||
let secret_key = secret_key.into();
|
||||
|
||||
let mut old_verification_key = None;
|
||||
|
||||
let keys: Vec<StoredKey> = stored_keys
|
||||
.iter()
|
||||
.filter_map(|key| {
|
||||
if key.uuid.is_nil() {
|
||||
if key.key_type == StoredKeyType::Root {
|
||||
old_verification_key = Some(key.clone());
|
||||
None
|
||||
} else {
|
||||
|
@ -343,17 +453,17 @@ impl KeyManager {
|
|||
let hashed_password = old_verification_key.hashing_algorithm.hash(
|
||||
Protected::new(master_password.expose().as_bytes().to_vec()),
|
||||
old_verification_key.content_salt,
|
||||
secret_key,
|
||||
Some(secret_key),
|
||||
)?;
|
||||
|
||||
// decrypt the root key's KEK
|
||||
let master_key = StreamDecryption::decrypt_bytes(
|
||||
derive_key(
|
||||
Key::derive(
|
||||
hashed_password,
|
||||
old_verification_key.salt,
|
||||
MASTER_PASSWORD_CONTEXT,
|
||||
),
|
||||
&old_verification_key.master_key_nonce,
|
||||
old_verification_key.master_key_nonce,
|
||||
old_verification_key.algorithm,
|
||||
&old_verification_key.master_key,
|
||||
&[],
|
||||
|
@ -362,15 +472,15 @@ impl KeyManager {
|
|||
|
||||
// get the root key from the backup
|
||||
let old_root_key = StreamDecryption::decrypt_bytes(
|
||||
Protected::new(to_array(master_key.into_inner())?),
|
||||
&old_verification_key.key_nonce,
|
||||
Key::try_from(master_key)?,
|
||||
old_verification_key.key_nonce,
|
||||
old_verification_key.algorithm,
|
||||
&old_verification_key.key,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Protected::new(to_array(old_root_key.into_inner())?)
|
||||
Key::try_from(old_root_key)?
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -385,27 +495,25 @@ impl KeyManager {
|
|||
StoredKeyVersion::V1 => {
|
||||
// decrypt the key's master key
|
||||
let master_key = StreamDecryption::decrypt_bytes(
|
||||
derive_key(old_root_key.clone(), key.salt, ROOT_KEY_CONTEXT),
|
||||
&key.master_key_nonce,
|
||||
Key::derive(old_root_key.clone(), key.salt, ROOT_KEY_CONTEXT),
|
||||
key.master_key_nonce,
|
||||
key.algorithm,
|
||||
&key.master_key,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_or(Err(Error::IncorrectPassword), |v| {
|
||||
Ok(Protected::new(to_array::<KEY_LEN>(v.into_inner())?))
|
||||
})?;
|
||||
.map_or(Err(Error::IncorrectPassword), Key::try_from)?;
|
||||
|
||||
// generate a new nonce
|
||||
let master_key_nonce = generate_nonce(key.algorithm);
|
||||
let master_key_nonce = Nonce::generate(key.algorithm)?;
|
||||
|
||||
let salt = generate_salt();
|
||||
let salt = Salt::generate();
|
||||
|
||||
// encrypt the master key with the current root key
|
||||
let encrypted_master_key = to_array(
|
||||
let encrypted_master_key = EncryptedKey::try_from(
|
||||
StreamEncryption::encrypt_bytes(
|
||||
derive_key(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT),
|
||||
&master_key_nonce,
|
||||
Key::derive(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT),
|
||||
master_key_nonce,
|
||||
key.algorithm,
|
||||
master_key.expose(),
|
||||
&[],
|
||||
|
@ -430,6 +538,9 @@ impl KeyManager {
|
|||
/// This is used for unlocking the key manager, and requires both the master password and the secret key.
|
||||
///
|
||||
/// The master password and secret key are hashed together.
|
||||
///
|
||||
/// Only provide the secret key if it should not/can not be sourced from an OS keychain (e.g. web, OS keychains not enabled/available, etc).
|
||||
///
|
||||
/// This minimises the risk of an attacker obtaining the master password, as both of these are required to unlock the vault (and both should be stored separately).
|
||||
///
|
||||
/// Both values need to be correct, otherwise this function will return a generic error.
|
||||
|
@ -440,28 +551,40 @@ impl KeyManager {
|
|||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn unlock<F>(
|
||||
&self,
|
||||
master_password: Protected<String>,
|
||||
secret_key: Option<Protected<String>>,
|
||||
master_password: Password,
|
||||
provided_secret_key: Option<SecretKeyString>,
|
||||
library_uuid: Uuid,
|
||||
invalidate: F,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: Fn() + Send,
|
||||
{
|
||||
let uuid = Uuid::nil();
|
||||
|
||||
if self.has_master_password().await? {
|
||||
return Err(Error::KeyAlreadyMounted);
|
||||
} else if self.is_queued(uuid) {
|
||||
return Err(Error::KeyAlreadyQueued);
|
||||
}
|
||||
|
||||
let verification_key = (*self.verification_key.lock().await)
|
||||
.as_ref()
|
||||
.map_or(Err(Error::NoVerificationKey), |k| Ok(k.clone()))?;
|
||||
|
||||
let secret_key = secret_key.map(Self::convert_secret_key_string);
|
||||
if self.is_unlocked().await? {
|
||||
return Err(Error::KeyAlreadyMounted);
|
||||
} else if self.is_queued(verification_key.uuid) {
|
||||
return Err(Error::KeyAlreadyQueued);
|
||||
}
|
||||
|
||||
self.mounting_queue.insert(uuid);
|
||||
let secret_key = if let Some(secret_key) = provided_secret_key.clone() {
|
||||
secret_key.into()
|
||||
} else {
|
||||
self.get_keyring()?
|
||||
.lock()
|
||||
.await
|
||||
.retrieve(Identifier {
|
||||
application: APP_IDENTIFIER,
|
||||
library_uuid: &library_uuid.to_string(),
|
||||
usage: SECRET_KEY_IDENTIFIER,
|
||||
})
|
||||
.map(|x| SecretKeyString::new(String::from_utf8(x.expose().clone()).unwrap()))?
|
||||
.into()
|
||||
};
|
||||
|
||||
self.mounting_queue.insert(verification_key.uuid);
|
||||
invalidate();
|
||||
|
||||
match verification_key.version {
|
||||
|
@ -471,53 +594,62 @@ impl KeyManager {
|
|||
.hash(
|
||||
Protected::new(master_password.expose().as_bytes().to_vec()),
|
||||
verification_key.content_salt,
|
||||
secret_key,
|
||||
Some(secret_key),
|
||||
)
|
||||
.map_err(|e| {
|
||||
self.remove_from_queue(uuid).ok();
|
||||
self.remove_from_queue(verification_key.uuid).ok();
|
||||
e
|
||||
})?;
|
||||
|
||||
let master_key = StreamDecryption::decrypt_bytes(
|
||||
derive_key(
|
||||
Key::derive(
|
||||
hashed_password,
|
||||
verification_key.salt,
|
||||
MASTER_PASSWORD_CONTEXT,
|
||||
),
|
||||
&verification_key.master_key_nonce,
|
||||
verification_key.master_key_nonce,
|
||||
verification_key.algorithm,
|
||||
&verification_key.master_key,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
self.remove_from_queue(uuid).ok();
|
||||
self.remove_from_queue(verification_key.uuid).ok();
|
||||
Error::IncorrectKeymanagerDetails
|
||||
})?;
|
||||
|
||||
*self.root_key.lock().await = Some(Protected::new(
|
||||
to_array(
|
||||
*self.root_key.lock().await = Some(
|
||||
Key::try_from(
|
||||
StreamDecryption::decrypt_bytes(
|
||||
Protected::new(to_array(master_key.into_inner())?),
|
||||
&verification_key.key_nonce,
|
||||
Key::try_from(master_key)?,
|
||||
verification_key.key_nonce,
|
||||
verification_key.algorithm,
|
||||
&verification_key.key,
|
||||
&[],
|
||||
)
|
||||
.await?
|
||||
.expose()
|
||||
.clone(),
|
||||
.await?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
self.remove_from_queue(uuid).ok();
|
||||
self.remove_from_queue(verification_key.uuid).ok();
|
||||
e
|
||||
})?,
|
||||
));
|
||||
);
|
||||
|
||||
self.remove_from_queue(uuid)?;
|
||||
self.remove_from_queue(verification_key.uuid)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(secret_key) = provided_secret_key {
|
||||
// converting twice ensures it's formatted correctly
|
||||
self.keyring_insert(
|
||||
library_uuid,
|
||||
SECRET_KEY_IDENTIFIER.to_string(),
|
||||
SecretKeyString::from(SecretKey::from(secret_key)),
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
invalidate();
|
||||
|
||||
Ok(())
|
||||
|
@ -543,12 +675,12 @@ impl KeyManager {
|
|||
self.mounting_queue.insert(uuid);
|
||||
|
||||
let master_key = StreamDecryption::decrypt_bytes(
|
||||
derive_key(
|
||||
Key::derive(
|
||||
self.get_root_key().await?,
|
||||
stored_key.salt,
|
||||
ROOT_KEY_CONTEXT,
|
||||
),
|
||||
&stored_key.master_key_nonce,
|
||||
stored_key.master_key_nonce,
|
||||
stored_key.algorithm,
|
||||
&stored_key.master_key,
|
||||
&[],
|
||||
|
@ -559,12 +691,12 @@ impl KeyManager {
|
|||
self.remove_from_queue(uuid).ok();
|
||||
Err(Error::IncorrectPassword)
|
||||
},
|
||||
|v| Ok(Protected::new(to_array(v.into_inner())?)),
|
||||
Key::try_from,
|
||||
)?;
|
||||
// Decrypt the StoredKey using the decrypted master key
|
||||
let key = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
&stored_key.key_nonce,
|
||||
stored_key.key_nonce,
|
||||
stored_key.algorithm,
|
||||
&stored_key.key,
|
||||
&[],
|
||||
|
@ -605,35 +737,33 @@ impl KeyManager {
|
|||
/// This function is used for getting the key value itself, from a given UUID.
|
||||
///
|
||||
/// The master password/salt needs to be present, so we are able to decrypt the key itself from the stored key.
|
||||
pub async fn get_key(&self, uuid: Uuid) -> Result<ProtectedVec<u8>> {
|
||||
pub async fn get_key(&self, uuid: Uuid) -> Result<Password> {
|
||||
if let Some(stored_key) = self.keystore.get(&uuid) {
|
||||
let master_key = StreamDecryption::decrypt_bytes(
|
||||
derive_key(
|
||||
Key::derive(
|
||||
self.get_root_key().await?,
|
||||
stored_key.salt,
|
||||
ROOT_KEY_CONTEXT,
|
||||
),
|
||||
&stored_key.master_key_nonce,
|
||||
stored_key.master_key_nonce,
|
||||
stored_key.algorithm,
|
||||
&stored_key.master_key,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_or(Err(Error::IncorrectPassword), |k| {
|
||||
Ok(Protected::new(to_array(k.into_inner())?))
|
||||
})?;
|
||||
.map_or(Err(Error::IncorrectPassword), Key::try_from)?;
|
||||
|
||||
// Decrypt the StoredKey using the decrypted master key
|
||||
let key = StreamDecryption::decrypt_bytes(
|
||||
master_key,
|
||||
&stored_key.key_nonce,
|
||||
stored_key.key_nonce,
|
||||
stored_key.algorithm,
|
||||
&stored_key.key,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(key)
|
||||
Ok(Password::new(String::from_utf8(key.expose().clone())?))
|
||||
} else {
|
||||
Err(Error::KeyNotFound)
|
||||
}
|
||||
|
@ -653,30 +783,30 @@ impl KeyManager {
|
|||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub async fn add_to_keystore(
|
||||
&self,
|
||||
key: ProtectedVec<u8>,
|
||||
key: Password,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
memory_only: bool,
|
||||
automount: bool,
|
||||
content_salt: Option<Salt>,
|
||||
) -> Result<Uuid> {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
// Generate items we'll need for encryption
|
||||
let key_nonce = generate_nonce(algorithm);
|
||||
let master_key = generate_master_key();
|
||||
let master_key_nonce = generate_nonce(algorithm);
|
||||
let key_nonce = Nonce::generate(algorithm)?;
|
||||
let master_key = Key::generate();
|
||||
let master_key_nonce = Nonce::generate(algorithm)?;
|
||||
|
||||
let content_salt = content_salt.map_or_else(generate_salt, |v| v);
|
||||
let content_salt = content_salt.map_or_else(Salt::generate, |v| v);
|
||||
|
||||
// salt used for the kdf
|
||||
let salt = generate_salt();
|
||||
let salt = Salt::generate();
|
||||
|
||||
// Encrypt the master key with a derived key (derived from the root key)
|
||||
let encrypted_master_key = to_array::<ENCRYPTED_KEY_LEN>(
|
||||
let encrypted_master_key = EncryptedKey::try_from(
|
||||
StreamEncryption::encrypt_bytes(
|
||||
derive_key(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT),
|
||||
&master_key_nonce,
|
||||
Key::derive(self.get_root_key().await?, salt, ROOT_KEY_CONTEXT),
|
||||
master_key_nonce,
|
||||
algorithm,
|
||||
master_key.expose(),
|
||||
&[],
|
||||
|
@ -685,8 +815,14 @@ impl KeyManager {
|
|||
)?;
|
||||
|
||||
// Encrypt the actual key (e.g. user-added/autogenerated, text-encodable)
|
||||
let encrypted_key =
|
||||
StreamEncryption::encrypt_bytes(master_key, &key_nonce, algorithm, &key, &[]).await?;
|
||||
let encrypted_key = StreamEncryption::encrypt_bytes(
|
||||
master_key,
|
||||
key_nonce,
|
||||
algorithm,
|
||||
key.expose().as_bytes(),
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Insert it into the Keystore
|
||||
self.keystore.insert(
|
||||
|
@ -694,6 +830,7 @@ impl KeyManager {
|
|||
StoredKey {
|
||||
uuid,
|
||||
version: LATEST_STORED_KEY,
|
||||
key_type: StoredKeyType::User,
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
content_salt,
|
||||
|
@ -711,11 +848,6 @@ impl KeyManager {
|
|||
Ok(uuid)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn convert_secret_key_string(secret_key: Protected<String>) -> ProtectedVec<u8> {
|
||||
Protected::new(secret_key.expose().as_bytes().to_vec())
|
||||
}
|
||||
|
||||
/// This function is for accessing the internal keymount.
|
||||
///
|
||||
/// We could add a log to this, so that the user can view accesses
|
||||
|
@ -748,7 +880,7 @@ impl KeyManager {
|
|||
}
|
||||
|
||||
/// This should ONLY be used internally.
|
||||
async fn get_root_key(&self) -> Result<Protected<Key>> {
|
||||
async fn get_root_key(&self) -> Result<Key> {
|
||||
self.root_key
|
||||
.lock()
|
||||
.await
|
||||
|
@ -791,11 +923,11 @@ impl KeyManager {
|
|||
///
|
||||
/// This means we don't need to keep super specific track of which key goes to which file, and we can just throw all of them at it.
|
||||
#[must_use]
|
||||
pub fn enumerate_hashed_keys(&self) -> Vec<Protected<Key>> {
|
||||
pub fn enumerate_hashed_keys(&self) -> Vec<Key> {
|
||||
self.keymount
|
||||
.iter()
|
||||
.map(|mounted_key| mounted_key.hashed_key.clone())
|
||||
.collect::<Vec<Protected<Key>>>()
|
||||
.collect::<Vec<Key>>()
|
||||
}
|
||||
|
||||
/// This function is for converting a memory-only key to a saved key which syncs to the library.
|
||||
|
@ -828,10 +960,8 @@ impl KeyManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is used for seeing if the key manager has a master password.
|
||||
///
|
||||
/// Technically this checks for the root key, but it makes no difference to the front end.
|
||||
pub async fn has_master_password(&self) -> Result<bool> {
|
||||
/// This function is used for checking if the key manager is unlocked.
|
||||
pub async fn is_unlocked(&self) -> Result<bool> {
|
||||
Ok(self.root_key.lock().await.is_some())
|
||||
}
|
||||
|
||||
|
@ -873,6 +1003,12 @@ impl KeyManager {
|
|||
self.mounting_queue.contains(&uuid)
|
||||
}
|
||||
|
||||
pub async fn is_unlocking(&self) -> Result<bool> {
|
||||
Ok(self
|
||||
.mounting_queue
|
||||
.contains(&self.get_verification_key().await?.uuid))
|
||||
}
|
||||
|
||||
pub fn remove_from_queue(&self, uuid: Uuid) -> Result<()> {
|
||||
self.mounting_queue
|
||||
.remove(&uuid)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//! This is Spacedrive's Apple OS keychain integration. It has no strict dependencies.
|
||||
//! This is Spacedrive's Apple OS keyring integration. It has no strict dependencies.
|
||||
//!
|
||||
//! This has been tested on MacOS, but should work just the same for iOS (according to the `security_framework` documentation)
|
||||
|
||||
use super::{Identifier, Keyring};
|
||||
use crate::{Error, Protected, Result};
|
||||
use crate::{primitives::types::SecretKeyString, Error, Protected, Result};
|
||||
use security_framework::passwords::{
|
||||
delete_generic_password, get_generic_password, set_generic_password,
|
||||
};
|
||||
|
@ -11,7 +11,7 @@ use security_framework::passwords::{
|
|||
pub struct AppleKeyring;
|
||||
|
||||
impl Keyring for AppleKeyring {
|
||||
fn insert(&self, identifier: Identifier, value: Protected<String>) -> Result<()> {
|
||||
fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()> {
|
||||
set_generic_password(
|
||||
&identifier.application,
|
||||
&identifier.to_apple_account(),
|
|
@ -1,11 +1,12 @@
|
|||
//! This is Spacedrive's Linux keychain implementation, which makes use of the Secret Service API.
|
||||
//! This is Spacedrive's Linux keyring implementation, which makes use of the Secret Service API.
|
||||
//!
|
||||
//! This does strictly require `DBus`, and either `gnome-keyring`, `kwallet` or another implementor of the Secret Service API.
|
||||
|
||||
use secret_service::{Collection, EncryptionType, SecretService};
|
||||
|
||||
use crate::{
|
||||
keys::keychain::{Identifier, Keyring},
|
||||
keys::keyring::{Identifier, Keyring},
|
||||
primitives::types::SecretKeyString,
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
|
@ -33,7 +34,7 @@ impl<'a> LinuxKeyring<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Keyring for LinuxKeyring<'a> {
|
||||
fn insert(&self, identifier: Identifier, value: Protected<String>) -> Result<()> {
|
||||
fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()> {
|
||||
self.get_collection()?.create_item(
|
||||
&identifier.generate_linux_label(),
|
||||
identifier.to_hashmap(),
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Protected, Result};
|
||||
use crate::{primitives::types::SecretKeyString, Protected, Result};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
@ -40,7 +40,7 @@ impl<'a> Identifier<'a> {
|
|||
}
|
||||
|
||||
pub trait Keyring {
|
||||
fn insert(&self, identifier: Identifier, value: Protected<String>) -> Result<()>;
|
||||
fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()>;
|
||||
fn retrieve(&self, identifier: Identifier) -> Result<Protected<Vec<u8>>>;
|
||||
fn delete(&self, identifier: Identifier) -> Result<()>;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ impl KeyringInterface {
|
|||
Ok(Self { keyring })
|
||||
}
|
||||
|
||||
pub fn insert(&self, identifier: Identifier, value: Protected<String>) -> Result<()> {
|
||||
pub fn insert(&self, identifier: Identifier, value: SecretKeyString) -> Result<()> {
|
||||
self.keyring.insert(identifier, value)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//! This module contains all key and hashing related functions.
|
||||
|
||||
pub mod hashing;
|
||||
pub mod keychain;
|
||||
pub mod keymanager;
|
||||
pub mod keyring;
|
||||
|
|
|
@ -24,7 +24,6 @@ pub use aead::Payload;
|
|||
|
||||
// Make this easier to use (e.g. `sd_crypto::Protected`)
|
||||
pub use protected::Protected;
|
||||
pub use protected::ProtectedVec;
|
||||
|
||||
// Re-export zeroize so it can be used elsewhere
|
||||
pub use zeroize::Zeroize;
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
//! This module contains constant values and functions that are used around the crate.
|
||||
//!
|
||||
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
|
||||
//! lengths for master keys and even the streaming block size.
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
crypto::stream::Algorithm,
|
||||
header::{
|
||||
file::FileHeaderVersion, keyslot::KeyslotVersion, metadata::MetadataVersion,
|
||||
preview_media::PreviewMediaVersion,
|
||||
},
|
||||
keys::{hashing::HashingAlgorithm, keymanager::StoredKeyVersion},
|
||||
Error, Protected, Result,
|
||||
};
|
||||
|
||||
/// This is the default salt size, and the recommended size for argon2id.
|
||||
pub const SALT_LEN: usize = 16;
|
||||
|
||||
/// The size used for streaming encryption/decryption. This size seems to offer the best performance compared to alternatives.
|
||||
///
|
||||
/// The file size gain is 16 bytes per 1048576 bytes (due to the AEAD tag). Plus the size of the header.
|
||||
pub const BLOCK_SIZE: usize = 1_048_576;
|
||||
|
||||
pub const AEAD_TAG_SIZE: usize = 16;
|
||||
|
||||
/// The length of the encrypted master key
|
||||
pub const ENCRYPTED_KEY_LEN: usize = 48;
|
||||
|
||||
/// The length of the (unencrypted) master key
|
||||
pub const KEY_LEN: usize = 32;
|
||||
|
||||
pub const PASSPHRASE_LEN: usize = 7;
|
||||
|
||||
pub const LATEST_FILE_HEADER: FileHeaderVersion = FileHeaderVersion::V1;
|
||||
pub const LATEST_KEYSLOT: KeyslotVersion = KeyslotVersion::V1;
|
||||
pub const LATEST_METADATA: MetadataVersion = MetadataVersion::V1;
|
||||
pub const LATEST_PREVIEW_MEDIA: PreviewMediaVersion = PreviewMediaVersion::V1;
|
||||
pub const LATEST_STORED_KEY: StoredKeyVersion = StoredKeyVersion::V1;
|
||||
|
||||
pub const ROOT_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:53:54 root key derivation"; // used for deriving keys from the root key
|
||||
pub const MASTER_PASSWORD_CONTEXT: &str =
|
||||
"spacedrive 2022-12-14 15:35:41 master password hash derivation"; // used for deriving keys from the master password hash
|
||||
pub const FILE_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:54:12 file key derivation"; // used for deriving keys from user key/content salt hashes (for file encryption)
|
||||
|
||||
pub type Key = [u8; KEY_LEN];
|
||||
pub type EncryptedKey = [u8; ENCRYPTED_KEY_LEN];
|
||||
pub type Salt = [u8; SALT_LEN];
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub struct OnboardingConfig {
|
||||
pub password: Protected<String>,
|
||||
pub secret_key: Option<Protected<String>>,
|
||||
pub algorithm: Algorithm,
|
||||
pub hashing_algorithm: HashingAlgorithm,
|
||||
}
|
||||
|
||||
/// This should be used for generating nonces for encryption.
|
||||
///
|
||||
/// An algorithm is required so this function can calculate the length of the nonce.
|
||||
///
|
||||
/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data
|
||||
#[must_use]
|
||||
pub fn generate_nonce(algorithm: Algorithm) -> Vec<u8> {
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len()];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce);
|
||||
nonce
|
||||
}
|
||||
|
||||
/// This should be used for generating salts for hashing.
|
||||
///
|
||||
/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data
|
||||
#[must_use]
|
||||
pub fn generate_salt() -> Salt {
|
||||
let mut salt = [0u8; SALT_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut salt);
|
||||
salt
|
||||
}
|
||||
|
||||
/// This generates a master key, which should be used for encrypting the data
|
||||
///
|
||||
/// This is then stored (encrypted) within the header.
|
||||
///
|
||||
/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data
|
||||
#[must_use]
|
||||
pub fn generate_master_key() -> Protected<Key> {
|
||||
let mut master_key = [0u8; KEY_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut master_key);
|
||||
Protected::new(master_key)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn derive_key(key: Protected<Key>, salt: Salt, context: &str) -> Protected<Key> {
|
||||
let mut input = key.expose().to_vec();
|
||||
input.extend_from_slice(&salt);
|
||||
|
||||
let key = blake3::derive_key(context, &input);
|
||||
|
||||
input.zeroize();
|
||||
|
||||
Protected::new(key)
|
||||
}
|
||||
|
||||
/// This is used for converting a `Vec<u8>` to an array of bytes
|
||||
///
|
||||
/// It's main usage is for converting an encrypted master key from a `Vec<u8>` to `EncryptedKey`
|
||||
///
|
||||
/// As the master key is encrypted at this point, it does not need to be `Protected<>`
|
||||
///
|
||||
/// This function still `zeroize`s any data it can
|
||||
pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I]> {
|
||||
bytes.try_into().map_err(|mut b: Vec<u8>| {
|
||||
b.zeroize();
|
||||
Error::VecArrSizeMismatch
|
||||
})
|
||||
}
|
||||
|
||||
// /// This generates a 7 word diceware passphrase, separated with `-`
|
||||
// #[must_use]
|
||||
// pub fn generate_passphrase() -> Protected<String> {
|
||||
// let wordlist = include_str!("../assets/eff_large_wordlist.txt")
|
||||
// .lines()
|
||||
// .collect::<Vec<&str>>();
|
||||
|
||||
// let words: Vec<String> = wordlist
|
||||
// .choose_multiple(
|
||||
// &mut rand_chacha::ChaCha20Rng::from_entropy(),
|
||||
// PASSPHRASE_LEN,
|
||||
// )
|
||||
// .map(ToString::to_string)
|
||||
// .collect();
|
||||
|
||||
// let passphrase = words
|
||||
// .iter()
|
||||
// .enumerate()
|
||||
// .map(|(i, word)| {
|
||||
// if i < PASSPHRASE_LEN - 1 {
|
||||
// word.clone() + "-"
|
||||
// } else {
|
||||
// word.clone()
|
||||
// }
|
||||
// })
|
||||
// .into_iter()
|
||||
// .collect();
|
||||
|
||||
// Protected::new(passphrase)
|
||||
// }
|
64
crates/crypto/src/primitives/mod.rs
Normal file
64
crates/crypto/src/primitives/mod.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
//! This module contains constant values and functions that are used around the crate.
|
||||
//!
|
||||
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
|
||||
//! lengths for master keys and even the streaming block size.
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
header::{
|
||||
file::FileHeaderVersion, keyslot::KeyslotVersion, metadata::MetadataVersion,
|
||||
preview_media::PreviewMediaVersion,
|
||||
},
|
||||
keys::keymanager::StoredKeyVersion,
|
||||
Error, Result,
|
||||
};
|
||||
|
||||
pub mod types;
|
||||
|
||||
/// This is the default salt size, and the recommended size for argon2id.
|
||||
pub const SALT_LEN: usize = 16;
|
||||
|
||||
pub const SECRET_KEY_LEN: usize = 18;
|
||||
|
||||
/// The size used for streaming encryption/decryption. This size seems to offer the best performance compared to alternatives.
|
||||
///
|
||||
/// The file size gain is 16 bytes per 1048576 bytes (due to the AEAD tag). Plus the size of the header.
|
||||
pub const BLOCK_SIZE: usize = 1_048_576;
|
||||
|
||||
pub const AEAD_TAG_SIZE: usize = 16;
|
||||
|
||||
/// The length of the encrypted master key
|
||||
pub const ENCRYPTED_KEY_LEN: usize = 48;
|
||||
|
||||
/// The length of the (unencrypted) master key
|
||||
pub const KEY_LEN: usize = 32;
|
||||
|
||||
pub const PASSPHRASE_LEN: usize = 7;
|
||||
|
||||
pub const APP_IDENTIFIER: &str = "Spacedrive";
|
||||
pub const SECRET_KEY_IDENTIFIER: &str = "Secret key";
|
||||
|
||||
pub const LATEST_FILE_HEADER: FileHeaderVersion = FileHeaderVersion::V1;
|
||||
pub const LATEST_KEYSLOT: KeyslotVersion = KeyslotVersion::V1;
|
||||
pub const LATEST_METADATA: MetadataVersion = MetadataVersion::V1;
|
||||
pub const LATEST_PREVIEW_MEDIA: PreviewMediaVersion = PreviewMediaVersion::V1;
|
||||
pub const LATEST_STORED_KEY: StoredKeyVersion = StoredKeyVersion::V1;
|
||||
|
||||
pub const ROOT_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:53:54 root key derivation"; // used for deriving keys from the root key
|
||||
pub const MASTER_PASSWORD_CONTEXT: &str =
|
||||
"spacedrive 2022-12-14 15:35:41 master password hash derivation"; // used for deriving keys from the master password hash
|
||||
pub const FILE_KEY_CONTEXT: &str = "spacedrive 2022-12-14 12:54:12 file key derivation"; // used for deriving keys from user key/content salt hashes (for file encryption)
|
||||
|
||||
/// This is used for converting a `Vec<u8>` to an array of bytes
|
||||
///
|
||||
/// It's main usage is for converting an encrypted master key from a `Vec<u8>` to `EncryptedKey`
|
||||
///
|
||||
/// As the master key is encrypted at this point, it does not need to be `Protected<>`
|
||||
///
|
||||
/// This function still `zeroize`s any data it can
|
||||
pub fn to_array<const I: usize>(bytes: &[u8]) -> Result<[u8; I]> {
|
||||
bytes.to_vec().try_into().map_err(|mut b: Vec<u8>| {
|
||||
b.zeroize();
|
||||
Error::VecArrSizeMismatch
|
||||
})
|
||||
}
|
282
crates/crypto/src/primitives/types.rs
Normal file
282
crates/crypto/src/primitives/types.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
use rand::{RngCore, SeedableRng};
|
||||
use std::ops::Deref;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{crypto::stream::Algorithm, keys::hashing::HashingAlgorithm, Error, Protected};
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub enum Nonce {
|
||||
XChaCha20Poly1305([u8; 20]),
|
||||
Aes256Gcm([u8; 8]),
|
||||
}
|
||||
|
||||
impl Nonce {
|
||||
pub fn generate(algorithm: Algorithm) -> crate::Result<Self> {
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len()];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce);
|
||||
Self::try_from(nonce)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::Aes256Gcm(_) => 8,
|
||||
Self::XChaCha20Poly1305(_) => 20,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Aes256Gcm(x) => x.is_empty(),
|
||||
Self::XChaCha20Poly1305(x) => x.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Nonce {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
match value.len() {
|
||||
8 => Ok(Self::Aes256Gcm(to_array(&value)?)),
|
||||
20 => Ok(Self::XChaCha20Poly1305(to_array(&value)?)),
|
||||
_ => Err(Error::VecArrSizeMismatch),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Nonce {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Aes256Gcm(x) => x,
|
||||
Self::XChaCha20Poly1305(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Nonce {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Aes256Gcm(x) => x,
|
||||
Self::XChaCha20Poly1305(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Key(pub Protected<[u8; KEY_LEN]>);
|
||||
|
||||
impl Key {
|
||||
#[must_use]
|
||||
pub const fn new(v: [u8; KEY_LEN]) -> Self {
|
||||
Self(Protected::new(v))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn derive(key: Self, salt: Salt, context: &str) -> Self {
|
||||
let mut input = key.expose().to_vec();
|
||||
input.extend_from_slice(&salt);
|
||||
let key = blake3::derive_key(context, &input);
|
||||
|
||||
input.zeroize();
|
||||
|
||||
Self::new(key)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn expose(&self) -> &[u8; KEY_LEN] {
|
||||
self.0.expose()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate() -> Self {
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut key);
|
||||
Self::new(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Protected<Vec<u8>>> for Key {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Protected<Vec<u8>>) -> Result<Self, Self::Error> {
|
||||
Ok(Self::new(to_array(value.expose())?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Key {
|
||||
type Target = Protected<[u8; KEY_LEN]>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SecretKey(pub Protected<[u8; SECRET_KEY_LEN]>);
|
||||
|
||||
impl SecretKey {
|
||||
#[must_use]
|
||||
pub const fn new(v: [u8; SECRET_KEY_LEN]) -> Self {
|
||||
Self(Protected::new(v))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn expose(&self) -> &[u8; SECRET_KEY_LEN] {
|
||||
self.0.expose()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate() -> Self {
|
||||
let mut secret_key = [0u8; SECRET_KEY_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut secret_key);
|
||||
Self::new(secret_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SecretKey {
|
||||
type Target = Protected<[u8; SECRET_KEY_LEN]>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKey> for SecretKeyString {
|
||||
fn from(v: SecretKey) -> Self {
|
||||
let hex_string: String = hex::encode_upper(v.0.expose())
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(i, c)| {
|
||||
if (i + 1) % 6 == 0 && i != 35 {
|
||||
c.to_string() + "-"
|
||||
} else {
|
||||
c.to_string()
|
||||
}
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
Self::new(hex_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SecretKeyString> for SecretKey {
|
||||
fn from(v: SecretKeyString) -> Self {
|
||||
let mut secret_key_sanitized = v.expose().clone();
|
||||
secret_key_sanitized.retain(|c| c != '-' && !c.is_whitespace());
|
||||
|
||||
// we shouldn't be letting on to *what* failed so we use a random secret key here if it's still invalid
|
||||
// could maybe do this better (and make use of the subtle crate)
|
||||
|
||||
let secret_key = hex::decode(secret_key_sanitized)
|
||||
.ok()
|
||||
.map_or(Vec::new(), |v| v);
|
||||
|
||||
to_array(&secret_key)
|
||||
.ok()
|
||||
.map_or_else(Self::generate, Self::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Password(pub Protected<String>);
|
||||
|
||||
impl Password {
|
||||
#[must_use]
|
||||
pub const fn new(v: String) -> Self {
|
||||
Self(Protected::new(v))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn expose(&self) -> &String {
|
||||
self.0.expose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SecretKeyString(pub Protected<String>);
|
||||
|
||||
impl SecretKeyString {
|
||||
#[must_use]
|
||||
pub const fn new(v: String) -> Self {
|
||||
Self(Protected::new(v))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn expose(&self) -> &String {
|
||||
self.0.expose()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde_big_array::BigArray;
|
||||
|
||||
use super::{to_array, ENCRYPTED_KEY_LEN, KEY_LEN, SALT_LEN, SECRET_KEY_LEN};
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub struct EncryptedKey(
|
||||
#[cfg_attr(feature = "serde", serde(with = "BigArray"))] // salt used for file data
|
||||
pub [u8; ENCRYPTED_KEY_LEN],
|
||||
);
|
||||
|
||||
impl Deref for EncryptedKey {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for EncryptedKey {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Ok(Self(to_array(&value)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub struct Salt(pub [u8; SALT_LEN]);
|
||||
|
||||
impl Salt {
|
||||
#[must_use]
|
||||
pub fn generate() -> Self {
|
||||
let mut salt = [0u8; SALT_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut salt);
|
||||
Self(salt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Salt {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Salt {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Ok(Self(to_array(&value)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub struct OnboardingConfig {
|
||||
pub password: Protected<String>,
|
||||
pub algorithm: Algorithm,
|
||||
pub hashing_algorithm: HashingAlgorithm,
|
||||
}
|
|
@ -30,8 +30,6 @@
|
|||
//!
|
||||
use std::{fmt::Debug, mem::swap};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub type ProtectedVec<T> = Protected<Vec<T>>;
|
||||
#[derive(Clone)]
|
||||
pub struct Protected<T>
|
||||
where
|
||||
|
@ -40,17 +38,6 @@ where
|
|||
data: T,
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
|
|
|
@ -10,8 +10,9 @@ export type Procedures = {
|
|||
{ key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "keys.getDefault", input: LibraryArgs<null>, result: string | null } |
|
||||
{ key: "keys.getKey", input: LibraryArgs<string>, result: string } |
|
||||
{ key: "keys.hasMasterPassword", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "keys.isKeyManagerUnlocking", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "keys.getSecretKey", input: LibraryArgs<null>, result: string | null } |
|
||||
{ key: "keys.isKeyManagerUnlocking", input: LibraryArgs<null>, result: boolean | null } |
|
||||
{ key: "keys.isUnlocked", input: LibraryArgs<null>, result: boolean } |
|
||||
{ key: "keys.list", input: LibraryArgs<null>, result: Array<StoredKey> } |
|
||||
{ key: "keys.listMounted", input: LibraryArgs<null>, result: Array<string> } |
|
||||
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
|
||||
|
@ -89,10 +90,12 @@ export interface BuildInfo { version: string, commit: string }
|
|||
|
||||
export interface ConfigMetadata { version: string | null }
|
||||
|
||||
export interface CreateLibraryArgs { name: string, password: string, secret_key: string | null, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
|
||||
export interface CreateLibraryArgs { name: string, password: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
|
||||
|
||||
export interface EditLibraryArgs { id: string, name: string | null, description: string | null }
|
||||
|
||||
export type EncryptedKey = Array<number>
|
||||
|
||||
export type ExplorerContext = { type: "Location" } & Location | { type: "Tag" } & Tag
|
||||
|
||||
export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerItem> }
|
||||
|
@ -147,7 +150,7 @@ export interface LocationExplorerArgs { location_id: number, path: string, limit
|
|||
|
||||
export interface LocationUpdateArgs { id: number, name: string | null, indexer_rules_ids: Array<number> }
|
||||
|
||||
export interface MasterPasswordChangeArgs { password: string, secret_key: string | null, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
|
||||
export interface MasterPasswordChangeArgs { password: string, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm }
|
||||
|
||||
export interface MediaData { id: number, pixel_width: number | null, pixel_height: number | null, longitude: number | null, latitude: number | null, fps: number | null, capture_device_make: string | null, capture_device_model: string | null, capture_device_software: string | null, duration_seconds: number | null, codecs: string | null, streams: number | null }
|
||||
|
||||
|
@ -157,6 +160,8 @@ export interface NodeConfig { version: string | null, id: string, name: string,
|
|||
|
||||
export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string }
|
||||
|
||||
export type Nonce = { XChaCha20Poly1305: Array<number> } | { Aes256Gcm: Array<number> }
|
||||
|
||||
export interface NormalisedCompositeId { $type: string, $id: any, org_id: string, user_id: string }
|
||||
|
||||
export interface NormalisedOrganisation { $type: string, $id: any, id: string, name: string, users: NormalizedVec<NormalisedUser>, owner: NormalisedUser, non_normalised_data: Array<null> }
|
||||
|
@ -171,17 +176,21 @@ export interface ObjectValidatorArgs { id: number, path: string }
|
|||
|
||||
export type Params = "Standard" | "Hardened" | "Paranoid"
|
||||
|
||||
export interface RestoreBackupArgs { password: string, secret_key: string | null, path: string }
|
||||
export interface RestoreBackupArgs { password: string, secret_key: string, path: string }
|
||||
|
||||
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
|
||||
|
||||
export type Salt = Array<number>
|
||||
|
||||
export interface SetFavoriteArgs { id: number, favorite: boolean }
|
||||
|
||||
export interface SetNoteArgs { id: number, note: string | null }
|
||||
|
||||
export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
|
||||
|
||||
export interface StoredKey { uuid: string, version: StoredKeyVersion, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Array<number>, master_key: Array<number>, master_key_nonce: Array<number>, key_nonce: Array<number>, key: Array<number>, salt: Array<number>, memory_only: boolean, automount: boolean }
|
||||
export interface StoredKey { uuid: string, version: StoredKeyVersion, key_type: StoredKeyType, algorithm: Algorithm, hashing_algorithm: HashingAlgorithm, content_salt: Salt, master_key: EncryptedKey, master_key_nonce: Nonce, key_nonce: Nonce, key: Array<number>, salt: Salt, memory_only: boolean, automount: boolean }
|
||||
|
||||
export type StoredKeyType = "User" | "Root"
|
||||
|
||||
export type StoredKeyVersion = "V1"
|
||||
|
||||
|
@ -193,7 +202,7 @@ export interface TagCreateArgs { name: string, color: string }
|
|||
|
||||
export interface TagUpdateArgs { id: number, name: string | null, color: string | null }
|
||||
|
||||
export interface UnlockKeyManagerArgs { password: string, secret_key: string | null }
|
||||
export interface UnlockKeyManagerArgs { password: string, secret_key: string }
|
||||
|
||||
export interface Volume { name: string, mount_point: string, total_capacity: bigint, available_capacity: bigint, is_removable: boolean, disk_type: string | null, file_system: string | null, is_root_filesystem: boolean }
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"react-hook-form": "^7.36.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"react-router": "6.4.2",
|
||||
"react-router-dom": "6.4.2",
|
||||
"rooks": "^5.14.0",
|
||||
|
|
|
@ -49,12 +49,10 @@ export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => {
|
|||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((data) => {
|
||||
const sk = data.secretKey || null;
|
||||
|
||||
if (data.filePath !== '') {
|
||||
restoreKeystoreMutation.mutate({
|
||||
password: data.masterPassword,
|
||||
secret_key: sk,
|
||||
secret_key: data.secretKey,
|
||||
path: data.filePath
|
||||
});
|
||||
form.reset();
|
||||
|
|
|
@ -15,7 +15,6 @@ const schema = z.object({
|
|||
name: z.string(),
|
||||
password: z.string(),
|
||||
password_validate: z.string(),
|
||||
secret_key: z.string(),
|
||||
algorithm: z.string(),
|
||||
hashing_algorithm: z.string()
|
||||
});
|
||||
|
@ -36,10 +35,8 @@ export default function CreateLibraryDialog(props: Props) {
|
|||
|
||||
const [showMasterPassword1, setShowMasterPassword1] = useState(false);
|
||||
const [showMasterPassword2, setShowMasterPassword2] = useState(false);
|
||||
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||
const MP1CurrentEyeIcon = showMasterPassword1 ? EyeSlash : Eye;
|
||||
const MP2CurrentEyeIcon = showMasterPassword2 ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const createLibrary = useBridgeMutation('library.create', {
|
||||
|
@ -151,43 +148,6 @@ export default function CreateLibraryDialog(props: Props) {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className="text-sm mt-2 mb-2 font-bold">Key secret (optional)</p>
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret"
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
{...form.register('secret_key', { required: true })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.setValue('secret_key', cryptoRandomString({ length: 24 }));
|
||||
setShowSecretKey(true);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('secret_key') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
<div className="flex flex-col">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react';
|
||||
import { useState } from 'react';
|
||||
import { Algorithm, useLibraryMutation } from '@sd/client';
|
||||
|
@ -14,7 +13,6 @@ export type MasterPasswordChangeDialogProps = UseDialogProps;
|
|||
const schema = z.object({
|
||||
masterPassword: z.string(),
|
||||
masterPassword2: z.string(),
|
||||
secretKey: z.string().nullable(),
|
||||
encryptionAlgo: z.string(),
|
||||
hashingAlgo: z.string()
|
||||
});
|
||||
|
@ -38,15 +36,13 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
|||
|
||||
const [show, setShow] = useState({
|
||||
masterPassword: false,
|
||||
masterPassword2: false,
|
||||
secretKey: false
|
||||
masterPassword2: false
|
||||
});
|
||||
|
||||
const dialog = useDialog(props);
|
||||
|
||||
const MP1CurrentEyeIcon = show.masterPassword ? EyeSlash : Eye;
|
||||
const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = show.secretKey ? EyeSlash : Eye;
|
||||
|
||||
const form = useZodForm({
|
||||
schema,
|
||||
|
@ -69,8 +65,7 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
|||
return changeMasterPassword.mutateAsync({
|
||||
algorithm: data.encryptionAlgo as Algorithm,
|
||||
hashing_algorithm,
|
||||
password: data.masterPassword,
|
||||
secret_key: data.secretKey || null
|
||||
password: data.masterPassword
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -81,7 +76,7 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
|||
onSubmit={onSubmit}
|
||||
dialog={dialog}
|
||||
title="Change Master Password"
|
||||
description="Select a new master password for your key manager. Leave the key secret blank to disable it."
|
||||
description="Select a new master password for your key manager."
|
||||
ctaDanger={true}
|
||||
ctaLabel="Change"
|
||||
>
|
||||
|
@ -145,44 +140,6 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
className={`flex-grow !py-0.5}`}
|
||||
placeholder="Key secret"
|
||||
type={show.secretKey ? 'text' : 'password'}
|
||||
{...form.register('secretKey', { required: false })}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.setValue('secretKey', cryptoRandomString({ length: 24 }));
|
||||
setShow((old) => ({ ...old, secretKey: true }));
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[65px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<ArrowsClockwise className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(form.watch('secretKey') as string);
|
||||
}}
|
||||
size="icon"
|
||||
className="border-none absolute right-[35px] top-[5px]"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShow((old) => ({ ...old, secretKey: !old.secretKey }))}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
type="button"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PasswordMeter password={form.watch('masterPassword')} />
|
||||
|
||||
<div className="grid w-full grid-cols-2 gap-4 mt-4 mb-3">
|
||||
|
|
|
@ -211,11 +211,9 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
|||
const params = useExplorerParams();
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
|
||||
const hasMasterPasswordQuery = useLibraryQuery(['keys.hasMasterPassword']);
|
||||
const hasMasterPassword =
|
||||
hasMasterPasswordQuery.data !== undefined && hasMasterPasswordQuery.data === true
|
||||
? true
|
||||
: false;
|
||||
const isUnlockedQuery = useLibraryQuery(['keys.isUnlocked']);
|
||||
const isUnlocked =
|
||||
isUnlockedQuery.data !== undefined && isUnlockedQuery.data === true ? true : false;
|
||||
|
||||
const mountedUuids = useLibraryQuery(['keys.listMounted']);
|
||||
const hasMountedKeys =
|
||||
|
@ -319,7 +317,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
|||
icon={LockSimple}
|
||||
keybind="⌘E"
|
||||
onClick={() => {
|
||||
if (hasMasterPassword && hasMountedKeys) {
|
||||
if (isUnlocked && hasMountedKeys) {
|
||||
dialogManager.create((dp) => (
|
||||
<EncryptFileDialog
|
||||
{...dp}
|
||||
|
@ -327,7 +325,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
|||
path_id={data.item.id}
|
||||
/>
|
||||
));
|
||||
} else if (!hasMasterPassword) {
|
||||
} else if (!isUnlocked) {
|
||||
showAlertDialog({
|
||||
title: 'Key manager locked',
|
||||
value: 'The key manager is currently locked. Please unlock it and try again.'
|
||||
|
@ -346,7 +344,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
|||
icon={LockSimpleOpen}
|
||||
keybind="⌘D"
|
||||
onClick={() => {
|
||||
if (hasMasterPassword) {
|
||||
if (isUnlocked) {
|
||||
dialogManager.create((dp) => (
|
||||
<DecryptFileDialog
|
||||
{...dp}
|
||||
|
|
|
@ -10,9 +10,10 @@ import { KeyMounter } from './KeyMounter';
|
|||
export type KeyManagerProps = DefaultProps;
|
||||
|
||||
export function KeyManager(props: KeyManagerProps) {
|
||||
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
|
||||
const isUnlocked = useLibraryQuery(['keys.isUnlocked']);
|
||||
const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' });
|
||||
const isKeyManagerUnlocking = useLibraryQuery(['keys.isKeyManagerUnlocking']);
|
||||
const setMasterPasswordMutation = useLibraryMutation('keys.unlockKeyManager', {
|
||||
const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', {
|
||||
onError: () => {
|
||||
showAlertDialog({
|
||||
title: 'Unlock Error',
|
||||
|
@ -29,7 +30,9 @@ export function KeyManager(props: KeyManagerProps) {
|
|||
const [masterPassword, setMasterPassword] = useState('');
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
|
||||
if (!hasMasterPw?.data) {
|
||||
const [enterSkManually, setEnterSkManually] = useState(keyringSk?.data === null);
|
||||
|
||||
if (!isUnlocked?.data) {
|
||||
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
||||
|
@ -53,37 +56,54 @@ export function KeyManager(props: KeyManagerProps) {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{enterSkManually && (
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="accent"
|
||||
disabled={setMasterPasswordMutation.isLoading || isKeyManagerUnlocking.data}
|
||||
disabled={
|
||||
unlockKeyManager.isLoading || isKeyManagerUnlocking.data !== null
|
||||
? isKeyManagerUnlocking.data!
|
||||
: false
|
||||
}
|
||||
onClick={() => {
|
||||
if (masterPassword !== '') {
|
||||
const sk = secretKey || null;
|
||||
setMasterPassword('');
|
||||
setSecretKey('');
|
||||
setMasterPasswordMutation.mutate({ password: masterPassword, secret_key: sk });
|
||||
unlockKeyManager.mutate({ password: masterPassword, secret_key: secretKey });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
{!enterSkManually && (
|
||||
<div className="relative flex flex-grow">
|
||||
<p
|
||||
className="text-accent mt-2"
|
||||
onClick={(e) => {
|
||||
setEnterSkManually(true);
|
||||
}}
|
||||
>
|
||||
or enter secret key manually
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|||
import clsx from 'clsx';
|
||||
import { Eye, EyeSlash, Lock, Plus } from 'phosphor-react';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { animated, useTransition } from 'react-spring';
|
||||
import { HashingAlgorithm, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
import { Button, Input, dialogManager } from '@sd/ui';
|
||||
|
@ -10,6 +11,7 @@ import { KeyViewerDialog } from '~/components/dialog/KeyViewerDialog';
|
|||
import { MasterPasswordChangeDialog } from '~/components/dialog/MasterPasswordChangeDialog';
|
||||
import { ListOfKeys } from '~/components/key/KeyList';
|
||||
import { KeyMounter } from '~/components/key/KeyMounter';
|
||||
import { DefaultProps } from '~/components/primitive/types';
|
||||
import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
||||
import { SettingsHeader } from '~/components/settings/SettingsHeader';
|
||||
import { SettingsSubHeader } from '~/components/settings/SettingsSubHeader';
|
||||
|
@ -75,8 +77,9 @@ export const KeyMounterDropdown = ({
|
|||
|
||||
export default function KeysSettings() {
|
||||
const platform = usePlatform();
|
||||
const hasMasterPw = useLibraryQuery(['keys.hasMasterPassword']);
|
||||
const setMasterPasswordMutation = useLibraryMutation('keys.unlockKeyManager', {
|
||||
const isUnlocked = useLibraryQuery(['keys.isUnlocked']);
|
||||
const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' }); // assume true by default, as it will often be the case. need to fix this with an rspc subscription+such
|
||||
const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', {
|
||||
onError: () => {
|
||||
showAlertDialog({
|
||||
title: 'Unlock Error',
|
||||
|
@ -84,6 +87,7 @@ export default function KeysSettings() {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
const unmountAll = useLibraryMutation('keys.unmountAll');
|
||||
const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
|
||||
const backupKeystore = useLibraryMutation('keys.backupKeystore');
|
||||
|
@ -92,14 +96,17 @@ export default function KeysSettings() {
|
|||
const [showMasterPassword, setShowMasterPassword] = useState(false);
|
||||
const [showSecretKey, setShowSecretKey] = useState(false);
|
||||
const [masterPassword, setMasterPassword] = useState('');
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
const [secretKey, setSecretKey] = useState(''); // for the unlock form
|
||||
const [viewSecretKey, setViewSecretKey] = useState(false); // for the settings page
|
||||
|
||||
const keys = useLibraryQuery(['keys.list']);
|
||||
|
||||
const MPCurrentEyeIcon = showMasterPassword ? EyeSlash : Eye;
|
||||
const SKCurrentEyeIcon = showSecretKey ? EyeSlash : Eye;
|
||||
|
||||
if (!hasMasterPw?.data) {
|
||||
const [enterSkManually, setEnterSkManually] = useState(keyringSk?.data === null);
|
||||
|
||||
if (!isUnlocked?.data) {
|
||||
return (
|
||||
<div className="p-2 mr-20 ml-20 mt-10">
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
|
@ -119,39 +126,55 @@ export default function KeysSettings() {
|
|||
<MPCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{enterSkManually && (
|
||||
<div className="relative flex flex-grow mb-2">
|
||||
<Input
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
type={showSecretKey ? 'text' : 'password'}
|
||||
className="flex-grow !py-0.5"
|
||||
placeholder="Secret Key"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setShowSecretKey(!showSecretKey)}
|
||||
size="icon"
|
||||
className="border-none absolute right-[5px] top-[5px]"
|
||||
>
|
||||
<SKCurrentEyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="accent"
|
||||
disabled={setMasterPasswordMutation.isLoading || isKeyManagerUnlocking.data}
|
||||
disabled={
|
||||
unlockKeyManager.isLoading || isKeyManagerUnlocking.data !== null
|
||||
? isKeyManagerUnlocking.data!
|
||||
: false
|
||||
}
|
||||
onClick={() => {
|
||||
if (masterPassword !== '') {
|
||||
const sk = secretKey || null;
|
||||
setMasterPassword('');
|
||||
setSecretKey('');
|
||||
setMasterPasswordMutation.mutate({ password: masterPassword, secret_key: sk });
|
||||
unlockKeyManager.mutate({ password: masterPassword, secret_key: secretKey });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
{!enterSkManually && (
|
||||
<div className="relative flex flex-grow">
|
||||
<p
|
||||
className="text-accent mt-2"
|
||||
onClick={(e) => {
|
||||
setEnterSkManually(true);
|
||||
}}
|
||||
>
|
||||
or enter secret key manually
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -186,10 +209,37 @@ export default function KeysSettings() {
|
|||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid space-y-2">
|
||||
<ListOfKeys />
|
||||
</div>
|
||||
|
||||
{keyringSk?.data && (
|
||||
<>
|
||||
<SettingsSubHeader title="Secret key" />
|
||||
{!viewSecretKey && (
|
||||
<div className="flex flex-row">
|
||||
<Button size="sm" variant="gray" onClick={() => setViewSecretKey(true)}>
|
||||
View Secret Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{viewSecretKey && (
|
||||
<div
|
||||
className="flex flex-row"
|
||||
onClick={() => {
|
||||
keyringSk.data && navigator.clipboard.writeText(keyringSk.data);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<QRCode size={128} value={keyringSk.data} />
|
||||
<p className="mt-14 ml-6 text-xl font-bold">{keyringSk.data}</p>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingsSubHeader title="Password Options" />
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
|
|
|
@ -453,6 +453,7 @@ importers:
|
|||
react-hook-form: ^7.36.1
|
||||
react-json-view: ^1.21.3
|
||||
react-loading-skeleton: ^3.1.0
|
||||
react-qr-code: ^2.0.11
|
||||
react-router: 6.4.2
|
||||
react-router-dom: 6.4.2
|
||||
rooks: ^5.14.0
|
||||
|
@ -501,6 +502,7 @@ importers:
|
|||
react-hook-form: 7.42.1_react@18.2.0
|
||||
react-json-view: 1.21.3_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||
react-loading-skeleton: 3.1.0_react@18.2.0
|
||||
react-qr-code: 2.0.11_react@18.2.0
|
||||
react-router: 6.4.2_react@18.2.0
|
||||
react-router-dom: 6.4.2_biqbaboplfbrettd7655fr4n2y
|
||||
rooks: 5.14.1_biqbaboplfbrettd7655fr4n2y
|
||||
|
@ -7440,7 +7442,7 @@ packages:
|
|||
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
|
||||
dependencies:
|
||||
mini-svg-data-uri: 1.4.4
|
||||
tailwindcss: 3.2.4_postcss@8.4.21
|
||||
tailwindcss: 3.2.4
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/line-clamp/0.4.2:
|
||||
|
@ -17037,6 +17039,10 @@ packages:
|
|||
resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==}
|
||||
dev: false
|
||||
|
||||
/qr.js/0.0.0:
|
||||
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||
dev: false
|
||||
|
||||
/qrcode-terminal/0.11.0:
|
||||
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==}
|
||||
hasBin: true
|
||||
|
@ -17535,6 +17541,20 @@ packages:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
/react-qr-code/2.0.11_react@18.2.0:
|
||||
resolution: {integrity: sha512-P7mvVM5vk9NjGdHMt4Z0KWeeJYwRAtonHTghZT2r+AASinLUUKQ9wfsGH2lPKsT++gps7hXmaiMGRvwTDEL9OA==}
|
||||
peerDependencies:
|
||||
react: ^16.x || ^17.x || ^18.x
|
||||
react-native-svg: '*'
|
||||
peerDependenciesMeta:
|
||||
react-native-svg:
|
||||
optional: true
|
||||
dependencies:
|
||||
prop-types: 15.8.1
|
||||
qr.js: 0.0.0
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-refresh/0.11.0:
|
||||
resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
Loading…
Reference in a new issue