Database keys encryption#475
Conversation
WalkthroughThis update introduces password-based encryption for sensitive database fields and enforces strong password authentication for database access. It adds new dependencies to support encryption, secure password handling, and terminal management. The database schema and access logic are refactored to handle encrypted public keys, and password prompts are integrated into the database connection flow. Several modules are updated to support these changes. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Terminal
participant Main
participant DB as Database
participant Crypto as CryptoUtils
User->>Terminal: Start application
Main->>Terminal: Clear screen
Main->>User: Prompt for DB password (if new DB)
User->>Main: Enter password
Main->>Crypto: Validate password strength
Crypto-->>Main: OK/Fail
alt New DB
Main->>Crypto: Hash password
Main->>DB: Store admin password hash
Main->>DB: Restrict file permissions
Main->>Crypto: Store password in MOSTRO_DB_PASSWORD
else Existing DB
loop Up to 3 attempts
Main->>User: Prompt for password
User->>Main: Enter password
Main->>DB: Retrieve stored hash
Main->>Crypto: Verify password
Crypto-->>Main: OK/Fail
end
alt Password correct
Main->>Crypto: Store password in MOSTRO_DB_PASSWORD
else
Main->>Terminal: Exit (authentication failed)
end
end
Main->>DB: Open connection (with password)
Possibly related PRs
Suggested reviewers
Poem
Note ⚡️ AI Code Reviews for VS Code, Cursor, WindsurfCodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms (1)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
What do you think about to wrap the current hex sec key that user could have and multiply it by the password: That would be strong enough, don't? |
20ae674 to
7da854e
Compare
|
UPDATE: Trying to execute the flow of the orders I have found some possible limitations, mostly caused by the fact the encryption/decryption requires CPU time. Examples: Now when a message comes in My idea is to treat only
In this way I am yet able to do simple order flow and timings seems acceptable. There is still two points in
in these case we have to extract the list of order in a particular state and check if user is having orders pending so we should cycle decrypting some keys, will check if we can improve the check in some ways, but i think it's tolerable. Ok will go on testing orders cycle, but I am glad to have your opinions. |
|
@grunch , I added a nice ( I think... ) idea for caching keys and speed up search when mostro is scanning the vector of index0 keys encrypted and we have to check if one is yet present: Cache Argon2 Key Derivations This works because for a given (password, salt), the derived key is always the same. I have created a tests with good results, these inserts 10 encrypted string and then some are decrypted normally and some are cached: running 1 test
In-memory database and table created for test.
Inserting 10 entries...
Inserting value Entry 0
Salt: 3bLfcYlFRs41Jm/6Byd46A
Inserting value Entry 1
###Salt: c2FsdE5QZXBwYVBlcHBhMQ
Inserting value Entry 2
Salt: bi4gVvI4Na9xfQiDvxtUMw
Inserting value Entry 3
Salt: pHnzGJzxZDUisXV/D8CQng
Inserting value Entry 4
###Salt: c2FsdE5QZXBwYVBlcHBhMQ
Inserting value Entry 0
Salt: dHtIk1RRv+BDTU7NYi0rKw
Inserting value Entry 1
Salt: zCO424lJY0Kvo71J8YJjsw
Inserting value Entry 2
###Salt: c2FsdE5QZXBwYVBlcHBhMQ
Inserting value Entry 3
Salt: SX1JLNbyhQwc52njNWD9bA
Inserting value Entry 4
Salt: f5YLnA46iuOdOjBG4qbt2Q
Entries inserted.
Fetching 'value' column...
Cache key: 16609968908560343058
Key not found in cache, creating new one! 16609968908560343058
Time taken to decrypt: 289 ms
Cache key: 14650601133528733758
Key not found in cache, creating new one! 14650601133528733758
Time taken to decrypt: 209 ms
Cache key: 7000585519848012028
Key not found in cache, creating new one! 7000585519848012028
Time taken to decrypt: 194 ms
Cache key: 11579821871313292092
Key not found in cache, creating new one! 11579821871313292092
Time taken to decrypt: 226 ms
Cache key: 14650601133528733758
###Key found in cache!
###Time taken to decrypt: 0 ms
Cache key: 7881818280167815519
Key not found in cache, creating new one! 7881818280167815519
Time taken to decrypt: 278 ms
Cache key: 16204407719138663129
Key not found in cache, creating new one! 16204407719138663129
Time taken to decrypt: 269 ms
Cache key: 14650601133528733758
###Key found in cache!
###Time taken to decrypt: 0 ms
Cache key: 8474487473980109098
Key not found in cache, creating new one! 8474487473980109098
Time taken to decrypt: 264 ms
Cache key: 14569125712987388781
Key not found in cache, creating new one! 14569125712987388781
Time taken to decrypt: 258 ms
Entry 0 found in the hash set.
Test passed!
test db::tests::test_fetch_string_column_scalar ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out; finished in 4.32sYou can see that there is an insertion of three entries with the same salt and the result in decryption that two are with 0 time because the combination of Salt+Password was in cache. I used queue of 50 elements now to start, but it is just a number we can also increase it. So in this way typically new user will have the first time some delay due to encrytpion decryption, but than with cache they are super fast response. I tested just locally with rust tests, tonite I will try this in a real scenario while completing flows of orders. |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/util.rs (1)
406-411: Same misleading error mapping for seller sidePlease apply the same fix here as in the buyer branch to keep error semantics consistent.
🧹 Nitpick comments (6)
src/main.rs (1)
75-75: Consider implementing the database connection pooling suggestionThe TODO comment correctly identifies a potential improvement. Moving the database connection pool to a shared reference-counted container would prevent database reinitialization across threads.
Consider implementing this using an Arc<Pool> stored in a static OnceLock, similar to how MOSTRO_DB_PASSWORD is managed.
src/app/take_sell.rs (1)
53-55: Consider performance impact of buyer_has_pending_order.Based on the relevant code snippets from
src/db.rs, thebuyer_has_pending_orderfunction now needs to decrypt all pending orders' master buyer pubkeys to check for matches. This could cause performance issues as the number of pending orders grows.Consider implementing a caching mechanism similar to the one mentioned in the PR comments, or explore indexing strategies that would minimize the number of decryptions needed.
src/util.rs (1)
548-552: Left-over debuggingprintln!The
println!statement will spam STDOUT in production.
Switch totracing::debug!(or remove entirely).- println!( - "Inside update_order_event order_updated status {:?} - order id {:?}", - order_updated.status, order_updated.id, - ); + tracing::debug!( + "update_order_event → status {:?} - order id {:?}", + order_updated.status, + order_updated.id, + );src/db.rs (3)
161-201: Unnecessary confirmation prompt when password is skippedIf the admin presses to skip encryption you still prompt for confirmation, forcing the user to hit twice.
Add an early return when
password.is_empty()to streamline UX:- if password.is_empty() { - println!("Password skipped!!"); - break; - } else { - // Confirm password - print!("Confirm database password: "); - } + if password.is_empty() { + println!("Password skipped – database will stay unencrypted."); + break; + } + // Confirm password + print!("Confirm database password: ");
300-338: Redundant double parsing of Argon2 hash
hash_passwordalready returns aPasswordHashstring ready for storage; immediately parsing it again and converting back to string adds cost and cognitive load.- let key = argon2 + let password_hash = argon2 .hash_password(password.expose_secret().as_bytes(), &salt)? .to_string(); - - // Create a key suitable for ChaCha20Poly1305 - let key = PasswordHash::new(&key)?.to_string();Store
password_hashdirectly.
35-50: Permission hardening bypassed on WindowsOn Windows the function silently prints a warning but still returns
Ok(()), leaving the DB wide-open.
Either:
- Abort the start-up when secure permissions cannot be set, or
- Provide an actionable message (e.g., “run
icacls ...”) and clearly mark the instance as unsafe.Fail-fast is preferable to avoid a false sense of security.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
Cargo.toml(1 hunks)migrations/20231005195154_users.sql(1 hunks)src/app.rs(1 hunks)src/app/take_buy.rs(2 hunks)src/app/take_sell.rs(2 hunks)src/app/trade_pubkey.rs(2 hunks)src/db.rs(9 hunks)src/main.rs(4 hunks)src/scheduler.rs(2 hunks)src/util.rs(5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/app/take_sell.rs (1)
src/db.rs (2)
buyer_has_pending_order(821-870)update_user_trade_index(735-767)
🔇 Additional comments (12)
src/scheduler.rs (2)
207-207: LGTM! Improved log message clarityConverting seconds to minutes in the log message makes it more readable for users.
233-253: LGTM! Simplified error handling patternThe error handling has been improved by replacing the
Resultreturn type with early returns and explicit error logging, which is more consistent with the codebase's error handling approach.src/app/take_buy.rs (1)
70-74: LGTM! Implemented encryption for seller's public keyThe seller's public key is now properly encrypted before storage, improving privacy by decoupling user identities from orders. The use of
MOSTRO_DB_PASSWORDfor encryption aligns with the PR objectives.src/main.rs (2)
35-35: LGTM! Implemented secure password storageUsing
OnceLock<SecretString>forMOSTRO_DB_PASSWORDprovides thread-safe initialization and secure memory handling for the database password.
49-51: LGTM! Security improvement for password entryClearing the terminal screen before startup prevents sensitive information like passwords from being visible in terminal scrollback history.
Cargo.toml (1)
49-52: New dependencies align with encryption requirements.The additions of
rpassword,argon2,secrecy, andclearscreenare appropriate for implementing the password-based encryption system described in the PR objectives. These libraries are well-established choices for their respective functions.src/app/trade_pubkey.rs (2)
28-49: Key decryption logic properly handles both buyer and seller cases.The implementation correctly decrypts either the master buyer or master seller public key based on which one exists in the order. Error handling is comprehensive with appropriate error mapping.
51-59: New code correctly matches decrypted keys.The matching logic properly uses the decrypted keys when comparing against the event sender's public key, maintaining the intended functionality while accommodating the new encryption scheme.
src/app.rs (1)
52-72: Enhanced error logging improves diagnostics.The improved
warning_msgfunction now provides more detailed logging by extracting specific error messages from variousServiceErrorvariants. This will be particularly helpful for diagnosing encryption/decryption issues in production.src/app/take_sell.rs (2)
87-91: Master buyer pubkey is now properly encrypted.The implementation correctly uses the
store_encryptedfunction to encrypt the buyer's identity public key before storing it in the database. The error handling is appropriate, mapping encryption errors to database access errors.
5-5: Import additions support the encryption logic.The new imports correctly bring in the necessary components:
MOSTRO_DB_PASSWORDfor access to the encryption key, andstore_encryptedfunction from the order module.Also applies to: 7-7, 11-11
src/db.rs (1)
123-145:⚠️ Potential issuePassword left un-zeroised after verification
After a successful comparison the plaintext password remains on the stack.
Callpassword.zeroize()once it has been stored inMOSTRO_DB_PASSWORD.if MOSTRO_DB_PASSWORD.set(SecretString::from(password)).is_ok() { + // wipe stack copy + password.zeroize(); Ok(true)Likely an incorrect or invalid review comment.
|
Opened for review, just delete your I will look to rabbit comments asap. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/db.rs (2)
218-236: Blocking STDIN in an async context can stall the reactor
get_user_password()performs synchronousread_password()calls inside the
asyncconnect()path. On Tokio’s multithreaded runtime this blocks an
executor thread and may introduce latency spikes.Consider isolating the prompt in a
tokio::task::spawn_blockingclosure or
moving database initialisation to a dedicated synchronous bootstrap step
executed before the async runtime starts.
784-816: Linear scan & decrypt in hot path may hurt performanceBoth
seller_has_pending_orderandbuyer_has_pending_orderfetch all
pending orders and decrypt each key to find a match.
With a large order table this becomes O(n × decrypt_cost).Ideas:
- Store a deterministic, non-reversible hash (e.g. SHA-256) of the master
pubkey next to the ciphertext and index that column; compare hashes before
decrypting.- Keep an in-memory
HashSetof decrypted keys populated at startup and
updated through triggers/callbacks.This keeps O(1) lookup per request while preserving on-disk secrecy.
src/app/rate_user.rs (1)
101-104: Avoid movingcounterpartwhen borrowing suffices
is_user_presentonly needs an immutable reference to the key; passing the
ownedStringforces an unnecessary move.- let mut user_to_vote = is_user_present(pool, counterpart) + let mut user_to_vote = is_user_present(pool, counterpart.clone())Minor, but keeps
counterpartavailable for future use without another clone.src/app/release.rs (1)
397-405:identity_pubkeyis moved before reuse
identity_pubkeyis moved intois_user_present; if later logic needs the
same value (e.g. for logging), it will be unavailable.- let user = crate::db::is_user_present(&pool, identity_pubkey) + let user = crate::db::is_user_present(&pool, identity_pubkey.clone())This preserves the original string for any subsequent operations at negligible
cost.src/app/admin_take_dispute.rs (1)
30-37: Consider caching the global password retrieval
MOSTRO_DB_PASSWORD.get()is inexpensive, but the expression appears several
times in tight loops. Binding it once improves readability and avoids repeated
calls.let db_pwd = MOSTRO_DB_PASSWORD.get(); let master_buyer_key = order.get_master_buyer_pubkey(db_pwd)?; // …Purely stylistic; feel free to skip if you prefer the explicit calls.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/app/admin_take_dispute.rs(6 hunks)src/app/rate_user.rs(4 hunks)src/app/release.rs(2 hunks)src/app/take_buy.rs(2 hunks)src/app/take_sell.rs(2 hunks)src/app/trade_pubkey.rs(2 hunks)src/db.rs(9 hunks)src/util.rs(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/app/trade_pubkey.rs
- src/app/take_buy.rs
- src/app/take_sell.rs
- src/util.rs
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/app/rate_user.rs (1)
src/db.rs (1)
is_user_present(689-704)
src/app/admin_take_dispute.rs (1)
src/db.rs (1)
is_user_present(689-704)
src/app/release.rs (1)
src/db.rs (1)
is_user_present(689-704)
src/db.rs (2)
src/cli/settings.rs (2)
new(127-154)get_db(164-166)src/util.rs (1)
get_keys(468-478)
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
src/db.rs (4)
53-120: Password requirements implementation follows security best practices.The password validation logic implements strong requirements (12+ chars, mixed case, digits, special chars) with clear error messages. The implementation allows bypassing requirements for empty passwords, which appears to be an intentional design choice to make encryption optional.
Consider using a more specific check for special characters rather than
!c.is_alphanumeric()which also includes spaces and other non-printing characters:- if self.requires_special && !password.chars().any(|c| !c.is_alphanumeric()) { + if self.requires_special && !password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?/".contains(c)) {
273-294: Secure authentication with retry limits for existing databases.The implementation provides a good security practice of limiting password attempts to prevent brute force attacks, with appropriate termination on repeated failures.
Consider using a cleaner loop structure for retry attempts:
- let max_attempts = 3; - let mut attempts = 0; - - if MOSTRO_DB_PASSWORD.get().is_none() { - while let Some(argon2_hash) = get_admin_password(&conn).await? { - // Database already exists - and yet opened - let parsed_hash = PasswordHash::new(&argon2_hash) - .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; - if check_password_hash(&parsed_hash).is_ok() { - break; - } else { - attempts += 1; - println!("Wrong password, attempts: {}", attempts); - if attempts >= max_attempts { - println!("Maximum password attempts exceeded!!"); - std::process::exit(1); - } - } - } - } + const MAX_ATTEMPTS: usize = 3; + + if MOSTRO_DB_PASSWORD.get().is_none() { + if let Some(argon2_hash) = get_admin_password(&conn).await? { + let parsed_hash = PasswordHash::new(&argon2_hash) + .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; + + for attempt in 1..=MAX_ATTEMPTS { + if check_password_hash(&parsed_hash).is_ok() { + break; + } + + println!("Wrong password, attempts: {}/{}", attempt, MAX_ATTEMPTS); + if attempt >= MAX_ATTEMPTS { + println!("Maximum password attempts exceeded!!"); + std::process::exit(1); + } + } + } + }
300-334: Strong password hashing implementation with Argon2.The function correctly generates a random salt and uses Argon2 for secure password hashing, following cryptographic best practices.
The comment on line 322 refers to storing "the key and salt securely", but only the key (password hash) is stored. Consider updating the comment for clarity:
- // Store the key and salt securely (e.g., in a file or database) + // Store the password hash securely in the database
959-1057: Good test coverage for encryption and decryption functionality.The test module properly tests the encryption and decryption of stored strings in an in-memory SQLite database, which is valuable for validating the core functionality of the encryption feature.
Consider these improvements to the test:
- Use a constant for the number of test entries rather than hardcoding the value in multiple places
- Instead of using
unwrap()which would cause panics, propagate errors consistently using the?operator- Either use a consistent approach for salts (all fixed or all random) or explicitly document why you're testing both approaches
for i in 0..total_entries { let value_string = format!("Entry {}", i % 5); println!("Inserting value {}", value_string); - let value_string = if i % 3 == 1 { - store_encrypted(&value_string, Some(&password), Some(salt)) - .await - .unwrap() // Encrypt the string - // Note: id is INTEGER PRIMARY KEY, so it will auto-increment if we don't specify it, - // or we can specify it like this. Let's specify for clarity. - } else { - store_encrypted(&value_string, Some(&password), None) - .await - .unwrap() // Encrypt the string - }; + // Test both fixed and random salts + let salt_option = if i % 3 == 1 { Some(salt) } else { None }; + let value_string = store_encrypted(&value_string, Some(&password), salt_option) + .await + .map_err(|e| Error::RowNotFound)?; // Convert the error to sqlx::Error
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(9 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (1)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.522Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
🔇 Additional comments (9)
src/db.rs (9)
35-50: Well-implemented file permission restriction with cross-platform handling.The function properly secures database files with owner-only permissions on Unix systems and provides appropriate messaging for Windows environments where different permission models exist.
122-145: Well-structured password verification with secure handling.The password verification function properly uses Argon2 for hash verification and securely stores the password in memory using SecretString.
147-203: Comprehensive password setup process with secure memory handling.The function provides a robust password creation workflow with clear requirements display, strength validation, confirmation matching, and secure storage. The use of
zeroize()on line 186 to clear sensitive data from memory is an excellent security practice.
219-222: File permission restriction is correctly applied during database creation.The implementation ensures that newly created database files have restricted permissions from the start, which is a good security practice.
232-240: Secure admin password handling during database initialization.The code properly collects and stores the admin password hash during database creation, ensuring that encryption can be set up right from the beginning.
643-663: Clean implementation to fetch admin password from database.The function properly retrieves the admin password hash from the users table, handling potential errors appropriately.
702-713: User creation updated to include password handling.The function now correctly handles the admin_password field in the User struct, enabling the encryption feature.
780-812: Secure seller pending order check with encryption support.The implementation handles both encrypted and unencrypted database scenarios, properly decrypting stored public keys when needed. This maintains security while preserving functionality.
Based on the retrieved learning, the error handling for
decrypt_dataon line 793 is correct. Previous review comments had suggested removing themap_err(MostroInternalErr)call, but we now understand that this conversion is necessary becausedecrypt_datafrom mostro_core::order returns a type that needs to be wrapped.
830-863: Secure buyer pending order check with encryption support.The implementation mirrors the seller check logic, maintaining consistent error handling and encryption support throughout the codebase.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
src/db.rs (5)
219-221: Redundant TODO comment on restricting file permissionsThe TODO comment about checking if the permission restriction works on Windows is misleading since the code already handles Windows by using cfg attributes.
- // TODO: check if this is works on windows restrict_file_permissions(db_path) .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
235-239: Error handling inconsistency with password operationsThe password hash storage error is converted to a
DbAccessErrorwhich isn't sufficiently specific about the actual operation failing.Consider using a more specific error variant for password handling operations:
if let Some(password) = MOSTRO_DB_PASSWORD.get() { - store_password_hash(password, &pool).await.map_err(|e| { - MostroInternalErr(ServiceError::DbAccessError(e.to_string())) - })?; + store_password_hash(password, &pool).await?; }This assumes
store_password_hashalready returns an appropriateMostroErrortype. If not, consider adding a specific error variant for password operations.
282-291: Improve user feedback for password attemptsThe current implementation shows "Wrong password, attempts: X" without indicating how many attempts are allowed.
Make the password attempt feedback more user-friendly:
- println!("Wrong password, attempts: {}", attempts); + println!("Wrong password. {} of {} attempts used.", attempts, max_attempts); if attempts >= max_attempts { - println!("Maximum password attempts exceeded!!"); + println!("Maximum password attempts exceeded. Please contact the administrator."); std::process::exit(1); }
792-798: Fix typo in "Foundd" commentThere's a typo in the comment which indicates a lack of attention to detail.
if master_pubkey_decrypted == pubkey { - // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-payment order with the same pubkey return Ok(true); }
845-848: Fix typo in "Foundd" comment here tooSame typo appears in another comment.
if master_pubkey_decrypted == pubkey { - // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-buyer-invoice order with the same pubkey return Ok(true); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(9 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (1)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.522Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
🔇 Additional comments (2)
src/db.rs (2)
802-812: Query optimization with query_scalarYou're correctly using
query_scalarfor this database check, which is cleaner than manually extracting values from rows. This is a good practice.
959-1056: 🛠️ Refactor suggestionTest hardcodes credentials and doesn't clean up properly
The test hardcodes a password and doesn't properly clean up sensitive data after the test.
Apply these practices to improve security in the test:
- Use a randomly generated password for testing
- Properly zeroize the password after use
- Don't expose encryption timing details in logs
- const SECRET_PASSWORD: &str = "test_password"; // Example password for encryption + // Generate a random password for testing + fn generate_test_password() -> String { + use rand::{distributions::Alphanumeric, Rng}; + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect() + } #[tokio::test] async fn test_fetch_string_column_scalar() -> Result<(), Error> { // 1. Setup: Create in-memory DB and table let pool = setup_db().await?; println!("In-memory database and table created for test."); // 2. Populate: Insert 100 entries let total_entries = 10; - // Use a SecretString for the password - let password = SecretString::from(SECRET_PASSWORD); + // Use a SecretString with a random password + let mut raw_password = generate_test_password(); + let password = SecretString::from(raw_password.clone()); println!("Inserting {} entries...", total_entries);And at the end of the test:
println!("Test passed!"); + // Clean up sensitive data + raw_password.zeroize(); Ok(()) // Return Ok to indicate test success }Likely an incorrect or invalid review comment.
Catrya
left a comment
There was a problem hiding this comment.
Hi @arkanoider
When at least one of the two users in an order is in privacy mode, if they open a dispute, the admin attempts to accept it and this happens:
2025-04-30T17:33:25.144229Z WARN mostrod::app: Error in AdminTakeDispute with context Error in database access: Error caused by Error in database access: no rows returned by a query that expected to return at least one row - inner message Error caused by Error in database access: no rows returned by a query that expected to return at least one rowIn the database, the dispute goes to in-progress status, but the new dispute event with the updated status is not published, nor are messages sent to the users or the admin.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
src/db.rs (6)
214-267: Consider removing database file on password setup failureIf the
get_user_password()orstore_password_hash()functions fail after the database file is created, the file remains but might be in an inconsistent state. Consider extending the error handling to clean up the file in these cases as well.match sqlx::migrate!().run(&pool).await { Ok(_) => { tracing::info!( "Successfully created database file at {}", db_path.display(), ); - // Get user password - get_user_password().await?; - // Save admin password hash securely - if let Some(password) = MOSTRO_DB_PASSWORD.get() { - store_password_hash(password, &pool).await.map_err(|e| { - MostroInternalErr(ServiceError::DbAccessError(e.to_string())) - })?; - } - pool + // Get user password + match get_user_password().await { + Ok(_) => { + // Save admin password hash securely + if let Some(password) = MOSTRO_DB_PASSWORD.get() { + match store_password_hash(password, &pool).await { + Ok(_) => pool, + Err(e) => { + if let Err(cleanup_err) = std::fs::remove_file(db_path) { + tracing::error!( + error = %cleanup_err, + path = %db_path.display(), + "Failed to clean up database file after password storage failure" + ); + } + return Err(MostroInternalErr(ServiceError::DbAccessError(e.to_string()))) + } + } + } else { + pool + } + }, + Err(e) => { + if let Err(cleanup_err) = std::fs::remove_file(db_path) { + tracing::error!( + error = %cleanup_err, + path = %db_path.display(), + "Failed to clean up database file after password setup failure" + ); + } + return Err(e) + } + } } Err(e) => {
780-813: Add caching for decrypted pubkeys in seller_has_pending_orderThe function decrypts potentially many public keys in a loop, which could be inefficient. Consider implementing a cache to avoid repeated decryption of the same key, especially in cases where a seller might have multiple pending orders.
// Check if database is encrypted if MOSTRO_DB_PASSWORD.get().is_some() { + // Cache for decrypted pubkeys to avoid repeated decryption + let mut decrypted_cache = std::collections::HashMap::new(); + let orders_to_check: Vec<String> = sqlx::query_scalar( r#" SELECT master_seller_pubkey FROM orders WHERE status = 'waiting-payment' "#, ) .fetch_all(&mut conn) .await .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; // search for a waiting-buyer-invoice order with the same pubkey for master_key in orders_to_check { - // Decrypt master buyer pubkey - let master_pubkey_decrypted = - decrypt_data(master_key, MOSTRO_DB_PASSWORD.get()).map_err(MostroInternalErr)?; + // Check cache first, decrypt only if not found + let master_pubkey_decrypted = if let Some(decrypted) = decrypted_cache.get(&master_key) { + decrypted.clone() + } else { + let decrypted = decrypt_data(master_key.clone(), MOSTRO_DB_PASSWORD.get()).map_err(MostroInternalErr)?; + decrypted_cache.insert(master_key, decrypted.clone()); + decrypted + }; if master_pubkey_decrypted == pubkey { // Foundd another waiting-buyer-invoice order with the same pubkey return Ok(true);
831-864: Add caching for decrypted pubkeys in buyer_has_pending_orderSimilar to the seller function, this function decrypts potentially many public keys in a loop. Implementing a cache would improve performance.
// Check if database is encrypted if MOSTRO_DB_PASSWORD.get().is_some() { + // Cache for decrypted pubkeys to avoid repeated decryption + let mut decrypted_cache = std::collections::HashMap::new(); + let orders_to_check: Vec<String> = sqlx::query_scalar( r#" SELECT master_buyer_pubkey FROM orders WHERE status = 'waiting-buyer-invoice' "#, ) .fetch_all(&mut conn) .await .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; // search for a waiting-buyer-invoice order with the same pubkey for master_key in orders_to_check { - // Decrypt master buyer pubkey - let master_pubkey_decrypted = - decrypt_data(master_key, MOSTRO_DB_PASSWORD.get()).map_err(MostroInternalErr)?; + // Check cache first, decrypt only if not found + let master_pubkey_decrypted = if let Some(decrypted) = decrypted_cache.get(&master_key) { + decrypted.clone() + } else { + let decrypted = decrypt_data(master_key.clone(), MOSTRO_DB_PASSWORD.get()).map_err(MostroInternalErr)?; + decrypted_cache.insert(master_key, decrypted.clone()); + decrypted + }; if master_pubkey_decrypted == pubkey { // Foundd another waiting-buyer-invoice order with the same pubkey return Ok(true);
795-798: Fix typo in commentThere's a typo in both
seller_has_pending_orderandbuyer_has_pending_orderfunctions: "Foundd" should be "Found".if master_pubkey_decrypted == pubkey { - // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-buyer-invoice order with the same pubkey return Ok(true); }Also applies to: 846-849
959-1057: Test function looks good, but consider adding more comprehensive testsThe test for encryption/decryption functionality is well-implemented. Consider adding tests for other scenarios:
- Empty password handling
- Password validation
- Testing password attempt limits
- Performance comparison with and without caching
960-998: Refactor test to use constants for predictable test casesFor more predictable test results and clearer assertions, refactor the test to use predefined test data instead of generating entries in a loop with varying conditions.
#[tokio::test] async fn test_fetch_string_column_scalar() -> Result<(), Error> { // 1. Setup: Create in-memory DB and table let pool = setup_db().await?; println!("In-memory database and table created for test."); - // 2. Populate: Insert 100 entries - let total_entries = 10; + // 2. Populate: Insert test entries with known values + let test_entries = [ + "Test Entry 1", + "Test Entry 2", + "Test Entry 3", + "Test Entry 4", + "Test Entry 5", + ]; // Use a SecretString for the password let password = SecretString::from(SECRET_PASSWORD); - println!("Inserting {} entries...", total_entries); - for i in 0..total_entries { - let value_string = format!("Entry {}", i % 5); - println!("Inserting value {}", value_string); - let value_string = if i % 3 == 1 { - store_encrypted(&value_string, Some(&password)) - .await - .unwrap() // Encrypt the string - // Note: id is INTEGER PRIMARY KEY, so it will auto-increment if we don't specify it, - // or we can specify it like this. Let's specify for clarity. - } else { - store_encrypted(&value_string, Some(&password)) - .await - .unwrap() // Encrypt the string - }; + println!("Inserting {} entries...", test_entries.len()); + for (i, entry) in test_entries.iter().enumerate() { + println!("Inserting value {}", entry); + let encrypted_string = store_encrypted(entry, Some(&password)) + .await + .unwrap(); // Encrypt the string
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
Cargo.toml(2 hunks)src/app/admin_take_dispute.rs(2 hunks)src/app/release.rs(5 hunks)src/db.rs(9 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- Cargo.toml
- src/app/admin_take_dispute.rs
- src/app/release.rs
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (1)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
🔇 Additional comments (3)
src/db.rs (3)
35-50: LGTM: Secure file permission implementationGood implementation of platform-specific file permissions. The code correctly sets owner-only read/write permissions (0o600) on Unix systems and provides a warning for Windows where the equivalent functionality isn't implemented.
52-120: Password validation is well implementedThe password requirements implementation is thorough and follows security best practices with appropriate validation checks for length, complexity, and character variety.
274-294: 🛠️ Refactor suggestionHandle password attempt counting properly
The code correctly implements a maximum number of password attempts but has a potential issue. If a password is correct but
MOSTRO_DB_PASSWORD.set()fails, it will still count as a failed attempt. Consider restructuring to separate validation from storage failures.if MOSTRO_DB_PASSWORD.get().is_none() { while let Some(argon2_hash) = get_admin_password(&conn).await? { // Database already exists - and yet opened let parsed_hash = PasswordHash::new(&argon2_hash) .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; - if check_password_hash(&parsed_hash).is_ok() { + // Verify password without storing it first + print!("Enter database password (attempt {}/{}): ", attempts + 1, max_attempts); + std::io::stdout().flush().unwrap(); + let password = read_password() + .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; + + if Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok() { + // Password is correct, now try to store it + if MOSTRO_DB_PASSWORD.set(SecretString::from(password)).is_ok() { + break; + } else { + return Err(MostroInternalErr(ServiceError::DbAccessError( + "Failed to save password".to_string(), + ))); + } break; } else { attempts += 1;Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/db.rs (2)
53-120: Password strength validation is well-implemented but has a small optimization opportunity.The password requirements implementation is thorough with clear validation messages. The intentional handling of empty passwords (line 101-103) aligns with the project's approach of making encryption optional.
In the
is_strong_passwordmethod, you're callingvalidatetwice unnecessarily:fn is_strong_password(&self, password: &str) -> bool { - match self.validate(password).is_empty() { + let failures = self.validate(password); + match failures.is_empty() { true => true, false => { println!("\nPassword is not strong enough:"); - for failure in self.validate(password) { + for failure in failures { println!("- {}", failure); } false } } }
779-813: Encryption-aware pending order check with proper error handling.The implementation correctly handles both encrypted and non-encrypted database scenarios. For encrypted databases, it fetches all relevant orders and decrypts each key for comparison.
I notice a small typo in line 796: "Foundd" should be "Found", but this is just a comment and doesn't affect functionality.
- // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-buyer-invoice order with the same pubkey
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(9 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (2)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
🔇 Additional comments (10)
src/db.rs (10)
36-50: Good security implementation with platform-specific behavior.The function properly restricts database file permissions to owner-only read/write (0o600) on Unix systems, with an appropriate placeholder message for Windows. This helps protect the database from unauthorized access by other users on the same system.
122-145: Properly implemented password verification with security focus.The password verification implementation correctly uses Argon2 for secure password hashing and saves the password securely using SecretString to prevent memory exposure.
I note you've implemented the 3-attempt limit with application exit elsewhere in the code, which is an excellent security practice to prevent brute force attacks.
147-203: Well-structured password creation flow with appropriate security prompts.The password creation interface provides clear requirements to users, handles empty passwords gracefully, and securely stores the credentials. The use of
zeroize()on line 186 is an excellent security practice to clear sensitive data from memory.
213-266: Good database initialization with security considerations.The database file creation properly restricts permissions and implements the password setup flow. The error handling is comprehensive and includes proper cleanup on failure.
267-296: Strong password verification flow with brute-force protection.The implementation properly handles existing database access by verifying the password against the stored hash. The 3-attempt limit with application exit on line 288-290 is an excellent security measure against brute force attacks.
301-334: Strong password hashing using industry-standard Argon2.The implementation correctly uses Argon2 with a random salt for secure password hashing. The hash is properly stored in the user database entry.
643-663: Admin password retrieval function looks good.The function correctly retrieves the admin password from the users table with appropriate error handling.
702-729: User creation function properly extended to handle admin password.The SQL query has been updated to include the admin_password field, with appropriate binding in the query parameters.
830-863: Encryption-aware buyer pending order check works correctly.Similar to the seller check, this implementation correctly handles both encrypted and non-encrypted databases. The error handling is consistent and appropriate.
Same typo in line 846:
- // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-buyer-invoice order with the same pubkey
959-1056: Well-structured test for encryption/decryption functionality.The test correctly sets up an in-memory database, inserts encrypted entries, retrieves and decrypts them. The timing information logged is valuable for performance assessment during testing.
|
@arkanoider I hadn't noticed the conflicts, I'm sure you had, but just in case, I remind you to keep an eye on it :) |
Hi @Catrya , i was able to reproduce it....seems that for a reason I cannot understand CLI didn't send in that case the |
Okay, but that bug I mentioned isn't related to this PR; it's older. What do you think about fixing that bug in a separate PR?
I was referring to that here in this PR says that there are branch conflicts that prevent this PR from being merged. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/db.rs (2)
797-815: Fix typo in commentThere's a small typo in the comment.
- // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-buyer-invoice order with the same pubkey
863-864: Fix typo in commentSame typo appears here.
- // Foundd another waiting-buyer-invoice order with the same pubkey + // Found another waiting-buyer-invoice order with the same pubkey
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
Cargo.toml(1 hunks)src/app/take_buy.rs(2 hunks)src/app/take_sell.rs(2 hunks)src/db.rs(9 hunks)src/util.rs(5 hunks)
✅ Files skipped from review due to trivial changes (1)
- Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (3)
- src/app/take_buy.rs
- src/app/take_sell.rs
- src/util.rs
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (2)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
🔇 Additional comments (5)
src/db.rs (5)
35-50: Good platform-specific permission handlingThe implementation correctly restricts file permissions on Unix systems using 0o600 (owner read/write only) which is appropriate for sensitive files like encrypted databases. The code also properly handles Windows with a clear warning message.
52-120: Password strength validation is well-implementedThe
PasswordRequirementsstruct provides a robust set of validation rules with clear error messages. The default requirements (12+ chars, mixed case, numbers, and special characters) follow modern security best practices.
122-153: Security best practice: Anti-timing attack measuresThe random delay implementation in password verification is an excellent security measure that helps mitigate timing attacks. The combination of consistent delay (100ms) plus random component (0-999ms) provides good protection while remaining responsive.
155-220: Robust password creation flow with secure handlingThe password creation flow follows security best practices including:
- Clear requirements communication
- Confirmation input
- Secure input via
read_password- Strong validation
- Immediate memory cleanup with
zeroize- Secure storage using
SecretString
976-1073: Well-structured test for encryption functionalityThe test module provides good coverage of the core encryption/decryption functionality, using an in-memory SQLite database for isolation. The performance measurement is helpful for understanding the real-world impact of the encryption layer.
Yes I will rebase this branch on main when we close smaller prs. Will take care of that. Yes I agree that is better to split that possible mostro-cli issue in another PR when the cause it's clear. |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/db.rs (2)
805-837: 🛠️ Refactor suggestionApply the same caching mechanism here for consistent implementation.
Similar to the seller pending order check, the buyer pending order check would benefit from the same caching mechanism to improve performance with large numbers of orders.
759-791: 🛠️ Refactor suggestionConsider caching decrypted keys for better performance.
The approach of decrypting all master seller keys for every pending order check could become performance-intensive as the database grows. Consider implementing a caching mechanism for recently decrypted keys to reduce repeated computations.
+ // Add this at the module level with appropriate mutex protection + use std::collections::HashMap; + use std::sync::Mutex; + lazy_static! { + static ref DECRYPTED_KEYS_CACHE: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new()); + } + // Inside the function where decryption happens if MOSTRO_DB_PASSWORD.get().is_some() { let orders_to_check: Vec<String> = sqlx::query_scalar(...) .fetch_all(pool) .await .map_err(...)?; for master_key in orders_to_check { + // Check cache first + let master_pubkey_decrypted = { + let mut cache = DECRYPTED_KEYS_CACHE.lock().unwrap(); + if let Some(cached) = cache.get(&master_key) { + cached.clone() + } else { + let decrypted = decrypt_data(master_key.clone(), MOSTRO_DB_PASSWORD.get()) + .map_err(MostroInternalErr)?; + // Cache with reasonable size limit + if cache.len() > 1000 { + // Simple eviction policy - clear half the cache when full + let keys_to_remove: Vec<String> = cache.keys() + .take(cache.len() / 2) + .cloned() + .collect(); + for key in keys_to_remove { + cache.remove(&key); + } + } + cache.insert(master_key.clone(), decrypted.clone()); + decrypted + } + }; - // Decrypt master seller pubkey - let master_pubkey_decrypted = - decrypt_data(master_key, MOSTRO_DB_PASSWORD.get()).map_err(MostroInternalErr)?; if master_pubkey_decrypted == pubkey { return Ok(true); } } }
🧹 Nitpick comments (3)
src/db.rs (3)
52-106: Clarify the intentional bypass of validation for empty passwords.The password validation logic includes a special case that clears all validation failures if the password is empty. While this might be intentional to allow skipping encryption, it should be explicitly documented for clarity.
// If password is empty, clear failures if password.is_empty() { + // Empty password is allowed to support optional encryption failures.clear(); }
933-1030: Add more assertions to strengthen the test.The encryption/decryption test is well-structured but could benefit from more explicit assertions to verify the expected behavior beyond just checking for success.
// At line 1021 if hash_set_values.contains("Entry 0") { println!("Entry 0 found in the hash set."); } else { println!("Entry 0 not found in the hash set."); } +// Add explicit assertions to verify expected behavior +assert!(hash_set_values.contains("Entry 0"), "Should contain Entry 0"); +assert!(hash_set_values.contains("Entry 1"), "Should contain Entry 1"); +assert!(hash_set_values.contains("Entry 2"), "Should contain Entry 2"); +assert!(hash_set_values.contains("Entry 3"), "Should contain Entry 3"); +assert!(hash_set_values.contains("Entry 4"), "Should contain Entry 4"); +assert_eq!(hash_set_values.len(), 5, "Should have exactly 5 unique entries");
994-998: Consider batching database insertions for better performance in tests.For large test datasets, using a batch insert approach could significantly improve test execution time.
-// Individual inserts -for i in 0..total_entries { - let value_string = format!("Entry {}", i % 5); - println!("Inserting value {}", value_string); - let value_string = if i % 3 == 1 { - store_encrypted(&value_string, Some(&password)) - .await - .unwrap() - } else { - store_encrypted(&value_string, Some(&password)) - .await - .unwrap() - }; - sqlx::query("INSERT INTO items (id, value) VALUES (?, ?)") - .bind(i as i64) - .bind(&value_string) - .execute(&pool) - .await?; -} +// Prepare batch values +let mut query_builder = String::from("INSERT INTO items (id, value) VALUES "); +let mut params = Vec::new(); + +for i in 0..total_entries { + let value_string = format!("Entry {}", i % 5); + println!("Inserting value {}", value_string); + let encrypted_value = store_encrypted(&value_string, Some(&password)) + .await + .unwrap(); + + if i > 0 { + query_builder.push_str(", "); + } + query_builder.push_str(&format!("({}, ?)", i)); + params.push(encrypted_value); +} + +// Execute batch insert +let mut query = sqlx::query(&query_builder); +for param in params { + query = query.bind(param); +} +query.execute(&pool).await?;Note: This is a performance optimization suggestion for the test code, but may not be necessary for the small number of entries (10) currently used.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(17 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (2)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
🔇 Additional comments (5)
src/db.rs (5)
35-50: Good implementation of platform-specific file permission restrictions.The
restrict_file_permissionsfunction properly handles platform differences with conditional compilation for Unix vs Windows. Setting permissions to 0o600 (owner read/write only) is a good security measure for protecting sensitive database files.
127-135: Good security practice with randomized delay.Adding a small, randomized delay before password verification is an excellent security practice that helps mitigate timing attacks by obscuring the actual processing time of password validation.
294-310: Good implementation of password attempt limiting.The code limits password attempts to 3 tries, which is a solid security practice to prevent brute force attacks. Exiting the application after the limit is reached is an appropriate action for a sensitive database operation.
827-837: Good use of query_scalar for boolean existence check.Using
query_scalar::<_, bool>directly when the database isn't encrypted is an efficient approach that avoids unnecessary type conversions fromi64tobool.
631-651: Well-structured admin password retrieval function.The
get_admin_passwordfunction is cleanly implemented to fetch the admin's password hash from the database with proper error handling and optional result handling.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/db.rs (2)
52-121: Robust password validation implementation with room for minor improvement.The password requirements implementation is thorough and provides good security validation. The empty password handling with the comment on line 102 clarifies the intent.
Consider implementing a strength score calculation method rather than just binary validation, which would allow for more nuanced feedback to users based on password complexity.
759-793: Secure seller pending order check with encrypted key support.The implementation properly handles both encrypted and unencrypted database states, providing a fallback mechanism for unencrypted databases.
There's a typo in the comment on line 775: "Foundd" should be "Found".
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(17 hunks)
🔇 Additional comments (9)
src/db.rs (9)
24-50: Good security implementation of file permission restrictions.The implementation of Unix-specific file permission restrictions is well thought out. It's great to see a proper cross-platform approach with conditionally compiled code for Unix and Windows systems.
123-154: Good security practice with randomized delays for password verification.Including randomized delays between 100-1100ms during password verification is an excellent security practice to mitigate timing attacks. The error handling is also robust.
156-221: Well-implemented secure password input flow.The user password input flow is well-designed with:
- Clear password requirements presentation
- Secure password reading
- Confirmation mechanism
- Proper memory cleanup with zeroize
- Support for optional encryption (empty password)
230-314: Comprehensive database initialization with security checks.The database connection flow properly handles both new and existing databases with appropriate security measures. The 3-attempt limit for password entry is a good security practice.
319-352: Secure password hashing implementation.The implementation uses industry-standard Argon2 for password hashing with random salt generation, which is a strong security choice. The admin user creation is also handled properly.
354-530: Good refactoring of database operations.Several database functions have been refactored to execute queries directly on the connection pool rather than acquiring individual connections first, which is a good performance practice.
632-652: Clean admin password retrieval implementation.The function properly retrieves the admin password hash from the database with appropriate error handling.
805-838: Secure buyer pending order check with encrypted key support.Similar to the seller pending order check, this implementation handles both encrypted and unencrypted database states appropriately.
934-1031: Comprehensive encryption testing with benchmarking.The test module provides good validation of the encryption/decryption functionality with appropriate benchmarking of performance. Using an in-memory SQLite database for tests is an excellent approach.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
src/db.rs (4)
228-255: Consider handling database creation failure more gracefullyWhen setting up a new database, the code currently exits the application if password setup fails. Consider handling this more gracefully to prevent data loss.
// Get user password - get_user_password().await?; + match get_user_password().await { + Ok(_) => {} + Err(e) => { + tracing::error!("Failed to set up database password: {}", e); + println!("Failed to set up database password. Continuing without encryption."); + } + } // Save admin password hash securely if let Some(password) = MOSTRO_DB_PASSWORD.get() { store_password_hash(password, &pool).await.map_err(|e| { MostroInternalErr(ServiceError::DbAccessError(e.to_string())) })?; }
282-311: Three-attempt password verification with forced exitThe implementation of a three-attempt limit with application exit on failure is a strong security practice. This prevents brute force attacks by terminating the program after three failures.
Consider logging these failed attempts to help identify potential attacks.
println!("Wrong password, attempts: {}", attempts); + tracing::warn!("Failed database password attempt {}/{}", attempts, max_attempts); if attempts >= max_attempts { println!("Maximum password attempts exceeded!!"); + tracing::error!("Maximum password attempts exceeded, terminating application"); std::process::exit(1); }
797-841: Duplicate implementation pattern in buyer and seller functionsThe pattern for checking encrypted/unencrypted databases is identical in both
seller_has_pending_orderandbuyer_has_pending_order, with only the field and status names changed.Consider extracting this pattern into a helper function to reduce code duplication:
async fn has_pending_order_with_status( pool: &SqlitePool, pubkey: String, master_key_field: &str, status: &str ) -> Result<bool, MostroError> { // Validate public key format (32-bytes hex) if !pubkey.chars().all(|c| c.is_ascii_hexdigit()) || pubkey.len() != 64 { return Err(MostroCantDo(CantDoReason::InvalidPubkey)); } // Check if database is encrypted if MOSTRO_DB_PASSWORD.get().is_some() { let orders_to_check: Vec<String> = sqlx::query_scalar( &format!("SELECT {} FROM orders WHERE status = ?", master_key_field) ) .bind(status) .fetch_all(pool) .await .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; // search for orders with the same pubkey for master_key in orders_to_check { // Decrypt master pubkey let master_pubkey_decrypted = decrypt_data(master_key, MOSTRO_DB_PASSWORD.get()).map_err(MostroInternalErr)?; if master_pubkey_decrypted == pubkey { return Ok(true); } } Ok(false) } // if not encrypted, use the default search else { let exists = sqlx::query_scalar::<_, bool>( &format!("SELECT EXISTS (SELECT 1 FROM orders WHERE {} = ? AND status = ?)", master_key_field) ) .bind(pubkey) .bind(status) .fetch_one(pool) .await .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; Ok(exists) } }Then call it with the appropriate parameters:
pub async fn buyer_has_pending_order(pool: &SqlitePool, pubkey: String) -> Result<bool, MostroError> { has_pending_order_with_status(pool, pubkey, "master_buyer_pubkey", "waiting-buyer-invoice").await } pub async fn seller_has_pending_order(pool: &SqlitePool, pubkey: String) -> Result<bool, MostroError> { has_pending_order_with_status(pool, pubkey, "master_seller_pubkey", "waiting-payment").await }
936-1062: Comprehensive encryption test with performance trackingThe test module thoroughly verifies the encryption/decryption functionality:
- Creates in-memory test database
- Inserts multiple encrypted values
- Retrieves and decrypts them
- Verifies correctness with assertions
- Measures decryption performance
The performance tracking is valuable for understanding the real-world impact of encryption on the application.
One minor concern: The test uses a deterministic salt pattern similar to the main code. Consider using random salts in the test as well.
- let salt_base = b"1H/aaYsf8&asduA"; - for i in 0..total_entries { - let salt = format!("{}{}", String::from_utf8_lossy(salt_base), i % 5); - salt_vec.push(SaltString::encode_b64(salt.as_bytes()).unwrap()); - } + // Generate random salts for testing + for _ in 0..total_entries { + salt_vec.push(SaltString::generate(&mut rand_core::OsRng)); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/take_buy.rs(2 hunks)src/app/take_sell.rs(2 hunks)src/db.rs(17 hunks)src/util.rs(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/app/take_buy.rs
- src/app/take_sell.rs
- src/util.rs
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (3)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:0-0
Timestamp: 2025-05-08T13:07:29.455Z
Learning: For the database encryption in Mostro, the developer prefers caching only derived encryption keys rather than caching decrypted master keys in RAM, as a security measure to reduce the attack surface from potential memory dumps or runtime vulnerabilities.
🧬 Code Graph Analysis (1)
src/db.rs (2)
src/cli/settings.rs (2)
new(127-154)get_db(164-166)src/util.rs (1)
get_keys(466-476)
🔇 Additional comments (8)
src/db.rs (8)
1-31: Import choices align well with the encryption strategyThe imports reflect a well-thought-out approach to security:
argon2for password hashing (industry-standard KDF)secrecyfor zero-knowledge memory managementrpasswordfor secure terminal input- Platform-specific imports for proper file permission handling
Good security practice using multiple specialized crates rather than trying to implement security primitives in-house.
32-47: Well-implemented cross-platform file permissionsGood implementation of file permission restrictions with platform-specific handling:
- Unix: Sets 0o600 permissions (owner read/write only)
- Windows: Provides a clear user notice
The error handling is appropriate, mapping OS errors to the application's error type.
49-118: Strong password validation with clear user feedbackThe
PasswordRequirementsimplementation enforces industry-standard security practices:
- 12+ character length
- Mix of upper/lowercase, numbers, and special characters
- Clear error messages for each requirement
The special handling for empty passwords (lines 97-101) makes encryption optional, which aligns with the PR objectives.
120-151: Effective mitigation against timing attacksThe password verification function includes several security best practices:
- Random delay (100-1100ms) to mitigate timing attacks
- Secure password reading that doesn't echo to screen
- No password caching in logs
The random delay implementation is an excellent defense against brute force attacks by making automated attempts significantly slower.
153-218: User-friendly password creation with strong securityThis password creation flow balances security with usability:
- Clear requirements explanation upfront
- Option to skip encryption entirely
- Confirmation step to prevent typos
- Secure memory handling with
zeroize()for the confirmation password- Storage in a
SecretStringto prevent memory dumps exposing the passwordThe loop structure ensures users meet requirements before proceeding.
693-720: Good schema update handling for admin passwordsThe
add_new_userfunction has been appropriately updated to include theadmin_passwordfield in the SQL query. The updated code accounts for the database schema changes by:
- Including
admin_passwordin the column list- Adding the corresponding parameter binding
- Including a clear comment about returning the unencrypted public key
751-795: Good implementation of conditional encryption checkingThe
seller_has_pending_orderfunction smartly handles both encrypted and unencrypted databases:
- First checks if encryption is enabled via
MOSTRO_DB_PASSWORD.get().is_some()- If encrypted, fetches and decrypts all relevant keys individually
- Otherwise falls back to direct SQL checking with
EXISTSclauseThe approach balances security with performance while maintaining backward compatibility.
754-755:Details
❓ Verification inconclusive
Add input validation for inactive pubkeys
While the function validates pubkey format, it doesn't check if the key is for an active user.
🏁 Script executed:
#!/bin/bash # Check if there's a related validation in the codebase # Search for banning or user status checks rg "is_banned" --type rustLength of output: 269
I’d like to inspect the function body around that
pubkey: Stringparameter to see where and how it’s used before recommending adding theis_bannedcheck.
🏁 Script executed:
#!/bin/bash set -e # Show 5 lines before and after the pubkey parameter in src/db.rs rg -n "pubkey: String" -C5 src/db.rsLength of output: 946
I’d like to see how the codebase currently retrieves or checks users by pubkey—this will help determine where to hook in the
is_bannedcheck:
🏁 Script executed:
#!/bin/bash set -e # Find any SELECTs against the users table rg -n "SELECT .*FROM users" -t rust -C3 # Find any async fns handling users by pubkey rg -n "pub async fn .*user.*pubkey" -t rust -C3Length of output: 50
I want to locate any existing user‐lookup by pubkey to see where to enforce
is_banned—let’s search for SELECT/WHERE clauses and binds aroundpubkey:
🏁 Script executed:
#!/bin/bash set -e # Search for any SELECTs against users rg -n -i "select .*from users" -C3 src # Look for WHERE clauses on pubkey rg -n -i "where pubkey" -C3 src # Find any .bind(pubkey) calls rg -n "bind(pubkey" -C3 srcLength of output: 49
Verify ban status in order checks
The
seller_has_pending_orderandbuyer_has_pending_orderfunctions only validate the pubkey format and don’t appear to fetch the user record or checkis_banned. Please ensure you:
- Retrieve the user from the
userstable bypubkey.- Enforce
is_banned == falsebefore allowing any order‐related logic.If this check happens upstream, confirm banned users can’t reach these calls.
Catrya
left a comment
There was a problem hiding this comment.
Hi @arkanoider the database is now called mostro.dbmostro.db , it should be called mostro.db
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/db.rs (2)
322-338: Simplify salt generation since only the first salt is usedYou're generating 50 salts but only using the first one. If deterministic salt generation is intentional for testing purposes, you can simplify this to just generate the one needed salt.
- // Generate a random salt - let salt_base = b"1H/aaYsf8&asduA"; - let mut salt_vec: Vec<SaltString> = Vec::new(); - for i in 0..50 { - let salt = format!("{}{}", String::from_utf8_lossy(salt_base), i % 5); - salt_vec.push(SaltString::encode_b64(salt.as_bytes()).unwrap()); - } - - // Configure Argon2 parameters - let argon2 = Argon2::default(); - - // Derive the key - let key = argon2 - .hash_password(password.expose_secret().as_bytes(), &salt_vec[0]) - .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))? - .to_string(); + // Generate a deterministic salt for testing + let salt_base = b"1H/aaYsf8&asduA"; + let salt = format!("{}{}", String::from_utf8_lossy(salt_base), 0); + let salt_string = SaltString::encode_b64(salt.as_bytes()) + .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; + + // Configure Argon2 parameters + let argon2 = Argon2::default(); + + // Derive the key + let key = argon2 + .hash_password(password.expose_secret().as_bytes(), &salt_string) + .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))? + .to_string();
768-815: Consider optimizing encrypted key searches with indexesThe current implementation decrypts all keys matching a status to find a match, which could become inefficient as the database grows. While your approach of caching derived keys helps with decryption speed, you could further optimize query performance.
Add indexes on the
statuscolumn to improve query performance:CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);Additionally, consider adding a comment explaining the performance characteristics and why this approach was chosen over alternatives, to help future maintainers understand the design decisions.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (1)
src/db.rs(14 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (4)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:0-0
Timestamp: 2025-05-08T13:07:29.484Z
Learning: For the database encryption in Mostro, the developer prefers caching only derived encryption keys rather than caching decrypted master keys in RAM, as a security measure to reduce the attack surface from potential memory dumps or runtime vulnerabilities.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:315-354
Timestamp: 2025-05-08T18:10:03.818Z
Learning: In the Mostro codebase, deterministic salt generation is used intentionally in testing functions to benchmark decryption timing and test caching mechanisms. This is a valid testing approach when consistent, reproducible results are needed for performance evaluation.
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: tests
🔇 Additional comments (5)
src/db.rs (5)
37-106: LGTM! Solid password requirements implementationThe password validation is comprehensive with good defaults (12+ chars, mixed case, numbers, special chars). The special handling for empty passwords makes sense for optional encryption.
108-139: Great security implementation for password verificationGood use of Argon2 for password verification with added random delays to mitigate timing attacks. The error messages are appropriately generic.
913-1039: Good test coverage for encryption/decryptionThe test provides thorough coverage of the encryption and decryption functionality with performance measurement, using an in-memory database for isolation.
636-656: Clean implementation of the admin password retrievalGood error handling and clean implementation for retrieving the admin password from the database.
792-796: Correct error handling for decrypt_dataThe implementation correctly maps the error from
decrypt_datausingmap_err(MostroInternalErr)as needed for the error type conversion.
| fn restrict_file_permissions(path: &Path) -> Result<(), MostroError> { | ||
| #[cfg(unix)] | ||
| { | ||
| let perms = Permissions::from_mode(0o600); | ||
| set_permissions(path, perms) | ||
| .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; | ||
| } | ||
|
|
||
| #[cfg(windows)] | ||
| { | ||
| // Optional: could integrate with `winapi` or use a placeholder | ||
| println!("⚠️ Skipping permission change on Windows. Set it manually if needed."); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Windows file permission handling is incomplete
While the Unix implementation correctly restricts file permissions to owner-only (0o600), the Windows implementation only prints a warning without actually securing the file.
Consider implementing proper Windows file security using the windows-acl crate:
#[cfg(windows)]
{
use windows_acl::acl::{AclEntry, AccessMode};
use windows_acl::helper;
// Get current user SID
let current_user = helper::get_current_username()
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
// Create ACL allowing full access to current user only
let entries = vec![
AclEntry {
application_type: Some(AccessMode::Allow),
trustee: current_user,
permissions: windows_acl::helper::Permissions::FULL,
}
];
// Apply ACL to database file
windows_acl::acl::apply_to_file(path.to_str().unwrap(), entries)
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
}Also, add a docstring to describe the function's purpose and behavior on different platforms.
🤖 Prompt for AI Agents
In src/db.rs around lines 20 to 35, the Windows implementation of
restrict_file_permissions only prints a warning and does not actually restrict
file permissions. Replace the placeholder code with proper Windows ACL handling
using the windows-acl crate: retrieve the current user SID, create an ACL entry
granting full access to that user only, and apply this ACL to the file path.
Also, add a docstring to the function explaining its purpose and
platform-specific behavior.
There was a problem hiding this comment.
Yes @coderabbitai! I don't love windows sorry...
There was a problem hiding this comment.
@arkanoider No worries! Many developers share that sentiment 😄
If you'd prefer a simpler approach without adding the windows-acl dependency, you could at least warn Windows users more explicitly in the logs about the security implications:
#[cfg(windows)]
{
tracing::warn!(
"Database file permissions on Windows are not restricted. For security, ensure the database directory has appropriate access controls."
);
// Optionally check if file is in a user-only directory like AppData
}The current implementation is fine for now, especially if Windows isn't a primary target platform.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
src/db.rs (5)
20-35: Add docstring to clarify platform-specific behaviorThe
restrict_file_permissionsfunction lacks a docstring explaining its purpose and how it behaves differently across platforms. This is important since Windows users need to understand the security implications.+/// Restricts file permissions to only allow access by the owner. +/// +/// On Unix systems, this sets the permissions to 0o600 (owner read/write only). +/// On Windows, this function only displays a warning as Windows permission model +/// differs from Unix-like systems. Windows users should ensure proper ACLs are +/// set manually or store the database in a secure location. fn restrict_file_permissions(path: &Path) -> Result<(), MostroError> { #[cfg(unix)] { let perms = Permissions::from_mode(0o600); set_permissions(path, perms) .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; } #[cfg(windows)] { // Optional: could integrate with `winapi` or use a placeholder - println!("⚠️ Skipping permission change on Windows. Set it manually if needed."); + tracing::warn!("Database file permissions cannot be automatically restricted on Windows. Please ensure the file is stored in a secure location with appropriate access controls."); } Ok(()) }
94-105: Avoid duplicate validation inis_strong_passwordThe function calls
validatetwice, which is inefficient. Store the validation result first, then check if it's empty.fn is_strong_password(&self, password: &str) -> bool { - match self.validate(password).is_empty() { + let failures = self.validate(password); + match failures.is_empty() { true => true, false => { println!("\nPassword is not strong enough:"); - for failure in self.validate(password) { + for failure in failures { println!("- {}", failure); } false } } }
768-815: Consider indexing for better encrypted lookup performanceThe current approach of fetching all orders with a given status and then decrypting each key could become a performance bottleneck as the database grows.
Consider these performance optimizations:
- Add database indices on the
statusfield to speed up the initial query- Process batches of orders in parallel using Tokio's join operations
- Consider a caching mechanism for frequently accessed decrypted keys
// Example of parallel processing with Tokio let decryption_tasks = orders_to_check .into_iter() .map(|master_key| { let password = MOSTRO_DB_PASSWORD.get().clone(); tokio::spawn(async move { CryptoUtils::decrypt_data(master_key, Some(password)) }) }) .collect::<Vec<_>>(); // Wait for all tasks to complete for task in decryption_tasks { if let Ok(Ok(decrypted)) = task.await { if decrypted == pubkey { return Ok(true); } } }
791-799: Use early return for decryption error handlingThe code currently unwraps the decryption result with
map_errand?, which will return early if there's an error. For better error handling and visibility, consider using a more explicit pattern that logs decryption failures.// Decrypt master pubkey -let master_pubkey_decrypted = - CryptoUtils::decrypt_data(master_key, MOSTRO_DB_PASSWORD.get()) - .map_err(MostroInternalErr)?; -if master_pubkey_decrypted == pubkey { - return Ok(true); +match CryptoUtils::decrypt_data(master_key, MOSTRO_DB_PASSWORD.get()) { + Ok(master_pubkey_decrypted) => { + if master_pubkey_decrypted == pubkey { + return Ok(true); + } + }, + Err(e) => { + tracing::warn!("Failed to decrypt master key: {}", e); + // Continue with next key instead of failing the entire operation + } }
141-207: Consider extracting common password input logicThe password input code in
get_user_passwordandcheck_password_hashcould be extracted into a shared helper function to reduce duplication./// Reads a password from stdin with anti-timing attack measures fn read_password_secure() -> Result<String, MostroError> { // Add a random delay to mitigate timing attacks let random_delay = rand::random::<u16>() % 1000; let password = read_password() .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; // Add delay after password input std::thread::sleep(std::time::Duration::from_millis( 100_u64 + random_delay as u64, )); Ok(password) }This function could then be used in both places to reduce code duplication.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(14 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (4)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:0-0
Timestamp: 2025-05-08T13:07:29.484Z
Learning: For the database encryption in Mostro, the developer prefers caching only derived encryption keys rather than caching decrypted master keys in RAM, as a security measure to reduce the attack surface from potential memory dumps or runtime vulnerabilities.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:315-354
Timestamp: 2025-05-08T18:10:03.818Z
Learning: In the Mostro codebase, deterministic salt generation is used intentionally in testing functions to benchmark decryption timing and test caching mechanisms. This is a valid testing approach when consistent, reproducible results are needed for performance evaluation.
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: tests
🔇 Additional comments (5)
src/db.rs (5)
86-89: Good: Empty password handling is now commentedThe empty password handling has a clear comment explaining that it's intentional to support optional encryption. This is an improvement from earlier versions of the code.
108-139: Good: Implemented anti-timing attack measuresThe addition of a random delay before password verification is a good security practice to mitigate brute force attempts. The code also properly handles password verification and storage in the global OnceCell.
323-328: Deterministic salt generation is appropriate for testingWhile deterministic salts would be concerning in production code, this is appropriate for testing since it allows for consistent benchmark results. The code also clearly documents that these are test functions.
913-1038: Good test implementation for database encryption performanceThe test function effectively measures decryption performance and validates the encryption/decryption functionality. It uses fixed salts for reproducible test results, appropriate for test environments.
804-813: Improved query for unencrypted database modeThe code efficiently uses
query_scalarwithEXISTSfor the unencrypted case, which is more efficient than retrieving full records. This is a good optimization.
Catrya
left a comment
There was a problem hiding this comment.
Hi @arkanoider
when admin tries to take a dispute of child order in privacy mode
2025-05-20T05:43:23.859013Z WARN mostrod::app: Error in AdminTakeDispute with context Error in database access: no rows returned by a query that expected to return at least one row - inner message no rows returned by a query that expected to return at least one row
Great pick! I will check @Catrya , so just when the dispute is about a child order. |
|
But when is it in privacy mode. |
There was a problem hiding this comment.
Hi @arkanoider When republishing a range order, in event 38383 of the pending order the reputation label is not displayed even though it has reputation
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/db.rs (2)
113-117:⚠️ Potential issueCritical security issue: Hardcoded password in production code
The function uses a hardcoded password ("PippoPippo#75") instead of reading it securely from user input. This is a major security vulnerability as it exposes the database encryption key in plaintext within the source code.
Replace the hardcoded password with the commented-out secure password reading implementation:
-let password = "PippoPippo#75"; -// Simulate a delay in password input to avoid timing attacks -// let random_delay = rand::random::<u16>() % 1000; -// let password = read_password() -// .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; +print!("Enter database password: "); +std::io::stdout().flush().unwrap(); +let password = read_password() + .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?; + +// Add a small delay to mitigate brute force attempts +let random_delay = rand::random::<u16>() % 1000; +std::thread::sleep(std::time::Duration::from_millis( + 100_u64 + random_delay as u64, +));
21-36: 🛠️ Refactor suggestionFile permission security is incomplete for Windows systems
The
restrict_file_permissionsfunction properly restricts permissions on Unix systems (mode 0o600), but only prints a warning on Windows without actually securing the file. This could leave the database file accessible to other users on Windows systems.Consider either:
- Implementing proper Windows ACL handling to restrict permissions, or
- Enhancing the warning to use
tracing::warn!for better visibility and adding more details:#[cfg(windows)] { - // Optional: could integrate with `winapi` or use a placeholder - println!("⚠️ Skipping permission change on Windows. Set it manually if needed."); + tracing::warn!( + "Database file permissions on Windows are not restricted. For security, ensure the database directory has appropriate access controls." + ); }
🧹 Nitpick comments (1)
src/db.rs (1)
776-810: Consider caching decrypted results for better performanceThe implementation decrypts every order's key when checking for pending orders, which could become slow as the number of stored orders grows.
Consider adding a time-limited cache for decrypted keys to improve performance:
// At module level: use std::collections::HashMap; use std::sync::Mutex; use std::time::{Duration, Instant}; lazy_static! { static ref DECRYPTION_CACHE: Mutex<HashMap<String, (String, Instant)>> = Mutex::new(HashMap::new()); } // In the function: let master_pubkey_decrypted = { // Try cache first with a TTL of 5 minutes let mut cache = DECRYPTION_CACHE.lock().unwrap(); if let Some((cached_value, timestamp)) = cache.get(&master_key) { if timestamp.elapsed() < Duration::from_secs(300) { cached_value.clone() } else { // Expired, remove and decrypt again cache.remove(&master_key); let decrypted = CryptoUtils::decrypt_data(master_key.clone(), MOSTRO_DB_PASSWORD.get()) .map_err(MostroInternalErr)?; cache.insert(master_key.clone(), (decrypted.clone(), Instant::now())); decrypted } } else { // Not in cache, decrypt and cache let decrypted = CryptoUtils::decrypt_data(master_key.clone(), MOSTRO_DB_PASSWORD.get()) .map_err(MostroInternalErr)?; cache.insert(master_key.clone(), (decrypted.clone(), Instant::now())); decrypted } };Based on the retrieved learnings, I see you already have caching for derived encryption keys which should help performance. If this proves to be sufficient, the additional cache layer may not be necessary.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/app/release.rs(6 hunks)src/db.rs(14 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/release.rs
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (4)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:0-0
Timestamp: 2025-05-08T13:07:29.484Z
Learning: For the database encryption in Mostro, the developer prefers caching only derived encryption keys rather than caching decrypted master keys in RAM, as a security measure to reduce the attack surface from potential memory dumps or runtime vulnerabilities.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:315-354
Timestamp: 2025-05-08T18:10:03.818Z
Learning: In the Mostro codebase, deterministic salt generation is used intentionally in testing functions to benchmark decryption timing and test caching mechanisms. This is a valid testing approach when consistent, reproducible results are needed for performance evaluation.
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: tests
🔇 Additional comments (5)
src/db.rs (5)
87-90: Good explanation for empty password handlingThe comment clarifying that empty passwords are allowed for optional encryption is a good practice, making the design decision clear to future readers.
168-171: Secure implementation of delay to prevent timing attacksAdding a randomized delay after password input is an excellent security practice to prevent timing attacks. The combination of a fixed base delay (100ms) plus random component makes it difficult for attackers to determine password validity through timing differences.
292-311: Good security practice: Limited password attemptsLimiting password attempts to 3 tries before exiting the application is an effective protection against brute force attacks. This is particularly appropriate for a desktop application where legitimate users should know their password.
324-333: Proper cryptographic practices for password hashingThe implementation correctly uses:
- Cryptographically secure random salt generation with
SaltString::generate(&mut OsRng)- Argon2 for password hashing, which is a memory-hard function resistant to hardware acceleration attacks
- Proper error handling for cryptographic operations
909-1035: Well-designed test for encryption functionalityThe test module effectively validates the encryption/decryption functionality:
- Uses in-memory SQLite for fast, isolated testing
- Properly tests both encryption and decryption paths
- Provides performance measurements for decryption operations
- Verifies expected outcomes with clear assertions
The deterministic salt generation in the test is appropriate for reproducible results and benchmarking the caching mechanism, as indicated in the retrieved learnings.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/db.rs (1)
87-90: Empty password handling bypasses validationThe function skips all validations if the password is empty, but the intention isn't immediately clear from the code.
Add a comment explaining the design decision:
// If password is empty, clear failures if password.is_empty() { + // Empty password is allowed to support optional encryption failures.clear(); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/db.rs(14 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/db.rs (4)
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:792-802
Timestamp: 2025-04-27T20:07:24.558Z
Learning: The decrypt_data function from mostro_core::order returns a type that needs to be wrapped with MostroInternalErr to convert it to MostroError, so calls should keep the .map_err(MostroInternalErr) conversion.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:122-145
Timestamp: 2025-04-28T13:21:36.613Z
Learning: Adding a small delay (e.g., 100ms) before password verification operations is a good security practice to mitigate brute force attacks by limiting the rate at which an attacker can attempt different passwords.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:0-0
Timestamp: 2025-05-08T13:07:29.484Z
Learning: For the database encryption in Mostro, the developer prefers caching only derived encryption keys rather than caching decrypted master keys in RAM, as a security measure to reduce the attack surface from potential memory dumps or runtime vulnerabilities.
Learnt from: arkanoider
PR: MostroP2P/mostro#475
File: src/db.rs:315-354
Timestamp: 2025-05-08T18:10:03.818Z
Learning: In the Mostro codebase, deterministic salt generation is used intentionally in testing functions to benchmark decryption timing and test caching mechanisms. This is a valid testing approach when consistent, reproducible results are needed for performance evaluation.
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: tests
🔇 Additional comments (8)
src/db.rs (8)
3-9: Strong security foundation with appropriate cryptographic librariesYou've chosen excellent libraries for secure password management and data encryption:
- Argon2 for password hashing (memory-hard KDF resistant to hardware attacks)
- SecretString for protecting sensitive data in memory
- Read_password for secure terminal input
113-121: Excellent timing attack mitigationAdding both a random and fixed delay before password verification helps mitigate timing attacks while maintaining a minimum delay for brute force protection.
294-310: Good password attempt limitingLimiting password attempts to 3 tries before exiting the application is an effective defense against brute force attacks.
631-651: Admin password retrieval implementation is secureThe
get_admin_passwordfunction properly retrieves the stored password hash while maintaining appropriate error handling and avoiding exposing the hash unnecessarily.
789-790: Correct error conversion for CryptoUtils::decrypt_dataThe error handling for
decrypt_datacorrectly maps the error toMostroInternalErrbased on the retrieved learning that it returns a type that needs this conversion to become aMostroError.
908-1034: Comprehensive encryption testingThe test module thoroughly validates the encryption/decryption functionality with:
- Proper setup of an in-memory database
- Testing with a variety of entries
- Performance timing for decryption operations
- Verification of both encryption and decryption
955-959: Deterministic salt generation in testsThe test uses deterministic salt generation based on a fixed value and index modulo, which is appropriate for testing but would be insecure in production code.
This is perfectly fine for testing purposes where reproducible results are needed.
1000-1004: Performance measurement in testsIncluding timing measurements for decryption operations in tests is excellent for benchmarking performance of the encryption implementation.
…rmation also if order is normal (not full privacy)
|
@Catrya last commit should fix the missing user information in a child order when the creator of the order is not in full privacy mode. Please test it when you're available, will do the same tonite at home. |
|
Hi @arkanoider if the order is in privacy mode, with range, whe is republished is mising in the 38383 event: |
…in the event wit all zeroes
This should be fixed with last commit @Catrya ! Thanks for founding a way to replicate the encryption error...great job! Tomorrow I will analyze it! |
- when I recreate the child order master keys were saved without encryption so when a takesell or takebuy comes in the search for pending user orders generate a decrytpion error.
|
@Catrya i found the cause of decryption error, last commit has the fix. When you want and you can test it let me know if it's ok also for you! |
Finallyyyyy!!!! Great @grunch also #OFF can't stop you from mostro...thanks this is good to move on with other things. |

Hi @grunch,
I will drop some infos here about the idea of encryption on the identity keys in mostro database tables to decouple in a more definitive way relations between orders and nostr users.
I will use these two crates for the encryption part:
Argon2 is a famous and reccomended KDF, with it I will create a hashed value of the user password to be safely saved in the mostro database.
Cha Cha is a valid and quick algo for ciphering the keys that we want in the db.
Below a simple flowchart of what will happens when mostro will be started from admin:
flowchart TD A[Start Mostro] -->|mostro.db file <br>exists| B(Ask user database<br> password ) A[Start Mostro] -->|mostro.db file <br>not exists| C(Ask user a strong<br> password) C --> D[Double check <br>password] D --> |check failed|C D --> |valid password|E[Password saved in mostro.db] B --> |read password<br>from user|F[compare with hashed<br>password] F --> |valid password|G[Start mostro with encryption] F --> |not valid password|BMy initial idea is to save a
new user, who is the admin in this special case, when mostro is run for the first time. I will add a new string field inusers table( optional - I will let also user give an empty string to have no encryption in case ) and this will be the admin password hashed in the format of Argon2, for those who wants to play a bit with argon2 this is a nice online site with a generator:Argon2 generator online
A typical stored string is:
$argon2d$v=19$m=1024,t=1,p=1$c2FsdHlzYWx0eXNhbHR5$VucvoVfuAxUpW4FTuc+8JbgZYBRnw5b/41AJugHoIkYWhen admin password is in place, with Argon2 crate is easy to check against the user password with a
verify_passwordmethod.User password is now set in RAM using a rust
OnceCellso it's just inited once and then will be used during encrypt/decrypt operations over the keys in database. I have introduced also another small crate called secrecy that helps avoiding leak of password in RAM in the logs/debug messages.I am still working on this but basically login part seems working ( still debugging edge cases...) than I will update also
mostro-corewith the method to ecnrypt/decrypt the requested data.Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores
Tests