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

tari-project / tari / 18097567115

29 Sep 2025 12:50PM UTC coverage: 58.554% (-2.3%) from 60.88%
18097567115

push

github

web-flow
chore(ci): switch rust toolchain to stable (#7524)

Description
switch rust toolchain to stable

Motivation and Context
use stable rust toolchain


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Standardized Rust toolchain on stable across CI workflows for more
predictable builds.
* Streamlined setup by removing unnecessary components and aligning
toolchain configuration with environment variables.
  * Enabled an environment flag to improve rustup behavior during CI.
* Improved coverage workflow consistency with dynamic toolchain
selection.

* **Tests**
* Removed nightly-only requirements, simplifying test commands and
improving compatibility.
* Expanded CI triggers to include ci-* branches for better pre-merge
validation.
* Maintained existing job logic while improving reliability and
maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

66336 of 113291 relevant lines covered (58.55%)

551641.45 hits per line

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

82.42
/base_layer/transaction_components/src/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::{transaction_components::MemoField, MicroMinotari};
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
pub 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)]
×
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,823✔
85
        encryption_key: &PrivateKey,
1,823✔
86
        commitment: &CompressedCommitment,
1,823✔
87
        value: MicroMinotari,
1,823✔
88
        mask: &PrivateKey,
1,823✔
89
        memo: MemoField,
1,823✔
90
    ) -> Result<EncryptedData, EncryptedDataError> {
1,823✔
91
        // Encode the value and mask
92
        let mut bytes = Zeroizing::new(vec![0; SIZE_VALUE + SIZE_MASK + memo.get_size()]);
1,823✔
93
        bytes
1,823✔
94
            .get_mut(..SIZE_VALUE)
1,823✔
95
            .expect("Already checked")
1,823✔
96
            .copy_from_slice(value.as_u64().to_le_bytes().as_ref());
1,823✔
97
        bytes
1,823✔
98
            .get_mut(SIZE_VALUE..SIZE_VALUE + SIZE_MASK)
1,823✔
99
            .expect("Already checked")
1,823✔
100
            .copy_from_slice(mask.as_bytes());
1,823✔
101
        bytes
1,823✔
102
            .get_mut(SIZE_VALUE + SIZE_MASK..)
1,823✔
103
            .expect("Already checked")
1,823✔
104
            .copy_from_slice(&memo.to_bytes());
1,823✔
105

106
        // Produce a secure random nonce
107
        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1,823✔
108

109
        // Set up the AEAD
110
        let aead_key = kdf_aead(encryption_key, commitment);
1,823✔
111
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(aead_key.reveal()));
1,823✔
112

113
        // Encrypt in place
114
        let tag = cipher.encrypt_in_place_detached(&nonce, ENCRYPTED_DATA_AAD, bytes.as_mut_slice())?;
1,823✔
115

116
        // Put everything together: nonce, ciphertext, tag
117
        let mut data = vec![0; STATIC_ENCRYPTED_DATA_SIZE_TOTAL + memo.get_size()];
1,823✔
118
        data.get_mut(..SIZE_TAG).expect("Already checked").copy_from_slice(&tag);
1,823✔
119
        data.get_mut(SIZE_TAG..SIZE_TAG + SIZE_NONCE)
1,823✔
120
            .expect("Already checked")
1,823✔
121
            .copy_from_slice(&nonce);
1,823✔
122
        data.get_mut(SIZE_TAG + SIZE_NONCE..SIZE_TAG + SIZE_NONCE + SIZE_VALUE + SIZE_MASK + memo.get_size())
1,823✔
123
            .expect("Already checked")
1,823✔
124
            .copy_from_slice(bytes.as_slice());
1,823✔
125
        Ok(Self {
126
            data: MaxSizeBytes::try_from(data)
1,823✔
127
                .map_err(|_| EncryptedDataError::IncorrectLength("Data too long".to_string()))?,
1,823✔
128
        })
129
    }
1,823✔
130

131
    /// Authenticate and decrypt the value and mask
132
    /// Note: This design (similar to other AEADs) is not key committing, thus the caller must not rely on successful
133
    ///       decryption to assert that the expected key was used
