Skip to content

Commit

Permalink
fix(wallet)!: use KDFs on ECDH shared secrets (#4847)
Browse files Browse the repository at this point in the history
Description
---
Uses KDFs on ECDH shared secrets for output generation. Closes [issue 4717](#4717).

Motivation and Context
---
Several uses of ECDH shared secrets in the output manager and transaction services parse an ECDH shared secret as a scalar spending key, and use this as input to a chain of hash functions for use in rewinding and value encryption. This is non-standard.

This work uses separate KDFs to independently produce a spending key, rewind key, and value encryption key from a `DiffieHellmanSharedSecret`-type ECDH shared secret.

How Has This Been Tested?
---
Existing tests pass.

BREAKING CHANGE: Changes the way output keys are derived.
  • Loading branch information
AaronFeickert authored Nov 8, 2022
1 parent f625f73 commit 3d1a51c
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 91 deletions.
20 changes: 17 additions & 3 deletions base_layer/wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,22 @@ pub type WalletSqlite = Wallet<
>;

hash_domain!(
WalletSecretKeysDomain,
"com.tari.tari_project.base_layer.wallet.secret_keys",
WalletOutputRewindKeysDomain,
"com.tari.tari_project.base_layer.wallet.output_rewind_keys",
1
);
type WalletSecretKeysDomainHasher = DomainSeparatedHasher<Blake256, WalletSecretKeysDomain>;
type WalletOutputRewindKeysDomainHasher = DomainSeparatedHasher<Blake256, WalletOutputRewindKeysDomain>;

hash_domain!(
WalletOutputEncryptionKeysDomain,
"com.tari.tari_project.base_layer.wallet.output_encryption_keys",
1
);
type WalletOutputEncryptionKeysDomainHasher = DomainSeparatedHasher<Blake256, WalletOutputEncryptionKeysDomain>;

hash_domain!(
WalletOutputSpendingKeysDomain,
"com.tari.tari_project.base_layer.wallet.output_spending_keys",
1
);
type WalletOutputSpendingKeysDomainHasher = DomainSeparatedHasher<Blake256, WalletOutputSpendingKeysDomain>;
109 changes: 49 additions & 60 deletions base_layer/wallet/src/output_manager_service/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,11 @@ use tari_crypto::{
commitment::HomomorphicCommitmentFactory,
errors::RangeProofError,
keys::{PublicKey as PublicKeyTrait, SecretKey},
ristretto::RistrettoSecretKey,
};
use tari_script::{inputs, script, Opcode, TariScript};
use tari_service_framework::reply_channel;
use tari_shutdown::ShutdownSignal;
use tari_utilities::{hex::Hex, ByteArray};
use tari_utilities::{hex::Hex, ByteArray, ByteArrayError};
use tokio::sync::Mutex;

use crate::{
Expand Down Expand Up @@ -98,7 +97,8 @@ use crate::{
tasks::TxoValidationTask,
},
types::WalletHasher,
WalletSecretKeysDomainHasher,
WalletOutputEncryptionKeysDomainHasher,
WalletOutputRewindKeysDomainHasher,
};

const LOG_TARGET: &str = "wallet::output_manager_service";
Expand Down Expand Up @@ -2321,15 +2321,12 @@ where
pre_image: PublicKey,
fee_per_gram: MicroTari,
) -> Result<(TxId, MicroTari, MicroTari, Transaction), OutputManagerError> {
let spending_key = PrivateKey::from_bytes(
CommsDHKE::new(
self.node_identity.as_ref().secret_key(),
&output.sender_offset_public_key,
)
.as_bytes(),
)?;
let blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spending_key))?;
let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&blinding_key))?;
let shared_secret = CommsDHKE::new(
self.node_identity.as_ref().secret_key(),
&output.sender_offset_public_key,
);
let blinding_key = shared_secret_to_output_rewind_key(&shared_secret)?;
let encryption_key = shared_secret_to_output_encryption_key(&shared_secret)?;
if let Ok(amount) = EncryptedValue::decrypt_value(&encryption_key, &output.commitment, &output.encrypted_value)
{
let blinding_factor = output.recover_mask(&self.resources.factories.range_proof, &blinding_key)?;
Expand Down Expand Up @@ -2557,24 +2554,14 @@ where

// match found
Some(matched_key) => {
match PrivateKey::from_bytes(
CommsDHKE::new(&matched_key.private_key, &output.sender_offset_public_key).as_bytes(),
) {
Ok(spending_sk) => scanned_outputs.push((
output.clone(),
OutputSource::OneSided,
matched_key.private_key.clone(),
spending_sk,
)),
Err(e) => {
error!(
target: LOG_TARGET,
"failed to derive private key from DH shared secret (simple one-sided): {:?}",
e
);
continue;
},
}
let shared_secret =
CommsDHKE::new(&matched_key.private_key, &output.sender_offset_public_key);
scanned_outputs.push((
output.clone(),
OutputSource::OneSided,
matched_key.private_key.clone(),
shared_secret,
));
},
}
},
Expand All @@ -2584,8 +2571,8 @@ where
// NOTE: Extracting the nonce R and a spending (public aka scan_key) key from the script
// NOTE: [RFC 203 on Stealth Addresses](https://rfc.tari.com/RFC-0203_StealthAddresses.html)
[Opcode::PushPubKey(nonce), Opcode::Drop, Opcode::PushPubKey(scanned_pk)] => {
// computing shared secret
let shared_secret = PrivateKey::from_bytes(
// Compute the stealth address offset
let stealth_address_offset = PrivateKey::from_bytes(
WalletHasher::new_with_label("stealth_address")
.chain(CommsDHKE::new(&wallet_sk, nonce.as_ref()).as_bytes())
.finalize()
Expand All @@ -2594,27 +2581,17 @@ where
.unwrap();

// matching spending (public) keys
if &(PublicKey::from_secret_key(&shared_secret) + wallet_pk) != scanned_pk.as_ref() {
if &(PublicKey::from_secret_key(&stealth_address_offset) + wallet_pk) != scanned_pk.as_ref() {
continue;
}

match PrivateKey::from_bytes(
CommsDHKE::new(&wallet_sk, &output.sender_offset_public_key).as_bytes(),
) {
Ok(spending_sk) => scanned_outputs.push((
output.clone(),
OutputSource::StealthOneSided,
wallet_sk.clone() + shared_secret,
spending_sk,
)),
Err(e) => {
error!(
target: LOG_TARGET,
"failed to derive private key from DH shared secret (stealth one-sided): {:?}", e
);
continue;
},
}
let shared_secret = CommsDHKE::new(&wallet_sk, &output.sender_offset_public_key);
scanned_outputs.push((
output.clone(),
OutputSource::StealthOneSided,
wallet_sk.clone() + stealth_address_offset,
shared_secret,
));
},

_ => {},
Expand All @@ -2627,13 +2604,13 @@ where
// Imports scanned outputs into the wallet
fn import_onesided_outputs(
&self,
scanned_outputs: Vec<(TransactionOutput, OutputSource, PrivateKey, RistrettoSecretKey)>,
scanned_outputs: Vec<(TransactionOutput, OutputSource, PrivateKey, CommsDHKE)>,
) -> Result<Vec<RecoveredOutput>, OutputManagerError> {
let mut rewound_outputs = Vec::with_capacity(scanned_outputs.len());

for (output, output_source, script_private_key, spending_sk) in scanned_outputs {
let rewind_blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spending_sk))?;
let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_blinding_key))?;
for (output, output_source, script_private_key, shared_secret) in scanned_outputs {
let rewind_blinding_key = shared_secret_to_output_rewind_key(&shared_secret)?;
let encryption_key = shared_secret_to_output_encryption_key(&shared_secret)?;
let committed_value =
EncryptedValue::decrypt_value(&encryption_key, &output.commitment, &output.encrypted_value);

Expand Down Expand Up @@ -2750,12 +2727,24 @@ impl fmt::Display for Balance {
}
}

fn hash_secret_key(key: &PrivateKey) -> Vec<u8> {
WalletSecretKeysDomainHasher::new()
.chain(key.as_bytes())
.finalize()
.as_ref()
.to_vec()
/// Generate an output rewind key from a Diffie-Hellman shared secret
fn shared_secret_to_output_rewind_key(shared_secret: &CommsDHKE) -> Result<PrivateKey, ByteArrayError> {
PrivateKey::from_bytes(
WalletOutputRewindKeysDomainHasher::new()
.chain(shared_secret.as_bytes())
.finalize()
.as_ref(),
)
}

/// Generate an output encryption key from a Diffie-Hellman shared secret
fn shared_secret_to_output_encryption_key(shared_secret: &CommsDHKE) -> Result<PrivateKey, ByteArrayError> {
PrivateKey::from_bytes(
WalletOutputEncryptionKeysDomainHasher::new()
.chain(shared_secret.as_bytes())
.finalize()
.as_ref(),
)
}

#[derive(Debug, Clone)]
Expand Down
80 changes: 52 additions & 28 deletions base_layer/wallet/src/transaction_service/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ use tari_core::{
use tari_crypto::{
commitment::HomomorphicCommitmentFactory,
keys::{PublicKey as PKtrait, SecretKey},
tari_utilities::ByteArray,
tari_utilities::{ByteArray, ByteArrayError},
};
use tari_p2p::domain_message::DomainMessage;
use tari_script::{inputs, script, TariScript};
Expand Down Expand Up @@ -122,7 +122,9 @@ use crate::{
util::watch::Watch,
utxo_scanner_service::RECOVERY_KEY,
OperationId,
WalletSecretKeysDomainHasher,
WalletOutputEncryptionKeysDomainHasher,
WalletOutputRewindKeysDomainHasher,
WalletOutputSpendingKeysDomainHasher,
};

const LOG_TARGET: &str = "wallet::transaction_service::service";
Expand Down Expand Up @@ -1059,29 +1061,29 @@ where

// Prepare receiver part of the transaction

// Diffie-Hellman shared secret `k_Ob * K_Sb = K_Ob * k_Sb` results in a public key, which is converted to
// bytes to enable conversion into a private key to be used as the spending key
// Diffie-Hellman shared secret `k_Ob * K_Sb = K_Ob * k_Sb` results in a public key, which is fed into
// KDFs to produce the spending, rewind, and encryption keys
let sender_offset_private_key = stp
.get_recipient_sender_offset_private_key(0)
.map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?;

let spend_key =
PrivateKey::from_bytes(CommsDHKE::new(&sender_offset_private_key.clone(), &dest_pubkey.clone()).as_bytes())
.map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?;
let shared_secret = CommsDHKE::new(&sender_offset_private_key, &dest_pubkey);
let spending_key = shared_secret_to_output_spending_key(&shared_secret)
.map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?;

let sender_message = TransactionSenderMessage::new_single_round_message(stp.get_single_round_message()?);
let rewind_blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spend_key))?;
let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_blinding_key))?;
let rewind_blinding_key = shared_secret_to_output_rewind_key(&shared_secret)?;
let encryption_key = shared_secret_to_output_encryption_key(&shared_secret)?;

