Repository navigation
feat: abstract key storage, in-memory fallback for non-linux platforms #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // linux kernel keyring backed key store | ||
|
|
||
| use std::time::Duration; | ||
|
|
||
| use anyhow::Result; | ||
| use linux_keyutils::{KeyError, KeyPermissions, KeyRing, KeyRingIdentifier}; | ||
|
|
||
| use super::KeyUnavailableError; | ||
|
|
||
| const KEYRING_PERM: u32 = 0x3f000000; | ||
|
|
||
| pub(super) fn store(key_id: &str, secret: &[u8], timeout: Duration) -> Result<()> { | ||
| let keyr = get_keyring()?; | ||
|
|
||
| let key = keyr.add_key(key_id, secret).map_err(map_err)?; | ||
| key.set_timeout(timeout.as_secs() as usize).map_err(map_err)?; | ||
| key.set_perms(KeyPermissions::from_u32(KEYRING_PERM)).map_err(map_err)?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| pub(super) fn retrieve(key_id: &str, timeout: Duration) -> Result<Box<[u8]>> { | ||
| let keyr = get_keyring()?; | ||
|
|
||
| let key = keyr.search(key_id).map_err(map_err)?; | ||
|
|
||
| // read the actual payload length instead of assuming 32 bytes, | ||
| // so secrets of any length roundtrip unchanged | ||
| let data = key.read_to_vec().map_err(map_err)?.into_boxed_slice(); | ||
|
|
||
| key.set_timeout(timeout.as_secs() as usize).map_err(map_err)?; | ||
|
|
||
| Ok(data) | ||
| } | ||
|
|
||
| pub(super) fn revoke(key_id: &str) -> Result<()> { | ||
| let keyr = get_keyring()?; | ||
|
|
||
| let key = keyr.search(key_id).map_err(map_err)?; | ||
| key.revoke().map_err(map_err)?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn map_err(err: KeyError) -> anyhow::Error { | ||
| match err { | ||
| KeyError::KeyDoesNotExist | KeyError::KeyExpired | KeyError::KeyRevoked => KeyUnavailableError.into(), | ||
| err => anyhow::Error::new(err), | ||
| } | ||
| } | ||
|
|
||
| fn get_keyring() -> Result<KeyRing> { | ||
| // TODO: Make other keyrings available | ||
| // TODO: Investigate why key in Process keyring doesn't persist | ||
| Ok(KeyRing::from_special_id(KeyRingIdentifier::Session, false)?) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // in-memory key store for platforms without kernel keyrings. | ||
| // keys stay in (zeroized-on-drop) process memory instead of the kernel, which | ||
| // enables native macos/windows runs but is weaker than the linux keyring: the | ||
| // pages are not locked, so a key can reach swap or a core dump. accepted for | ||
| // the platforms that have no kernel keyring to put it in. | ||
|
|
||
| use std::collections::HashMap; | ||
| use std::sync::{Mutex, OnceLock}; | ||
| use std::time::{Duration, Instant}; | ||
|
|
||
| use anyhow::Result; | ||
| use zeroize::Zeroize; | ||
|
|
||
| use super::KeyUnavailableError; | ||
|
|
||
| struct Entry { | ||
| secret: Vec<u8>, | ||
| expiry: Instant, | ||
| } | ||
|
|
||
| impl Drop for Entry { | ||
| fn drop(&mut self) { | ||
| self.secret.zeroize(); | ||
| } | ||
| } | ||
|
|
||
| fn entries() -> &'static Mutex<HashMap<String, Entry>> { | ||
| static STORE: OnceLock<Mutex<HashMap<String, Entry>>> = OnceLock::new(); | ||
| STORE.get_or_init(Default::default) | ||
| } | ||
|
|
||
| pub(super) fn store(key_id: &str, secret: &[u8], timeout: Duration) -> Result<()> { | ||
| let mut store = entries().lock().expect("key store lock poisoned"); | ||
|
|
||
| // drop expired keys so the map doesn't grow unboundedly | ||
| let now = Instant::now(); | ||
| store.retain(|_, entry| now < entry.expiry); | ||
|
|
||
| store.insert(key_id.to_string(), Entry { | ||
| secret: secret.to_vec(), | ||
| expiry: now + timeout, | ||
| }); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| pub(super) fn retrieve(key_id: &str, timeout: Duration) -> Result<Box<[u8]>> { | ||
| let mut store = entries().lock().expect("key store lock poisoned"); | ||
|
|
||
| let entry = store.get_mut(key_id).ok_or(KeyUnavailableError)?; | ||
| if Instant::now() >= entry.expiry { | ||
| store.remove(key_id); | ||
| return Err(KeyUnavailableError.into()); | ||
| } | ||
|
|
||
| entry.expiry = Instant::now() + timeout; | ||
| Ok(entry.secret.clone().into_boxed_slice()) | ||
| } | ||
|
|
||
| pub(super) fn revoke(key_id: &str) -> Result<()> { | ||
| entries().lock().expect("key store lock poisoned") | ||
| .remove(key_id) | ||
| .ok_or(KeyUnavailableError)?; | ||
|
|
||
| Ok(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lixmal/keepass4web-rs
Length of output: 8885
🏁 Script executed:
Repository: lixmal/keepass4web-rs
Length of output: 18976
🌐 Web query:
linux_keyutils Rust Key::set_timeout KEYCTL_SET_TIMEOUT zero timeout contract💡 Result:
In the Linux key retention service, the
KEYCTL_SET_TIMEOUToperation is used to set or clear an expiration timer on a key [1][2]. The contract for using a zero value as the timeout is explicitly defined to clear any existing expiration timer [3][4][2]. WhenKey::set_timeout(or the underlyingkeyctl_set_timeoutsystem call) is invoked with a timeout value of 0, the kernel removes the previously set expiration timer, effectively making the key persist indefinitely (provided it is not otherwise revoked or removed) [4][1][2]. Key technical points regarding this contract: - Zeroing the timeout: Specifying 0 as the timeout value explicitly cancels the expiration timer [4][1][2]. This behavior is consistent across both the raw kernel system call interface and the common Rust wrappers (such aslinux-keyutilsandkeyutils) [3][5][6]. - Prerequisites: To set or clear a timeout, the caller must havesetattrpermission on the target key or hold an instantiation authorization token [3][6][2]. - Restrictions: This operation cannot be applied to keys that are already revoked, expired, or negatively instantiated [3][6][2]. Additionally, if a key is marked with theKEY_FLAG_KEEPflag, the kernel will prevent modification of its timeout [7]. - Verification: The only way to discover the current timeout of a key is by inspecting the/proc/keysfile [3][6][2]. Sources: [3], [4], [6], [1], [7], [2]Citations:
Preserve zero-duration expiry semantics.
When
timeoutis zero or less than one second,timeout.as_secs()passes0tolinux_keyutils::Key::set_timeoutat lines 16 and 31. Linux interprets0as clearing the expiry timer. The key can therefore remain valid indefinitely, whilememory.rsrejects zero-duration keys immediately. Apply an explicit cross-backend policy that does not pass0for immediate expiry, and add a Linux regression test.🤖 Prompt for AI Agents