• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tari-project / tari / 16444225845

22 Jul 2025 12:25PM UTC coverage: 54.206% (+0.1%) from 54.068%
16444225845

push

github

stringhandler
chore(release): v4.10.1-pre.1

75379 of 139060 relevant lines covered (54.21%)

196021.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.97
/base_layer/core/src/transactions/transaction_components/encrypted_data.rs
1
// Copyright 2022 The Tari Project
2
//
3
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
4
// following conditions are met:
5
//
6
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
// disclaimer.
8
//
9
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
10
// following disclaimer in the documentation and/or other materials provided with the distribution.
11
//
12
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
13
// products derived from this software without specific prior written permission.
14
//
15
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
16
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
20
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
21
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
22
//
23
// Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License,
24
// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0.
25

26
//! Encrypted data using the extended-nonce variant XChaCha20-Poly1305 encryption with secure random nonce.
27

28
use std::{convert::TryFrom, mem::size_of};
29

30
use blake2::Blake2b;
31
use borsh::{BorshDeserialize, BorshSerialize};
32
use chacha20poly1305::{
33
    aead::{AeadCore, AeadInPlace, Error, OsRng},
34
    KeyInit,
35
    Tag,
36
    XChaCha20Poly1305,
37
    XNonce,
38
};
39
use digest::{consts::U32, generic_array::GenericArray, FixedOutput};
40
use primitive_types::U256;
41
use serde::{Deserialize, Serialize};
42
use tari_common_types::types::{CompressedCommitment, PrivateKey};
43
use tari_crypto::{hashing::DomainSeparatedHasher, keys::SecretKey};
44
use tari_hashing::TransactionSecureNonceKdfDomain;
45
use tari_max_size::MaxSizeBytes;
46
use tari_utilities::{
47
    hex::{from_hex, to_hex, Hex, HexError},
48
    safe_array::SafeArray,
49
    ByteArray,
50
    ByteArrayError,
51
};
52
use thiserror::Error;
53
use zeroize::{Zeroize, Zeroizing};
54

55
use super::EncryptedDataKey;
56
use crate::transactions::{tari_amount::MicroMinotari, transaction_components::memo_field::MemoField};
57

58
// Useful size constants, each in bytes
59
const SIZE_NONCE: usize = size_of::<XNonce>();
60
pub const SIZE_VALUE: usize = size_of::<u64>();
61
const SIZE_MASK: usize = PrivateKey::KEY_LEN;
62
const SIZE_TAG: usize = size_of::<Tag>();
63
pub const SIZE_U256: usize = size_of::<U256>();
64
pub const STATIC_ENCRYPTED_DATA_SIZE_TOTAL: usize = SIZE_NONCE + SIZE_VALUE + SIZE_MASK + SIZE_TAG;
65
const MAX_ENCRYPTED_DATA_SIZE: usize = 256 + STATIC_ENCRYPTED_DATA_SIZE_TOTAL;
66

67
// Number of hex characters of encrypted data to display on each side of ellipsis when truncating
68
const DISPLAY_CUTOFF: usize = 16;
69

70
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize, Zeroize)]
9,177✔
71
pub struct EncryptedData {
72
    #[serde(with = "tari_utilities::serde::hex")]
73
    data: MaxSizeBytes<MAX_ENCRYPTED_DATA_SIZE>,
74
}
75
/// AEAD associated data
76
const ENCRYPTED_DATA_AAD: &[u8] = b"TARI_AAD_VALUE_AND_MASK_EXTEND_NONCE_VARIANT";
77