let rewind_data = RewindData {
rewind_blinding_key: rewind_blinding_key.clone(),
encryption_key: encryption_key.clone(),
rewind_blinding_key,
encryption_key,
};

let rtp = ReceiverTransactionProtocol::new_with_rewindable_output(
sender_message,
PrivateKey::random(&mut OsRng),
spend_key.clone(),
spending_key.clone(),
&self.resources.factories,
&rewind_data,
);
Expand All @@ -1092,12 +1094,12 @@ where
.resources
.factories
.commitment
.commit_value(&spend_key, amount.into());
.commit_value(&spending_key, amount.into());
let encrypted_value = EncryptedValue::encrypt_value(&rewind_data.encryption_key, &commitment, amount)?;
let minimum_value_promise = MicroTari::zero();
let unblinded_output = UnblindedOutput::new_current_version(
amount,
spend_key,
spending_key,
output.features.clone(),
script,
inputs!(PublicKey::from_secret_key(self.node_identity.secret_key())),
Expand Down Expand Up @@ -1220,18 +1222,18 @@ where

// Prepare receiver part of the transaction

// Diffie-Hellman shared secret `k_Ob * K_Sb = K_Ob * k_Sb` results in a public key, which is converted to
// bytes to enable conversion into a private key to be used as the spending key
// Diffie-Hellman shared secret `k_Ob * K_Sb = K_Ob * k_Sb` results in a public key, which is fed into
// KDFs to produce the spending, rewind, and encryption keys
let sender_offset_private_key = stp
.get_recipient_sender_offset_private_key(0)
.map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?;
let spend_key =
PrivateKey::from_bytes(CommsDHKE::new(&sender_offset_private_key, &dest_pubkey.clone()).as_bytes())
.map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?;
let shared_secret = CommsDHKE::new(&sender_offset_private_key, &dest_pubkey);
let spending_key = shared_secret_to_output_spending_key(&shared_secret)
.map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?;

let sender_message = TransactionSenderMessage::new_single_round_message(stp.get_single_round_message()?);
let rewind_blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spend_key))?;
let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_blinding_key))?;
let rewind_blinding_key = shared_secret_to_output_rewind_key(&shared_secret)?;
let encryption_key = shared_secret_to_output_encryption_key(&shared_secret)?;
let rewind_data = RewindData {
rewind_blinding_key,
encryption_key,
Expand All @@ -1240,7 +1242,7 @@ where
let rtp = ReceiverTransactionProtocol::new_with_rewindable_output(
sender_message,
PrivateKey::random(&mut OsRng),
spend_key,
spending_key,
&self.resources.factories,
&rewind_data,
);
Expand Down Expand Up @@ -2657,12 +2659,34 @@ pub struct PendingCoinbaseSpendingKey {
pub spending_key: PrivateKey,
}

fn hash_secret_key(key: &PrivateKey) -> Vec<u8> {
WalletSecretKeysDomainHasher::new()
.chain(key.as_bytes())
.finalize()
.as_ref()
.to_vec()
/// Generate an output rewind key from a Diffie-Hellman shared secret
fn shared_secret_to_output_rewind_key(shared_secret: &CommsDHKE) -> Result<PrivateKey, ByteArrayError> {
PrivateKey::from_bytes(
WalletOutputRewindKeysDomainHasher::new()
.chain(shared_secret.as_bytes())
.finalize()
.as_ref(),
)
}

/// Generate an output encryption key from a Diffie-Hellman shared secret
fn shared_secret_to_output_encryption_key(shared_secret: &CommsDHKE) -> Result<PrivateKey, ByteArrayError> {
PrivateKey::from_bytes(
WalletOutputEncryptionKeysDomainHasher::new()
.chain(shared_secret.as_bytes())
.finalize()
.as_ref(),
)
}

/// Generate an output spending key from a Diffie-Hellman shared secret
fn shared_secret_to_output_spending_key(shared_secret: &CommsDHKE) -> Result<PrivateKey, ByteArrayError> {
PrivateKey::from_bytes(
WalletOutputSpendingKeysDomainHasher::new()
.chain(shared_secret.as_bytes())
.finalize()
.as_ref(),
)
}

/// Contains the generated TxId and TransactionStatus transaction send result
Expand Down

0 comments on commit 3d1a51c

Please sign in to comment.