diff --git a/Cargo.lock b/Cargo.lock index c87730dd1a..a45cb23d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,7 @@ version = "0.1.0" dependencies = [ "agency_client", "async-trait", + "bs58 0.5.0", "derive_builder", "futures", "indy-api-types", diff --git a/aries/aries_vcx/src/errors/error.rs b/aries/aries_vcx/src/errors/error.rs index 3cc23a89b1..e0203f8171 100644 --- a/aries/aries_vcx/src/errors/error.rs +++ b/aries/aries_vcx/src/errors/error.rs @@ -150,6 +150,9 @@ pub enum AriesVcxErrorKind { #[error("Could not parse a value")] ParsingError, + #[error("Unexpected wallet error")] + WalletUnexpected, + // A2A #[error("Invalid HTTP response.")] InvalidHttpResponse, diff --git a/aries/aries_vcx/src/errors/mapping_others.rs b/aries/aries_vcx/src/errors/mapping_others.rs index ec0a675d37..83c7a9bea8 100644 --- a/aries/aries_vcx/src/errors/mapping_others.rs +++ b/aries/aries_vcx/src/errors/mapping_others.rs @@ -151,6 +151,7 @@ impl From for AriesVcxError { AriesVcxErrorKind::DuplicationMasterSecret } AriesVcxCoreErrorKind::DuplicationDid => AriesVcxErrorKind::DuplicationDid, + AriesVcxCoreErrorKind::WalletUnexpected => AriesVcxErrorKind::WalletUnexpected, AriesVcxCoreErrorKind::LoggingError => AriesVcxErrorKind::LoggingError, AriesVcxCoreErrorKind::EncodeError => AriesVcxErrorKind::EncodeError, AriesVcxCoreErrorKind::UnknownError => AriesVcxErrorKind::UnknownError, diff --git a/aries/aries_vcx_core/Cargo.toml b/aries/aries_vcx_core/Cargo.toml index 2130f3e441..abd21144d7 100644 --- a/aries/aries_vcx_core/Cargo.toml +++ b/aries/aries_vcx_core/Cargo.toml @@ -14,6 +14,7 @@ legacy_proof = [] [dependencies] agency_client = { path = "../misc/legacy/agency_client" } +bs58 = { version = "0.5" } indy-vdr = { git = "https://github.com/hyperledger/indy-vdr.git", rev = "c143268", default-features = false, features = ["log"] } indy-credx = { git = "https://github.com/hyperledger/indy-shared-rs", tag = "v1.1.0", optional = true } libvdrtools = { path = "../misc/legacy/libvdrtools", optional = true } diff --git a/aries/aries_vcx_core/src/errors/error.rs b/aries/aries_vcx_core/src/errors/error.rs index 340777557d..9a3e729c63 100644 --- a/aries/aries_vcx_core/src/errors/error.rs +++ b/aries/aries_vcx_core/src/errors/error.rs @@ -124,6 +124,9 @@ pub enum AriesVcxCoreErrorKind { #[error("Attempted to add a DID to wallet when that DID already exists in wallet")] DuplicationDid, + #[error("Unexpected wallet error")] + WalletUnexpected, + // Logger #[error("Logging Error")] LoggingError, diff --git a/aries/aries_vcx_core/src/lib.rs b/aries/aries_vcx_core/src/lib.rs index adbbf43082..1dd1913381 100644 --- a/aries/aries_vcx_core/src/lib.rs +++ b/aries/aries_vcx_core/src/lib.rs @@ -30,6 +30,7 @@ pub mod global; pub mod ledger; pub mod utils; pub mod wallet; +pub mod wallet2; pub use indy_ledger_response_parser::ResponseParser; pub use indy_vdr::config::PoolConfig; diff --git a/aries/aries_vcx_core/src/wallet2/indy_wallet/indy_did_wallet.rs b/aries/aries_vcx_core/src/wallet2/indy_wallet/indy_did_wallet.rs new file mode 100644 index 0000000000..3e372b1f9b --- /dev/null +++ b/aries/aries_vcx_core/src/wallet2/indy_wallet/indy_did_wallet.rs @@ -0,0 +1,151 @@ +use async_trait::async_trait; +use vdrtools::{DidMethod, DidValue, KeyInfo, Locator, MyDidInfo}; + +use crate::{ + errors::error::{AriesVcxCoreError, VcxCoreResult}, + wallet::indy::IndySdkWallet, + wallet2::{DidData, DidWallet, SigType}, +}; + +#[async_trait] +impl DidWallet for IndySdkWallet { + async fn create_and_store_my_did( + &self, + seed: &str, + method_name: Option<&str>, + ) -> VcxCoreResult { + let res = Locator::instance() + .did_controller + .create_and_store_my_did( + self.wallet_handle, + MyDidInfo { + method_name: method_name.map(|m| DidMethod(m.into())), + seed: Some(seed.into()), + ..MyDidInfo::default() + }, + ) + .await + .map_err::(From::from)?; + + Ok(DidData { + did: res.0, + verkey: res.1, + }) + } + + async fn did_key(&self, did: &str) -> VcxCoreResult { + Locator::instance() + .did_controller + .key_for_local_did(self.wallet_handle, DidValue(did.into())) + .await + .map_err(From::from) + } + + async fn replace_did_key(&self, did: &str, seed: &str) -> VcxCoreResult { + let key_info = KeyInfo { + seed: Some(seed.into()), + ..Default::default() + }; + + let key = Locator::instance() + .did_controller + .replace_keys_start(self.wallet_handle, key_info, DidValue(did.into())) + .await?; + + Locator::instance() + .did_controller + .replace_keys_apply(self.wallet_handle, DidValue(did.into())) + .await?; + + Ok(key) + } + + async fn sign(&self, key: &str, msg: &[u8], _sig_type: SigType) -> VcxCoreResult> { + Locator::instance() + .crypto_controller + .crypto_sign(self.wallet_handle, key, msg) + .await + .map_err(From::from) + } + + async fn verify( + &self, + key: &str, + msg: &[u8], + signature: &[u8], + _sig_type: SigType, + ) -> VcxCoreResult { + Locator::instance() + .crypto_controller + .crypto_verify(key, msg, signature) + .await + .map_err(From::from) + } +} + +#[cfg(test)] +mod tests { + use rand::{distributions::Alphanumeric, Rng}; + + use crate::wallet2::{indy_wallet::test_helper::create_test_wallet, DidWallet, SigType}; + + #[tokio::test] + #[ignore] + async fn test_indy_should_sign_and_verify() { + let wallet = create_test_wallet().await; + + let seed: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + let did_data = DidWallet::create_and_store_my_did(&wallet, &seed, None) + .await + .unwrap(); + + let msg = "sign this".as_bytes(); + let sig = DidWallet::sign(&wallet, &did_data.verkey, msg, SigType::EdDSA) + .await + .unwrap(); + + let res = DidWallet::verify(&wallet, &did_data.verkey, msg, &sig, SigType::EdDSA) + .await + .unwrap(); + assert!(res); + } + + #[tokio::test] + #[ignore] + async fn test_indy_should_rotate_keys() { + let wallet = create_test_wallet().await; + + let seed: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + let did_data = DidWallet::create_and_store_my_did(&wallet, &seed, None) + .await + .unwrap(); + + let key = wallet.did_key(&did_data.did).await.unwrap(); + + assert_eq!(did_data.verkey, key); + + let new_seed: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + let res = wallet + .replace_did_key(&did_data.did, &new_seed) + .await + .unwrap(); + + let new_key = wallet.did_key(&did_data.did).await.unwrap(); + assert_eq!(res, new_key); + } +} diff --git a/aries/aries_vcx_core/src/wallet2/indy_wallet/indy_record_wallet.rs b/aries/aries_vcx_core/src/wallet2/indy_wallet/indy_record_wallet.rs new file mode 100644 index 0000000000..a7f3fe57a1 --- /dev/null +++ b/aries/aries_vcx_core/src/wallet2/indy_wallet/indy_record_wallet.rs @@ -0,0 +1,272 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use indy_api_types::domain::wallet::Record as IndyRecord; +use serde::Deserialize; +use serde_json::Value; +use vdrtools::Locator; + +use super::{SEARCH_OPTIONS, WALLET_OPTIONS}; +use crate::{ + errors::error::{AriesVcxCoreError, AriesVcxCoreErrorKind, VcxCoreResult}, + wallet::indy::IndySdkWallet, + wallet2::{EntryTag, Record, RecordWallet, SearchFilter}, +}; + +#[async_trait] +impl RecordWallet for IndySdkWallet { + async fn add_record(&self, record: Record) -> VcxCoreResult<()> { + let tags_map = if record.tags.is_empty() { + None + } else { + let mut tags_map = HashMap::new(); + for item in record.tags.into_iter() { + match item { + EntryTag::Encrypted(key, value) => tags_map.insert(key, value), + EntryTag::Plaintext(key, value) => tags_map.insert(format!("~{}", key), value), + }; + } + Some(tags_map) + }; + + Ok(Locator::instance() + .non_secret_controller + .add_record( + self.wallet_handle, + record.category, + record.name, + record.value, + tags_map, + ) + .await?) + } + + async fn get_record(&self, name: &str, category: &str) -> VcxCoreResult { + let res = Locator::instance() + .non_secret_controller + .get_record( + self.wallet_handle, + category.into(), + name.into(), + WALLET_OPTIONS.into(), + ) + .await?; + + let indy_record: IndyRecord = serde_json::from_str(&res)?; + + Ok(indy_record.into()) + } + + async fn update_record(&self, record: Record) -> VcxCoreResult<()> { + let indy_record: IndyRecord = record.into(); + + Locator::instance() + .non_secret_controller + .update_record_tags( + self.wallet_handle, + indy_record.type_.clone(), + indy_record.id.clone(), + indy_record.tags, + ) + .await?; + + Locator::instance() + .non_secret_controller + .update_record_value( + self.wallet_handle, + indy_record.type_, + indy_record.id, + indy_record.value, + ) + .await?; + + Ok(()) + } + + async fn delete_record(&self, name: &str, category: &str) -> VcxCoreResult<()> { + Ok(Locator::instance() + .non_secret_controller + .delete_record(self.wallet_handle, category.into(), name.into()) + .await?) + } + + async fn search_record( + &self, + category: &str, + search_filter: Option, + ) -> VcxCoreResult> { + let json_filter = search_filter + .map(|filter| match filter { + SearchFilter::JsonFilter(inner) => Ok(inner), + }) + .transpose()?; + + let query_json = json_filter.unwrap_or("{}".into()); + + let search_handle = Locator::instance() + .non_secret_controller + .open_search( + self.wallet_handle, + category.into(), + query_json, + SEARCH_OPTIONS.into(), + ) + .await?; + + let next = || async { + let record = Locator::instance() + .non_secret_controller + .fetch_search_next_records(self.wallet_handle, search_handle, 1) + .await?; + + let indy_res: Value = serde_json::from_str(&record)?; + + indy_res + .get("records") + .and_then(|v| v.as_array()) + .and_then(|arr| arr.first()) + .map(|item| IndyRecord::deserialize(item).map_err(AriesVcxCoreError::from)) + .transpose() + }; + + let mut records = Vec::new(); + while let Some(record) = next().await? { + records.push(record.into()); + } + + Ok(records) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::error::AriesVcxCoreErrorKind, + wallet2::{indy_wallet::test_helper::create_test_wallet, RecordBuilder}, + }; + + #[tokio::test] + #[ignore] + async fn indy_wallet_should_create_record() { + let wallet = create_test_wallet().await; + + let name = "foo"; + let category = "my"; + let value = "bar"; + + let record1 = RecordBuilder::default() + .name(name.into()) + .category(category.into()) + .value(value.into()) + .build() + .unwrap(); + let record2 = RecordBuilder::default() + .name("baz".into()) + .category(category.into()) + .value("box".into()) + .build() + .unwrap(); + + wallet.add_record(record1).await.unwrap(); + wallet.add_record(record2).await.unwrap(); + + let res = wallet.get_record(name, category).await.unwrap(); + + assert_eq!(value, res.value); + } + + #[tokio::test] + #[ignore] + async fn indy_wallet_should_delete_record() { + let wallet = create_test_wallet().await; + + let name = "foo"; + let category = "my"; + let value = "bar"; + + let record = RecordBuilder::default() + .name(name.into()) + .category(category.into()) + .value(value.into()) + .build() + .unwrap(); + + wallet.add_record(record).await.unwrap(); + + let res = wallet.get_record(name, category).await.unwrap(); + + assert_eq!(value, res.value); + + wallet.delete_record(name, category).await.unwrap(); + + let err = wallet.get_record(name, category).await.unwrap_err(); + assert_eq!(AriesVcxCoreErrorKind::WalletRecordNotFound, err.kind()); + } + + #[tokio::test] + #[ignore] + async fn indy_wallet_should_search_for_records() { + let wallet = create_test_wallet().await; + + let name1 = "foo"; + let name2 = "foa"; + let name3 = "fob"; + let category1 = "my"; + let category2 = "your"; + let value = "xxx"; + + let mut record_builder = RecordBuilder::default(); + record_builder + .name(name1.into()) + .category(category1.into()) + .value(value.into()); + + let record1 = record_builder.build().unwrap(); + wallet.add_record(record1).await.unwrap(); + + let record2 = record_builder.name(name2.into()).build().unwrap(); + wallet.add_record(record2).await.unwrap(); + + let record3 = record_builder + .name(name3.into()) + .category(category2.into()) + .build() + .unwrap(); + wallet.add_record(record3).await.unwrap(); + + let res = wallet.search_record(category1, None).await.unwrap(); + + assert_eq!(2, res.len()); + } + + #[tokio::test] + #[ignore] + async fn indy_wallet_should_update_record() { + let wallet = create_test_wallet().await; + + let name = "foo"; + let category = "my"; + let value1 = "xxx"; + let value2 = "yyy"; + let tags = vec![EntryTag::Plaintext("a".into(), "b".into())]; + + let mut record = RecordBuilder::default() + .name(name.into()) + .category(category.into()) + .tags(tags.clone()) + .value(value1.into()) + .build() + .unwrap(); + wallet.add_record(record.clone()).await.unwrap(); + + record.value = value2.into(); + record.tags = vec![]; + + wallet.update_record(record.clone()).await.unwrap(); + + let res = wallet.get_record(name, category).await.unwrap(); + assert_eq!(record.value, res.value); + assert_eq!(record.tags, res.tags); + } +} diff --git a/aries/aries_vcx_core/src/wallet2/indy_wallet/mod.rs b/aries/aries_vcx_core/src/wallet2/indy_wallet/mod.rs new file mode 100644 index 0000000000..e294a9e823 --- /dev/null +++ b/aries/aries_vcx_core/src/wallet2/indy_wallet/mod.rs @@ -0,0 +1,52 @@ +use super::BaseWallet2; +use crate::wallet::indy::IndySdkWallet; + +pub mod indy_did_wallet; +pub mod indy_record_wallet; + +const WALLET_OPTIONS: &str = + r#"{"retrieveType": true, "retrieveValue": true, "retrieveTags": true}"#; + +const SEARCH_OPTIONS: &str = r#"{"retrieveType": true, "retrieveValue": true, "retrieveTags": true, "retrieveRecords": true}"#; + +impl BaseWallet2 for IndySdkWallet {} + +#[allow(dead_code)] +pub(crate) mod test_helper { + use serde_json::json; + + use crate::wallet::indy::{wallet::create_and_open_wallet, IndySdkWallet, WalletConfigBuilder}; + + pub async fn create_test_wallet() -> IndySdkWallet { + let default_wallet_key = "8dvfYSt5d1taSd6yJdpjq4emkwsPDDLYxkNFysFD2cZY"; + let wallet_kdf_raw = "RAW"; + + let db_name = format!("mysqltest_{}", uuid::Uuid::new_v4()).replace('-', "_"); + let storage_config = json!({ + "read_host": "localhost", + "write_host": "localhost", + "port": 3306, + "db_name": db_name, + "default_connection_limit": 50 + }) + .to_string(); + let storage_credentials = json!({ + "user": "root", + "pass": "mysecretpassword" + }) + .to_string(); + let config_wallet = WalletConfigBuilder::default() + .wallet_name(format!("faber_wallet_{}", uuid::Uuid::new_v4())) + .wallet_key(default_wallet_key) + .wallet_key_derivation(wallet_kdf_raw) + .wallet_type("mysql") + .storage_config(storage_config) + .storage_credentials(storage_credentials) + .build() + .unwrap(); + + let wallet_handle = create_and_open_wallet(&config_wallet).await.unwrap(); + + IndySdkWallet::new(wallet_handle) + } +} diff --git a/aries/aries_vcx_core/src/wallet2/mod.rs b/aries/aries_vcx_core/src/wallet2/mod.rs new file mode 100644 index 0000000000..bd4e619b39 --- /dev/null +++ b/aries/aries_vcx_core/src/wallet2/mod.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use derive_builder::Builder; +#[cfg(feature = "vdrtools_wallet")] +use indy_api_types::domain::wallet::Record as IndyRecord; +use serde::{Deserialize, Serialize}; + +use crate::errors::error::VcxCoreResult; + +#[cfg(feature = "vdrtools_wallet")] +pub mod indy_wallet; + +pub enum SigType { + EdDSA, + ES256, + ES256K, + ES384, +} + +impl From for &str { + fn from(value: SigType) -> Self { + match value { + SigType::EdDSA => "eddsa", + SigType::ES256 => "es256", + SigType::ES256K => "es256k", + SigType::ES384 => "es384", + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum EntryTag { + Encrypted(String, String), + Plaintext(String, String), +} + +#[derive(Debug, Default, Clone, Builder)] +pub struct Record { + pub category: String, + pub name: String, + pub value: String, + #[builder(default = "vec![]")] + pub tags: Vec, +} + +#[cfg(feature = "vdrtools_wallet")] +impl From for Record { + fn from(ir: IndyRecord) -> Self { + let tags = ir + .tags + .into_iter() + .map(|(key, value)| EntryTag::Plaintext(key, value)) + .collect(); + Self { + name: ir.id, + category: ir.type_, + value: ir.value, + tags, + } + } +} + +#[cfg(feature = "vdrtools_wallet")] +impl From for IndyRecord { + fn from(record: Record) -> Self { + let tags = record + .tags + .into_iter() + .fold(HashMap::new(), |mut memo, item| { + match item { + EntryTag::Encrypted(key, val) => memo.insert(key, val), + EntryTag::Plaintext(key, val) => memo.insert(format!("~{}", key), val), + }; + memo + }); + Self { + id: record.name, + type_: record.category, + value: record.value, + tags, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DidData { + did: String, + verkey: String, +} + +pub enum SearchFilter { + JsonFilter(String), +} + +#[async_trait] +pub trait BaseWallet2: RecordWallet + DidWallet {} + +#[async_trait] +pub trait DidWallet { + async fn create_and_store_my_did( + &self, + seed: &str, + method_name: Option<&str>, + ) -> VcxCoreResult; + + async fn did_key(&self, name: &str) -> VcxCoreResult; + + async fn replace_did_key(&self, did: &str, seed: &str) -> VcxCoreResult; + + async fn sign(&self, key: &str, msg: &[u8], sig_type: SigType) -> VcxCoreResult>; + + async fn verify( + &self, + key: &str, + msg: &[u8], + signature: &[u8], + sig_type: SigType, + ) -> VcxCoreResult; +} + +#[async_trait] +pub trait RecordWallet { + async fn add_record(&self, record: Record) -> VcxCoreResult<()>; + + async fn get_record(&self, name: &str, category: &str) -> VcxCoreResult; + + async fn update_record(&self, record: Record) -> VcxCoreResult<()>; + + async fn delete_record(&self, name: &str, category: &str) -> VcxCoreResult<()>; + + async fn search_record( + &self, + category: &str, + search_filter: Option, + ) -> VcxCoreResult>; +} diff --git a/aries/misc/legacy/libvcx_core/src/errors/error.rs b/aries/misc/legacy/libvcx_core/src/errors/error.rs index 2a8b5c58fc..8058d03187 100644 --- a/aries/misc/legacy/libvcx_core/src/errors/error.rs +++ b/aries/misc/legacy/libvcx_core/src/errors/error.rs @@ -160,6 +160,9 @@ pub enum LibvcxErrorKind { #[error("Attempted to add a DID to wallet when that DID already exists in wallet")] DuplicationDid, + #[error("Unexpected wallet error")] + WalletUnexpected, + // Logger #[error("Logging Error")] LoggingError, diff --git a/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcx.rs b/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcx.rs index 48ef005599..70e70fd41b 100644 --- a/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcx.rs +++ b/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcx.rs @@ -73,6 +73,7 @@ impl From for LibvcxErrorKind { AriesVcxErrorKind::WalletAlreadyOpen => LibvcxErrorKind::WalletAlreadyOpen, AriesVcxErrorKind::DuplicationMasterSecret => LibvcxErrorKind::DuplicationMasterSecret, AriesVcxErrorKind::DuplicationDid => LibvcxErrorKind::DuplicationDid, + AriesVcxErrorKind::WalletUnexpected => LibvcxErrorKind::WalletUnexpected, AriesVcxErrorKind::LoggingError => LibvcxErrorKind::LoggingError, AriesVcxErrorKind::EncodeError => LibvcxErrorKind::EncodeError, AriesVcxErrorKind::UnknownError => LibvcxErrorKind::UnknownError, diff --git a/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcxcore.rs b/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcxcore.rs index 05ab64debf..71dfc66af3 100644 --- a/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcxcore.rs +++ b/aries/misc/legacy/libvcx_core/src/errors/mapping_from_ariesvcxcore.rs @@ -85,6 +85,7 @@ impl From for LibvcxErrorKind { LibvcxErrorKind::DuplicationMasterSecret } AriesVcxCoreErrorKind::DuplicationDid => LibvcxErrorKind::DuplicationDid, + AriesVcxCoreErrorKind::WalletUnexpected => LibvcxErrorKind::WalletUnexpected, AriesVcxCoreErrorKind::LoggingError => LibvcxErrorKind::LoggingError, AriesVcxCoreErrorKind::EncodeError => LibvcxErrorKind::EncodeError, AriesVcxCoreErrorKind::UnknownError => LibvcxErrorKind::UnknownError,