diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index 9fd0c611..1f0cd87f 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -40,20 +40,24 @@ pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] { hash(&u) } -// Hs("view_tag" || 8Ra || o) and Hs(8Ra || o) with uniqueness inclusion as an option +// Hs("view_tag" || 8Ra || o), Hs(8Ra || o), and H(8Ra || 0x8d) with uniqueness inclusion in the +// Scalar as an option #[allow(non_snake_case)] pub(crate) fn shared_key( uniqueness: Option<[u8; 32]>, s: &Scalar, P: &EdwardsPoint, o: usize, -) -> (u8, Scalar) { +) -> (u8, Scalar, [u8; 8]) { // 8Ra let mut output_derivation = (s * P).mul_by_cofactor().compress().to_bytes().to_vec(); // || o write_varint(&o.try_into().unwrap(), &mut output_derivation).unwrap(); let view_tag = hash(&[b"view_tag".as_ref(), &output_derivation].concat())[0]; + let mut payment_id_xor = [0; 8]; + payment_id_xor + .copy_from_slice(&hash(&[output_derivation.as_ref(), [0x8d].as_ref()].concat())[.. 8]); // uniqueness || let shared_key = if let Some(uniqueness) = uniqueness { @@ -62,13 +66,13 @@ pub(crate) fn shared_key( output_derivation }; - (view_tag, hash_to_scalar(&shared_key)) + (view_tag, hash_to_scalar(&shared_key), payment_id_xor) } pub(crate) fn amount_encryption(amount: u64, key: Scalar) -> [u8; 8] { let mut amount_mask = b"amount".to_vec(); amount_mask.extend(key.to_bytes()); - (amount ^ u64::from_le_bytes(hash(&amount_mask)[0 .. 8].try_into().unwrap())).to_le_bytes() + (amount ^ u64::from_le_bytes(hash(&amount_mask)[.. 8].try_into().unwrap())).to_le_bytes() } fn amount_decryption(amount: [u8; 8], key: Scalar) -> u64 { diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index cf08cb91..b2aee606 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -81,7 +81,7 @@ impl Transaction { let mut res = vec![]; for (o, output) in self.prefix.outputs.iter().enumerate() { for key in &keys { - let (view_tag, key_offset) = shared_key( + let (view_tag, key_offset, _) = shared_key( Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed), &view.view, key, diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index 19194cb1..6210d430 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -47,35 +47,39 @@ impl SendOutput { fn new( rng: &mut R, unique: [u8; 32], - output: (Address, u64), - o: usize, - ) -> SendOutput { + output: (usize, (Address, u64)), + ) -> (SendOutput, Option<[u8; 8]>) { + let o = output.0; + let output = output.1; + let r = random_scalar(rng); - let (view_tag, shared_key) = + let (view_tag, shared_key, payment_id_xor) = shared_key(Some(unique).filter(|_| output.0.meta.kind.guaranteed()), &r, &output.0.view, o); - if output.0.meta.kind.payment_id().is_some() { - unimplemented!("integrated addresses aren't currently supported"); - } - - SendOutput { - R: if !output.0.meta.kind.subaddress() { - &r * &ED25519_BASEPOINT_TABLE - } else { - r * output.0.spend + ( + SendOutput { + R: if !output.0.meta.kind.subaddress() { + &r * &ED25519_BASEPOINT_TABLE + } else { + r * output.0.spend + }, + view_tag, + dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + output.0.spend), + commitment: Commitment::new(commitment_mask(shared_key), output.1), + amount: amount_encryption(output.1, shared_key), }, - view_tag, - dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + output.0.spend), - commitment: Commitment::new(commitment_mask(shared_key), output.1), - amount: amount_encryption(output.1, shared_key), - } + output + .0 + .payment_id() + .map(|id| (u64::from_le_bytes(id) ^ u64::from_le_bytes(payment_id_xor)).to_le_bytes()), + ) } } #[derive(Clone, Error, Debug)] pub enum TransactionError { - #[error("invalid address")] - InvalidAddress, + #[error("multiple addresses with payment IDs")] + MultiplePaymentIds, #[error("no inputs")] NoInputs, #[error("no outputs")] @@ -178,21 +182,23 @@ impl SignableTransaction { change_address: Option
, fee_rate: Fee, ) -> Result { - // Make sure all addresses are valid - let test = |addr: Address| { - if addr.meta.kind.payment_id().is_some() { - // TODO - Err(TransactionError::InvalidAddress) - } else { - Ok(()) + // Make sure there's only one payment ID + { + let mut payment_ids = 0; + let mut count = |addr: Address| { + if addr.payment_id().is_some() { + payment_ids += 1 + } + }; + for payment in &payments { + count(payment.0); + } + if let Some(change) = change_address { + count(change); + } + if payment_ids > 1 { + Err(TransactionError::MultiplePaymentIds)?; } - }; - - for payment in &payments { - test(payment.0)?; - } - if let Some(change) = change_address { - test(change)?; } if inputs.is_empty() { @@ -258,12 +264,23 @@ impl SignableTransaction { self.payments.shuffle(rng); // Actually create the outputs - let outputs = self - .payments - .drain(..) - .enumerate() - .map(|(o, output)| SendOutput::new(rng, uniqueness, output, o)) - .collect::>(); + let mut outputs = Vec::with_capacity(self.payments.len()); + let mut id = None; + for payment in self.payments.drain(..).enumerate() { + let (output, payment_id) = SendOutput::new(rng, uniqueness, payment); + outputs.push(output); + id = id.or(payment_id); + } + + // Include a random payment ID if we don't actually have one + // It prevents transactions from leaking if they're sending to integrated addresses or not + let id = if let Some(id) = id { + id + } else { + let mut id = [0; 8]; + rng.fill_bytes(&mut id); + id + }; let commitments = outputs.iter().map(|output| output.commitment.clone()).collect::>(); let sum = commitments.iter().map(|commitment| commitment.mask).sum(); @@ -276,8 +293,6 @@ impl SignableTransaction { let mut extra = Extra::new(outputs.iter().map(|output| output.R).collect()); // Additionally include a random payment ID - let mut id = [0; 8]; - rng.fill_bytes(&mut id); extra.push(ExtraField::PaymentId(PaymentId::Encrypted(id))); let mut serialized = Vec::with_capacity(Extra::fee_weight(outputs.len()));