134
    pub fn decrypt_data(
141✔
135
        encryption_key: &PrivateKey,
141✔
136
        commitment: &CompressedCommitment,
141✔
137
        encrypted_data: &EncryptedData,
141✔
138
    ) -> Result<(MicroMinotari, PrivateKey, MemoField), EncryptedDataError> {
141✔
139
        // Extract the nonce, ciphertext, and tag
140
        let tag = Tag::from_slice(
141✔
141
            encrypted_data
141✔
142
                .as_bytes()
141✔
143
                .get(..SIZE_TAG)
141✔
144
                .ok_or(EncryptedDataError::IncorrectLength("Tag too short".to_string()))?,
141✔
145
        );
146
        let nonce = XNonce::from_slice(
141✔
147
            encrypted_data
141✔
148
                .as_bytes()
141✔
149
                .get(SIZE_TAG..SIZE_TAG + SIZE_NONCE)
141✔
150
                .ok_or(EncryptedDataError::IncorrectLength("Data too short".to_string()))?,
141✔
151
        );
152
        let mut bytes = Zeroizing::new(vec![
141✔
153
            0;
154
            encrypted_data
141✔
155
                .data
141✔
156
                .len()
141✔
157
                .saturating_sub(SIZE_TAG)
141✔
158
                .saturating_sub(SIZE_NONCE)
141✔
159
        ]);
160
        bytes.copy_from_slice(
141✔
161
            encrypted_data
141✔
162
                .as_bytes()
141✔
163
                .get(SIZE_TAG + SIZE_NONCE..)
141✔
164
                .ok_or(EncryptedDataError::IncorrectLength("Data too short".to_string()))?,
141✔
165
        );
166

167
        // Set up the AEAD
168
        let aead_key = kdf_aead(encryption_key, commitment);
141✔
169
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(aead_key.reveal()));
141✔
170

171
        // Decrypt in place
172
        cipher.decrypt_in_place_detached(nonce, ENCRYPTED_DATA_AAD, bytes.as_mut_slice(), tag)?;
141✔
173

174
        // Decode the value and mask
175
        let mut value_bytes = [0u8; SIZE_VALUE];
111✔
176
        value_bytes.copy_from_slice(
111✔
177
            bytes
111✔
178
                .get(0..SIZE_VALUE)
111✔
179
                .ok_or(EncryptedDataError::IncorrectLength("Value too short".to_string()))?,
111✔
180
        );
181
        Ok((
182
            u64::from_le_bytes(value_bytes).into(),
111✔
183
            PrivateKey::from_canonical_bytes(
111✔
184
                bytes
111✔
185
                    .get(SIZE_VALUE..SIZE_VALUE + SIZE_MASK)
111✔
186
                    .ok_or(EncryptedDataError::IncorrectLength("Data too short".to_string()))?,
111✔
187
            )?,
×
188
            MemoField::from_bytes(
111✔
189
                bytes
111✔
190
                    .get(SIZE_VALUE + SIZE_MASK..)
111✔
191
                    .ok_or(EncryptedDataError::IncorrectLength("Data too long".to_string()))?,
111✔
192
            ),
193
        ))
194
    }
141✔
195

196
    /// Parse encrypted data from a byte slice
197
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, EncryptedDataError> {
146✔
198
        if bytes.len() < STATIC_ENCRYPTED_DATA_SIZE_TOTAL {
146✔
199
            return Err(EncryptedDataError::IncorrectLength(format!(
×
200
                "Expected bytes to be at least {}, got {}",
×
201
                STATIC_ENCRYPTED_DATA_SIZE_TOTAL,
×
202
                bytes.len()
×
203
            )));
×
204
        }
146✔
205
        Ok(Self {
206
            data: MaxSizeBytes::from_bytes_checked(bytes)
146✔
207
                .ok_or(EncryptedDataError::IncorrectLength("Data too long".to_string()))?,
146✔
208
        })
209
    }
146✔
210

211
    /// Get a byte vector with the encrypted data contents
212
    pub fn to_byte_vec(&self) -> Vec<u8> {
155✔
213
        self.data.clone().into()
155✔
214
    }
155✔
215

