[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:
jake 2023-02-07 12:03:12 +00:00 committed by GitHub
parent d99adcf7ca
commit d1b6263ae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1168 additions and 720 deletions

1
Cargo.lock generated
View file

@ -5542,6 +5542,7 @@ dependencies = [
"blake3",
"chacha20poly1305",
"dashmap",
"hex",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rspc",

View file

@ -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)
};
});
}

View file

@ -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' }
})

View 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");

View file

@ -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?

View file

@ -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?;

View file

@ -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,
},

View file

@ -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?;

View file

@ -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)

View file

@ -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())

View file

@ -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(),

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)?;

View file

@ -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?;

View file

@ -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,
};

View file

@ -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()])

View file

@ -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()])

View file

@ -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)))
}
}

View file

@ -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)

View file

@ -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(),

View file

@ -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(),

View file

@ -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)
}

View file

@ -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;

View file

@ -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;

View file

@ -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)
// }

View 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
})
}

View 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,
}

View file

@ -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,

View file

@ -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 }

View file

@ -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",

View file

@ -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();

View file

@ -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">

View file

@ -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">

View file

@ -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}

View file

@ -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 {

View file

@ -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

View file

@ -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'}