atomic_lib/
agents.rs

1//! Logic for Agents
2//! Agents are actors (such as users) that can edit content.
3//! https://docs.atomicdata.dev/commits/concepts.html
4
5use base64::{engine::general_purpose, Engine};
6use serde_json::from_slice;
7
8use crate::{errors::AtomicResult, urls, Resource, Storelike, Value};
9
10/// None represents no right checks will be performed, effectively SUDO mode.
11#[derive(Clone, Debug, PartialEq)]
12pub enum ForAgent {
13    /// The Subject URL agent that is performing the action.
14    AgentSubject(String),
15    /// Allows all checks to pass.
16    /// See [urls::SUDO_AGENT]
17    Sudo,
18    /// Public Agent, most strict.
19    /// See [urls::PUBLIC_AGENT]
20    Public,
21}
22
23impl std::fmt::Display for ForAgent {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            ForAgent::AgentSubject(subject) => write!(f, "{}", subject),
27            ForAgent::Sudo => write!(f, "{}", urls::SUDO_AGENT),
28            ForAgent::Public => write!(f, "{}", urls::PUBLIC_AGENT),
29        }
30    }
31}
32
33// From all string-likes
34impl<T: Into<String>> From<T> for ForAgent {
35    fn from(subject: T) -> Self {
36        let subject = subject.into();
37        if subject == urls::SUDO_AGENT {
38            ForAgent::Sudo
39        } else if subject == urls::PUBLIC_AGENT {
40            ForAgent::Public
41        } else {
42            ForAgent::AgentSubject(subject)
43        }
44    }
45}
46
47/// An Agent can be thought of as a User. Agents are used for authentication and authorization.
48/// The private key of the Agent is used to sign [crate::Commit]s.
49#[derive(Clone, Debug)]
50pub struct Agent {
51    /// Private key for signing commits
52    pub private_key: Option<String>,
53    /// Used for validating commit signatures and for the username.
54    pub public_key: String,
55    /// URL of the Agent
56    pub subject: String,
57    pub created_at: i64,
58    pub name: Option<String>,
59}
60
61impl Agent {
62    /// Converts Agent to Resource.
63    /// Does not include private key, only public.
64    pub fn to_resource(&self) -> AtomicResult<Resource> {
65        let mut resource = Resource::new(self.subject.clone());
66        resource.set_class(urls::AGENT);
67        resource.set_subject(self.subject.clone());
68        if let Some(name) = &self.name {
69            resource.set_unsafe(crate::urls::NAME.into(), Value::String(name.into()));
70        }
71        resource.set_unsafe(
72            crate::urls::PUBLIC_KEY.into(),
73            Value::String(self.public_key.clone()),
74        );
75        // Agents must be read by anyone when validating their keys
76        resource.push(crate::urls::READ, urls::PUBLIC_AGENT.into(), true)?;
77        resource.set_unsafe(
78            crate::urls::CREATED_AT.into(),
79            Value::Timestamp(self.created_at),
80        );
81        Ok(resource)
82    }
83
84    /// Creates a new Agent, generates a new Keypair.
85    pub fn new(name: Option<&str>, store: &impl Storelike) -> AtomicResult<Agent> {
86        let keypair = generate_keypair()?;
87
88        Ok(Agent::new_from_private_key(name, store, &keypair.private))
89    }
90
91    /// Creates a new Agent on this server, using the server's Server URL.
92    /// Derives the public key.
93    pub fn new_from_private_key(
94        name: Option<&str>,
95        store: &impl Storelike,
96        private_key: &str,
97    ) -> Agent {
98        let keypair = generate_public_key(private_key);
99
100        Agent {
101            private_key: Some(keypair.private),
102            public_key: keypair.public.clone(),
103            subject: format!("{}/agents/{}", store.get_server_url(), keypair.public),
104            name: name.map(|x| x.to_owned()),
105            created_at: crate::utils::now(),
106        }
107    }
108
109    /// Creates a new Agent on this server, using the server's Server URL.
110    /// This will not be able to write, because there is no private key.
111    pub fn new_from_public_key(store: &impl Storelike, public_key: &str) -> AtomicResult<Agent> {
112        verify_public_key(public_key)?;
113
114        Ok(Agent {
115            private_key: None,
116            public_key: public_key.into(),
117            subject: format!("{}/agents/{}", store.get_server_url(), public_key),
118            name: None,
119            created_at: crate::utils::now(),
120        })
121    }
122
123    pub fn from_secret(secret_b64: &str) -> AtomicResult<Agent> {
124        let agent_bytes = decode_base64(secret_b64)?;
125        let parsed: serde_json::Value = from_slice(&agent_bytes)?;
126        let private_key = parsed["privateKey"].as_str().ok_or("Invalid private key")?;
127        let subject = parsed["subject"].as_str().ok_or("Invalid subject")?;
128        let agent = Agent {
129            private_key: Some(private_key.into()),
130            public_key: generate_public_key(private_key).public,
131            subject: subject.into(),
132            name: None,
133            created_at: crate::utils::now(),
134        };
135        Ok(agent)
136    }
137
138    pub fn from_private_key_and_subject(private_key: &str, subject: &str) -> AtomicResult<Agent> {
139        let keypair = generate_public_key(private_key);
140
141        Ok(Agent {
142            private_key: Some(keypair.private),
143            public_key: keypair.public.clone(),
144            subject: subject.into(),
145            name: None,
146            created_at: crate::utils::now(),
147        })
148    }
149}
150
151/// keypair, serialized using base64
152pub struct Pair {
153    pub private: String,
154    pub public: String,
155}
156
157/// Returns a new random keypair.
158fn generate_keypair() -> AtomicResult<Pair> {
159    use ring::signature::KeyPair;
160    let rng = ring::rand::SystemRandom::new();
161    const SEED_LEN: usize = 32;
162    let seed: [u8; SEED_LEN] = ring::rand::generate(&rng)
163        .map_err(|e| format!("Error generating random seed: {}", e))?
164        .expose();
165    let key_pair = ring::signature::Ed25519KeyPair::from_seed_unchecked(&seed)
166        .map_err(|e| format!("Error generating keypair: {}", e))
167        .unwrap();
168    Ok(Pair {
169        private: encode_base64(&seed),
170        public: encode_base64(key_pair.public_key().as_ref()),
171    })
172}
173
174/// Returns a Key Pair (including public key) from a private key, base64 encoded.
175pub fn generate_public_key(private_key: &str) -> Pair {
176    use ring::signature::KeyPair;
177    let private_key_bytes = decode_base64(private_key).unwrap();
178    let key_pair = ring::signature::Ed25519KeyPair::from_seed_unchecked(private_key_bytes.as_ref())
179        .map_err(|e| format!("Error generating keypair: {e}"))
180        .unwrap();
181    Pair {
182        private: encode_base64(&private_key_bytes),
183        public: encode_base64(key_pair.public_key().as_ref()),
184    }
185}
186
187pub fn decode_base64(string: &str) -> AtomicResult<Vec<u8>> {
188    let vec = general_purpose::STANDARD
189        .decode(string)
190        .map_err(|e| format!("Invalid key. Not valid Base64. {}", e))?;
191    Ok(vec)
192}
193
194pub fn encode_base64(bytes: &[u8]) -> String {
195    general_purpose::STANDARD.encode(bytes)
196}
197
198/// Checks if the public key is a valid ED25519 base64 key.
199/// Not perfect - only checks byte length and parses base64.
200pub fn verify_public_key(public_key: &str) -> AtomicResult<()> {
201    let pubkey_bin = decode_base64(public_key)
202        .map_err(|e| format!("Invalid public key. Not valid Base64. {}", e))?;
203    if pubkey_bin.len() != 32 {
204        return Err(format!(
205            "Invalid public key, should be 32 bytes long instead of {}. Key: {}",
206            pubkey_bin.len(),
207            public_key
208        )
209        .into());
210    }
211    Ok(())
212}
213
214#[cfg(test)]
215mod test {
216    #[cfg(test)]
217    use super::*;
218
219    #[test]
220    fn keypair() {
221        let pair = generate_keypair().unwrap();
222        let regenerated_pair = generate_public_key(&pair.private);
223        assert_eq!(pair.public, regenerated_pair.public);
224    }
225
226    #[test]
227    fn generate_from_private_key() {
228        let private_key = "CapMWIhFUT+w7ANv9oCPqrHrwZpkP2JhzF9JnyT6WcI=";
229        let public_key = "7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=";
230        let regenerated_pair = generate_public_key(private_key);
231        assert_eq!(public_key, regenerated_pair.public);
232    }
233
234    #[test]
235    fn verifies_public_keys() {
236        let valid_public_key = "7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=";
237        let invalid_length = "7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwm+h8U";
238        let invalid_char = "7LsjMW5gOfDdJzK/atgjQ1t20^/rw8MjVg6xwqm+h8U=";
239        verify_public_key(valid_public_key).unwrap();
240        verify_public_key(invalid_length).unwrap_err();
241        verify_public_key(invalid_char).unwrap_err();
242    }
243
244    #[test]
245    fn creates_from_secret() {
246        let secret = "eyJjbGllbnQiOnt9LCJzdWJqZWN0IjoiaHR0cDovL2xvY2FsaG9zdDo5ODgzL2FnZW50cy9ScVB3cGdIditQSzdQbnovZFZhYjhobUhqWW52VEwxWXJsVmE2TDlHOVpnPSIsInByaXZhdGVLZXkiOiJTTXl4UmdGN1FoaUM3QzUwNnFYU1VLZkUrU0tBdENkTkZ1NVhlVGp6YWRBPSIsInB1YmxpY0tleSI6IlJxUHdwZ0h2K1BLN1Buei9kVmFiOGhtSGpZbnZUTDFZcmxWYTZMOUc5Wmc9In0=";
247        let agent = Agent::from_secret(secret).unwrap();
248        assert_eq!(
249            agent.private_key.unwrap(),
250            "SMyxRgF7QhiC7C506qXSUKfE+SKAtCdNFu5XeTjzadA="
251        );
252        assert_eq!(
253            agent.subject,
254            "http://localhost:9883/agents/RqPwpgHv+PK7Pnz/dVab8hmHjYnvTL1YrlVa6L9G9Zg="
255        );
256    }
257}