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

tari-project / tari / 24654956413

20 Apr 2026 07:52AM UTC coverage: 61.03% (+0.09%) from 60.945%
24654956413

push

github

web-flow
fix(sidechain)!: include epoch_hash in sidechain block header (#7767)

Description
---
fix(sidechain)!: include epoch_hash in sidechain block header

Motivation and Context
---
We need to read the epoch_hash from the sidechain header on Ootle
(https://github.com/tari-project/tari-ootle/pull/2017#issuecomment-4259006786)

This increases the proof size (over the wire) by 33 bytes

How Has This Been Tested?
---
Existing sidechain tests (fixtures updated)

Breaking Changes
---

- [ ] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [x] Other - Please specify

BREAKING CHANGE: this has no breaking change on current testnets or
mainnet. However, it is a breaking change for Ootle when it upgrades to
use these changes.

1 of 3 new or added lines in 2 files covered. (33.33%)

112 existing lines in 9 files now uncovered.

70783 of 115981 relevant lines covered (61.03%)

224161.51 hits per line

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

76.0
/base_layer/wallet/src/storage/sqlite_db/wallet.rs
1
// Copyright 2019. 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
use std::{
24
    convert::TryFrom,
25
    mem::size_of,
26
    str::{FromStr, from_utf8},
27
    sync::{Arc, RwLock},
28
};
29

30
use argon2::password_hash::{
31
    SaltString,
32
    rand_core::{OsRng, RngCore},
33
};
34
use blake2::Blake2b;
35
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305};
36
use diesel::{prelude::*, result::Error};
37
use digest::{FixedOutput, consts::U32, generic_array::GenericArray};
38
use log::*;
39
use tari_common_sqlite::sqlite_connection_pool::PooledDbConnection;
40
use tari_common_types::{
41
    chain_metadata::ChainMetadata,
42
    encryption::{Encryptable, decrypt_bytes_integral_nonce, encrypt_bytes_integral_nonce},
43
    seeds::cipher_seed::CipherSeed,
44
    types::CompressedCommitment,
45
};
46
use tari_comms::{
47
    multiaddr::Multiaddr,
48
    peer_manager::{IdentitySignature, PeerFeatures},
49
};
50
use tari_crypto::{hash_domain, hashing::DomainSeparatedHasher};
51
use tari_utilities::{
52
    ByteArray,
53
    Hidden,
54
    SafePassword,
55
    hex::{Hex, from_hex},
56
    hidden_type,
57
    safe_array::SafeArray,
58
};
59
use tokio::time::Instant;
60
use zeroize::Zeroize;
61

62
use crate::{
63
    error::WalletStorageError,
64
    schema,
65
    schema::{client_key_values, wallet_settings},
66
    storage::{
67
        database::{DbKey, DbKeyValuePair, DbValue, WalletBackend, WriteOperation},
68
        serializers::{bincode_decode, bincode_encode},
69
        sqlite_db::{
70
            models::{BurntProofSql, DbBurnProof},
71
            scanned_blocks::ScannedBlockSql,
72
        },
73
        sqlite_utilities::wallet_db_connection::WalletDbConnection,
74
    },
75
    utxo_scanner_service::service::ScannedBlock,
76
};
77

78
const LOG_TARGET: &str = "wallet::storage::wallet";
79

80
// The main `XChaCha20-Poly1305` key used for database encryption
81
// This isn't a `SafeArray` because of how we populate it from an authenticated decryption
82
// However, it is `Hidden` and therefore should be safe to use
83
hidden_type!(WalletMainEncryptionKey, Vec<u8>);
84

85
// The `XChaCha20-Poly1305` key used to derive the secondary key
86
hidden_type!(WalletSecondaryDerivationKey, SafeArray<u8, { size_of::<Key>() }>);
87

88
// The secondary `XChaCha20-Poly1305` key used to encrypt the main key
89
hidden_type!(WalletSecondaryEncryptionKey, SafeArray<u8, { size_of::<Key>() }>);
90

91
// Authenticated data prefix for main key encryption; append the encryption version later
92
const MAIN_KEY_AAD_PREFIX: &str = "wallet_main_key_encryption_v";
93

94
// Hash domains for secondary key derivation
95
hash_domain!(SecondaryKeyDomain, "com.tari.base_layer.wallet.secondary_key", 0);
96
hash_domain!(
97
    SecondaryKeyHashDomain,
98
    "com.tari.base_layer.wallet.secondary_key_hash_commitment",
99
    0
100
);
101

102
/// A structure to hold `Argon2` parameter versions, which may change over time and must be supported
103
#[derive(Clone)]
104
pub struct Argon2Parameters {
105
    id: u8,                       // version identifier
106
    algorithm: argon2::Algorithm, // algorithm variant
107
    version: argon2::Version,     // algorithm version
108
    params: argon2::Params,       // memory, iteration count, parallelism, output length
109
}
110
impl Argon2Parameters {
111
    /// Construct and return `Argon2` parameters by version identifier
112
    /// If you pass in `None`, you'll get the most recent
113
    pub fn from_version(id: Option<u8>) -> Result<Self, WalletStorageError> {
18✔
114
        // Each subsequent version identifier _must_ increase!
115
        // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
116
        match id {
18✔
117
            // Be sure to update the `None` behavior when updating this!
118
            None | Some(1) => Ok(Argon2Parameters {
119
                id: 1,
120
                algorithm: argon2::Algorithm::Argon2id,
18✔
121
                version: argon2::Version::V0x13,
18✔
122
                params: argon2::Params::new(46 * 1024, 1, 1, Some(size_of::<Key>()))
18✔
123
                    .map_err(|e| WalletStorageError::AeadError(e.to_string()))?,
18✔
124
            }),
125
            Some(id) => Err(WalletStorageError::BadEncryptionVersion(id.to_string())),
×
126
        }
127
    }
18✔
128
}
129

130
/// A structure to hold encryption-related database field data, to make atomic operations cleaner
131
pub struct DatabaseEncryptionFields {
132
    secondary_key_version: u8,   // the encryption parameter version
133
    secondary_key_salt: String,  // the high-entropy salt used to derive the secondary derivation key
134
    secondary_key_hash: Vec<u8>, // a hash commitment to the secondary derivation key
135
    encrypted_main_key: Vec<u8>, // the main key, encrypted with the secondary key
136
}
137
impl DatabaseEncryptionFields {
138
    /// Read and parse field data from the database atomically
139
    pub fn read(connection: &mut SqliteConnection) -> Result<Option<Self>, WalletStorageError> {
17✔
140
        let mut secondary_key_version: Option<String> = None;
17✔
141
        let mut secondary_key_salt: Option<String> = None;
17✔
142
        let mut secondary_key_hash: Option<String> = None;
17✔
143
        let mut encrypted_main_key: Option<String> = None;
17✔
144

145
        // Read all fields atomically
146
        connection
17✔
147
            .transaction::<_, Error, _>(|connection| {
17✔
148
                secondary_key_version = WalletSettingSql::get(&DbKey::SecondaryKeyVersion, connection)
17✔
149
                    .map_err(|_| Error::RollbackTransaction)?;
17✔
150
                secondary_key_salt = WalletSettingSql::get(&DbKey::SecondaryKeySalt, connection)
17✔
151
                    .map_err(|_| Error::RollbackTransaction)?;
17✔
152
                secondary_key_hash = WalletSettingSql::get(&DbKey::SecondaryKeyHash, connection)
17✔
153
                    .map_err(|_| Error::RollbackTransaction)?;
17✔
154
                encrypted_main_key = WalletSettingSql::get(&DbKey::EncryptedMainKey, connection)
17✔
155
                    .map_err(|_| Error::RollbackTransaction)?;
17✔
156

157
                Ok(())
17✔
158
            })
17✔
159
            .map_err(|_| WalletStorageError::UnexpectedResult("Unable to read key fields from database".into()))?;
17✔
160

161
        // Parse the fields
162
        match (
163
            secondary_key_version,
17✔
164
            secondary_key_salt,
17✔
165
            secondary_key_hash,
17✔
166
            encrypted_main_key,
17✔
167
        ) {
168
            // It's fine if none of the fields are set
169
            (None, None, None, None) => Ok(None),
7✔
170

171
            // If all of the fields are set, they must be parsed as valid
172
            (
173
                Some(secondary_key_version),
10✔
174
                Some(secondary_key_salt),
10✔
175
                Some(secondary_key_hash),
10✔
176
                Some(encrypted_main_key),
10✔
177
            ) => {
178
                let secondary_key_version = u8::from_str(&secondary_key_version)
10✔
179
                    .map_err(|e| WalletStorageError::BadEncryptionVersion(e.to_string()))?;
10✔
180
                let secondary_key_hash =
10✔
181
                    from_hex(&secondary_key_hash).map_err(|e| WalletStorageError::ConversionError(e.to_string()))?;
10✔
182
                let encrypted_main_key =
10✔
183
                    from_hex(&encrypted_main_key).map_err(|e| WalletStorageError::ConversionError(e.to_string()))?;
10✔
184

185
                Ok(Some(DatabaseEncryptionFields {
10✔
186
                    secondary_key_version,
10✔
187
                    secondary_key_salt,
10✔
188
                    secondary_key_hash,
10✔
189
                    encrypted_main_key,
10✔
190
                }))
10✔
191
            },
192

193
            // If only some fields are present, there is an invalid state
194
            _ => Err(WalletStorageError::UnexpectedResult(
×
195
                "Not all key data is present in the database".into(),
×
196
            )),
×
197
        }
198
    }
17✔
199

200
    /// Encode and write field data to the database atomically
201
    pub fn write(&self, connection: &mut SqliteConnection) -> Result<(), WalletStorageError> {
8✔
202
        // Because the encoding can't fail, just do it inside the write transaction
203
        connection
8✔
204
            .transaction::<_, Error, _>(|connection| {
8✔
205
                WalletSettingSql::new(DbKey::SecondaryKeyVersion, self.secondary_key_version.to_string())
8✔
206
                    .set(connection)
8✔
207
                    .map_err(|_| Error::RollbackTransaction)?;
8✔
208
                WalletSettingSql::new(DbKey::SecondaryKeySalt, self.secondary_key_salt.to_string())
8✔
209
                    .set(connection)
8✔
210
                    .map_err(|_| Error::RollbackTransaction)?;
8✔
211
                WalletSettingSql::new(DbKey::SecondaryKeyHash, self.secondary_key_hash.to_hex())
8✔
212
                    .set(connection)
8✔
213
                    .map_err(|_| Error::RollbackTransaction)?;
8✔
214
                WalletSettingSql::new(DbKey::EncryptedMainKey, self.encrypted_main_key.to_hex())
8✔
215
                    .set(connection)
8✔
216
                    .map_err(|_| Error::RollbackTransaction)?;
8✔
217

218
                Ok(())
8✔
219
            })
8✔
220
            .map_err(|_| WalletStorageError::UnexpectedResult("Unable to write key fields into database".into()))?;
8✔
221

222
        Ok(())
8✔
223
    }
8✔
224
}
225

226
/// A Sqlite backend for the Output Manager Service. The Backend is accessed via a connection pool to the Sqlite file.
227
#[derive(Clone)]
228
pub struct WalletSqliteDatabase {
229
    database_connection: WalletDbConnection,
230
    cipher: Arc<RwLock<XChaCha20Poly1305>>,
231
}
232
impl WalletSqliteDatabase {
233
    pub fn new(database_connection: WalletDbConnection, passphrase: SafePassword) -> Result<Self, WalletStorageError> {
15✔
234
        let cipher = get_db_cipher(&database_connection, &passphrase)?;
15✔
235

236
        Ok(Self {
11✔
237
            database_connection,
11✔
238
            cipher: Arc::new(RwLock::new(cipher)),
11✔
239
        })
11✔
240
    }
15✔
241

242
    fn set_master_seed(&self, seed: &CipherSeed, conn: &mut SqliteConnection) -> Result<(), WalletStorageError> {
4✔
243
        let cipher = acquire_read_lock!(self.cipher);
4✔
244
        if WalletSettingSql::get(&DbKey::WalletBirthday, conn)?.is_none() {
4✔
245
            let birthday = seed.birthday();
3✔
246
            WalletSettingSql::new(DbKey::WalletBirthday, birthday.to_string()).set(conn)?;
3✔
247
        }
1✔
248

249
        let seed_bytes = Hidden::hide(seed.encipher(None)?);
4✔
250
        let ciphertext_integral_nonce =
4✔
251
            encrypt_bytes_integral_nonce(&cipher, b"wallet_setting_master_seed".to_vec(), seed_bytes)
4✔
252
                .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{e}")))?;
4✔
253
        WalletSettingSql::new(DbKey::MasterSeed, ciphertext_integral_nonce.to_hex()).set(conn)?;
4✔
254

255
        Ok(())
4✔
256
    }
4✔
257

258
    fn get_master_seed(&self, conn: &mut SqliteConnection) -> Result<Option<CipherSeed>, WalletStorageError> {
6✔
259
        let cipher = acquire_read_lock!(self.cipher);
6✔
260
        if let Some(seed_str) = WalletSettingSql::get(&DbKey::MasterSeed, conn)? {
6✔
261
            let seed = {
4✔
262
                // Decrypted_key_bytes contains sensitive data regarding decrypted
263
                // seed words. For this reason, we should zeroize the underlying data buffer
264
                let decrypted_key_bytes = Hidden::hide(
4✔
265
                    decrypt_bytes_integral_nonce(
4✔
266
                        &cipher,
4✔
267
                        b"wallet_setting_master_seed".to_vec(),
4✔
268
                        &from_hex(seed_str.as_str())?,
4✔
269
                    )
270
                    .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{e}")))?,
4✔
271
                );
272
                CipherSeed::from_enciphered_bytes(decrypted_key_bytes.reveal(), None)?
4✔
273
            };
274

275
            Ok(Some(seed))
4✔
276
        } else {
277
            Ok(None)
2✔
278
        }
279
    }
6✔
280

281
    fn decrypt_value<T: Encryptable<XChaCha20Poly1305>>(&self, o: T) -> Result<T, WalletStorageError> {
8✔
282
        let cipher = acquire_read_lock!(self.cipher);
8✔
283
        let o = o
8✔
284
            .decrypt(&cipher)
8✔
285
            .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{e}")))?;
8✔
286
        Ok(o)
8✔
287
    }
8✔
288

289
    #[allow(dead_code)]
290
    fn encrypt_value<T: Encryptable<XChaCha20Poly1305>>(&self, o: T) -> Result<T, WalletStorageError> {
×
291
        let cipher = acquire_read_lock!(self.cipher);
×
292
        o.encrypt(&cipher)
×
293
            .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{e}")))
×
294
    }
×
295

296
    fn get_comms_address(&self, conn: &mut SqliteConnection) -> Result<Option<Multiaddr>, WalletStorageError> {
×
297
        if let Some(key_str) = WalletSettingSql::get(&DbKey::CommsAddress, conn)? {
×
298
            Ok(Some(
299
                Multiaddr::from_str(key_str.as_str())
×
300
                    .map_err(|e| WalletStorageError::ConversionError(e.to_string()))?,
×
301
            ))
302
        } else {
303
            Ok(None)
×
304
        }
305
    }
×
306

307
    fn get_comms_features(&self, conn: &mut SqliteConnection) -> Result<Option<PeerFeatures>, WalletStorageError> {
×
308
        if let Some(key_str) = WalletSettingSql::get(&DbKey::CommsFeatures, conn)? {
×
309
            let features = u32::from_str(&key_str).map_err(|e| WalletStorageError::ConversionError(e.to_string()))?;
×
310
            let peer_features = PeerFeatures::from_bits(features);
×
311
            Ok(peer_features)
×
312
        } else {
313
            Ok(None)
×
314
        }
315
    }
×
316

317
    fn set_chain_metadata(&self, chain: ChainMetadata, conn: &mut SqliteConnection) -> Result<(), WalletStorageError> {
×
318
        let bytes = bincode_encode(&chain)?;
×
319
        WalletSettingSql::new(DbKey::BaseNodeChainMetadata, bytes.to_hex()).set(conn)?;
×
320
        Ok(())
×
321
    }
×
322

323
    fn get_chain_metadata(&self, conn: &mut SqliteConnection) -> Result<Option<ChainMetadata>, WalletStorageError> {
×
324
        if let Some(key_str) = WalletSettingSql::get(&DbKey::BaseNodeChainMetadata, conn)? {
×
325
            let chain_metadata = bincode_decode(&from_hex(&key_str)?)?;
×
326
            Ok(Some(chain_metadata))
×
327
        } else {
328
            Ok(None)
×
329
        }
330
    }
×
331

332
    fn insert_key_value_pair(&self, kvp: DbKeyValuePair) -> Result<Option<DbValue>, WalletStorageError> {
5✔
333
        let start = Instant::now();
5✔
334
        let mut conn = self.database_connection.get_pooled_connection()?;
5✔
335
        let acquire_lock = start.elapsed();
5✔
336
        let cipher = acquire_read_lock!(self.cipher);
5✔
337
        let kvp_text;
338
        match kvp {
5✔
339
            DbKeyValuePair::MasterSeed(seed) => {
1✔
340
                kvp_text = "MasterSeed";
1✔
341
                self.set_master_seed(&seed, &mut conn)?;
1✔
342
            },
343
            DbKeyValuePair::BaseNodeChainMetadata(metadata) => {
×
344
                kvp_text = "BaseNodeChainMetadata";
×
345
                self.set_chain_metadata(metadata, &mut conn)?;
×
346
            },
347
            DbKeyValuePair::ClientKeyValue(k, v) => {
4✔
348
                // First see if we will overwrite a value so we can return the old value
349
                let value_to_return = if let Some(found_value) = ClientKeyValueSql::get(&k, &mut conn)? {
4✔
350
                    let found_value = self.decrypt_value(found_value)?;
1✔
351
                    Some(found_value)
1✔
352
                } else {
353
                    None
3✔
354
                };
355

356
                let client_key_value = ClientKeyValueSql::new(k, v, &cipher)?;
4✔
357

358
                client_key_value.set(&mut conn)?;
4✔
359
                if start.elapsed().as_millis() > 0 {
4✔
360
                    trace!(
4✔
361
                        target: LOG_TARGET,
×
362
                        "sqlite profile - insert_key_value_pair 'ClientKeyValue': lock {} + db_op {} = {} ms",
363
                        acquire_lock.as_millis(),
×
364
                        (start.elapsed() - acquire_lock).as_millis(),
×
365
                        start.elapsed().as_millis()
×
366
                    );
367
                }
×
368

369
                return Ok(value_to_return.map(|v| DbValue::ClientValue(v.value)));
4✔
370
            },
371
            DbKeyValuePair::CommsAddress(ca) => {
×
372
                kvp_text = "CommsAddress";
×
373
                WalletSettingSql::new(DbKey::CommsAddress, ca.to_string()).set(&mut conn)?;
×
374
            },
375
            DbKeyValuePair::CommsFeatures(cf) => {
×
376
                kvp_text = "CommsFeatures";
×
377
                WalletSettingSql::new(DbKey::CommsFeatures, cf.bits().to_string()).set(&mut conn)?;
×
378
            },
379
            DbKeyValuePair::CommsIdentitySignature(identity_sig) => {
×
380
                kvp_text = "CommsIdentitySignature";
×
381
                WalletSettingSql::new(DbKey::CommsIdentitySignature, identity_sig.to_bytes().to_hex())
×
382
                    .set(&mut conn)?;
×
383
            },
384
            DbKeyValuePair::NetworkAndVersion((network, version)) => {
×
385
                kvp_text = "NetworkAndVersion";
×
386

387
                WalletSettingSql::new(DbKey::LastAccessedNetwork, network).set(&mut conn)?;
×
388
                WalletSettingSql::new(DbKey::LastAccessedVersion, version).set(&mut conn)?;
×
389
            },
390
            DbKeyValuePair::WalletType(wallet_type) => {
×
391
                kvp_text = "WalletType";
×
392
                WalletSettingSql::new(DbKey::WalletType, serde_json::to_string(&wallet_type).unwrap())
×
393
                    .set(&mut conn)?;
×
394
            },
395
        }
396

397
        if start.elapsed().as_millis() > 0 {
1✔
398
            trace!(
1✔
399
                target: LOG_TARGET,
×
400
                "sqlite profile - insert_key_value_pair '{}': lock {} + db_op {} = {} ms",
401
                kvp_text,
402
                acquire_lock.as_millis(),
×
403
                (start.elapsed() - acquire_lock).as_millis(),
×
404
                start.elapsed().as_millis()
×
405
            );
406
        }
×
407
        Ok(None)
1✔
408
    }
5✔
409

410
    fn remove_key(&self, k: DbKey) -> Result<Option<DbValue>, WalletStorageError> {
4✔
411
        let start = Instant::now();
4✔
412
        let mut conn = self.database_connection.get_pooled_connection()?;
4✔
413
        let acquire_lock = start.elapsed();
4✔
414
        match k {
4✔
415
            DbKey::MasterSeed => {
416
                let _ = WalletSettingSql::clear(&DbKey::MasterSeed, &mut conn)?;
1✔
417
            },
418
            DbKey::ClientKey(ref k) => {
3✔
419
                if ClientKeyValueSql::clear(k, &mut conn)? {
3✔
420
                    return Ok(Some(DbValue::ValueCleared));
1✔
421
                }
2✔
422
            },
423
            DbKey::CommsFeatures |
424
            DbKey::CommsAddress |
425
            DbKey::BaseNodeChainMetadata |
426
            DbKey::EncryptedMainKey |
427
            DbKey::SecondaryKeyVersion |
428
            DbKey::SecondaryKeySalt |
429
            DbKey::SecondaryKeyHash |
430
            DbKey::WalletBirthday |
431
            DbKey::WalletType |
432
            DbKey::CommsIdentitySignature |
433
            DbKey::LastAccessedNetwork |
434
            DbKey::LastAccessedVersion => {
435
                return Err(WalletStorageError::OperationNotSupported);
×
436
            },
437
        };
438
        if start.elapsed().as_millis() > 0 {
3✔
439
            trace!(
1✔
440
                target: LOG_TARGET,
×
441
                "sqlite profile - remove_key '{}': lock {} + db_op {} = {} &ms",
442
                k.to_key_string(),
×
443
                acquire_lock.as_millis(),
×
444
                (start.elapsed() - acquire_lock).as_millis(),
×
445
                start.elapsed().as_millis()
×
446
            );
447
        }
2✔
448
        Ok(None)
3✔
449
    }
4✔
450

451
    pub fn cipher(&self) -> XChaCha20Poly1305 {
6✔
452
        let cipher = acquire_read_lock!(self.cipher);
6✔
453
        (*cipher).clone()
6✔
454
    }
6✔
455
}
456

457
impl WalletBackend for WalletSqliteDatabase {
458
    fn fetch(&self, key: &DbKey) -> Result<Option<DbValue>, WalletStorageError> {
14✔
459
        let start = Instant::now();
14✔
460
        let mut conn = self.database_connection.get_pooled_connection()?;
14✔
461
        let acquire_lock = start.elapsed();
14✔
462

463
        let result = match key {
14✔
464
            DbKey::MasterSeed => self.get_master_seed(&mut conn)?.map(DbValue::MasterSeed),
6✔
465
            DbKey::ClientKey(k) => match ClientKeyValueSql::get(k, &mut conn)? {
8✔
466
                None => None,
1✔
467
                Some(v) => {
7✔
468
                    let v = self.decrypt_value(v)?;
7✔
469
                    Some(DbValue::ClientValue(v.value))
7✔
470
                },
471
            },
472
            DbKey::CommsAddress => self.get_comms_address(&mut conn)?.map(DbValue::CommsAddress),
×
473
            DbKey::CommsFeatures => self.get_comms_features(&mut conn)?.map(DbValue::CommsFeatures),
×
474
            DbKey::BaseNodeChainMetadata => self.get_chain_metadata(&mut conn)?.map(DbValue::BaseNodeChainMetadata),
×
475
            DbKey::EncryptedMainKey => WalletSettingSql::get(key, &mut conn)?.map(DbValue::EncryptedMainKey),
×
476
            DbKey::SecondaryKeyVersion => WalletSettingSql::get(key, &mut conn)?.map(DbValue::SecondaryKeyVersion),
×
477
            DbKey::SecondaryKeySalt => WalletSettingSql::get(key, &mut conn)?.map(DbValue::SecondaryKeySalt),
×
478
            DbKey::SecondaryKeyHash => WalletSettingSql::get(key, &mut conn)?.map(DbValue::SecondaryKeyHash),
×
479
            DbKey::WalletBirthday => WalletSettingSql::get(key, &mut conn)?.map(DbValue::WalletBirthday),
×
480
            DbKey::WalletType => {
481
                WalletSettingSql::get(key, &mut conn)?.map(|d| DbValue::WalletType(serde_json::from_str(&d).unwrap()))
×
482
            },
483
            DbKey::LastAccessedNetwork => WalletSettingSql::get(key, &mut conn)?.map(DbValue::LastAccessedNetwork),
×
484
            DbKey::LastAccessedVersion => WalletSettingSql::get(key, &mut conn)?.map(DbValue::LastAccessedVersion),
×
485
            DbKey::CommsIdentitySignature => WalletSettingSql::get(key, &mut conn)?
×
486
                .and_then(|s| from_hex(&s).ok())
×
487
                .and_then(|bytes| IdentitySignature::from_bytes(&bytes).ok())
×
488
                .map(Box::new)
×
489
                .map(DbValue::CommsIdentitySignature),
×
490
        };
491
        if start.elapsed().as_millis() > 0 {
14✔
492
            trace!(
4✔
493
                target: LOG_TARGET,
×
494
                "sqlite profile - fetch '{}': lock {} + db_op {} = {} ms",
495
                key.to_key_string(),
×
496
                acquire_lock.as_millis(),
×
497
                (start.elapsed() - acquire_lock).as_millis(),
×
498
                start.elapsed().as_millis()
×
499
            );
500
        }
10✔
501

502
        Ok(result)
14✔
503
    }
14✔
504

505
    fn write(&self, op: WriteOperation) -> Result<Option<DbValue>, WalletStorageError> {
9✔
506
        match op {
9✔
507
            WriteOperation::Insert(kvp) => self.insert_key_value_pair(kvp),
5✔
508
            WriteOperation::Remove(k) => self.remove_key(k),
4✔
509
        }
510
    }
9✔
511

512
    fn get_last_scanned_height(&self) -> Result<Option<u64>, WalletStorageError> {
×
513
        let mut conn = self.database_connection.get_pooled_connection()?;
×
514
        match ScannedBlockSql::last_height(&mut conn)? {
×
515
            Some(height) => Ok(Some(height as u64)),
×
516
            None => Ok(None),
×
517
        }
518
    }
×
519

520
    fn get_scanned_blocks(&self) -> Result<Vec<ScannedBlock>, WalletStorageError> {
1✔
521
        let mut conn = self.database_connection.get_pooled_connection()?;
1✔
522
        let sql_blocks = ScannedBlockSql::index(&mut conn)?;
1✔
523
        sql_blocks
1✔
524
            .into_iter()
1✔
525
            .map(ScannedBlock::try_from)
1✔
526
            .collect::<Result<Vec<_>, _>>()
1✔
527
            .map_err(WalletStorageError::ConversionError)
1✔
528
    }
1✔
529

530
    fn save_scanned_block(&self, scanned_block: ScannedBlock) -> Result<(), WalletStorageError> {
2✔
531
        let mut conn = self.database_connection.get_pooled_connection()?;
2✔
532
        ScannedBlockSql::from(scanned_block).commit(&mut conn)
2✔
533
    }
2✔
534

535
    fn clear_scanned_blocks(&self) -> Result<(), WalletStorageError> {
×
536
        let mut conn = self.database_connection.get_pooled_connection()?;
×
537
        ScannedBlockSql::clear_all(&mut conn)
×
538
    }
×
539

540
    fn clear_scanned_blocks_from_and_higher(&self, height: u64) -> Result<(), WalletStorageError> {
×
541
        let mut conn = self.database_connection.get_pooled_connection()?;
×
542
        ScannedBlockSql::clear_from_and_higher(height, &mut conn)
×
543
    }
×
544

545
    fn clear_scanned_blocks_before_height(&self, height: u64) -> Result<(), WalletStorageError> {
×
546
        let mut conn = self.database_connection.get_pooled_connection()?;
×
547
        ScannedBlockSql::clear_before_height(height, &mut conn)
×
548
    }
×
549

550
    fn apply_sparse_scanned_blocks_schedule(&self, tip_height: u64) -> Result<(), WalletStorageError> {
×
551
        let mut conn = self.database_connection.get_pooled_connection()?;
×
552
        ScannedBlockSql::apply_sparse_schedule(tip_height, &mut conn)
×
UNCOV
553
    }
×
554

555
    fn change_passphrase(&self, existing: &SafePassword, new: &SafePassword) -> Result<(), WalletStorageError> {
2✔
556
        let mut conn = self.database_connection.get_pooled_connection()?;
2✔
557

558
        // Get the existing key-related data so we can decrypt the main key
559
        match DatabaseEncryptionFields::read(&mut conn) {
2✔
560
            // Key-related data was present and valid
561
            Ok(Some(data)) => {
2✔
562
                // Use the given version if it is valid
563
                let argon2_params = Argon2Parameters::from_version(Some(data.secondary_key_version))?;
2✔
564

565
                // Derive a secondary key from the existing passphrase and salt
566
                let (secondary_key, secondary_key_hash) =
2✔
567
                    derive_secondary_key(existing, argon2_params.clone(), &data.secondary_key_salt)?;
2✔
568

569
                // Attempt to decrypt the encrypted main key
570
                if data.secondary_key_hash != secondary_key_hash {
2✔
571
                    return Err(WalletStorageError::InvalidPassphrase);
1✔
572
                }
1✔
573
                let main_key = decrypt_main_key(&secondary_key, &data.encrypted_main_key, argon2_params.id)?;
1✔
574

575
                // Now use the most recent version
576
                let new_argon2_params = Argon2Parameters::from_version(None)?;
1✔
577

578
                // Derive a new secondary key from the new passphrase and a fresh salt
579
                let new_secondary_key_salt = SaltString::generate(&mut OsRng).to_string();
1✔
580
                let (new_secondary_key, new_secondary_key_hash) =
1✔
581
                    derive_secondary_key(new, new_argon2_params.clone(), &new_secondary_key_salt)?;
1✔
582

583
                // Encrypt the main key with the new secondary key
584
                let new_encrypted_main_key = encrypt_main_key(&new_secondary_key, &main_key, new_argon2_params.id)?;
1✔
585

586
                // Store the new key-related fields
587
                DatabaseEncryptionFields {
1✔
588
                    secondary_key_version: new_argon2_params.id,
1✔
589
                    secondary_key_salt: new_secondary_key_salt,
1✔
590
                    secondary_key_hash: new_secondary_key_hash,
1✔
591
                    encrypted_main_key: new_encrypted_main_key,
1✔
592
                }
1✔
593
                .write(&mut conn)?;
1✔
594
            },
595

596
            // If any key-related is not present, this is an invalid state
597
            _ => {
598
                return Err(WalletStorageError::UnexpectedResult(
×
599
                    "Unable to get valid key-related data from database".into(),
×
UNCOV
600
                ));
×
601
            },
602
        };
603

604
        Ok(())
1✔
605
    }
2✔
606

607
    fn fetch_burn_proofs(&self) -> Result<Vec<DbBurnProof>, WalletStorageError> {
×
UNCOV
608
        let mut conn = self.database_connection.get_pooled_connection()?;
×
609

610
        let proofs = schema::burn_proofs::table
×
611
            .order(schema::burn_proofs::created_at.desc())
×
UNCOV
612
            .load::<BurntProofSql>(&mut conn)?;
×
613

614
        proofs
×
615
            .into_iter()
×
616
            .map(|entry| {
×
617
                let decrypted = self.decrypt_value(entry)?;
×
618
                decrypted.try_into()
×
619
            })
×
620
            .collect()
×
UNCOV
621
    }
×
622

623
    fn get_burn_proof_by_commitment(
×
624
        &self,
×
625
        commitment: &CompressedCommitment,
×
626
    ) -> Result<Option<DbBurnProof>, WalletStorageError> {
×
UNCOV
627
        let mut conn = self.database_connection.get_pooled_connection()?;
×
628

629
        let proof = schema::burn_proofs::table
×
630
            .filter(schema::burn_proofs::commitment.eq(commitment.as_bytes()))
×
631
            .get_result::<BurntProofSql>(&mut conn)
×
UNCOV
632
            .optional()?;
×
633

634
        proof
×
635
            .map(|entry| {
×
636
                let decrypted = self.decrypt_value(entry)?;
×
637
                decrypted.try_into()
×
638
            })
×
639
            .transpose()
×
UNCOV
640
    }
×
641

642
    fn delete_burn_proof(&self, id: i32) -> Result<(), WalletStorageError> {
×
643
        let mut conn = self.database_connection.get_pooled_connection()?;
×
644
        let num_deleted =
×
645
            diesel::delete(schema::burn_proofs::table.filter(schema::burn_proofs::id.eq(id))).execute(&mut conn)?;
×
646
        if num_deleted == 0 {
×
647
            return Err(WalletStorageError::BurnProofNotFound(id));
×
648
        }
×
649
        Ok(())
×
UNCOV
650
    }
×
651
}
652

653
/// Derive a secondary database key and associated commitment
654
fn derive_secondary_key(
18✔
655
    passphrase: &SafePassword,
18✔
656
    params: Argon2Parameters,
18✔
657
    salt: &String,
18✔
658
) -> Result<(WalletSecondaryEncryptionKey, Vec<u8>), WalletStorageError> {
18✔
659
    // Produce the secondary derivation key from the passphrase and salt
660
    let mut secondary_derivation_key = WalletSecondaryDerivationKey::from(SafeArray::default());
18✔
661
    argon2::Argon2::new(params.algorithm, params.version, params.params)
18✔
662
        .hash_password_into(
18✔
663
            passphrase.reveal(),
18✔
664
            salt.as_bytes(),
18✔
665
            secondary_derivation_key.reveal_mut(),
18✔
666
        )
667
        .map_err(|e| WalletStorageError::AeadError(e.to_string()))?;
18✔
668

669
    // Derive the secondary key
670
    let mut secondary_key = WalletSecondaryEncryptionKey::from(SafeArray::default());
18✔
671
    DomainSeparatedHasher::<Blake2b<U32>, SecondaryKeyDomain>::new()
18✔
672
        .chain(secondary_derivation_key.reveal())
18✔
673
        .finalize_into(GenericArray::from_mut_slice(secondary_key.reveal_mut()));
18✔
674

675
    // Produce the associated commitment
676
    let secondary_key_hash = DomainSeparatedHasher::<Blake2b<U32>, SecondaryKeyDomain>::new()
18✔
677
        .chain(secondary_derivation_key.reveal())
18✔
678
        .finalize()
18✔
679
        .as_ref()
18✔
680
        .to_vec();
18✔
681

682
    Ok((secondary_key, secondary_key_hash))
18✔
683
}
18✔
684

685
/// Encrypt the main database key using the secondary key
686
fn encrypt_main_key(
8✔
687
    secondary_key: &WalletSecondaryEncryptionKey,
8✔
688
    main_key: &WalletMainEncryptionKey,
8✔
689
    version: u8,
8✔
690
) -> Result<Vec<u8>, WalletStorageError> {
8✔
691
    // Set up the authenticated data
692
    let mut aad = MAIN_KEY_AAD_PREFIX.as_bytes().to_owned();
8✔
693
    aad.push(version);
8✔
694

695
    // Encrypt the main key
696
    let cipher = XChaCha20Poly1305::new(Key::from_slice(secondary_key.reveal()));
8✔
697
    let encrypted_main_key = encrypt_bytes_integral_nonce(&cipher, aad, Hidden::hide(main_key.reveal().clone()))
8✔
698
        .map_err(WalletStorageError::AeadError)?;
8✔
699

700
    Ok(encrypted_main_key)
8✔
701
}
8✔
702

703
/// Decrypt the main database key using the secondary key
704
fn decrypt_main_key(
5✔
705
    secondary_key: &WalletSecondaryEncryptionKey,
5✔
706
    encrypted_main_key: &[u8],
5✔
707
    version: u8,
5✔
708
) -> Result<WalletMainEncryptionKey, WalletStorageError> {
5✔
709
    // Set up the authenticated data
710
    let mut aad = MAIN_KEY_AAD_PREFIX.as_bytes().to_owned();
5✔
711
    aad.push(version);
5✔
712

713
    // Authenticate and decrypt the main key
714
    let cipher = XChaCha20Poly1305::new(Key::from_slice(secondary_key.reveal()));
5✔
715

716
    Ok(WalletMainEncryptionKey::from(
5✔
717
        decrypt_bytes_integral_nonce(&cipher, aad, encrypted_main_key)
5✔
718
            .map_err(|_| WalletStorageError::InvalidPassphrase)?,
5✔
719
    ))
720
}
5✔
721

722
/// Prepare the database encryption cipher
723
fn get_db_cipher(
15✔
724
    database_connection: &WalletDbConnection,
15✔
725
    passphrase: &SafePassword,
15✔
726
) -> Result<XChaCha20Poly1305, WalletStorageError> {
15✔
727
    let mut conn = database_connection.get_pooled_connection()?;
15✔
728

729
    // Either set up a new main key, or decrypt it using existing data
730
    let main_key = match DatabaseEncryptionFields::read(&mut conn) {
15✔
731
        // Encryption is not set up yet
732
        Ok(None) => {
733
            // Generate a high-entropy main key
734
            let mut main_key = WalletMainEncryptionKey::from(vec![0u8; size_of::<Key>()]);
7✔
735
            let mut rng = OsRng;
7✔
736
            rng.fill_bytes(main_key.reveal_mut());
7✔
737

738
            // Use the most recent `Argon2` parameters
739
            let argon2_params = Argon2Parameters::from_version(None)?;
7✔
740

741
            // Derive the secondary key from the user's passphrase and a high-entropy salt
742
            let secondary_key_salt = SaltString::generate(&mut rng).to_string();
7✔
743
            let (secondary_key, secondary_key_hash) =
7✔
744
                derive_secondary_key(passphrase, argon2_params.clone(), &secondary_key_salt)?;
7✔
745

746
            // Use the secondary key to encrypt the main key
747
            let encrypted_main_key = encrypt_main_key(&secondary_key, &main_key, argon2_params.id)?;
7✔
748

749
            // Store the key-related fields
750
            DatabaseEncryptionFields {
7✔
751
                secondary_key_version: argon2_params.id,
7✔
752
                secondary_key_salt,
7✔
753
                secondary_key_hash,
7✔
754
                encrypted_main_key,
7✔
755
            }
7✔
756
            .write(&mut conn)?;
7✔
757

758
            // Return the unencrypted main key
759
            main_key
7✔
760
        },
761

762
        // Encryption has already been set up
763
        Ok(Some(data)) => {
8✔
764
            // Use the given version if it is valid
765
            let argon2_params = Argon2Parameters::from_version(Some(data.secondary_key_version))?;
8✔
766

767
            // Derive the secondary key from the user's passphrase and salt
768
            let (secondary_key, secondary_key_hash) =
8✔
769
                derive_secondary_key(passphrase, argon2_params, &data.secondary_key_salt)?;
8✔
770

771
            // Attempt to decrypt and return the encrypted main key
772
            if data.secondary_key_hash != secondary_key_hash {
8✔
773
                return Err(WalletStorageError::InvalidPassphrase);
4✔
774
            }
4✔
775
            decrypt_main_key(&secondary_key, &data.encrypted_main_key, data.secondary_key_version)?
4✔
776
        },
777

778
        // We couldn't get valid key-related data
779
        Err(_) => {
780
            return Err(WalletStorageError::UnexpectedResult(
×
781
                "Unable to parse key fields from database".into(),
×
UNCOV
782
            ));
×
783
        },
784
    };
785

786
    Ok(XChaCha20Poly1305::new(Key::from_slice(main_key.reveal())))
11✔
787
}
15✔
788

789
/// A Sql version of the wallet setting key-value table
790
#[derive(Clone, Debug, Queryable, Insertable, PartialEq)]
791
#[diesel(table_name = wallet_settings)]
792
pub(crate) struct WalletSettingSql {
793
    key: String,
794
    value: String,
795
}
796

797
impl WalletSettingSql {
798
    pub fn new(key: DbKey, value: String) -> Self {
40✔
799
        Self {
40✔
800
            key: key.to_key_string(),
40✔
801
            value,
40✔
802
        }
40✔
803
    }
40✔
804

805
    pub fn set(&self, conn: &mut SqliteConnection) -> Result<(), WalletStorageError> {
40✔
806
        diesel::replace_into(wallet_settings::table)
40✔
807
            .values(self)
40✔
808
            .execute(conn)?;
40✔
809

810
        Ok(())
40✔
811
    }
40✔
812

813
    pub fn get(key: &DbKey, conn: &mut SqliteConnection) -> Result<Option<String>, WalletStorageError> {
81✔
814
        wallet_settings::table
81✔
815
            .filter(wallet_settings::key.eq(key.to_key_string()))
81✔
816
            .first::<WalletSettingSql>(conn)
81✔
817
            .map(|v: WalletSettingSql| Some(v.value))
81✔
818
            .or_else(|err| match err {
81✔
819
                diesel::result::Error::NotFound => Ok(None),
33✔
UNCOV
820
                err => Err(err.into()),
×
821
            })
33✔
822
    }
81✔
823

824
    pub fn clear(key: &DbKey, conn: &mut SqliteConnection) -> Result<bool, WalletStorageError> {
1✔
825
        let num_deleted = diesel::delete(wallet_settings::table.filter(wallet_settings::key.eq(key.to_key_string())))
1✔
826
            .execute(conn)?;
1✔
827
        Ok(num_deleted > 0)
1✔
828
    }
1✔
829
}
830

831
/// A Sql version of the wallet setting key-value table
832
#[derive(Clone, Debug, Queryable, Insertable, PartialEq)]
833
#[diesel(table_name = client_key_values)]
834
struct ClientKeyValueSql {
835
    key: String,
836
    value: String,
837
}
838

839
impl ClientKeyValueSql {
840
    pub fn new(key: String, value: String, cipher: &XChaCha20Poly1305) -> Result<Self, WalletStorageError> {
9✔
841
        let client_kv = Self { key, value };
9✔
842
        client_kv.encrypt(cipher).map_err(WalletStorageError::AeadError)
9✔
843
    }
9✔
844

845
    #[allow(dead_code)]
846
    pub fn index(conn: &mut SqliteConnection) -> Result<Vec<Self>, WalletStorageError> {
1✔
847
        Ok(client_key_values::table.load::<ClientKeyValueSql>(conn)?)
1✔
848
    }
1✔
849

850
    pub fn set(&self, conn: &mut SqliteConnection) -> Result<(), WalletStorageError> {
9✔
851
        diesel::replace_into(client_key_values::table)
9✔
852
            .values(self)
9✔
853
            .execute(conn)?;
9✔
854

855
        Ok(())
9✔
856
    }
9✔
857

858
    pub fn get(key: &str, conn: &mut SqliteConnection) -> Result<Option<Self>, WalletStorageError> {
16✔
859
        client_key_values::table
16✔
860
            .filter(client_key_values::key.eq(key))
16✔
861
            .first::<ClientKeyValueSql>(conn)
16✔
862
            .map(Some)
16✔
863
            .or_else(|err| match err {
16✔
864
                diesel::result::Error::NotFound => Ok(None),
6✔
UNCOV
865
                err => Err(err.into()),
×
866
            })
6✔
867
    }
16✔
868

869
    pub fn clear(key: &str, conn: &mut SqliteConnection) -> Result<bool, WalletStorageError> {
5✔
870
        let num_deleted =
5✔
871
            diesel::delete(client_key_values::table.filter(client_key_values::key.eq(key))).execute(conn)?;
5✔
872

873
        Ok(num_deleted > 0)
5✔
874
    }
5✔
875
}
876

877
impl Encryptable<XChaCha20Poly1305> for ClientKeyValueSql {
878
    fn domain(&self, field_name: &'static str) -> Vec<u8> {
24✔
879
        // Because there are two variable-length inputs in the concatenation, we prepend the length of the first
880
        [
24✔
881
            Self::CLIENT_KEY_VALUE,
24✔
882
            (self.key.len() as u64).to_le_bytes().as_bytes(),
24✔
883
            self.key.as_bytes(),
24✔
884
            field_name.as_bytes(),
24✔
885
        ]
24✔
886
        .concat()
24✔
887
        .to_vec()
24✔
888
    }
24✔
889

890
    #[allow(unused_assignments)]
891
    fn encrypt(mut self, cipher: &XChaCha20Poly1305) -> Result<Self, String> {
9✔
892
        self.value = encrypt_bytes_integral_nonce(
9✔
893
            cipher,
9✔
894
            self.domain("value"),
9✔
895
            Hidden::hide(self.value.as_bytes().to_vec()),
9✔
UNCOV
896
        )?
×
897
        .to_hex();
9✔
898

899
        Ok(self)
9✔
900
    }
9✔
901

902
    #[allow(unused_assignments)]
903
    fn decrypt(mut self, cipher: &XChaCha20Poly1305) -> Result<Self, String> {
15✔
904
        let mut decrypted_value = decrypt_bytes_integral_nonce(
15✔
905
            cipher,
15✔
906
            self.domain("value"),
15✔
907
            &from_hex(self.value.as_str()).map_err(|e| e.to_string())?,
15✔
UNCOV
908
        )?;
×
909

910
        self.value = from_utf8(decrypted_value.as_slice())
15✔
911
            .map_err(|e| e.to_string())?
15✔
912
            .to_string();
15✔
913

914
        // we zeroize the decrypted value
915
        decrypted_value.zeroize();
15✔
916

917
        Ok(self)
15✔
918
    }
15✔
919
}
920

921
#[cfg(test)]
922
mod test {
923
    #![allow(clippy::indexing_slicing)]
924

925
    use chrono::Utc;
926
    use tari_common_sqlite::sqlite_connection_pool::PooledDbConnection;
927
    use tari_common_types::{
928
        encryption::{Encryptable, decrypt_bytes_integral_nonce},
929
        seeds::cipher_seed::CipherSeed,
930
        types::FixedHash,
931
    };
932
    use tari_test_utils::random::string;
933
    use tari_utilities::{
934
        ByteArray,
935
        SafePassword,
936
        hex::{Hex, from_hex},
937
    };
938
    use tempfile::tempdir;
939

940
    use crate::{
941
        storage::{
942
            database::{DbKey, DbValue, WalletBackend},
943
            sqlite_db::wallet::{ClientKeyValueSql, WalletSettingSql, WalletSqliteDatabase},
944
            sqlite_utilities::run_migration_and_create_sqlite_connection,
945
        },
946
        utxo_scanner_service::service::ScannedBlock,
947
    };
948
    #[test]
949
    fn test_passphrase() {
1✔
950
        // Set up a database
951
        let db_name = format!("{}.sqlite3", string(8).as_str());
1✔
952
        let db_tempdir = tempdir().unwrap();
1✔
953
        let db_folder = db_tempdir.path().to_str().unwrap().to_string();
1✔
954
        let db_path = format!("{db_folder}/{db_name}");
1✔
955
        let connection = run_migration_and_create_sqlite_connection(db_path, 16).unwrap();
1✔
956

957
        // Encrypt with a passphrase
958
        let db = WalletSqliteDatabase::new(connection.clone(), "passphrase".to_string().into()).unwrap();
1✔
959

960
        // Load again with the correct passphrase
961
        assert!(WalletSqliteDatabase::new(connection.clone(), "passphrase".to_string().into()).is_ok());
1✔
962

963
        // Try to load with the wrong passphrase
964
        assert!(WalletSqliteDatabase::new(connection.clone(), "evil passphrase".to_string().into()).is_err());
1✔
965

966
        // Try to change the passphrase, but fail
967
        assert!(
1✔
968
            db.change_passphrase(
1✔
969
                &"evil passphrase".to_string().into(),
1✔
970
                &"new passphrase".to_string().into()
1✔
971
            )
1✔
972
            .is_err()
1✔
973
        );
974

975
        // The existing passphrase still works
976
        assert!(WalletSqliteDatabase::new(connection.clone(), "passphrase".to_string().into()).is_ok());
1✔
977

978
        // The new passphrase doesn't
979
        assert!(WalletSqliteDatabase::new(connection.clone(), "new passphrase".to_string().into()).is_err());
1✔
980

981
        // Successfully change the passphrase
982
        assert!(
1✔
983
            db.change_passphrase(&"passphrase".to_string().into(), &"new passphrase".to_string().into())
1✔
984
                .is_ok()
1✔
985
        );
986

987
        // The existing passphrase no longer works
988
        assert!(WalletSqliteDatabase::new(connection.clone(), "passphrase".to_string().into()).is_err());
1✔
989

990
        // The new passphrase does
991
        assert!(WalletSqliteDatabase::new(connection, "new passphrase".to_string().into()).is_ok());
1✔
992
    }
1✔
993

994
    #[test]
995
    #[allow(unused_must_use)]
996
    fn test_malleated_secondary_key_hash() {
1✔
997
        // Set up a database
998
        let db_name = format!("{}.sqlite3", string(8).as_str());
1✔
999
        let db_tempdir = tempdir().unwrap();
1✔
1000
        let db_folder = db_tempdir.path().to_str().unwrap().to_string();
1✔
1001
        let db_path = format!("{db_folder}/{db_name}");
1✔
1002
        let connection = run_migration_and_create_sqlite_connection(db_path, 16).unwrap();
1✔
1003

1004
        // Encrypt with a passphrase
1005
        WalletSqliteDatabase::new(connection.clone(), "passphrase".to_string().into()).unwrap();
1✔
1006

1007
        // Loading the wallet should succeed
1008
        assert!(WalletSqliteDatabase::new(connection.clone(), "passphrase".to_string().into()).is_ok());
1✔
1009

1010
        // Manipulate the secondary key hash; this is a (poor) proxy for an AEAD attack
1011
        let evil_secondary_key_hash = vec![0u8; 32];
1✔
1012
        WalletSettingSql::new(DbKey::SecondaryKeyHash, evil_secondary_key_hash.to_hex())
1✔
1013
            .set(&mut connection.get_pooled_connection().unwrap());
1✔
1014

1015
        // Loading the wallet should fail
1016
        assert!(WalletSqliteDatabase::new(connection, "passphrase".to_string().into()).is_err());
1✔
1017
    }
1✔
1018

1019
    #[test]
1020
    fn test_encryption_is_forced() {
1✔
1021
        let db_name = format!("{}.sqlite3", string(8).as_str());
1✔
1022
        let db_tempdir = tempdir().unwrap();
1✔
1023
        let db_folder = db_tempdir.path().to_str().unwrap().to_string();
1✔
1024
        let db_path = format!("{db_folder}/{db_name}");
1✔
1025
        let connection = run_migration_and_create_sqlite_connection(db_path, 16).unwrap();
1✔
1026

1027
        let seed = CipherSeed::random();
1✔
1028
        let passphrase = "a very very secret key example.".to_string().into();
1✔
1029
        let db = WalletSqliteDatabase::new(connection.clone(), passphrase).unwrap();
1✔
1030
        let cipher = db.cipher();
1✔
1031

1032
        let mut key_values = vec![
1✔
1033
            ClientKeyValueSql::new("key1".to_string(), "value1".to_string(), &cipher).unwrap(),
1✔
1034
            ClientKeyValueSql::new("key2".to_string(), "value2".to_string(), &cipher).unwrap(),
1✔
1035
            ClientKeyValueSql::new("key3".to_string(), "value3".to_string(), &cipher).unwrap(),
1✔
1036
        ];
1037
        {
1038
            let mut conn = connection.get_pooled_connection().unwrap();
1✔
1039
            db.set_master_seed(&seed, &mut conn).unwrap();
1✔
1040
            for kv in &mut key_values {
3✔
1041
                kv.set(&mut conn).unwrap();
3✔
1042
            }
3✔
1043
        }
1044

1045
        let read_seed1 = match db.fetch(&DbKey::MasterSeed).unwrap().unwrap() {
1✔
1046
            DbValue::MasterSeed(sk) => sk,
1✔
1047
            _ => {
UNCOV
1048
                panic!("Should be able to read Key");
×
1049
            },
1050
        };
1051
        assert_eq!(seed, read_seed1);
1✔
1052

1053
        let read_seed2 = match db.fetch(&DbKey::MasterSeed).unwrap().unwrap() {
1✔
1054
            DbValue::MasterSeed(sk) => sk,
1✔
1055
            _ => {
UNCOV
1056
                panic!("Should be able to read Key");
×
1057
            },
1058
        };
1059

1060
        for kv in &mut key_values {
3✔
1061
            *kv = kv.clone().decrypt(&db.cipher()).unwrap();
3✔
1062
            match db.fetch(&DbKey::ClientKey(kv.key.clone())).unwrap().unwrap() {
3✔
1063
                DbValue::ClientValue(v) => {
3✔
1064
                    assert_eq!(kv.value, v);
3✔
1065
                },
1066
                _ => {
UNCOV
1067
                    panic!("Should be able to read Key/Value");
×
1068
                },
1069
            }
1070
        }
1071

1072
        assert_eq!(seed, read_seed2);
1✔
1073
        {
1074
            let mut conn = connection.get_pooled_connection().unwrap();
1✔
1075
            let secret_key_str = WalletSettingSql::get(&DbKey::MasterSeed, &mut conn).unwrap().unwrap();
1✔
1076
            assert!(secret_key_str.len() > 64);
1✔
1077
            db.set_master_seed(&seed, &mut conn).unwrap();
1✔
1078
            let secret_key_str = WalletSettingSql::get(&DbKey::MasterSeed, &mut conn).unwrap().unwrap();
1✔
1079
            assert!(secret_key_str.len() > 64);
1✔
1080
        }
1081

1082
        let read_seed3 = match db.fetch(&DbKey::MasterSeed).unwrap().unwrap() {
1✔
1083
            DbValue::MasterSeed(sk) => sk,
1✔
1084
            _ => {
UNCOV
1085
                panic!("Should be able to read Key");
×
1086
            },
1087
        };
1088
        assert_eq!(seed, read_seed3);
1✔
1089

1090
        for kv in &key_values {
3✔
1091
            match db.fetch(&DbKey::ClientKey(kv.key.clone())).unwrap().unwrap() {
3✔
1092
                DbValue::ClientValue(v) => {
3✔
1093
                    assert_eq!(kv.value, v);
3✔
1094
                },
1095
                _ => {
UNCOV
1096
                    panic!("Should be able to read Key/Value2");
×
1097
                },
1098
            }
1099
        }
1100
    }
1✔
1101

1102
    #[test]
1103
    fn test_client_key_value_store() {
1✔
1104
        let db_name = format!("{}.sqlite3", string(8).as_str());
1✔
1105
        let db_tempdir = tempdir().unwrap();
1✔
1106
        let db_folder = db_tempdir.path().to_str().unwrap().to_string();
1✔
1107
        let connection = run_migration_and_create_sqlite_connection(format!("{db_folder}{db_name}"), 16).unwrap();
1✔
1108
        let mut conn = connection.get_pooled_connection().unwrap();
1✔
1109

1110
        let key1 = "key1".to_string();
1✔
1111
        let value1 = "value1".to_string();
1✔
1112
        let key2 = "key2".to_string();
1✔
1113
        let value2 = "value2".to_string();
1✔
1114

1115
        let passphrase = "a very very secret key example.".to_string().into();
1✔
1116
        let db = WalletSqliteDatabase::new(connection, passphrase).unwrap();
1✔
1117
        let cipher = db.cipher();
1✔
1118

1119
        ClientKeyValueSql::new(key1.clone(), value1.clone(), &cipher)
1✔
1120
            .unwrap()
1✔
1121
            .set(&mut conn)
1✔
1122
            .unwrap();
1✔
1123
        assert!(ClientKeyValueSql::get(&key2, &mut conn).unwrap().is_none());
1✔
1124
        if let Some(ckv) = ClientKeyValueSql::get(&key1, &mut conn).unwrap() {
1✔
1125
            let ckv = ckv.decrypt(&cipher).unwrap();
1✔
1126
            assert_eq!(ckv.value, value1);
1✔
1127
        } else {
UNCOV
1128
            panic!("Should find value");
×
1129
        }
1130
        assert!(!ClientKeyValueSql::clear(&key2, &mut conn).unwrap());
1✔
1131

1132
        ClientKeyValueSql::new(key2.clone(), value2.clone(), &cipher)
1✔
1133
            .unwrap()
1✔
1134
            .set(&mut conn)
1✔
1135
            .unwrap();
1✔
1136

1137
        let values = ClientKeyValueSql::index(&mut conn).unwrap();
1✔
1138
        assert_eq!(values.len(), 2);
1✔
1139

1140
        assert_eq!(values[0].clone().decrypt(&cipher).unwrap().value, value1);
1✔
1141
        assert_eq!(values[1].clone().decrypt(&cipher).unwrap().value, value2);
1✔
1142

1143
        assert!(ClientKeyValueSql::clear(&key1, &mut conn).unwrap());
1✔
1144
        assert!(ClientKeyValueSql::get(&key1, &mut conn).unwrap().is_none());
1✔
1145

1146
        if let Some(ckv) = ClientKeyValueSql::get(&key2, &mut conn).unwrap() {
1✔
1147
            let ckv = ckv.decrypt(&cipher).unwrap();
1✔
1148
            assert_eq!(ckv.value, value2);
1✔
1149
        } else {
UNCOV
1150
            panic!("Should find value2");
×
1151
        }
1152
    }
1✔
1153

1154
    #[test]
1155
    fn test_set_master_seed() {
1✔
1156
        let db_name = format!("{}.sqlite3", string(8).as_str());
1✔
1157
        let db_tempdir = tempdir().unwrap();
1✔
1158
        let db_folder = db_tempdir.path().to_str().unwrap().to_string();
1✔
1159
        let connection = run_migration_and_create_sqlite_connection(format!("{db_folder}{db_name}"), 16).unwrap();
1✔
1160

1161
        let passphrase = SafePassword::from("an example very very secret key.".to_string());
1✔
1162

1163
        let wallet = WalletSqliteDatabase::new(connection.clone(), passphrase).unwrap();
1✔
1164

1165
        let seed = CipherSeed::random();
1✔
1166

1167
        let mut conn = connection.get_pooled_connection().unwrap();
1✔
1168
        wallet.set_master_seed(&seed, &mut conn).unwrap();
1✔
1169

1170
        let seed_bytes = seed.encipher(None).unwrap();
1✔
1171

1172
        let db_seed = WalletSettingSql::get(&DbKey::MasterSeed, &mut conn).unwrap().unwrap();
1✔
1173
        assert_eq!(db_seed.len(), 146);
1✔
1174

1175
        let decrypted_db_seed = decrypt_bytes_integral_nonce(
1✔
1176
            &wallet.cipher(),
1✔
1177
            b"wallet_setting_master_seed".to_vec(),
1✔
1178
            &from_hex(db_seed.as_str()).unwrap(),
1✔
1179
        )
1180
        .unwrap();
1✔
1181

1182
        assert_eq!(decrypted_db_seed, seed_bytes);
1✔
1183
    }
1✔
1184

1185
    #[test]
1186
    fn duplicate_blocks() {
1✔
1187
        let db_name = format!("{}.sqlite3", string(8).as_str());
1✔
1188
        let db_tempdir = tempdir().unwrap();
1✔
1189
        let db_folder = db_tempdir.path().to_str().unwrap().to_string();
1✔
1190
        let connection = run_migration_and_create_sqlite_connection(format!("{db_folder}{db_name}"), 16).unwrap();
1✔
1191

1192
        let passphrase = SafePassword::from("an example very very secret key.".to_string());
1✔
1193

1194
        let wallet = WalletSqliteDatabase::new(connection.clone(), passphrase).unwrap();
1✔
1195

1196
        let block1 = ScannedBlock {
1✔
1197
            header_hash: FixedHash::from_hex("0000000000000000000769b1c3f6a1b4b3c2d1e0f9e8d7c6b5a4b3c2d1e0f9e8")
1✔
1198
                .unwrap(),
1✔
1199
            height: 700000,
1✔
1200
            timestamp: Utc::now().naive_utc(),
1✔
1201
        };
1✔
1202

1203
        let block2 = ScannedBlock {
1✔
1204
            header_hash: FixedHash::from_hex("0000000000000000000769b1c3f6a1b4b3c2d1e0f9e8d7c6b5a4b3c2d1e0f9e8")
1✔
1205
                .unwrap(),
1✔
1206
            height: 700001,
1✔
1207
            timestamp: Utc::now().naive_utc(),
1✔
1208
        };
1✔
1209

1210
        wallet.save_scanned_block(block1).unwrap();
1✔
1211
        let result = wallet.save_scanned_block(block2.clone());
1✔
1212
        assert!(result.is_ok());
1✔
1213
        let blocks = wallet.get_scanned_blocks().unwrap();
1✔
1214
        assert_eq!(blocks.len(), 1);
1✔
1215
        assert_eq!(blocks[0], block2);
1✔
1216
    }
1✔
1217
}
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