78
impl EncryptedData {
79
    /// Encrypt the value and mask (with fixed length) using XChaCha20-Poly1305 with a secure random nonce
80
    /// Notes: - This implementation does not require or assume any uniqueness for `encryption_key` or `commitment`
81
    ///        - With the use of a secure random nonce, there's no added security benefit in using the commitment in the
82
    ///          internal key derivation; but it binds the encrypted data to the commitment
83
    ///        - Consecutive calls to this function with the same inputs will produce different ciphertexts
84
    pub fn encrypt_data(
1,851✔
85
        encryption_key: &PrivateKey,
1,851✔
86
        commitment: &CompressedCommitment,
1,851✔
87
        value: MicroMinotari,
1,851✔
88
        mask: &PrivateKey,
1,851✔
89
        payment_id: MemoField,
1,851✔
90
    ) -> Result<EncryptedData, EncryptedDataError> {
1,851✔
91
        // Encode the value and mask
1,851✔
92
        let mut bytes = Zeroizing::new(vec![0; SIZE_VALUE + SIZE_MASK + payment_id.get_size()]);
1,851✔
93
        bytes[..SIZE_VALUE].clone_from_slice(value.as_u64().to_le_bytes().as_ref());
1,851✔
94
        bytes[SIZE_VALUE..SIZE_VALUE + SIZE_MASK].clone_from_slice(mask.as_bytes());
1,851✔
95
        bytes[SIZE_VALUE + SIZE_MASK..].clone_from_slice(&payment_id.to_bytes());
1,851✔
96

1,851✔
97
        // Produce a secure random nonce
1,851✔
98
        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1,851✔
99

1,851✔
100
        // Set up the AEAD
1,851✔
101
        let aead_key = kdf_aead(encryption_key, commitment);
1,851✔
102
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(aead_key.reveal()));
1,851✔
103

104
        // Encrypt in place
105
        let tag = cipher.encrypt_in_place_detached(&nonce, ENCRYPTED_DATA_AAD, bytes.as_mut_slice())?;
1,851✔
106

107
        // Put everything together: nonce, ciphertext, tag
108
        let mut data = vec![0; STATIC_ENCRYPTED_DATA_SIZE_TOTAL + payment_id.get_size()];
1,851✔
109
        data[..SIZE_TAG].clone_from_slice(&tag);
1,851✔
110
        data[SIZE_TAG..SIZE_TAG + SIZE_NONCE].clone_from_slice(&nonce);
1,851✔
111
        data[SIZE_TAG + SIZE_NONCE..SIZE_TAG + SIZE_NONCE + SIZE_VALUE + SIZE_MASK + payment_id.get_size()]
1,851✔
112
            .clone_from_slice(bytes.as_slice());
1,851✔
113
        Ok(Self {
1,851✔
114
            data: MaxSizeBytes::try_from(data)
1,851✔
115
                .map_err(|_| EncryptedDataError::IncorrectLength("Data too long".to_string()))?,
1,851✔
116
        })
117
    }
1,851✔
118

119
    /// Authenticate and decrypt the value and mask
120
    /// Note: This design (similar to other AEADs) is not key committing, thus the caller must not rely on successful
121
    ///       decryption to assert that the expected key was used
122
    pub fn decrypt_data(
102✔
123
        encryption_key: &PrivateKey,
102✔
124
        commitment: &CompressedCommitment,
102✔
125
        encrypted_data: &EncryptedData,
102✔
126
    ) -> Result<(MicroMinotari, PrivateKey, MemoField), EncryptedDataError> {
102✔
127
        // Extract the nonce, ciphertext, and tag
102✔
128
        let tag = Tag::from_slice(&encrypted_data.as_bytes()[..SIZE_TAG]);
102✔
129
        let nonce = XNonce::from_slice(&encrypted_data.as_bytes()[SIZE_TAG..SIZE_TAG + SIZE_NONCE]);
102✔
130
        let mut bytes = Zeroizing::new(vec![
102✔
131
            0;
102✔
132
            encrypted_data
102✔
133
                .data
102✔
134
                .len()
102✔
135
                .saturating_sub(SIZE_TAG)
102✔
136
                .saturating_sub(SIZE_NONCE)
102✔
137
        ]);
102✔
138
        bytes.clone_from_slice(&encrypted_data.as_bytes()[SIZE_TAG + SIZE_NONCE..]);
102✔
139

102✔
140
        // Set up the AEAD
102✔
141
        let aead_key = kdf_aead(encryption_key, commitment);
102✔
142
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(aead_key.reveal()));
102✔
143

102✔
144
        // Decrypt in place
102✔
145
        cipher.decrypt_in_place_detached(nonce, ENCRYPTED_DATA_AAD, bytes.as_mut_slice(), tag)?;
102✔
146

147
        // Decode the value and mask
148
        let mut value_bytes = [0u8; SIZE_VALUE];
102✔
149
        value_bytes.clone_from_slice(&bytes[0..SIZE_VALUE]);
102✔
150
        Ok((
102✔
151
            u64::from_le_bytes(value_bytes).into(),
102✔
152
            PrivateKey::from_canonical_bytes(&bytes[SIZE_VALUE..SIZE_VALUE + SIZE_MASK])?,
102✔
153
            MemoField::from_bytes(&bytes[SIZE_VALUE + SIZE_MASK..]),
102✔
154
        ))
155
    }
102✔
156

157
    /// Parse encrypted data from a byte slice