216
    /// Get a byte slice with the encrypted data contents
217
    pub fn as_bytes(&self) -> &[u8] {
1,200✔
218
        &self.data
1,200✔
219
    }
1,200✔
220

221
    /// Consumes self and returns the encrypted data as a byte vector
222
    pub fn into_vec(self) -> Vec<u8> {
×
223
        self.data.into_vec()
×
224
    }
×
225

226
    /// Accessor method for the encrypted data hex display
227
    pub fn hex_display(&self, full: bool) -> String {
×
228
        if full {
×
229
            self.to_hex()
×
230
        } else {
231
            let encrypted_data_hex = self.to_hex();
×
232
            if encrypted_data_hex.len() > 2 * DISPLAY_CUTOFF {
×
233
                format!(
×
234
                    "Some({}..{})",
×
235
                    &encrypted_data_hex[0..DISPLAY_CUTOFF],
×
236
                    &encrypted_data_hex[encrypted_data_hex.len() - DISPLAY_CUTOFF..encrypted_data_hex.len()]
×
237
                )
238
            } else {
239
                encrypted_data_hex
×
240
            }
241
        }
242
    }
×
243

244
    /// Returns the size of the payment id
245
    pub fn get_payment_id_size(&self) -> usize {
467✔
246
        // the length should always at least be the static total size, the extra len is the payment id
247
        self.data.len().saturating_sub(STATIC_ENCRYPTED_DATA_SIZE_TOTAL)
467✔
248
    }
467✔
249
}
250

251
impl Hex for EncryptedData {
252
    fn from_hex(hex: &str) -> Result<Self, HexError> {
×
253
        let v = from_hex(hex)?;
×
254
        Self::from_bytes(&v).map_err(|_| HexError::HexConversionError {})
×
255
    }
×
256

257
    fn to_hex(&self) -> String {
×
258
        to_hex(&self.to_byte_vec())
×
259
    }
×
260
}
261
impl Default for EncryptedData {
262
    fn default() -> Self {
1,473✔
263
        Self {
1,473✔
264
            data: MaxSizeBytes::try_from(vec![0; STATIC_ENCRYPTED_DATA_SIZE_TOTAL])
1,473✔
265
                .expect("This will always be less then the max length"),
1,473✔
266
        }
1,473✔
267
    }
1,473✔
268
}
269
// EncryptedOpenings errors
270
#[derive(Debug, Error)]
271
pub enum EncryptedDataError {
272
    #[error("Encryption failed: {0}")]
273
    EncryptionFailed(Error),
274
    #[error("Conversion failed: {0}")]
275
    ByteArrayError(String),
276
    #[error("Incorrect length: {0}")]
277
    IncorrectLength(String),
278
}
279

280
impl From<ByteArrayError> for EncryptedDataError {
281
    fn from(e: ByteArrayError) -> Self {
×
282
        EncryptedDataError::ByteArrayError(e.to_string())
×
283
    }
×
284
}
285

286
// Chacha error is not StdError compatible
287
impl From<Error> for EncryptedDataError {
288
    fn from(err: Error) -> Self {
30✔
289
        Self::EncryptionFailed(err)
30✔
290
    }
30✔
291
}
292

293
// Generate a ChaCha20-Poly1305 key from a private key and commitment using Blake2b
294
fn kdf_aead(encryption_key: &PrivateKey, commitment: &CompressedCommitment) -> EncryptedDataKey {
1,965✔
295
    let mut aead_key = EncryptedDataKey::from(SafeArray::default());
1,965✔
296
    DomainSeparatedHasher::<Blake2b<U32>, TransactionSecureNonceKdfDomain>::new_with_label("encrypted_value_and_mask")
1,965✔
297
        .chain(encryption_key.as_bytes())
1,965✔
298
        .chain(commitment.as_bytes())
1,965✔
299
        .finalize_into(GenericArray::from_mut_slice(aead_key.reveal_mut()));
1,965✔
300

301
    aead_key
1,965✔
302
}
1,965✔
303

