Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:02:13.534Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

secret.rs

184 lines · 6.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.