Skip to content

Commit

Permalink
feat: add encryption service (#4225)
Browse files Browse the repository at this point in the history
Description
---
* The PR implements encryption of the `EncryptedValue` type.
* The new methods `encrypt_value` and `decrypt_value` added to the `OutputManagerService` (handle).
* Implemented `wallet_ffi` methods: `encrypted_value_encrypt` and `encrypted_value_decrypt`

Motivation and Context
---
The part of the BP+ feature.

How Has This Been Tested?
---
Unit tests + CI
  • Loading branch information
therustmonk authored Jul 1, 2022
1 parent 0432f23 commit 6ce6b89
Show file tree
Hide file tree
Showing 28 changed files with 5,160 additions and 5,313 deletions.
1,862 changes: 796 additions & 1,066 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion applications/test_faucet/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ fn create_utxo(
if !factories.range_proof.verify(&proof, &commitment) {
panic!("Range proof does not verify");
};
let encrypted_value = EncryptedValue::todo_encrypt_from(value);
let encrypted_value = EncryptedValue::default();
let metadata_sig = TransactionOutput::create_final_metadata_signature(
TransactionOutputVersion::get_current_version(),
value,
Expand Down
1 change: 1 addition & 0 deletions base_layer/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ bincode = "1.1.4"
bitflags = "1.0.4"
blake2 = "^0.9.0"
bytes = "0.5"
chacha20poly1305 = "0.9.0"
chrono = { version = "0.4.19", default-features = false, features = ["serde"] }
criterion = { version = "0.3.5", optional = true }
croaring = { version = "0.5.2", optional = true }
Expand Down
8,002 changes: 4,001 additions & 4,001 deletions base_layer/core/src/blocks/faucets/dibbler_faucet.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions base_layer/core/src/blocks/genesis_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ pub fn get_dibbler_genesis_block() -> ChainBlock {
// println!("output mr: {}", block.header.output_mr.to_hex());

// Hardcode the Merkle roots once they've been computed above
block.header.kernel_mr = from_hex("79762ea3b7aeb45e932ad4a1def59744ebfd7b85b84269bd5ec2f3d1c4da8c00").unwrap();
block.header.witness_mr = from_hex("e84e9735700147a4dcbb2bb3161b6df91411cb71c0974c08f2e1e965936412d3").unwrap();
block.header.output_mr = from_hex("f4a6a58046dc15b446b572f559c112c5173b9c24a234c7c2059aaed99ac496e1").unwrap();
block.header.kernel_mr = from_hex("8bec1140bfac718ab3acd8a5e19c1bb28e0e4a57663c2fc7e8c7155cc355aac3").unwrap();
block.header.witness_mr = from_hex("1df4a4200338686763c784187f7077148986e088586cf4839147a3f56adc4af6").unwrap();
block.header.output_mr = from_hex("f9616ca84e798022f638546e6ce372d1344eee56e5cf47ba7e2bf58b5e28bf45").unwrap();

let accumulated_data = BlockHeaderAccumulatedData {
hash: block.hash(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ mod fetch_utxo_by_unique_id {
.fetch_utxo_by_unique_id(Some(asset_pk.clone()), unique_id.clone(), None)
.unwrap()
.unwrap();
features.set_recovery_byte(info.output.as_transaction_output().unwrap().features.recovery_byte);
assert_eq!(info.output.as_transaction_output().unwrap().features, features);
let expected_commitment =
CommitmentFactory::default().commit_value(&asset_utxo1.spending_key, asset_utxo1.value.as_u64());
Expand Down
25 changes: 19 additions & 6 deletions base_layer/core/src/transactions/coinbase_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub enum CoinbaseBuildError {
MissingSpendKey,
#[error("The script key for this coinbase transaction wasn't provided")]
MissingScriptKey,
#[error("The value encryption was not succeed")]
ValueEncryptionFailed,
#[error("An error occurred building the final transaction: `{0}`")]
BuildError(String),
#[error("Some inconsistent data was given to the builder. This transaction is not valid")]
Expand Down Expand Up @@ -206,7 +208,14 @@ impl CoinbaseBuilder {
let sender_offset_public_key = PublicKey::from_secret_key(&sender_offset_private_key);
let covenant = self.covenant;

let encrypted_value = EncryptedValue::todo_encrypt_from(total_reward);
let encrypted_value = self
.rewind_data
.as_ref()
.map(|rd| EncryptedValue::encrypt_value(&rd.encryption_key, &commitment, total_reward))
.transpose()
.map_err(|_| CoinbaseBuildError::ValueEncryptionFailed)?
.unwrap_or_default();

let metadata_sig = TransactionOutput::create_final_metadata_signature(
TransactionOutputVersion::get_current_version(),
total_reward,
Expand Down Expand Up @@ -276,9 +285,9 @@ mod test {
transactions::{
coinbase_builder::CoinbaseBuildError,
crypto_factories::CryptoFactories,
tari_amount::{uT, MicroTari},
tari_amount::uT,
test_helpers::TestParams,
transaction_components::{KernelFeatures, OutputFeatures, OutputType, TransactionError},
transaction_components::{EncryptedValue, KernelFeatures, OutputFeatures, OutputType, TransactionError},
transaction_protocol::RewindData,
CoinbaseBuilder,
},
Expand Down Expand Up @@ -366,6 +375,7 @@ mod test {
let rewind_data = RewindData {
rewind_blinding_key: rewind_blinding_key.clone(),
recovery_byte_key: PrivateKey::random(&mut OsRng),
encryption_key: PrivateKey::random(&mut OsRng),
};

let p = TestParams::new();
Expand All @@ -375,15 +385,18 @@ mod test {
.with_fees(145 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone())
.with_rewind_data(rewind_data);
.with_rewind_data(rewind_data.clone());
let (tx, _) = builder
.build(rules.consensus_constants(42), rules.emission_schedule())
.unwrap();
let block_reward = rules.emission_schedule().block_reward(42) + 145 * uT;

let committed_value = MicroTari::from(tx.body.outputs()[0].encrypted_value.todo_decrypt());
let output = &tx.body.outputs()[0];
let committed_value =
EncryptedValue::decrypt_value(&rewind_data.encryption_key, &output.commitment, &output.encrypted_value)
.unwrap();
assert_eq!(committed_value, block_reward);
let blinding_factor = tx.body.outputs()[0]
let blinding_factor = output
.recover_mask(&factories.range_proof, &rewind_blinding_key)
.unwrap();
assert_eq!(blinding_factor, p.spend_key);
Expand Down
33 changes: 26 additions & 7 deletions base_layer/core/src/transactions/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use tari_crypto::{
};
use tari_script::{inputs, script, ExecutionStack, TariScript};

use super::transaction_components::{EncryptedValue, TransactionInputVersion, TransactionOutputVersion};
use super::transaction_components::{TransactionInputVersion, TransactionOutputVersion};
use crate::{
consensus::{ConsensusEncodingSized, ConsensusManager},
covenants::Covenant,
Expand All @@ -42,6 +42,7 @@ use crate::{
fee::Fee,
tari_amount::MicroTari,
transaction_components::{
EncryptedValue,
KernelBuilder,
KernelFeatures,
OutputFeatures,
Expand Down Expand Up @@ -153,6 +154,7 @@ impl TestParams {
rewind_data: RewindData {
rewind_blinding_key: PrivateKey::random(&mut OsRng),
recovery_byte_key: PrivateKey::random(&mut OsRng),
encryption_key: PrivateKey::random(&mut OsRng),
},
}
}
Expand All @@ -176,7 +178,11 @@ impl TestParams {
let updated_features =
OutputFeatures::features_with_updated_recovery_byte(&commitment, rewind_data, &params.features);

let encrypted_value = EncryptedValue::todo_encrypt_from(params.value);
let encrypted_value = if let Some(rewind_data) = rewind_data {
EncryptedValue::encrypt_value(&rewind_data.encryption_key, &commitment, params.value).unwrap()
} else {
EncryptedValue::default()
};
let metadata_signature = TransactionOutput::create_final_metadata_signature(
TransactionOutputVersion::get_current_version(),
params.value,
Expand Down Expand Up @@ -344,6 +350,20 @@ pub fn create_unblinded_output(
})
}

pub fn create_unblinded_output_with_rewind_data(
script: TariScript,
output_features: OutputFeatures,
test_params: &TestParams,
value: MicroTari,
) -> UnblindedOutput {
test_params.create_unblinded_output_with_rewind_data(UtxoTestParams {
value,
script,
features: output_features,
..Default::default()
})
}

pub fn update_unblinded_output_with_updated_output_features(
test_params: &TestParams,
uo: UnblindedOutput,
Expand Down Expand Up @@ -738,8 +758,8 @@ pub fn create_stx_protocol(schema: TransactionSchema) -> (SenderTransactionProto
..Default::default()
};

// TODO: Get it using `something.encrypt_value(change)`
let encrypted_value = EncryptedValue::todo_encrypt_from(change);
let encrypted_value = EncryptedValue::default();

let change_metadata_sig = TransactionOutput::create_final_metadata_signature(
output_version,
change,
Expand Down Expand Up @@ -809,7 +829,6 @@ pub fn create_utxo(

let updated_features = OutputFeatures::features_with_updated_recovery_byte(&commitment, None, features);

let encrypted_value = EncryptedValue::todo_encrypt_from(value);
let metadata_sig = TransactionOutput::create_final_metadata_signature(
TransactionOutputVersion::get_current_version(),
value,
Expand All @@ -818,7 +837,7 @@ pub fn create_utxo(
&updated_features,
&offset_keys.k,
covenant,
&encrypted_value,
&EncryptedValue::default(),
)
.unwrap();

Expand All @@ -830,7 +849,7 @@ pub fn create_utxo(
offset_keys.pk,
metadata_sig,
covenant.clone(),
encrypted_value,
EncryptedValue::default(),
);
utxo.verify_range_proof(&CryptoFactories::default().range_proof)
.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,23 @@

use std::io::{self, Read, Write};

use blake2::Digest;
use chacha20poly1305::{
aead::{Aead, Error, NewAead, Payload},
ChaCha20Poly1305,
Key,
Nonce,
};
use serde::{Deserialize, Serialize};
use tari_common_types::types::{Commitment, PrivateKey};
use tari_crypto::common::Blake256;
use tari_utilities::{ByteArray, ByteArrayError};
use thiserror::Error;

use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized};
use crate::{
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized},
transactions::tari_amount::MicroTari,
};

const SIZE: usize = 24;

Expand All @@ -52,25 +65,68 @@ impl ByteArray for EncryptedValue {
}
}

#[derive(Debug, Error)]
pub enum EncryptionError {
#[error("Encryption failed: {0}")]
EncryptionFailed(Error),
}

// chacha error is not StdError compatible
impl From<Error> for EncryptionError {
fn from(err: Error) -> Self {
Self::EncryptionFailed(err)
}
}

impl EncryptedValue {
/// TODO: Replace this method with a real call of encryption service
/// that will produce an encrypted value from the given `amount`.
pub fn todo_encrypt_from(amount: impl Into<u64>) -> Self {
const TAG: &'static [u8] = b"TARI_AAD_VALUE";

pub fn encrypt_value(
encryption_key: &PrivateKey,
commitment: &Commitment,
value: MicroTari,
) -> Result<EncryptedValue, EncryptionError> {
let aead_key = kdf_aead(encryption_key, commitment);
// Encrypt the value (with fixed length) using ChaCha20-Poly1305 with a fixed zero nonce
let aead_payload = Payload {
msg: &value.as_u64().to_le_bytes(),
aad: Self::TAG,
};
// Included in the public transaction
let buffer = ChaCha20Poly1305::new(&aead_key).encrypt(&Nonce::default(), aead_payload)?;
let mut data: [u8; SIZE] = [0; SIZE];
let value = amount.into().to_le_bytes();
data[0..8].copy_from_slice(&value);
Self(data)
data.copy_from_slice(&buffer);
Ok(EncryptedValue(data))
}

/// TODO: Replace this method with a real call of decryption service
/// that will produce a decrypted value from self.
pub fn todo_decrypt(&self) -> u64 {
let mut buffer = [0u8; 8];
(&mut buffer[0..8]).copy_from_slice(&self.0.as_slice()[0..8]);
u64::from_le_bytes(buffer)
pub fn decrypt_value(
encryption_key: &PrivateKey,
commitment: &Commitment,
value: &EncryptedValue,
) -> Result<MicroTari, EncryptionError> {
let aead_key = kdf_aead(encryption_key, commitment);
// Authenticate and decrypt the value
let aead_payload = Payload {
msg: value.as_bytes(),
aad: Self::TAG,
};
let mut value_bytes = [0u8; 8];
let decrypted_bytes = ChaCha20Poly1305::new(&aead_key).decrypt(&Nonce::default(), aead_payload)?;
value_bytes.clone_from_slice(&decrypted_bytes[..8]);
Ok(u64::from_le_bytes(value_bytes).into())
}
}

// Generate a ChaCha20-Poly1305 key from an ECDH shared secret and commitment using Blake2b
fn kdf_aead(shared_secret: &PrivateKey, commitment: &Commitment) -> Key {
const AEAD_KEY_LENGTH: usize = 32; // The length in bytes of a ChaCha20-Poly1305 AEAD key
let mut hasher = Blake256::with_params(&[], b"SCAN_AEAD".as_ref(), b"TARI_KDF".as_ref());
hasher.update(shared_secret.as_bytes());
hasher.update(commitment.as_bytes());
let output = hasher.finalize();
*Key::from_slice(&output[..AEAD_KEY_LENGTH])
}

impl ConsensusEncoding for EncryptedValue {
fn consensus_encode<W: Write>(&self, writer: &mut W) -> Result<(), io::Error> {
self.0.consensus_encode(writer)?;
Expand All @@ -86,27 +142,53 @@ impl ConsensusEncodingSized for EncryptedValue {

impl ConsensusDecoding for EncryptedValue {
fn consensus_decode<R: Read>(reader: &mut R) -> Result<Self, io::Error> {
let data = <[u8; 24]>::consensus_decode(reader)?;
let data = <[u8; SIZE]>::consensus_decode(reader)?;
Ok(Self(data))
}
}

#[cfg(test)]
mod test {
use rand::rngs::OsRng;
use tari_common_types::types::{CommitmentFactory, PrivateKey};
use tari_crypto::{
commitment::HomomorphicCommitmentFactory,
keys::{PublicKey, SecretKey},
};

use super::*;
use crate::consensus::ToConsensusBytes;

#[test]
fn it_encodes_to_bytes() {
let bytes = EncryptedValue::todo_encrypt_from(123u64).to_consensus_bytes();
assert_eq!(&bytes[0..8], &123u64.to_le_bytes());
let commitment_factory = CommitmentFactory::default();
let spending_key = PrivateKey::random(&mut OsRng);
let encryption_key = PrivateKey::random(&mut OsRng);
let value = 123u64;
let commitment = commitment_factory.commit(&spending_key, &PrivateKey::from(value));
let bytes = EncryptedValue::encrypt_value(&encryption_key, &commitment, value.into())
.unwrap()
.to_consensus_bytes();
assert_eq!(bytes.len(), SIZE);
}

#[test]
fn it_decodes_from_bytes() {
let value = &[0; 24];
let value = &[0; SIZE];
let encrypted_value = EncryptedValue::consensus_decode(&mut &value[..]).unwrap();
assert_eq!(encrypted_value, EncryptedValue::default());
}

#[test]
fn it_encrypts_and_decrypts_correctly() {
for value in [0, 123456, 654321, u64::MAX] {
let commitment = Commitment::from_public_key(&PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)));
let encryption_key = PrivateKey::random(&mut OsRng);
let amount = MicroTari::from(value);
let encrypted_value = EncryptedValue::encrypt_value(&encryption_key, &commitment, amount).unwrap();
let decrypted_value =
EncryptedValue::decrypt_value(&encryption_key, &commitment, &encrypted_value).unwrap();
assert_eq!(amount, decrypted_value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

pub use asset_output_features::AssetOutputFeatures;
pub use committee_definition_features::CommitteeDefinitionFeatures;
pub use encrypted_value::EncryptedValue;
pub use encrypted_value::{EncryptedValue, EncryptionError};
pub use error::TransactionError;
pub use kernel_builder::KernelBuilder;
pub use kernel_features::KernelFeatures;
Expand Down
3 changes: 3 additions & 0 deletions base_layer/core/src/transactions/transaction_protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ pub enum TransactionProtocolError {
ConversionError(String),
#[error("The script offset private key could not be found")]
ScriptOffsetPrivateKeyNotFound,
#[error("Value encryption failed")]
EncryptionError,
}

/// Transaction metadata, including the fee and lock height
Expand All @@ -146,6 +148,7 @@ pub struct RewindData {
#[derivative(Debug = "ignore")]
pub rewind_blinding_key: PrivateKey,
pub recovery_byte_key: PrivateKey,
pub encryption_key: PrivateKey,
}

/// Convenience function that calculates the challenge for the Schnorr signatures
Expand Down
Loading

0 comments on commit 6ce6b89

Please sign in to comment.