void_crypto/
scoped_keyring.rs1use std::time::{SystemTime, UNIX_EPOCH};
7
8use crate::kdf::{derive_scoped_key, ContentKey, SecretKey};
9use crate::CryptoResult;
10
11#[derive(Debug, Clone)]
13pub struct ScopedAccessToken {
14 pub scope: String,
16 pub derived_key: ContentKey,
18 pub created_at: u64,
20 pub expires_at: Option<u64>,
22}
23
24impl ScopedAccessToken {
25 pub fn new(scope: String, derived_key: ContentKey, expires_at: Option<u64>) -> Self {
27 let created_at = SystemTime::now()
28 .duration_since(UNIX_EPOCH)
29 .unwrap_or_default()
30 .as_secs();
31
32 Self {
33 scope,
34 derived_key,
35 created_at,
36 expires_at,
37 }
38 }
39
40 pub fn is_expired(&self) -> bool {
42 if let Some(expires_at) = self.expires_at {
43 let now = SystemTime::now()
44 .duration_since(UNIX_EPOCH)
45 .unwrap_or_default()
46 .as_secs();
47 now > expires_at
48 } else {
49 false
50 }
51 }
52
53 pub fn can_access(&self, path: &str) -> bool {
55 if self.is_expired() {
56 return false;
57 }
58
59 if self.scope == "*" {
60 return true;
61 }
62
63 if let Some(path_pattern) = self.scope.strip_prefix("path:") {
64 path.starts_with(path_pattern)
65 } else if self.scope.starts_with("refs/") {
66 true
67 } else {
68 path.starts_with(&self.scope)
69 }
70 }
71}
72
73pub struct ScopedKeyRing {
78 root_key: SecretKey,
79 tokens: Vec<ScopedAccessToken>,
80}
81
82impl std::fmt::Debug for ScopedKeyRing {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("ScopedKeyRing")
85 .field("root_key", &"[REDACTED]")
86 .field("tokens", &self.tokens)
87 .finish()
88 }
89}
90
91impl ScopedKeyRing {
92 pub fn new(root_key: [u8; 32]) -> Self {
97 Self {
98 root_key: SecretKey::new(root_key),
99 tokens: Vec::new(),
100 }
101 }
102
103 fn push_token(&mut self, token: ScopedAccessToken) -> &ScopedAccessToken {
104 self.tokens.push(token);
105 self.tokens.last().unwrap()
106 }
107
108 pub fn create_token(
110 &mut self,
111 scope: &str,
112 expires_at: Option<u64>,
113 ) -> CryptoResult<&ScopedAccessToken> {
114 let derived_key = ContentKey::new(derive_scoped_key(self.root_key.as_bytes(), scope)?);
115 let token = ScopedAccessToken::new(scope.to_string(), derived_key, expires_at);
116 Ok(self.push_token(token))
117 }
118
119 pub fn get_key_for_scope(&self, scope: &str) -> Option<&ContentKey> {
121 self.tokens
122 .iter()
123 .find(|t| t.scope == scope && !t.is_expired())
124 .map(|t| &t.derived_key)
125 }
126
127 pub fn revoke_scope(&mut self, scope: &str) -> bool {
129 let before = self.tokens.len();
130 self.tokens.retain(|t| t.scope != scope);
131 self.tokens.len() != before
132 }
133
134 pub fn can_access(&self, path: &str) -> bool {
136 self.tokens.iter().any(|t| t.can_access(path))
137 }
138
139 pub fn valid_tokens(&self) -> Vec<&ScopedAccessToken> {
141 self.tokens.iter().filter(|t| !t.is_expired()).collect()
142 }
143
144 pub fn token_count(&self) -> usize {
146 self.tokens.len()
147 }
148
149 pub fn prune_expired(&mut self) -> usize {
151 let before = self.tokens.len();
152 self.tokens.retain(|t| !t.is_expired());
153 before - self.tokens.len()
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::generate_key;
161
162 #[test]
163 fn scoped_token_creation() {
164 let root_key = generate_key();
165 let mut keyring = ScopedKeyRing::new(root_key);
166
167 let token = keyring.create_token("path:src/", None).unwrap();
168 assert_eq!(token.scope, "path:src/");
169 assert!(token.expires_at.is_none());
170 assert!(!token.is_expired());
171 }
172
173 #[test]
174 fn derive_scoped_key_deterministic() {
175 let root_key = generate_key();
176 let key1 = derive_scoped_key(&root_key, "path:src/").unwrap();
177 let key2 = derive_scoped_key(&root_key, "path:src/").unwrap();
178 assert_eq!(key1, key2);
179 }
180
181 #[test]
182 fn different_scopes_different_keys() {
183 let root_key = generate_key();
184 let key1 = derive_scoped_key(&root_key, "path:src/").unwrap();
185 let key2 = derive_scoped_key(&root_key, "path:test/").unwrap();
186 assert_ne!(key1, key2);
187 }
188
189 #[test]
190 fn keyring_can_access_checks_prefix() {
191 let root_key = generate_key();
192 let mut keyring = ScopedKeyRing::new(root_key);
193
194 keyring.create_token("path:src/", None).unwrap();
195
196 assert!(keyring.can_access("src/lib.rs"));
197 assert!(keyring.can_access("src/utils/helper.rs"));
198 assert!(!keyring.can_access("test/lib.rs"));
199 }
200
201 #[test]
202 fn token_can_access_wildcard() {
203 let token = ScopedAccessToken::new("*".to_string(), ContentKey::new([0u8; 32]), None);
204 assert!(token.can_access("anything"));
205 assert!(token.can_access("src/lib.rs"));
206 }
207
208 #[test]
209 fn refs_scope_grants_full_path_access() {
210 let token = ScopedAccessToken::new("refs/heads/main".to_string(), ContentKey::new([0u8; 32]), None);
211 assert!(token.can_access("src/lib.rs"));
212 assert!(token.can_access("test/integration.rs"));
213 }
214
215 #[test]
216 fn expired_token_denies_access() {
217 let past = SystemTime::now()
218 .duration_since(UNIX_EPOCH)
219 .unwrap()
220 .as_secs()
221 - 100;
222 let token = ScopedAccessToken::new("*".to_string(), ContentKey::new([0u8; 32]), Some(past));
223 assert!(!token.can_access("anything"));
224 assert!(token.is_expired());
225 }
226}