fix: fallback to pk decryption when failed to decrypt with machine uid#486
fix: fallback to pk decryption when failed to decrypt with machine uid#48621pages wants to merge 1 commit intorustdesk:mainfrom
Conversation
3650dea to
7fc18b7
Compare
There was a problem hiding this comment.
Pull request overview
Improves resilience of the password/config encryption/decryption path when machine-UID retrieval is flaky (e.g., during shutdown) and when historical encryptions may have used the config key-pair instead of the machine UID.
Changes:
- Cache machine UID after successful retrieval and add retry/backoff behavior when retrieval fails.
- Add a decryption fallback path: if decryption with machine UID fails, retry with the key-pair-derived key.
- Add a unit test intended to cover the pk fallback behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/lib.rs |
Adds machine-UID caching plus retry logic in get_uuid() before falling back to key-pair bytes. |
src/password_security.rs |
Updates symmetric_crypt to retry decryption with pk-derived key on failure; adds a new test for the fallback behavior. |
Comments suppressed due to low confidence (1)
src/password_security.rs:192
secretboxis used with a constant all-zero nonce. Reusing a nonce with the same key in XSalsa20-Poly1305 breaks confidentiality and can allow plaintext recovery across multiple encrypted values. Consider generating a random nonce per encryption (and storing it alongside the ciphertext, e.g., prefixing it) so decrypt can read it back safely; this can be rolled out via a new version tag.
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| // After MAX_FAILED_ROUNDS failures, only try once per call to avoid blocking | ||
| // Each round: 6 retries with 30ms sleep between them, max 150ms total |
There was a problem hiding this comment.
Have you really tested this? or can be tested anv verified?
There was a problem hiding this comment.
The logic has been verified by the test code above. Real-world failure scenarios are not tested.
- Cache machine uid after successful retrieval - Retry when getting machine uid fails - Fallback to pk decryption when failed to decrypt with machine uid Signed-off-by: 21pages <sunboeasy@gmail.com>
7fc18b7 to
2b698cf
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let mut lock = KEY_PAIR.lock().unwrap(); | ||
| if let Some(p) = lock.as_ref() { | ||
| return Some(p.clone()); | ||
| } | ||
|
|
||
| let config = Config::load_::<Config>(""); | ||
| if !config.key_pair.0.is_empty() { | ||
| *lock = Some(config.key_pair.clone()); | ||
| Some(config.key_pair) | ||
| } else { | ||
| None | ||
| } |
There was a problem hiding this comment.
get_existing_key_pair holds the KEY_PAIR mutex while doing config file I/O (Config::load_). This can block other threads that need the keypair (including encryption/decryption paths) for the duration of disk access. Consider releasing the lock before loading from disk (double-checked locking pattern: check cache under lock, drop lock, load, then re-lock and set if still empty).
| let mut lock = KEY_PAIR.lock().unwrap(); | |
| if let Some(p) = lock.as_ref() { | |
| return Some(p.clone()); | |
| } | |
| let config = Config::load_::<Config>(""); | |
| if !config.key_pair.0.is_empty() { | |
| *lock = Some(config.key_pair.clone()); | |
| Some(config.key_pair) | |
| } else { | |
| None | |
| } | |
| // First, check the cache under the mutex. | |
| { | |
| let lock = KEY_PAIR.lock().unwrap(); | |
| if let Some(p) = lock.as_ref() { | |
| return Some(p.clone()); | |
| } | |
| } | |
| // Load config from disk without holding the mutex. | |
| let config = Config::load_::<Config>(""); | |
| if config.key_pair.0.is_empty() { | |
| return None; | |
| } | |
| // Re-lock and perform a second check in case another thread | |
| // populated the cache while we were loading the config. | |
| let mut lock = KEY_PAIR.lock().unwrap(); | |
| if let Some(p) = lock.as_ref() { | |
| return Some(p.clone()); | |
| } | |
| *lock = Some(config.key_pair.clone()); | |
| Some(config.key_pair) |
| let uuid = crate::get_uuid(); | ||
| let pk = crate::config::Config::get_key_pair().1; | ||
|
|
||
| // Ensure uuid != pk, otherwise fallback branch won't be tested | ||
| assert_ne!(uuid, pk, "uuid == pk, fallback branch won't be tested"); | ||
|
|
There was a problem hiding this comment.
This test assumes crate::get_uuid() returns a machine UID distinct from the config public key. If machine_uid::get() fails in the test environment (CI, containers, permissions), get_uuid() can fall back to Config::get_key_pair().1, making uuid == pk and failing the test (or making the fallback path untestable). To make this deterministic, consider refactoring symmetric_crypt to call an internal helper that accepts the primary key material (uuid) and optional fallback key, so the test can inject a non-pk uuid without depending on the host machine UID behavior.
pk decryption
I found that decrypting with my password failed, but decrypting with the PK worked. So it’s possible that the machine UID failed to be retrieved during a previous encryption, and the PK was used for encryption instead.
The PK is only attempted during the second decryption. In the first attempt, the machine UID is given priority for decryption. If the PK were also used in the first attempt, then the second attempt to retrieve the machine UID would most likely fail as well.
Reproducing the machine UID read failure
On Windows, reading the machine UID may fail when the system is close to shutdown
Retry Test