snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted)
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
This commit is contained in:
52
plugins/stronghold/src/kdf.rs
Normal file
52
plugins/stronghold/src/kdf.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Key derivation used to turn the user password into the key that encrypts a snapshot.
|
||||
//!
|
||||
//! Only available when the **kdf** Cargo feature is enabled, which is the case by default.
|
||||
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
use std::path::Path;
|
||||
|
||||
/// NOTE: Hash supplied to Stronghold must be 32bits long.
|
||||
/// This is a current limitation of Stronghold.
|
||||
const HASH_LENGTH: usize = 32;
|
||||
|
||||
/// Password hashing functions that can be used as the key derivation function of
|
||||
/// [`Builder::new`](crate::Builder::new).
|
||||
pub struct KeyDerivation {}
|
||||
|
||||
impl KeyDerivation {
|
||||
/// Hashes `password` with Argon2 using the salt stored in `salt_path`, returning the
|
||||
/// 32 bytes key used to encrypt a snapshot.
|
||||
///
|
||||
/// The salt is read from `salt_path` when that file already exists, otherwise a new
|
||||
/// random salt is generated and written to it.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when the salt file cannot be read or written, when its contents are not
|
||||
/// 32 bytes long, or when hashing the password fails.
|
||||
pub fn argon2(password: &str, salt_path: &Path) -> Vec<u8> {
|
||||
let mut salt = [0u8; HASH_LENGTH];
|
||||
create_or_get_salt(&mut salt, salt_path);
|
||||
|
||||
argon2::hash_raw(password.as_bytes(), &salt, &Default::default())
|
||||
.expect("Failed to generate hash for password")
|
||||
}
|
||||
}
|
||||
|
||||
fn create_or_get_salt(salt: &mut [u8], salt_path: &Path) {
|
||||
if salt_path.is_file() {
|
||||
// Get existing salt
|
||||
let tmp = std::fs::read(salt_path).unwrap();
|
||||
salt.clone_from_slice(&tmp);
|
||||
} else {
|
||||
// Generate new salt
|
||||
let mut gen = ChaCha20Rng::from_os_rng();
|
||||
gen.fill_bytes(salt);
|
||||
std::fs::write(salt_path, salt).expect("Failed to write salt for Stronghold")
|
||||
}
|
||||
}
|
||||
533
plugins/stronghold/src/lib.rs
Normal file
533
plugins/stronghold/src/lib.rs
Normal file
@@ -0,0 +1,533 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs) encrypted database and secure runtime.
|
||||
//!
|
||||
//! ## Cargo features
|
||||
//!
|
||||
//! - **kdf** *(enabled by default)*: Exposes the key-derivation module and provides the default Argon2-based password-to-key derivation.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
||||
)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crypto::keys::bip39;
|
||||
use iota_stronghold::{
|
||||
procedures::{
|
||||
BIP39Generate, BIP39Recover, Curve, Ed25519Sign, KeyType as StrongholdKeyType,
|
||||
MnemonicLanguage, PublicKey, Slip10Derive, Slip10DeriveInput, Slip10Generate,
|
||||
StrongholdProcedure,
|
||||
},
|
||||
Client, Location,
|
||||
};
|
||||
use serde::{de::Visitor, Deserialize, Deserializer};
|
||||
use stronghold::{Error, Result, Stronghold};
|
||||
use tauri::{
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
Manager, Runtime, State,
|
||||
};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
#[cfg(feature = "kdf")]
|
||||
pub mod kdf;
|
||||
|
||||
pub mod stronghold;
|
||||
|
||||
type PasswordHashFn = dyn Fn(&str) -> Vec<u8> + Send + Sync;
|
||||
|
||||
#[derive(Default)]
|
||||
struct StrongholdCollection(Arc<Mutex<HashMap<PathBuf, Stronghold>>>);
|
||||
|
||||
struct PasswordHashFunction(Box<PasswordHashFn>);
|
||||
|
||||
#[derive(Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[serde(untagged)]
|
||||
enum BytesDto {
|
||||
Text(String),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for BytesDto {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Text(t) => t.as_ref(),
|
||||
Self::Raw(b) => b.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BytesDto> for Vec<u8> {
|
||||
fn from(v: BytesDto) -> Self {
|
||||
match v {
|
||||
BytesDto::Text(t) => t.as_bytes().to_vec(),
|
||||
BytesDto::Raw(b) => b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
enum LocationDto {
|
||||
Generic { vault: BytesDto, record: BytesDto },
|
||||
Counter { vault: BytesDto, counter: usize },
|
||||
}
|
||||
|
||||
impl From<LocationDto> for Location {
|
||||
fn from(dto: LocationDto) -> Location {
|
||||
match dto {
|
||||
LocationDto::Generic { vault, record } => Location::generic(vault, record),
|
||||
LocationDto::Counter { vault, counter } => Location::counter(vault, counter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
enum Slip10DeriveInputDto {
|
||||
Seed(LocationDto),
|
||||
Key(LocationDto),
|
||||
}
|
||||
|
||||
impl From<Slip10DeriveInputDto> for Slip10DeriveInput {
|
||||
fn from(dto: Slip10DeriveInputDto) -> Slip10DeriveInput {
|
||||
match dto {
|
||||
Slip10DeriveInputDto::Seed(location) => Slip10DeriveInput::Seed(location.into()),
|
||||
Slip10DeriveInputDto::Key(location) => Slip10DeriveInput::Key(location.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of a key pair handled by the plugin procedures.
|
||||
///
|
||||
/// Deserialized from the strings `ed25519` and `x25519`, ignoring case.
|
||||
pub enum KeyType {
|
||||
/// The Ed25519 signature scheme.
|
||||
Ed25519,
|
||||
/// The X25519 key exchange scheme.
|
||||
X25519,
|
||||
}
|
||||
|
||||
impl From<KeyType> for StrongholdKeyType {
|
||||
fn from(ty: KeyType) -> StrongholdKeyType {
|
||||
match ty {
|
||||
KeyType::Ed25519 => StrongholdKeyType::Ed25519,
|
||||
KeyType::X25519 => StrongholdKeyType::X25519,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyType {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct KeyTypeVisitor;
|
||||
|
||||
impl Visitor<'_> for KeyTypeVisitor {
|
||||
type Value = KeyType;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("ed25519 or x25519")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match value.to_lowercase().as_str() {
|
||||
"ed25519" => Ok(KeyType::Ed25519),
|
||||
"x25519" => Ok(KeyType::X25519),
|
||||
_ => Err(serde::de::Error::custom("unknown key type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(KeyTypeVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
enum ProcedureDto {
|
||||
SLIP10Generate {
|
||||
output: LocationDto,
|
||||
#[serde(rename = "sizeBytes")]
|
||||
size_bytes: Option<usize>,
|
||||
},
|
||||
SLIP10Derive {
|
||||
chain: Vec<u32>,
|
||||
input: Slip10DeriveInputDto,
|
||||
output: LocationDto,
|
||||
},
|
||||
BIP39Recover {
|
||||
mnemonic: String,
|
||||
passphrase: Option<String>,
|
||||
output: LocationDto,
|
||||
},
|
||||
BIP39Generate {
|
||||
passphrase: Option<String>,
|
||||
output: LocationDto,
|
||||
},
|
||||
PublicKey {
|
||||
#[serde(rename = "type")]
|
||||
ty: KeyType,
|
||||
#[serde(rename = "privateKey")]
|
||||
private_key: LocationDto,
|
||||
},
|
||||
Ed25519Sign {
|
||||
#[serde(rename = "privateKey")]
|
||||
private_key: LocationDto,
|
||||
msg: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ProcedureDto> for StrongholdProcedure {
|
||||
fn from(dto: ProcedureDto) -> StrongholdProcedure {
|
||||
match dto {
|
||||
ProcedureDto::SLIP10Generate { output, size_bytes } => {
|
||||
StrongholdProcedure::Slip10Generate(Slip10Generate {
|
||||
output: output.into(),
|
||||
size_bytes,
|
||||
})
|
||||
}
|
||||
ProcedureDto::SLIP10Derive {
|
||||
chain,
|
||||
input,
|
||||
output,
|
||||
} => StrongholdProcedure::Slip10Derive(Slip10Derive {
|
||||
curve: Curve::Ed25519,
|
||||
chain,
|
||||
input: input.into(),
|
||||
output: output.into(),
|
||||
}),
|
||||
ProcedureDto::BIP39Recover {
|
||||
mnemonic,
|
||||
passphrase,
|
||||
output,
|
||||
} => StrongholdProcedure::BIP39Recover(BIP39Recover {
|
||||
mnemonic: bip39::Mnemonic::from(mnemonic),
|
||||
passphrase: bip39::Passphrase::from(passphrase.unwrap_or_default()),
|
||||
output: output.into(),
|
||||
}),
|
||||
ProcedureDto::BIP39Generate { passphrase, output } => {
|
||||
StrongholdProcedure::BIP39Generate(BIP39Generate {
|
||||
passphrase: bip39::Passphrase::from(passphrase.unwrap_or_default()),
|
||||
output: output.into(),
|
||||
language: MnemonicLanguage::English,
|
||||
})
|
||||
}
|
||||
ProcedureDto::PublicKey { ty, private_key } => {
|
||||
StrongholdProcedure::PublicKey(PublicKey {
|
||||
ty: ty.into(),
|
||||
private_key: private_key.into(),
|
||||
})
|
||||
}
|
||||
ProcedureDto::Ed25519Sign { private_key, msg } => {
|
||||
StrongholdProcedure::Ed25519Sign(Ed25519Sign {
|
||||
private_key: private_key.into(),
|
||||
msg: msg.as_bytes().to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn initialize(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
hash_function: State<'_, PasswordHashFunction>,
|
||||
snapshot_path: PathBuf,
|
||||
mut password: String,
|
||||
) -> Result<()> {
|
||||
let hash = (hash_function.0)(&password);
|
||||
password.zeroize();
|
||||
let stronghold = Stronghold::new(snapshot_path.clone(), hash)?;
|
||||
|
||||
collection
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(snapshot_path, stronghold);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn destroy(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
) -> Result<()> {
|
||||
let mut collection = collection.0.lock().unwrap();
|
||||
if let Some(stronghold) = collection.remove(&snapshot_path) {
|
||||
if let Err(e) = stronghold.save() {
|
||||
collection.insert(snapshot_path, stronghold);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn save(collection: State<'_, StrongholdCollection>, snapshot_path: PathBuf) -> Result<()> {
|
||||
let collection = collection.0.lock().unwrap();
|
||||
if let Some(stronghold) = collection.get(&snapshot_path) {
|
||||
stronghold.save()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_client(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
) -> Result<()> {
|
||||
let stronghold = get_stronghold(collection, snapshot_path)?;
|
||||
stronghold.create_client(client)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn load_client(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
) -> Result<()> {
|
||||
let stronghold = get_stronghold(collection, snapshot_path)?;
|
||||
stronghold.load_client(client)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_store_record(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
key: String,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let client = get_client(collection, snapshot_path, client)?;
|
||||
client.store().get(key.as_ref()).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn save_store_record(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
key: String,
|
||||
value: Vec<u8>,
|
||||
lifetime: Option<Duration>,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let client = get_client(collection, snapshot_path, client)?;
|
||||
client
|
||||
.store()
|
||||
.insert(key.as_bytes().to_vec(), value, lifetime)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_store_record(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
key: String,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let client = get_client(collection, snapshot_path, client)?;
|
||||
client.store().delete(key.as_ref()).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn save_secret(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
vault: BytesDto,
|
||||
record_path: BytesDto,
|
||||
secret: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let client = get_client(collection, snapshot_path, client)?;
|
||||
client
|
||||
.vault(&vault)
|
||||
.write_secret(
|
||||
Location::generic(vault, record_path),
|
||||
Zeroizing::new(secret),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_secret(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
vault: BytesDto,
|
||||
record_path: BytesDto,
|
||||
) -> Result<()> {
|
||||
let client = get_client(collection, snapshot_path, client)?;
|
||||
client
|
||||
.vault(vault)
|
||||
.delete_secret(record_path)
|
||||
.map(|_| ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn execute_procedure(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
procedure: ProcedureDto,
|
||||
) -> Result<Vec<u8>> {
|
||||
let client = get_client(collection, snapshot_path, client)?;
|
||||
client
|
||||
.execute_procedure(StrongholdProcedure::from(procedure))
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn get_stronghold(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
) -> Result<iota_stronghold::Stronghold> {
|
||||
let collection = collection.0.lock().unwrap();
|
||||
if let Some(stronghold) = collection.get(&snapshot_path) {
|
||||
Ok(stronghold.inner().clone())
|
||||
} else {
|
||||
Err(Error::StrongholdNotInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_client(
|
||||
collection: State<'_, StrongholdCollection>,
|
||||
snapshot_path: PathBuf,
|
||||
client: BytesDto,
|
||||
) -> Result<Client> {
|
||||
let collection = collection.0.lock().unwrap();
|
||||
if let Some(stronghold) = collection.get(&snapshot_path) {
|
||||
stronghold.get_client(client).map_err(Into::into)
|
||||
} else {
|
||||
Err(Error::StrongholdNotInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
enum PasswordHashFunctionKind {
|
||||
#[cfg(feature = "kdf")]
|
||||
Argon2(PathBuf),
|
||||
Custom(Box<PasswordHashFn>),
|
||||
}
|
||||
|
||||
/// Builder for the stronghold plugin.
|
||||
///
|
||||
/// It defines how the password sent by the frontend is hashed into the key that
|
||||
/// encrypts the snapshot file.
|
||||
pub struct Builder {
|
||||
password_hash_function: PasswordHashFunctionKind,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Initializes [`Self`] with a custom password hash function.
|
||||
///
|
||||
/// The function is called with the password sent by the frontend and must return the
|
||||
/// key used to encrypt the snapshot, which must be 32 bytes long.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// fn init<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
|
||||
/// builder.plugin(
|
||||
/// tauri_plugin_stronghold::Builder::new(|_password| {
|
||||
/// // hash the password with a secure algorithm such as argon2 or blake2b
|
||||
/// // and return the resulting 32 bytes hash
|
||||
/// unimplemented!()
|
||||
/// })
|
||||
/// .build(),
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new<F: Fn(&str) -> Vec<u8> + Send + Sync + 'static>(password_hash_function: F) -> Self {
|
||||
Self {
|
||||
password_hash_function: PasswordHashFunctionKind::Custom(Box::new(
|
||||
password_hash_function,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes [`Self`] with argon2 as password hash function.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tauri::Manager;
|
||||
///
|
||||
/// fn init<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
|
||||
/// builder.setup(|app| {
|
||||
/// let salt_path = app
|
||||
/// .path()
|
||||
/// .app_local_data_dir()
|
||||
/// .expect("could not resolve app local data path")
|
||||
/// .join("salt.txt");
|
||||
/// app.handle()
|
||||
/// .plugin(tauri_plugin_stronghold::Builder::with_argon2(&salt_path).build())?;
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "kdf")]
|
||||
pub fn with_argon2(salt_path: &std::path::Path) -> Self {
|
||||
Self {
|
||||
password_hash_function: PasswordHashFunctionKind::Argon2(salt_path.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the plugin, registering the password hash function and the commands used
|
||||
/// by the JavaScript guest bindings.
|
||||
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
||||
let password_hash_function = self.password_hash_function;
|
||||
|
||||
let plugin_builder = PluginBuilder::new("stronghold").setup(move |app, _api| {
|
||||
app.manage(StrongholdCollection::default());
|
||||
app.manage(PasswordHashFunction(match password_hash_function {
|
||||
#[cfg(feature = "kdf")]
|
||||
PasswordHashFunctionKind::Argon2(path) => {
|
||||
Box::new(move |p| kdf::KeyDerivation::argon2(p, &path))
|
||||
}
|
||||
PasswordHashFunctionKind::Custom(f) => f,
|
||||
}));
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Builder::invoke_stronghold_handlers_and_build(plugin_builder)
|
||||
}
|
||||
|
||||
fn invoke_stronghold_handlers_and_build<R: Runtime>(
|
||||
builder: PluginBuilder<R>,
|
||||
) -> TauriPlugin<R> {
|
||||
builder
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
initialize,
|
||||
destroy,
|
||||
save,
|
||||
create_client,
|
||||
load_client,
|
||||
get_store_record,
|
||||
save_store_record,
|
||||
remove_store_record,
|
||||
save_secret,
|
||||
remove_secret,
|
||||
execute_procedure,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
}
|
||||
103
plugins/stronghold/src/stronghold.rs
Normal file
103
plugins/stronghold/src/stronghold.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Types to load, modify and persist an [IOTA Stronghold](https://github.com/iotaledger/stronghold.rs)
|
||||
//! snapshot file.
|
||||
|
||||
use std::{convert::TryFrom, ops::Deref, path::Path};
|
||||
|
||||
use iota_stronghold::{KeyProvider, SnapshotPath};
|
||||
use serde::{Serialize, Serializer};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// Alias for a [`std::result::Result`] with the error type [`Error`].
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors returned by the stronghold plugin.
|
||||
///
|
||||
/// Serialized as the error message string when returned to the frontend.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// No stronghold was initialized for the given snapshot path.
|
||||
#[error("stronghold not initialized")]
|
||||
StrongholdNotInitialized,
|
||||
/// An error from the underlying Stronghold client, e.g. when loading a snapshot
|
||||
/// with the wrong password or when addressing a client that does not exist.
|
||||
#[error(transparent)]
|
||||
Stronghold(#[from] iota_stronghold::ClientError),
|
||||
/// An error from the Stronghold secure memory implementation, e.g. when the
|
||||
/// password hash is not a key size Stronghold accepts.
|
||||
#[error(transparent)]
|
||||
Memory(#[from] iota_stronghold::MemoryError),
|
||||
/// A Stronghold procedure (key generation, key derivation, signing, ...) failed.
|
||||
#[error(transparent)]
|
||||
Procedure(#[from] iota_stronghold::procedures::ProcedureError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A Stronghold instance bound to a snapshot file and to the key it is encrypted with.
|
||||
///
|
||||
/// Dereferences to the underlying [`iota_stronghold::Stronghold`], so all of its client
|
||||
/// and vault operations are available on this type.
|
||||
pub struct Stronghold {
|
||||
inner: iota_stronghold::Stronghold,
|
||||
path: SnapshotPath,
|
||||
keyprovider: KeyProvider,
|
||||
}
|
||||
|
||||
impl Stronghold {
|
||||
/// Creates a Stronghold instance for the snapshot file at `path`, encrypted with
|
||||
/// `password` as the key.
|
||||
///
|
||||
/// When the file already exists its snapshot is loaded, which requires `password` to
|
||||
/// be the key it was encrypted with. Otherwise an empty instance is created and
|
||||
/// nothing is written to disk until [`Self::save`] is called.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Error::Memory`] when `password` is not a key size Stronghold accepts
|
||||
/// (it must be 32 bytes long) and [`Error::Stronghold`] when an existing snapshot
|
||||
/// cannot be loaded with it.
|
||||
pub fn new<P: AsRef<Path>>(path: P, password: Vec<u8>) -> Result<Self> {
|
||||
let path = SnapshotPath::from_path(path);
|
||||
let stronghold = iota_stronghold::Stronghold::default();
|
||||
let keyprovider = KeyProvider::try_from(Zeroizing::new(password))?;
|
||||
if path.exists() {
|
||||
stronghold.load_snapshot(&keyprovider, &path)?;
|
||||
}
|
||||
Ok(Self {
|
||||
inner: stronghold,
|
||||
path,
|
||||
keyprovider,
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes the state of all clients to the snapshot file, encrypted with the key
|
||||
/// this instance was created with.
|
||||
pub fn save(&self) -> Result<()> {
|
||||
self.inner
|
||||
.commit_with_keyprovider(&self.path, &self.keyprovider)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying [`iota_stronghold::Stronghold`] instance.
|
||||
pub fn inner(&self) -> &iota_stronghold::Stronghold {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Stronghold {
|
||||
type Target = iota_stronghold::Stronghold;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user