158
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, EncryptedDataError> {
145✔
159
        if bytes.len() < STATIC_ENCRYPTED_DATA_SIZE_TOTAL {
145✔
160
            return Err(EncryptedDataError::IncorrectLength(format!(
×
161
                "Expected bytes to be at least {}, got {}",
×
162
                STATIC_ENCRYPTED_DATA_SIZE_TOTAL,
×
163
                bytes.len()
×
164
            )));
×
165
        }
145✔
166
        Ok(Self {
145✔
167
            data: MaxSizeBytes::from_bytes_checked(bytes)
145✔
168
                .ok_or(EncryptedDataError::IncorrectLength("Data too long".to_string()))?,
145✔
169
        })
170
    }
145✔
171

172
    #[cfg(test)]
173
    pub fn from_vec_unsafe(data: Vec<u8>) -> Self {
1✔
174
        Self {
1✔
175
            data: MaxSizeBytes::from_bytes_checked(data).unwrap(),
1✔
176
        }
1✔
177
    }
1✔
178

179
    /// Get a byte vector with the encrypted data contents
180
    pub fn to_byte_vec(&self) -> Vec<u8> {
159✔
181
        self.data.clone().into()
159✔
182
    }
159✔
183

184
    /// Get a byte slice with the encrypted data contents
185
    pub fn as_bytes(&self) -> &[u8] {
873✔
186
        &self.data
873✔
187
    }
873✔
188

189
    /// Accessor method for the encrypted data hex display
190
    pub fn hex_display(&self, full: bool) -> String {
×
191
        if full {
×
192
            self.to_hex()
×
193
        } else {
194
            let encrypted_data_hex = self.to_hex();
×
195
            if encrypted_data_hex.len() > 2 * DISPLAY_CUTOFF {
×
196
                format!(
×
197
                    "Some({}..{})",
×
198
                    &encrypted_data_hex[0..DISPLAY_CUTOFF],
×
199
                    &encrypted_data_hex[encrypted_data_hex.len() - DISPLAY_CUTOFF..encrypted_data_hex.len()]
×
200
                )
×
201
            } else {
202
                encrypted_data_hex
×
203
            }
204
        }
205
    }
×
206

207
    /// Returns the size of the payment id
208
    pub fn get_payment_id_size(&self) -> usize {
330✔
209
        // the length should always at least be the static total size, the extra len is the payment id
330✔
210
        self.data.len().saturating_sub(STATIC_ENCRYPTED_DATA_SIZE_TOTAL)
330✔
211
    }
330✔
212
}
213

214
impl Hex for EncryptedData {
215
    fn from_hex(hex: &str) -> Result<Self, HexError> {
×
216
        let v = from_hex(hex)?;
×
217
        Self::from_bytes(&v).map_err(|_| HexError::HexConversionError {})
×
218
    }
×
219

220
    fn to_hex(&self) -> String {
×
221
        to_hex(&self.to_byte_vec())
×
222
    }
×
223
}
224
impl Default for EncryptedData {
225
    fn default() -> Self {
1,349✔
226
        Self {
1,349✔
227
            data: MaxSizeBytes::try_from(vec![0; STATIC_ENCRYPTED_DATA_SIZE_TOTAL])
1,349✔
228
                .expect("This will always be less then the max length"),
1,349✔
229
        }
1,349✔
230
    }
1,349✔
231
}
232
// EncryptedOpenings errors
233
#[derive(Debug, Error)]
234
pub enum EncryptedDataError {
235
    #[error("Encryption failed: {0}")]
236
    EncryptionFailed(Error),
237
    #[error("Conversion failed: {0}")]
238
    ByteArrayError(String),
239
    #[error("Incorrect length: {0}")]
240
    IncorrectLength(String),
241
}
242

243
impl From<ByteArrayError> for EncryptedDataError {
244
    fn from(e: ByteArrayError) -> Self {
×
245
        EncryptedDataError::ByteArrayError(e.to_string())
×
246
    }
×
247
}
248

249
// Chacha error is not StdError compatible
250
impl From<Error> for EncryptedDataError {
251
    fn from(err: Error) -> Self {
×
252
        Self::EncryptionFailed(err)
×
253
    }
×
254
}
255

256
// Generate a ChaCha20-Poly1305 key from a private key and commitment using Blake2b
257
fn kdf_aead(encryption_key: &PrivateKey, commitment: &CompressedCommitment) -> EncryptedDataKey {
1,954✔
258
    let mut aead_key = EncryptedDataKey::from(SafeArray::default());
1,954✔
259
    DomainSeparatedHasher::<Blake2b<U32>, TransactionSecureNonceKdfDomain>::new_with_label("encrypted_value_and_mask")
1,954✔
260
        .chain(encryption_key.as_bytes())
1,954✔
261
        .chain(commitment.as_bytes())
1,954✔
262
        .finalize_into(GenericArray::from_mut_slice(aead_key.reveal_mut()));
1,954✔
263

1,954✔
264
    aead_key
1,954✔
265
}
1,954✔
266

