Skip to repository content184 lines · 6.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:11:56.746Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
secret.rs
1use std::fmt;
2
3use nostr::{Keys, SecretKey};
4use thiserror::Error;
5use zeroize::Zeroizing;
6
7use crate::{ContractError, IdentityRef, KeyringLocator, PublicIdentity};
8
9pub struct ImportedSecret(Zeroizing<String>);
10
11impl ImportedSecret {
12 pub fn new(secret: String) -> Result<Self, InvalidImportedSecret> {
13 let secret = Zeroizing::new(secret);
14 Keys::parse(secret.as_str()).map_err(|_| InvalidImportedSecret)?;
15 Ok(Self(secret))
16 }
17}
18
19impl fmt::Debug for ImportedSecret {
20 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21 formatter.write_str("ImportedSecret([REDACTED])")
22 }
23}
24
25#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
26#[error("the imported identity secret is invalid")]
27pub struct InvalidImportedSecret;
28
29pub(crate) struct SecretKeyMaterial(Zeroizing<[u8; 32]>);
30
31impl SecretKeyMaterial {
32 pub(crate) fn generate() -> Self {
33 let keys = Keys::generate();
34 Self(Zeroizing::new(keys.secret_key().to_secret_bytes()))
35 }
36
37 pub(crate) fn from_imported(secret: ImportedSecret) -> Result<Self, SecretMaterialError> {
38 let keys = Keys::parse(secret.0.as_str()).map_err(|_| SecretMaterialError)?;
39 Ok(Self(Zeroizing::new(keys.secret_key().to_secret_bytes())))
40 }
41
42 pub(crate) fn from_bytes(bytes: Zeroizing<[u8; 32]>) -> Result<Self, SecretMaterialError> {
43 SecretKey::from_slice(bytes.as_ref()).map_err(|_| SecretMaterialError)?;
44 Ok(Self(bytes))
45 }
46
47 pub(crate) fn from_secret_key(secret_key: SecretKey) -> Self {
48 Self(Zeroizing::new(secret_key.to_secret_bytes()))
49 }
50
51 #[cfg(test)]
52 pub(crate) fn duplicate(&self) -> Self {
53 Self(Zeroizing::new(*self.0))
54 }
55
56 pub(crate) fn public_identity(&self) -> Result<PublicIdentity, SecretMaterialError> {
57 let keys = self.keys()?;
58 let public_key_hex = keys.public_key().to_hex();
59 let identity_ref = IdentityRef::new(format!("omega-nostr-{public_key_hex}"))
60 .map_err(SecretMaterialError::from)?;
61 PublicIdentity::from_public_key_hex(identity_ref, public_key_hex)
62 .map_err(SecretMaterialError::from)
63 }
64
65 pub(crate) fn keys(&self) -> Result<Keys, SecretMaterialError> {
66 let secret_key =
67 SecretKey::from_slice(self.0.as_ref()).map_err(|_| SecretMaterialError::invalid())?;
68 Ok(Keys::new(secret_key))
69 }
70}
71
72impl fmt::Debug for SecretKeyMaterial {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 formatter.write_str("SecretKeyMaterial([REDACTED])")
75 }
76}
77
78#[derive(Debug, Error)]
79#[error("secure identity material is invalid")]
80pub(crate) struct SecretMaterialError;
81
82impl SecretMaterialError {
83 fn invalid() -> Self {
84 Self
85 }
86}
87
88impl From<ContractError> for SecretMaterialError {
89 fn from(_error: ContractError) -> Self {
90 Self
91 }
92}
93
94#[derive(Debug, Copy, Clone, PartialEq, Eq)]
95pub(crate) enum StoreError {
96 Locked,
97 Unavailable,
98 Corrupt,
99 Conflict,
100 Configuration,
101}
102
103pub(crate) trait SecretStore: Send + Sync {
104 fn read(&self, locator: &KeyringLocator) -> Result<Option<SecretKeyMaterial>, StoreError>;
105 fn write(&self, locator: &KeyringLocator, secret: &SecretKeyMaterial)
106 -> Result<(), StoreError>;
107 fn delete(&self, locator: &KeyringLocator) -> Result<(), StoreError>;
108}
109
110pub(crate) struct SystemKeyringStore;
111
112impl SecretStore for SystemKeyringStore {
113 fn read(&self, locator: &KeyringLocator) -> Result<Option<SecretKeyMaterial>, StoreError> {
114 let entry = keyring_entry(locator)?;
115 let bytes = match entry.get_secret() {
116 Ok(bytes) => Zeroizing::new(bytes),
117 Err(keyring::Error::NoEntry) => return Ok(None),
118 Err(error) => return Err(classify_keyring_error(error)),
119 };
120 let secret_bytes: [u8; 32] = bytes
121 .as_slice()
122 .try_into()
123 .map_err(|_| StoreError::Corrupt)?;
124 SecretKeyMaterial::from_bytes(Zeroizing::new(secret_bytes))
125 .map(Some)
126 .map_err(|_| StoreError::Corrupt)
127 }
128
129 fn write(
130 &self,
131 locator: &KeyringLocator,
132 secret: &SecretKeyMaterial,
133 ) -> Result<(), StoreError> {
134 keyring_entry(locator)?
135 .set_secret(secret.0.as_ref())
136 .map_err(classify_keyring_error)
137 }
138
139 fn delete(&self, locator: &KeyringLocator) -> Result<(), StoreError> {
140 match keyring_entry(locator)?.delete_credential() {
141 Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
142 Err(error) => Err(classify_keyring_error(error)),
143 }
144 }
145}
146
147fn keyring_entry(locator: &KeyringLocator) -> Result<keyring::Entry, StoreError> {
148 keyring::Entry::new(locator.service(), locator.account()).map_err(classify_keyring_error)
149}
150
151fn classify_keyring_error(error: keyring::Error) -> StoreError {
152 match error {
153 keyring::Error::NoStorageAccess(_) => StoreError::Locked,
154 keyring::Error::PlatformFailure(_) => StoreError::Unavailable,
155 keyring::Error::NoEntry => StoreError::Unavailable,
156 keyring::Error::BadEncoding(_) => StoreError::Corrupt,
157 keyring::Error::Ambiguous(_) => StoreError::Conflict,
158 keyring::Error::TooLong(_, _) | keyring::Error::Invalid(_, _) => StoreError::Configuration,
159 _ => StoreError::Unavailable,
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn secret_debug_output_is_redacted() {
169 let secret =
170 SecretKeyMaterial::from_bytes(Zeroizing::new([42; 32])).expect("valid test secret");
171 assert_eq!(format!("{secret:?}"), "SecretKeyMaterial([REDACTED])");
172 assert!(!format!("{secret:?}").contains("42"));
173 }
174
175 #[test]
176 fn imported_secret_debug_output_is_redacted() {
177 let imported = ImportedSecret::new(
178 "0000000000000000000000000000000000000000000000000000000000000001".to_string(),
179 )
180 .expect("valid imported test secret");
181 assert_eq!(format!("{imported:?}"), "ImportedSecret([REDACTED])");
182 }
183}
184