Repository navigation
feat(storage): keyring backends as adapters over the SecretStore port (S4a) #7257
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
senamakel
merged 22 commits into
tinyhumansai:main
from
senamakel:storage-secrets-config
Oct 10, 2026
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
cdf9cb7
test(keyring): add fixture generation test for encrypted file backend
senamakel 06f8688
test(keyring): add fixture compatibility tests for encrypted file bac…
senamakel 4480ba2
test(keyring): update fixture path to secrets_enc.bin
senamakel a719407
test(keyring): commit the secrets.enc fixture generated by the pre-po…
senamakel bd63ace
feat(security): add encrypted file keyring backend
senamakel 39259d6
feat(security): add encrypted file keyring backend
senamakel 081d770
chore: files changed Cargo.lock,crates/openhuman-core/Cargo.toml,crat…
senamakel 7880b58
refactor(security): extract keyring ops into dedicated module
senamakel 1a911fd
chore(keyring): apply rustfmt and register adapter tests
senamakel ea98f8a
chore(deps): add keyring to openhuman-app lockfile
senamakel a221a84
Merge upstream/main into storage-secrets-config
senamakel 93ad98f
docs(keyring): update backend descriptions for tinystoragedrivers port
senamakel 1dd9e9a
fix(keyring): recheck legacy file under lock before import
senamakel a023245
Merge upstream/main into storage-secrets-config
senamakel b32eef7
fix(security): persist keyring entries to the file store
senamakel b3c6fb3
test(keyring): cover recovery and quarantine edge cases
senamakel ac085e0
Merge upstream/main into storage-secrets-config
senamakel 48977cc
Merge upstream/main into storage-secrets-config
senamakel 69fa9b9
chore(vendor): bump tinybus submodule
senamakel 4f9fe3a
refactor(relay): extract thread lock map type alias
senamakel 8128347
chore(deps): remove libc from lockfile dependencies
senamakel ce6e610
refactor(agent): simplify drain_target fallback arm
senamakel 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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,111 @@ | ||
| //! Shared plumbing for the keyring backends that sit over | ||
| //! `tinystoragedrivers`' `SecretStore` port: error mapping, UTF-8 decoding | ||
| //! and the desktop corruption policy. | ||
|
|
||
| use std::path::Path; | ||
|
|
||
| use tinystoragedrivers::secrets::{EncryptedFileSecrets, SecretStore as _}; | ||
|
senamakel marked this conversation as resolved.
|
||
| use tinystoragedrivers::{ErrorKind, StorageError}; | ||
| use zeroize::Zeroizing; | ||
|
|
||
| use crate::security::keyring::error::KeyringError; | ||
| use crate::security::keyring::file_store; | ||
|
|
||
| /// A driver error as a [`KeyringError::Backend`], for the file store. | ||
| pub(super) fn backend_error(error: StorageError) -> KeyringError { | ||
| KeyringError::Backend(format!("secret store: {error}")) | ||
| } | ||
|
|
||
| /// A driver error as the [`KeyringError::Os`] the OS-keychain callers (and | ||
| /// the credential-ref error classifier) have always seen. A locked or denied | ||
| /// store keeps its `NoStorageAccess` identity; the driver's error rides along | ||
| /// as the source (it never carries a secret value). | ||
| pub(super) fn os_error(key: &str, error: StorageError) -> KeyringError { | ||
| let source = if error.kind() == ErrorKind::Unavailable { | ||
|
senamakel marked this conversation as resolved.
|
||
| keyring::Error::NoStorageAccess(Box::new(error)) | ||
| } else { | ||
| keyring::Error::PlatformFailure(Box::new(error)) | ||
| }; | ||
| KeyringError::Os { | ||
| key: key.to_string(), | ||
| source, | ||
| } | ||
| } | ||
|
|
||
| /// Whether the keychain refused access rather than failed: reads and deletes | ||
| /// treat that as "nothing there", as they always have. | ||
| pub(super) fn is_no_access(error: &StorageError) -> bool { | ||
| error.kind() == ErrorKind::Unavailable | ||
| } | ||
|
|
||
| /// Whether the driver reports the secrets file itself as unreadable | ||
| /// (undecryptable under this key, or not the expected JSON). | ||
| pub(super) fn is_corruption(error: &StorageError) -> bool { | ||
| matches!(error.kind(), ErrorKind::Crypto | ErrorKind::Serialization) | ||
| } | ||
|
|
||
| /// Decode a stored value as UTF-8 text. | ||
| pub(super) fn utf8( | ||
| key: &str, | ||
| value: Option<Zeroizing<Vec<u8>>>, | ||
| ) -> Result<Option<String>, KeyringError> { | ||
| value | ||
| .map(|bytes| { | ||
| String::from_utf8(bytes.to_vec()).map_err(|source| KeyringError::InvalidUtf8 { | ||
| key: key.to_string(), | ||
| source, | ||
| }) | ||
| }) | ||
| .transpose() | ||
| } | ||
|
|
||
| /// Desktop policy for a `secrets.enc` the driver could not read: move it | ||
| /// aside and let the caller continue on an empty store. | ||
| /// | ||
| /// The driver fails closed (error, file untouched), which suits a server with | ||
| /// an operator but would wedge a desktop: every write fails, so the user can | ||
| /// never sign in again. Today's behaviour is kept instead: quarantine the | ||
| /// bytes (`secrets.enc.corrupt.<ts>`, never deleted) and log loudly. | ||
| /// | ||
| /// Under the same cross-process lock writers take, the file is probed again | ||
| /// first: a concurrent writer may already have replaced it, and quarantining | ||
| /// that replacement would destroy good secrets. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// When the file cannot be moved aside. Continuing would overwrite secrets | ||
| /// that could not be read, so the operation fails closed instead. | ||
| pub(super) fn recover_corrupt_file( | ||
| path: &Path, | ||
| key: &[u8; 32], | ||
| cause: &StorageError, | ||
| ) -> Result<(), KeyringError> { | ||
| let _guard = file_store::lock_for_write(path)?; | ||
| let store = EncryptedFileSecrets::at_path(path, Zeroizing::new(*key)); | ||
| match crate::storage::block_on(async move { store.list("").await }) { | ||
| Ok(_) => { | ||
| log::info!("[keyring:encrypted_file] secrets file readable again; not quarantining"); | ||
| return Ok(()); | ||
| } | ||
| // Still corrupt: fall through to quarantine. | ||
| Err(error) if is_corruption(&error) => {} | ||
| // Anything else (a transient read or permission error) says nothing | ||
| // about the file's contents: never move a possibly healthy file aside. | ||
| Err(error) => return Err(backend_error(error)), | ||
| } | ||
| log::error!( | ||
| "[keyring:encrypted_file] secrets file unreadable ({}): master key may have changed \ | ||
| or the file is corrupt", | ||
| cause.message() | ||
| ); | ||
| if file_store::quarantine_corrupt(path, "enc").is_none() { | ||
|
senamakel marked this conversation as resolved.
senamakel marked this conversation as resolved.
|
||
| return Err(KeyringError::Backend( | ||
| "secrets file is unreadable and could not be moved aside".to_string(), | ||
| )); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[path = "adapter_tests.rs"] | ||
| pub(crate) mod tests; | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.