267
#[cfg(test)]
268
mod test {
269
    use static_assertions::const_assert;
270
    use tari_common_types::{
271
        tari_address::{TARI_ADDRESS_INTERNAL_DUAL_SIZE, TARI_ADDRESS_INTERNAL_SINGLE_SIZE},
272
        types::CommitmentFactory,
273
    };
274
    use tari_crypto::commitment::HomomorphicCommitmentFactory;
275

276
    use super::*;
277

278
    #[test]
279
    fn test_premine() {
1✔
280
        let id = 999u64;
1✔
281
        let value = 123456;
1✔
282
        let mask = PrivateKey::default();
1✔
283
        let commitment =
1✔
284
            CompressedCommitment::from_commitment(CommitmentFactory::default().commit(&mask, &PrivateKey::from(value)));
1✔
285
        let encryption_key = PrivateKey::random(&mut OsRng);
1✔
286
        let amount = MicroMinotari::from(value);
1✔
287
        let encrypted_data = {
1✔
288
            let mut bytes = Zeroizing::new(vec![0; SIZE_VALUE + SIZE_MASK + SIZE_VALUE]);
1✔
289
            bytes[..SIZE_VALUE].clone_from_slice(value.to_le_bytes().as_ref());
1✔
290
            bytes[SIZE_VALUE..SIZE_VALUE + SIZE_MASK].clone_from_slice(mask.as_bytes());
1✔
291
            bytes[SIZE_VALUE + SIZE_MASK..].clone_from_slice(&id.to_le_bytes().to_vec());
1✔
292

1✔
293
            // Produce a secure random nonce
1✔
294
            let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1✔
295

1✔
296
            // Set up the AEAD
1✔
297
            let aead_key = kdf_aead(&encryption_key, &commitment);
1✔
298
            let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(aead_key.reveal()));
1✔
299

1✔
300
            // Encrypt in place
1✔
301
            let tag = cipher
1✔
302
                .encrypt_in_place_detached(&nonce, ENCRYPTED_DATA_AAD, bytes.as_mut_slice())
1✔
303
                .unwrap();
1✔
304

1✔
305
            // Put everything together: nonce, ciphertext, tag
1✔
306
            let mut data = vec![0; STATIC_ENCRYPTED_DATA_SIZE_TOTAL + SIZE_VALUE];
1✔
307
            data[..SIZE_TAG].clone_from_slice(&tag);
1✔
308
            data[SIZE_TAG..SIZE_TAG + SIZE_NONCE].clone_from_slice(&nonce);
1✔
309
            data[SIZE_TAG + SIZE_NONCE..SIZE_TAG + SIZE_NONCE + SIZE_VALUE + SIZE_MASK + SIZE_VALUE]
1✔
310
                .clone_from_slice(bytes.as_slice());
1✔
311
            EncryptedData {
1✔
312
                data: MaxSizeBytes::try_from(data)
1✔
313
                    .map_err(|_| EncryptedDataError::IncorrectLength("Data too long".to_string()))
1✔
314
                    .unwrap(),
1✔
315
            }
1✔
316
        };
1✔
317
        let (decrypted_value, decrypted_mask, decrypted_payment_id) =
1✔
318
            EncryptedData::decrypt_data(&encryption_key, &commitment, &encrypted_data).unwrap();
1✔
319
        assert_eq!(amount, decrypted_value);
1✔
320
        assert_eq!(mask, decrypted_mask);
1✔
321
        if decrypted_payment_id.is_open() {
1✔
322
            let data = decrypted_payment_id.get_payment_id();
1✔
323
            let bytes: [u8; SIZE_VALUE] = data.try_into().unwrap();
1✔
324
            let v = u64::from_le_bytes(bytes);
1✔
325
            assert_eq!(v, id);
1✔
326
        } else {
327
            panic!("Expected PaymentId::Open");
×
328
        }
329
    }
1✔
330

331
    #[test]
332
    fn address_sizes_increase_as_expected() {
1✔
333
        const_assert!(SIZE_VALUE < SIZE_U256);
1✔
334
        const_assert!(SIZE_U256 < TARI_ADDRESS_INTERNAL_SINGLE_SIZE);
1✔
335
        const_assert!(TARI_ADDRESS_INTERNAL_SINGLE_SIZE < TARI_ADDRESS_INTERNAL_DUAL_SIZE);
1✔
336
    }
1✔
337
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc