Skip to content

Commit

Permalink
Add PrivateKeySecretManager (#937)
Browse files Browse the repository at this point in the history
* Add PrivateKeySecretManager

* try_from_b58/try_from_hex

* zeroize bytes

* Nits

* Optional bs58

* Changelog

* Secret manager tests folder

* Add tests and fix

* Changelog

* Nit

* Nit

* Update sdk/tests/client/secret_manager/mod.rs

Co-authored-by: Thoralf-M <[email protected]>

* Replace panic with errors

* Remove unwrap

* Under private_key_secret_manager feature

---------

Co-authored-by: Thoralf-M <[email protected]>
  • Loading branch information
thibault-martinez and Thoralf-M committed Aug 31, 2023
1 parent 711f85c commit a8b5390
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 32 deletions.
11 changes: 9 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `migrate_db_chrysalis_to_stardust()` function;
- `Wallet::get_chrysalis_data()` method;
- `PrivateKeySecretManager` and `SecretManager::PrivateKey`;
- `SecretManager::from` impl for variants;

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ serde_json = { version = "1.0.105", default-features = false, features = [
# Optional dependencies
anymap = { version = "0.12.1", default-features = false, optional = true }
async-trait = { version = "0.1.73", default-features = false, optional = true }
bs58 = { version = "0.5.0", default-features = false, optional = true }
derive_builder = { version = "0.12.0", default-features = false, optional = true }
fern-logger = { version = "0.5.0", default-features = false, optional = true }
futures = { version = "0.3.28", default-features = false, features = [
Expand Down Expand Up @@ -185,6 +186,7 @@ stronghold = [
"dep:heck",
]
tls = ["reqwest?/rustls-tls", "rumqttc?/use-rustls"]
private_key_secret_manager = ["bs58"]

client = [
"pow",
Expand Down
89 changes: 87 additions & 2 deletions sdk/src/client/secret/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@

//! Secret manager module enabling address generation and transaction essence signing.

/// Module for ledger nano based secret management.
#[cfg(feature = "ledger_nano")]
#[cfg_attr(docsrs, doc(cfg(feature = "ledger_nano")))]
pub mod ledger_nano;
/// Module for signing with a mnemonic or seed
/// Module for mnemonic based secret management.
pub mod mnemonic;
/// Module for signing with a Stronghold vault
/// Module for single private key based secret management.
#[cfg(feature = "private_key_secret_manager")]
#[cfg_attr(docsrs, doc(cfg(feature = "private_key_secret_manager")))]
pub mod private_key;
/// Module for stronghold based secret management.
#[cfg(feature = "stronghold")]
#[cfg_attr(docsrs, doc(cfg(feature = "stronghold")))]
pub mod stronghold;
Expand All @@ -30,6 +35,8 @@ use zeroize::Zeroizing;
#[cfg(feature = "ledger_nano")]
use self::ledger_nano::LedgerSecretManager;
use self::mnemonic::MnemonicSecretManager;
#[cfg(feature = "private_key_secret_manager")]
use self::private_key::PrivateKeySecretManager;
#[cfg(feature = "stronghold")]
use self::stronghold::StrongholdSecretManager;
pub use self::types::{GenerateAddressOptions, LedgerNanoStatus};
Expand Down Expand Up @@ -137,11 +144,43 @@ pub enum SecretManager {
/// LedgerNano or Stronghold instead.
Mnemonic(MnemonicSecretManager),

/// Secret manager that uses a single private key.
#[cfg(feature = "private_key_secret_manager")]
#[cfg_attr(docsrs, doc(cfg(feature = "private_key_secret_manager")))]
PrivateKey(Box<PrivateKeySecretManager>),

/// Secret manager that's just a placeholder, so it can be provided to an online wallet, but can't be used for
/// signing.
Placeholder,
}

#[cfg(feature = "stronghold")]
impl From<StrongholdSecretManager> for SecretManager {
fn from(secret_manager: StrongholdSecretManager) -> Self {
Self::Stronghold(secret_manager)
}
}

#[cfg(feature = "ledger_nano")]
impl From<LedgerSecretManager> for SecretManager {
fn from(secret_manager: LedgerSecretManager) -> Self {
Self::LedgerNano(secret_manager)
}
}

impl From<MnemonicSecretManager> for SecretManager {
fn from(secret_manager: MnemonicSecretManager) -> Self {
Self::Mnemonic(secret_manager)
}
}

#[cfg(feature = "private_key_secret_manager")]
impl From<PrivateKeySecretManager> for SecretManager {
fn from(secret_manager: PrivateKeySecretManager) -> Self {
Self::PrivateKey(Box::new(secret_manager))
}
}

impl Debug for SecretManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expand All @@ -150,6 +189,8 @@ impl Debug for SecretManager {
#[cfg(feature = "ledger_nano")]
Self::LedgerNano(_) => f.debug_tuple("LedgerNano").field(&"...").finish(),
Self::Mnemonic(_) => f.debug_tuple("Mnemonic").field(&"...").finish(),
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(_) => f.debug_tuple("PrivateKey").field(&"...").finish(),
Self::Placeholder => f.debug_struct("Placeholder").finish(),
}
}
Expand Down Expand Up @@ -180,6 +221,11 @@ pub enum SecretManagerDto {
/// Mnemonic
#[serde(alias = "mnemonic")]
Mnemonic(Zeroizing<String>),
/// Private Key
#[cfg(feature = "private_key_secret_manager")]
#[cfg_attr(docsrs, doc(cfg(feature = "private_key_secret_manager")))]
#[serde(alias = "privateKey")]
PrivateKey(Zeroizing<String>),
/// Hex seed
#[serde(alias = "hexSeed")]
HexSeed(Zeroizing<String>),
Expand Down Expand Up @@ -215,6 +261,11 @@ impl TryFrom<SecretManagerDto> for SecretManager {
Self::Mnemonic(MnemonicSecretManager::try_from_mnemonic(mnemonic.as_str().to_owned())?)
}

#[cfg(feature = "private_key_secret_manager")]
SecretManagerDto::PrivateKey(private_key) => {
Self::PrivateKey(Box::new(PrivateKeySecretManager::try_from_hex(private_key)?))
}

SecretManagerDto::HexSeed(hex_seed) => {
// `SecretManagerDto` is `ZeroizeOnDrop` so it will take care of zeroizing the original.
Self::Mnemonic(MnemonicSecretManager::try_from_hex_seed(hex_seed)?)
Expand Down Expand Up @@ -247,6 +298,10 @@ impl From<&SecretManager> for SecretManagerDto {
// the client/wallet we also don't need to convert it in this direction with the mnemonic/seed, we only need
// to know the type
SecretManager::Mnemonic(_mnemonic) => Self::Mnemonic("...".to_string().into()),

#[cfg(feature = "private_key_secret_manager")]
SecretManager::PrivateKey(_private_key) => Self::PrivateKey("...".to_string().into()),

SecretManager::Placeholder => Self::Placeholder,
}
}
Expand Down Expand Up @@ -277,6 +332,12 @@ impl SecretManage for SecretManager {
.generate_ed25519_addresses(coin_type, account_index, address_indexes, options)
.await
}
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(secret_manager) => {
secret_manager
.generate_ed25519_addresses(coin_type, account_index, address_indexes, options)
.await
}
Self::Placeholder => Err(Error::PlaceholderSecretManager),
}
}
Expand All @@ -302,6 +363,12 @@ impl SecretManage for SecretManager {
.generate_evm_addresses(coin_type, account_index, address_indexes, options)
.await
}
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(secret_manager) => {
secret_manager
.generate_evm_addresses(coin_type, account_index, address_indexes, options)
.await
}
Self::Placeholder => Err(Error::PlaceholderSecretManager),
}
}
Expand All @@ -313,6 +380,8 @@ impl SecretManage for SecretManager {
#[cfg(feature = "ledger_nano")]
Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_ed25519(msg, chain).await?),
Self::Mnemonic(secret_manager) => secret_manager.sign_ed25519(msg, chain).await,
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(secret_manager) => secret_manager.sign_ed25519(msg, chain).await,
Self::Placeholder => Err(Error::PlaceholderSecretManager),
}
}
Expand All @@ -328,6 +397,8 @@ impl SecretManage for SecretManager {
#[cfg(feature = "ledger_nano")]
Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_secp256k1_ecdsa(msg, chain).await?),
Self::Mnemonic(secret_manager) => secret_manager.sign_secp256k1_ecdsa(msg, chain).await,
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(secret_manager) => secret_manager.sign_secp256k1_ecdsa(msg, chain).await,
Self::Placeholder => Err(Error::PlaceholderSecretManager),
}
}
Expand All @@ -351,6 +422,12 @@ impl SecretManage for SecretManager {
.sign_transaction_essence(prepared_transaction_data, time)
.await
}
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(secret_manager) => {
secret_manager
.sign_transaction_essence(prepared_transaction_data, time)
.await
}
Self::Placeholder => Err(Error::PlaceholderSecretManager),
}
}
Expand All @@ -365,6 +442,8 @@ impl SecretManage for SecretManager {
#[cfg(feature = "ledger_nano")]
Self::LedgerNano(secret_manager) => Ok(secret_manager.sign_transaction(prepared_transaction_data).await?),
Self::Mnemonic(secret_manager) => secret_manager.sign_transaction(prepared_transaction_data).await,
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(secret_manager) => secret_manager.sign_transaction(prepared_transaction_data).await,
Self::Placeholder => Err(Error::PlaceholderSecretManager),
}
}
Expand All @@ -390,6 +469,8 @@ impl SecretManagerConfig for SecretManager {
#[cfg(feature = "ledger_nano")]
Self::LedgerNano(s) => s.to_config().map(Self::Config::LedgerNano),
Self::Mnemonic(_) => None,
#[cfg(feature = "private_key_secret_manager")]
Self::PrivateKey(_) => None,
Self::Placeholder => None,
}
}
Expand All @@ -406,6 +487,10 @@ impl SecretManagerConfig for SecretManager {
SecretManagerDto::Mnemonic(mnemonic) => {
Self::Mnemonic(MnemonicSecretManager::try_from_mnemonic(mnemonic.as_str().to_owned())?)
}
#[cfg(feature = "private_key_secret_manager")]
SecretManagerDto::PrivateKey(private_key) => {
Self::PrivateKey(Box::new(PrivateKeySecretManager::try_from_hex(private_key.to_owned())?))
}
SecretManagerDto::Placeholder => Self::Placeholder,
})
}
Expand Down
132 changes: 132 additions & 0 deletions sdk/src/client/secret/private_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

