use std::{collections::HashMap, ops::Range};
use async_trait::async_trait;
use crypto::keys::slip10::Chain;
use iota_ledger_nano::{
get_app_config, get_buffer_size, get_ledger, get_opened_app, LedgerBIP32Index, Packable as LedgerNanoPackable,
TransportTypes,
};
use packable::{error::UnexpectedEOF, unpacker::SliceUnpacker, Packable, PackableExt};
use tokio::sync::Mutex;
use super::{GenerateAddressOptions, SecretManage, SecretManageExt};
use crate::{
client::secret::{
is_alias_transition,
types::{LedgerApp, LedgerDeviceType},
LedgerNanoStatus, PreparedTransactionData,
},
types::block::{
address::{Address, AliasAddress, Ed25519Address, NftAddress},
output::Output,
payload::transaction::TransactionEssence,
signature::{Ed25519Signature, Signature},
unlock::{AliasUnlock, NftUnlock, ReferenceUnlock, Unlock, Unlocks},
},
utils::unix_timestamp_now,
};
pub const HARDENED: u32 = 0x8000_0000;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("denied by user")]
DeniedByUser,
#[error("ledger locked")]
DongleLocked,
#[error("ledger device not found")]
DeviceNotFound,
#[error("ledger essence too large")]
EssenceTooLarge,
#[error("ledger transport error")]
MiscError,
#[error("unsupported operation")]
UnsupportedOperation,
#[error("{0}")]
Block(Box<crate::types::block::Error>),
#[error("missing input with ed25519 address")]
MissingInputWithEd25519Address,
#[error("missing bip32 chain")]
MissingBip32Chain,
#[error("Bip32 chain mismatch")]
Bip32ChainMismatch,
#[error("{0}")]
Unpack(#[from] packable::error::UnpackError<crate::types::block::Error, UnexpectedEOF>),
#[error("No available inputs provided")]
NoAvailableInputsProvided,
}
impl From<crate::types::block::Error> for Error {
fn from(error: crate::types::block::Error) -> Self {
Self::Block(Box::new(error))
}
}
#[cfg(feature = "ledger_nano")]
impl From<iota_ledger_nano::api::errors::APIError> for Error {
fn from(error: iota_ledger_nano::api::errors::APIError) -> Self {
log::info!("ledger error: {}", error);
match error {
iota_ledger_nano::api::errors::APIError::ConditionsOfUseNotSatisfied => Self::DeniedByUser,
iota_ledger_nano::api::errors::APIError::EssenceTooLarge => Self::EssenceTooLarge,
iota_ledger_nano::api::errors::APIError::SecurityStatusNotSatisfied => Self::DongleLocked,
iota_ledger_nano::api::errors::APIError::TransportError => Self::DeviceNotFound,
_ => Self::MiscError,
}
}
}
#[derive(Default)]
pub struct LedgerSecretManager {
pub is_simulator: bool,
pub mutex: Mutex<()>,
}
impl TryFrom<u8> for LedgerDeviceType {
type Error = Error;
fn try_from(device: u8) -> Result<Self, Self::Error> {
match device {
0 => Ok(Self::LedgerNanoS),
1 => Ok(Self::LedgerNanoX),
2 => Ok(Self::LedgerNanoSPlus),
_ => Err(Error::MiscError),
}
}
}
#[async_trait]
impl SecretManage for LedgerSecretManager {
type Error = Error;
async fn generate_addresses(
&self,
coin_type: u32,
account_index: u32,
address_indexes: Range<u32>,
options: Option<GenerateAddressOptions>,
) -> Result<Vec<Address>, Self::Error> {
let options = options.unwrap_or_default();
let bip32_account = account_index | HARDENED;
let bip32 = LedgerBIP32Index {
bip32_index: address_indexes.start | HARDENED,
bip32_change: u32::from(options.internal) | HARDENED,
};
let lock = self.mutex.lock().await;
let ledger = get_ledger(coin_type, bip32_account, self.is_simulator)?;
let addresses = if options.ledger_nano_prompt {
ledger.get_addresses(true, bip32, address_indexes.len())?
} else {
ledger.get_addresses(false, bip32, address_indexes.len())?
};
drop(lock);
let mut ed25519_addresses = Vec::new();
for address in addresses {
ed25519_addresses.push(Address::Ed25519(Ed25519Address::new(address)));
}
Ok(ed25519_addresses)
}
async fn sign_ed25519(&self, _msg: &[u8], _chain: &Chain) -> Result<Ed25519Signature, Self::Error> {
Err(Error::UnsupportedOperation)
}
}
pub fn needs_blind_signing(prepared_transaction: &PreparedTransactionData, buffer_size: usize) -> bool {
match &prepared_transaction.essence {
TransactionEssence::Regular(essence) => {
for output in essence.outputs().iter() {
if let Output::Basic(output) = output {
if output.simple_deposit_address().is_some() {
continue;
}
}
return true;
}
}
}
let essence_bytes = prepared_transaction.essence.pack_to_vec();
let total_size =
LedgerBIP32Index::default().packed_len() * prepared_transaction.inputs_data.len() + essence_bytes.len();
total_size > buffer_size
}
#[async_trait]
impl SecretManageExt for LedgerSecretManager {
async fn sign_transaction_essence(
&self,
prepared_transaction: &PreparedTransactionData,
time: Option<u32>,
) -> Result<Unlocks, <Self as SecretManage>::Error> {
let mut input_bip32_indices: Vec<LedgerBIP32Index> = Vec::new();
let mut coin_type: Option<u32> = None;
let mut account_index: Option<u32> = None;
let input_len = prepared_transaction.inputs_data.len();
for input in &prepared_transaction.inputs_data {
let bip32_indices: Vec<u32> = match &input.chain {
Some(chain) => {
chain
.segments()
.iter()
.map(|seg| u32::from_be_bytes(seg.bs()))
.collect()
}
None => return Err(Error::MissingBip32Chain)?,
};
if (coin_type.is_some() && coin_type != Some(bip32_indices[1]))
|| (account_index.is_some() && account_index != Some(bip32_indices[2]))
{
return Err(Error::Bip32ChainMismatch);
}
coin_type = Some(bip32_indices[1]);
account_index = Some(bip32_indices[2]);
input_bip32_indices.push(LedgerBIP32Index {
bip32_change: bip32_indices[3] | HARDENED,
bip32_index: bip32_indices[4] | HARDENED,
});
}
if coin_type.is_none() || account_index.is_none() {
return Err(Error::NoAvailableInputsProvided)?;
}
let coin_type = coin_type.unwrap() & !HARDENED;
let bip32_account = account_index.unwrap() | HARDENED;
let essence_bytes = prepared_transaction.essence.pack_to_vec();
let essence_hash = prepared_transaction.essence.hash().to_vec();
let lock = self.mutex.lock().await;
let ledger = get_ledger(coin_type, bip32_account, self.is_simulator)?;
let blind_signing = needs_blind_signing(prepared_transaction, ledger.get_buffer_size());
if blind_signing {
log::debug!("[LEDGER] prepare_blind_signing");
log::debug!("[LEDGER] {:?} {:?}", input_bip32_indices, essence_hash);
ledger.prepare_blind_signing(input_bip32_indices, essence_hash)?;
} else {
let (remainder_address, remainder_bip32): (Option<&Address>, LedgerBIP32Index) =
match &prepared_transaction.remainder {
Some(a) => {
let remainder_bip32_indices: Vec<u32> = match &a.chain {
Some(chain) => {
chain
.segments()
.iter()
.map(|seg| u32::from_be_bytes(seg.bs()))
.collect()
}
None => return Err(Error::MissingBip32Chain),
};
(
Some(&a.address),
LedgerBIP32Index {
bip32_change: remainder_bip32_indices[3] | HARDENED,
bip32_index: remainder_bip32_indices[4] | HARDENED,
},
)
}
None => (None, LedgerBIP32Index::default()),
};
let mut remainder_index = 0u16;
if let Some(remainder_address) = remainder_address {
match &prepared_transaction.essence {
TransactionEssence::Regular(essence) => {
'essence_outputs: for output in essence.outputs().iter() {
if let Output::Basic(s) = output {
if let Some(address) = s.unlock_conditions().address() {
if *remainder_address == *address.address() {
break 'essence_outputs;
}
}
} else {
log::debug!("[LEDGER] unsupported output");
return Err(Error::MiscError);
}
remainder_index += 1;
}
if remainder_index as usize == essence.outputs().len() {
log::debug!("[LEDGER] remainder_index not found");
return Err(Error::MiscError);
}
}
}
}
log::debug!("[LEDGER] prepare signing");
log::debug!(
"[LEDGER] {:?} {:02x?} {} {} {:?}",
input_bip32_indices,
essence_bytes,
remainder_address.is_some(),
remainder_index,
remainder_bip32
);
ledger.prepare_signing(
input_bip32_indices,
essence_bytes,
remainder_address.is_some(),
remainder_index,
remainder_bip32,
)?;
}
log::debug!("[LEDGER] await user confirmation");
ledger.user_confirm()?;
let signature_bytes = ledger.sign(input_len as u16)?;
drop(ledger);
drop(lock);
let mut unpacker = SliceUnpacker::new(&signature_bytes);
let mut unlocks = Vec::new();
for _ in 0..input_len {
let unlock = Unlock::unpack::<_, true>(&mut unpacker, &())?;
match unlock {
Unlock::Signature(_) => {
if !unlocks.contains(&unlock) {
unlocks.push(unlock);
}
}
_ => unlocks.push(unlock),
}
}
if blind_signing {
unlocks = merge_unlocks(prepared_transaction, unlocks.into_iter(), time)?;
}
Ok(Unlocks::new(unlocks)?)
}
}
impl LedgerSecretManager {
pub fn new(is_simulator: bool) -> Self {
Self {
is_simulator,
mutex: Mutex::new(()),
}
}
pub async fn get_ledger_nano_status(&self) -> LedgerNanoStatus {
log::debug!("get_ledger_nano_status");
let _lock = self.mutex.lock().await;
let transport_type = if self.is_simulator {
TransportTypes::TCP
} else {
TransportTypes::NativeHID
};
log::debug!("get_opened_app");
let app = match get_opened_app(&transport_type) {
Ok((name, version)) => Some(LedgerApp { name, version }),
_ => None,
};
log::debug!("get_app_config");
let (connected_, locked, blind_signing_enabled, device) =
get_app_config(&transport_type).map_or((false, false, false, None), |config| {
(
true,
config.flags & (1 << 0) != 0,
config.flags & (1 << 1) != 0,
LedgerDeviceType::try_from(config.device).ok(),
)
});
log::debug!("get_buffer_size");
let buffer_size = get_buffer_size(&transport_type).ok();
let connected = if app.is_some() { true } else { connected_ };
LedgerNanoStatus {
connected,
locked,
blind_signing_enabled,
app,
device,
buffer_size,
}
}
}
fn merge_unlocks(
prepared_transaction_data: &PreparedTransactionData,
mut unlocks: impl Iterator<Item = Unlock>,
time: Option<u32>,
) -> Result<Vec<Unlock>, Error> {
let hashed_essence = prepared_transaction_data.essence.hash();
let time = time.unwrap_or_else(|| unix_timestamp_now().as_secs() as u32);
let mut merged_unlocks = Vec::new();
let mut block_indexes = HashMap::<Address, usize>::new();
for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() {
let TransactionEssence::Regular(regular) = &prepared_transaction_data.essence;
let alias_transition = is_alias_transition(input, regular.outputs()).map(|t| t.0);
let (input_address, _) =
input
.output
.required_and_unlocked_address(time, input.output_metadata.output_id(), alias_transition)?;
match block_indexes.get(&input_address) {
Some(block_index) => match input_address {
Address::Alias(_alias) => merged_unlocks.push(Unlock::Alias(AliasUnlock::new(*block_index as u16)?)),
Address::Ed25519(_ed25519) => {
merged_unlocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?));
}
Address::Nft(_nft) => merged_unlocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)),
},
None => {
if !input_address.is_ed25519() {
return Err(Error::MissingInputWithEd25519Address)?;
}
let unlock = unlocks.next().ok_or(Error::MissingInputWithEd25519Address)?;
if let Unlock::Signature(signature_unlock) = &unlock {
let Signature::Ed25519(ed25519_signature) = signature_unlock.signature();
let ed25519_address = match input_address {
Address::Ed25519(ed25519_address) => ed25519_address,
_ => return Err(Error::MissingInputWithEd25519Address)?,
};
ed25519_signature.is_valid(&hashed_essence, &ed25519_address)?;
}
merged_unlocks.push(unlock);
block_indexes.insert(input_address, current_block_index);
}
}
match &input.output {
Output::Alias(alias_output) => block_indexes.insert(
Address::Alias(AliasAddress::new(alias_output.alias_id_non_null(input.output_id()))),
current_block_index,
),
Output::Nft(nft_output) => block_indexes.insert(
Address::Nft(NftAddress::new(nft_output.nft_id_non_null(input.output_id()))),
current_block_index,
),
_ => None,
};
}
Ok(merged_unlocks)
}