304
#[cfg(test)]
305
mod test {
306
    #![allow(clippy::indexing_slicing)]
307
    use static_assertions::const_assert;
308
    use tari_common_types::{
309
        tari_address::{TARI_ADDRESS_INTERNAL_DUAL_SIZE, TARI_ADDRESS_INTERNAL_SINGLE_SIZE},
310
        types::CommitmentFactory,
311
    };
312
    use tari_crypto::commitment::HomomorphicCommitmentFactory;
313

314
    use super::*;
315

316
    #[test]
317
    fn test_premine() {
1✔
318
        let id = 999u64;
1✔
319
        let value = 123456;
1✔
320
        let mask = PrivateKey::default();
1✔
321
        let commitment =
1✔
322
            CompressedCommitment::from_commitment(CommitmentFactory::default().commit(&mask, &PrivateKey::from(value)));
1✔
323
        let encryption_key = PrivateKey::random(&mut OsRng);
1✔
324
        let amount = MicroMinotari::from(value);
1✔
325
        let encrypted_data = {
1✔
326
            let mut bytes = Zeroizing::new(vec![0; SIZE_VALUE + SIZE_MASK + SIZE_VALUE]);
1✔
327
            bytes[..SIZE_VALUE].copy_from_slice(value.to_le_bytes().as_ref());
1✔
328
            bytes[SIZE_VALUE..SIZE_VALUE + SIZE_MASK].copy_from_slice(mask.as_bytes());
1✔
329
            bytes[SIZE_VALUE + SIZE_MASK..].copy_from_slice(&id.to_le_bytes().to_vec());
1✔
330

331
            // Produce a secure random nonce
332
            let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
1✔
333

334
            // Set up the AEAD
335
            let aead_key = kdf_aead(&encryption_key, &commitment);
1✔
336
            let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(aead_key.reveal()));
1✔
337

338
            // Encrypt in place
339
            let tag = cipher
1✔
340
                .encrypt_in_place_detached(&nonce, ENCRYPTED_DATA_AAD, bytes.as_mut_slice())
1✔
341
                .unwrap();
1✔
342

343
            // Put everything together: nonce, ciphertext, tag
344
            let mut data = vec![0; STATIC_ENCRYPTED_DATA_SIZE_TOTAL + SIZE_VALUE];
1✔
345
            data[..SIZE_TAG].copy_from_slice(&tag);
1✔
346
            data[SIZE_TAG..SIZE_TAG + SIZE_NONCE].copy_from_slice(&nonce);
1✔
347
            data[SIZE_TAG + SIZE_NONCE..SIZE_TAG + SIZE_NONCE + SIZE_VALUE + SIZE_MASK + SIZE_VALUE]
1✔
348
                .copy_from_slice(bytes.as_slice());
1✔
349
            EncryptedData {
350
                data: MaxSizeBytes::try_from(data)
1✔
351
                    .map_err(|_| EncryptedDataError::IncorrectLength("Data too long".to_string()))
1✔
352
                    .unwrap(),
1✔
353
            }
354
        };
355
        let (decrypted_value, decrypted_mask, decrypted_payment_id) =
1✔
356
            EncryptedData::decrypt_data(&encryption_key, &commitment, &encrypted_data).unwrap();
1✔
357
        assert_eq!(amount, decrypted_value);
1✔
358
        assert_eq!(mask, decrypted_mask);
1✔
359
        if decrypted_payment_id.is_open() {
1✔
360
            let data = decrypted_payment_id.get_payment_id();
1✔
361
            let bytes: [u8; SIZE_VALUE] = data.try_into().unwrap();
1✔
362
            let v = u64::from_le_bytes(bytes);
1✔
363
            assert_eq!(v, id);
1✔
364
        } else {
365
            panic!("Expected PaymentId::Open");
×
366
        }
367
    }
1✔
368

369
    #[test]
370
    fn address_sizes_increase_as_expected() {
1✔
371
        const_assert!(SIZE_VALUE < SIZE_U256);
372
        const_assert!(SIZE_U256 < TARI_ADDRESS_INTERNAL_SINGLE_SIZE);
373
        const_assert!(TARI_ADDRESS_INTERNAL_SINGLE_SIZE < TARI_ADDRESS_INTERNAL_DUAL_SIZE);
374
    }
1✔
375
}
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