//! Implementation of [`PrivateKeySecretManager`].

use std::ops::Range;

use async_trait::async_trait;
use crypto::{
hashes::{blake2b::Blake2b256, Digest},
keys::bip44::Bip44,
signatures::{
ed25519,
secp256k1_ecdsa::{self, EvmAddress},
},
};
use zeroize::{Zeroize, Zeroizing};

use super::{GenerateAddressOptions, SecretManage};
use crate::{
client::{api::PreparedTransactionData, Error},
types::block::{
address::Ed25519Address, payload::transaction::TransactionPayload, signature::Ed25519Signature, unlock::Unlocks,
},
};

/// Secret manager based on a single private key.
pub struct PrivateKeySecretManager(ed25519::SecretKey);

impl std::fmt::Debug for PrivateKeySecretManager {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("PrivateKeySecretManager").finish()
}
}

#[async_trait]
impl SecretManage for PrivateKeySecretManager {
type Error = Error;

async fn generate_ed25519_addresses(
&self,
_coin_type: u32,
_account_index: u32,
_address_indexes: Range<u32>,
_options: impl Into<Option<GenerateAddressOptions>> + Send,
) -> Result<Vec<Ed25519Address>, Self::Error> {
let public_key = self.0.public_key().to_bytes();

// Hash the public key to get the address
let result = Blake2b256::digest(public_key).try_into().map_err(|_e| {
crate::client::Error::Blake2b256("hashing the public key while generating the address failed.")
})?;

crate::client::Result::Ok(vec![Ed25519Address::new(result)])
}

async fn generate_evm_addresses(
&self,
_coin_type: u32,
_account_index: u32,
_address_indexes: Range<u32>,
_options: impl Into<Option<GenerateAddressOptions>> + Send,
) -> Result<Vec<EvmAddress>, Self::Error> {
// TODO replace with a more fitting variant.
Err(Error::SecretManagerMismatch)
}

async fn sign_ed25519(&self, msg: &[u8], _chain: Bip44) -> Result<Ed25519Signature, Self::Error> {
let public_key = self.0.public_key();
let signature = self.0.sign(msg);

Ok(Ed25519Signature::new(public_key, signature))
}

async fn sign_secp256k1_ecdsa(
&self,
_msg: &[u8],
_chain: Bip44,
) -> Result<(secp256k1_ecdsa::PublicKey, secp256k1_ecdsa::RecoverableSignature), Self::Error> {
// TODO replace with a more fitting variant.
Err(Error::SecretManagerMismatch)
}

async fn sign_transaction_essence(
&self,
prepared_transaction_data: &PreparedTransactionData,
time: Option<u32>,
) -> Result<Unlocks, Self::Error> {
super::default_sign_transaction_essence(self, prepared_transaction_data, time).await
}

async fn sign_transaction(
&self,
prepared_transaction_data: PreparedTransactionData,
) -> Result<TransactionPayload, Self::Error> {
super::default_sign_transaction(self, prepared_transaction_data).await
}
}

impl PrivateKeySecretManager {
/// Create a new [`PrivateKeySecretManager`] from a base 58 encoded private key.
pub fn try_from_b58<T: AsRef<[u8]>>(b58: T) -> Result<Self, Error> {
let mut bytes = [0u8; ed25519::SecretKey::LENGTH];

// TODO replace with a more fitting variant.
if bs58::decode(b58.as_ref())
.onto(&mut bytes)
.map_err(|_| crypto::Error::PrivateKeyError)?
!= ed25519::SecretKey::LENGTH
{
// TODO replace with a more fitting variant.
return Err(crypto::Error::PrivateKeyError.into());
}

let private_key = Self(ed25519::SecretKey::from_bytes(&bytes));

bytes.zeroize();

Ok(private_key)
}

/// Create a new [`PrivateKeySecretManager`] from an hex encoded private key.
pub fn try_from_hex(hex: impl Into<Zeroizing<String>>) -> Result<Self, Error> {
let mut bytes = prefix_hex::decode(hex.into())?;

let private_key = Self(ed25519::SecretKey::from_bytes(&bytes));

bytes.zeroize();

Ok(private_key)
}
}
Loading

0 comments on commit a8b5390

Please sign in to comment.