vigor_agent/
lib.rs

1#![deny(missing_docs,
2    missing_debug_implementations, missing_copy_implementations,
3    trivial_casts, trivial_numeric_casts,
4    unsafe_code,
5    unstable_features,
6    unused_import_braces, unused_qualifications)]
7
8//! # Vigor
9//! This library contains a Vigor authentication agent to manage credentials and perform HTTP/HTTPS requests.
10//!
11//! A note regarding Ed25519: this client library supports Ed25519 authentication, however will only accept PEM-encoded keys.
12//! Formats such as OpenSSH are not guaranteed to work.
13//! The private key is expected to adhere to RFC 7468, PKCS8 and unencrypted.
14//!
15//! Minimal format verification is done on private key material. For all intended purposes, assume the library would foolishly accept random noise as a private key.
16//! You are responsible for implementing safety checks for inappropriate private keys.
17//!
18//! Also keep in mind that this library is purely synchronous, for the purposes of simplicity and a less bloated dependency tree.
19//! For use cases where blocking execution is inappropriate and/or inadaquete, it should be noted that synchronous code can be executed asynchronously, however not vice versa.
20//! If all else fails, the rhetorical question "have you tried threading" should come to mind.
21//!
22//! ## Usage
23//! Use `Vigor::new()` to start an agent instance, after importing.
24//! See documentation for a full list of available methods.
25//!
26//! ```no_run
27//! use vigor_agent;
28//!
29//! fn main() {
30//!     // you're advised to apply error handling here, instead of just recklessly using .unwrap()
31//!     let mut agent = vigor_agent::Vigor::new().unwrap();
32//!     agent.init().unwrap();
33//!     println!(agent.get("http://example.com/claims/", vigor_agent::AuthMode::Auto).unwrap());
34//! }
35//! ```
36//!
37
38use std::{fs, fmt, path::PathBuf, error};
39
40extern crate dirs;
41extern crate serde;
42extern crate ureq;
43extern crate pem_rfc7468;
44extern crate ed25519_dalek;
45extern crate hex;
46use dirs::home_dir;
47use ed25519_dalek::Signer;
48
49/// Defines various kinds of errors, adding context to failures.
50#[derive(Debug, Clone, Copy)]
51pub enum ErrorKinds {
52    /// Something attempted was fundamentally illegal due to being impossible to satisfy, or certain to fail.
53    IllegalOperation,
54    /// The provided Ed25519 private key is not valid and/or could not be loaded.
55    InvalidKey,
56    /// Could not find the user's home directory, and thus cannot access agent configuration.
57    MissingHome,
58    /// Unable to access or (de)serialize configuration file, despite knowing path.
59    ConfigurationInaccessible,
60    /// Request to Vigor server failed.
61    RequestFailed,
62    /// Structure of JSON response from Vigor server is invalid.
63    ResponseInvalid,
64    /// Unable to access either the provided Ed25519 private or public key.
65    KeyInaccessible,
66    /// Signature creation with the provided Ed25519 private key failed.
67    SignatureFailed,
68    /// `vigor_agent::AuthMode::Auto` was specified, and no available authentication mode could be resolved.
69    /// At least one authentication mode is required to authenticate.
70    AuthModeUnresolved
71}
72
73impl fmt::Display for ErrorKinds {
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        write!(f, "{:?}", self)
76    }
77}
78
79/// This library's vender-specific error type.
80///
81/// The `kind` method provides `match`-able context to the error.
82#[derive(Debug, Clone)]
83pub struct Error {
84    message: String,
85    kind: ErrorKinds
86}
87
88impl Error {
89    fn new(msg: &str, kind: ErrorKinds) -> Error {
90        Error {
91            message: msg.to_owned(),
92            kind: kind
93        }
94    }
95
96    /// Returns error's inner kind.
97    ///
98    /// Useful for case matching, to decide how to recover from a `vigor_agent::Error`.
99    pub fn kind(&self) -> ErrorKinds {
100        return self.kind;
101    }
102}
103
104impl fmt::Display for Error {
105    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106        write!(f, "{}. {}", self.kind, self.message)
107    }
108}
109
110impl error::Error for Error {}
111
112/// Configuration structure for Ed25519 authentication, used in `ConfigSchema` structures.
113///
114/// Can be used in serializing and deserializing configuration data, especially those residing inside `ConfigSchema` structures.
115///
116/// **This is not the main agent structure.** See `Vigor` instead, your agent is in another castle.
117#[derive(serde::Serialize, serde::Deserialize, Debug)]
118pub struct ConfigEd25519Schema {
119    /// Path to the Ed25519 public key.
120    pub public: String,
121    /// Path to the Ed25519 private key.
122    pub private: String,
123    /// Whether Ed25519 authentication should be used.
124    pub enabled: bool
125}
126
127/// Configuration structure, used in `Vigor` structures.
128///
129/// Can be used in serializing and deserializing configuration data, especially those residing inside `Vigor` structures.
130///
131/// **This is not the main agent structure.** See `Vigor` instead, your agent is in another castle.
132#[derive(serde::Serialize, serde::Deserialize, Debug)]
133pub struct ConfigSchema {
134    /// User's name.
135    pub preferred_username: String,
136    /// User's email.
137    pub email: String,
138    /// Plain-text password, if empty password authentication will not be used.
139    pub password: String,
140    /// Ed25519 authentication configuration structure.
141    pub ed25519: ConfigEd25519Schema
142}
143
144// definitions for transmission structs.
145#[derive(serde::Serialize)]
146struct Authentication {
147    mode: String,
148    answer: String
149}
150
151#[derive(serde::Deserialize)]
152struct TokenResponse {
153    jwt: String
154}
155
156#[derive(serde::Deserialize)]
157struct ErrorResponse {
158    error: String
159}
160
161/// Configuration and path information for agent structure. Includes implementations for agent logic.
162///
163/// Consume implemented methods for initialization, see `new` method.
164pub struct Vigor {
165    /// Configuration structure.
166    pub config: ConfigSchema,
167    /// Path to configuration file.
168    pub path: PathBuf
169}
170
171impl fmt::Debug for Vigor {
172    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173        write!(f, "path: \"{}\"", self.path.display().to_string())
174    }
175}
176
177/// Represents mode to perform token retrieval with, specifically the authentication method.
178///
179/// The modes enumerated are to be passed onto agent methods that retrieve tokens, as arguments.
180#[derive(Debug, Copy, Clone)]
181pub enum AuthMode {
182    /// Instruction to use Ed25519 key signatures to authenticate.
183    Ed25519,
184    /// Instruction to use password to authenticate.
185    Password,
186    /// Instruction to automatically select mode, by the following order.
187    ///
188    /// 1. Ed25519
189    /// 2. Password
190    ///
191    /// If a mode is not available, the next mode will be used.
192    Auto
193}
194
195impl Vigor {
196    fn get_config_path() -> Result<PathBuf, Error> {
197        match home_dir() {
198            Some(mut home) => {
199                home.push(".vigor");
200                home.set_extension("conf");
201                Ok(home)
202            },
203            None => Err(Error::new("Failed to get user's home directory for Vigor configuration file.", ErrorKinds::MissingHome))
204        }
205    }
206
207    /// Reads configuration from disk.
208    /// Does not check to see if path to configuration file exists.
209    pub fn read(&mut self) -> Result<(), Error> {
210        match fs::read_to_string(&self.path) {
211            Ok(data) => {
212                let output: Result<ConfigSchema, serde_json::Error> = serde_json::from_str(&data);
213                match output {
214                    Ok(config) => {
215                        self.config = config;
216                        Ok(())
217                    },
218                    Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::ConfigurationInaccessible))
219                }
220            },
221            Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::ConfigurationInaccessible))
222        }
223    }
224
225    /// Writes configuration to disk.
226    pub fn write(&self) -> Result<(), Error> {
227        match fs::write(&self.path, serde_json::to_string(&self.config).unwrap()) {
228            Ok(_) => {
229                Ok(())
230            },
231            Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::ConfigurationInaccessible))
232        }
233    }
234
235    /// Runs initialization for Vigor agent.
236    ///
237    /// If configuration does not exist, `write` method is called.
238    /// If configuration does exist, `read` method is called.
239    pub fn init(&mut self) -> Result<(), Error> {
240        if !self.path.exists() {
241            match Vigor::write(self) {
242                Ok(_) => Ok(()),
243                Err(error) => Err(error)
244            }
245        } else {
246            match Vigor::read(self) {
247                Ok(_) => Ok(()),
248                Err(error) => Err(error)
249            }
250        }
251    }
252
253    /// Creates a new `Vigor` agent.
254    ///
255    /// The default configuration structure as JSON appears as follows:
256    ///
257    /// ```text
258    /// {
259    ///     "preferred_username": "nobody",
260    ///     "email": "nobody@localhost",
261    ///     "password": "hunter2",
262    ///     "ed25519": {
263    ///         "public": "/path/to/your/keys/vigor.pem.pub",
264    ///         "private": "/path/to/your/keys/vigor.pem",
265    ///         "enabled": false
266    ///     }
267    /// }
268    /// ```
269    ///
270    /// # Examples
271    ///
272    /// To initialize a new instance:
273    ///
274    /// ```no_run
275    /// let mut agent = vigor_agent::Vigor::new().unwrap();
276    /// agent.init().unwrap();
277    /// ```
278    pub fn new() -> Result<Vigor, Error> {
279        match Vigor::get_config_path() {
280            Ok(config_path) => {
281                Ok(Vigor {
282                    config: ConfigSchema {
283                        preferred_username: "nobody".to_owned(),
284                        email: "nobody@localhost".to_owned(),
285                        password: "hunter2".to_owned(), // i'm not funny.
286                        ed25519: ConfigEd25519Schema {
287                            public: "/path/to/your/keys/vigor.pem.pub".to_owned(),
288                            private: "/path/to/your/keys/vigor.pem".to_owned(),
289                            enabled: false
290                        }
291                    },
292                    path: config_path
293                })
294            },
295            Err(error) => Err(error)
296        }
297    }
298
299    fn host_finalize(&self, host: &str) -> String {
300        let mut url = PathBuf::from(host);
301        url.push(&self.config.preferred_username);
302        url.display().to_string()
303    }
304
305    fn process_request_response(response: Result<ureq::Response, ureq::Error>) -> Result<ureq::Response, Error> {
306        match response {
307            Ok(response) => Ok(response),
308            Err(ureq::Error::Status(code, response)) => {
309                match response.into_json::<ErrorResponse>() {
310                    Ok(payload) => {
311                        Err(Error::new(&format!("Code {}. {}", code.to_string(), payload.error), ErrorKinds::RequestFailed))
312                    },
313                    Err(error) => Err(Error::new(&format!("Code {}. Response invalid, unable to decode further details for cause of error. {}", code.to_string(), error.to_string()), ErrorKinds::ResponseInvalid))
314                }
315            }
316            Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::RequestFailed))
317        }
318    }
319
320    fn form_account_payload(&self, share_email: bool, use_password: bool, use_ed25519: bool) -> Result<serde_json::Map<String, serde_json::Value>, Error> {
321        let mut payload = serde_json::Map::new();
322        if share_email {
323            payload.insert("email".to_owned(), serde_json::Value::String(self.config.email.to_owned()));
324        }
325        if use_password {
326            match Vigor::get_authentication_password(self) {
327                Ok(password) => {
328                    payload.insert("password".to_owned(), serde_json::Value::String(password));
329                },
330                Err(error) => {
331                    return Err(error);
332                }
333            }
334        }
335        if use_ed25519 {
336            match fs::read_to_string(&self.config.ed25519.public) {
337                Ok(data) => {
338                    payload.insert("ed25519key".to_owned(), serde_json::Value::String(data));
339                }
340                Err(error) => {
341                    return Err(Error::new(&error.to_string(), ErrorKinds::KeyInaccessible))
342                }
343            }
344        }
345        Ok(payload)
346    }
347
348    /// Performs account creation to a Vigor host.
349    ///
350    /// This method expects three booleans after the host argument for whether email, password, and/or Ed25519 should be shared, respectively.
351    /// At least one authentication method must be shared.
352    ///
353    /// # Examples
354    ///
355    /// ```no_run
356    /// # let mut agent = vigor_agent::Vigor::new().unwrap();
357    /// # agent.init().unwrap();
358    /// // assuming you already have an instance called "agent"
359    /// agent.put("http://example.com/claims/", true, true, true).unwrap();
360    /// ```
361    pub fn put(&self, host: &str, share_email: bool, use_password: bool, use_ed25519: bool) -> Result<(), Error> {
362        if !use_password && !use_ed25519 {
363            return Err(Error::new("At least one authentication method must exist on the new account.", ErrorKinds::IllegalOperation))
364        }
365        match Vigor::form_account_payload(self, share_email, use_password, use_ed25519) {
366            Ok(payload) => {
367                match Vigor::process_request_response(ureq::put(&Vigor::host_finalize(self, &host)).send_json(payload)) {
368                    Ok(_) => Ok(()),
369                    Err(error) => Err(error)
370                }
371            },
372            Err(error) => Err(error)
373        }
374    }
375
376    fn get_authentication_ed25519(&self) -> Result<String, Error> {
377        match fs::read_to_string(&self.config.ed25519.private) {
378            Ok(data) => {
379                match pem_rfc7468::decode_vec(data.as_bytes()) {
380                    Ok(data) => {
381                        let raw = data.1;
382                        if raw.len() < 32  {
383                            return Err(Error::new("Ed25519 private key is not at least 32 bytes.", ErrorKinds::InvalidKey));
384                        }
385                        let key_as_bytes = &raw[(raw.len() - 32)..]; // drop excess bytes (i.e. ID bytes)
386                        match ed25519_dalek::SecretKey::from_bytes(&key_as_bytes) {
387                            Ok(secret_key) => {
388                                let public_key: ed25519_dalek::PublicKey = (&secret_key).into();
389                                let keypair = ed25519_dalek::Keypair {public: public_key, secret: secret_key};
390                                match keypair.try_sign("SIGNME".as_bytes()) {
391                                    Ok(signature) => {
392                                        Ok(hex::encode(signature.to_bytes()))
393                                    },
394                                    Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::SignatureFailed))
395                                }
396                            },
397                            Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::InvalidKey))
398                        }
399                    },
400                    Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::InvalidKey))
401                }
402            }
403            Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::KeyInaccessible))
404        }
405    }
406
407    fn get_authentication_password(&self) -> Result<String, Error> {
408        if self.config.password.is_empty() {
409            return Err(Error::new("Password cannot be of zero length.", ErrorKinds::IllegalOperation))
410        } else {
411            return Ok(self.config.password.to_owned())
412        }
413    }
414
415    fn form_authentication_ed25519(&self) -> Result<Authentication, Error> {
416        match Vigor::get_authentication_ed25519(self) {
417            Ok(answer) => {
418                Ok(Authentication {mode: "ed25519".to_owned(), answer: answer})
419            },
420            Err(error) => Err(error)
421        }
422    }
423
424    fn form_authentication_password(&self) -> Result<Authentication, Error> {
425        match Vigor::get_authentication_password(self) {
426            Ok(answer) => {
427                Ok(Authentication {mode: "password".to_owned(), answer: answer})
428            },
429            Err(error) => Err(error)
430        }
431    }
432
433    fn form_authentication(&self, mode: AuthMode) -> Result<Authentication, Error> {
434        match mode {
435            AuthMode::Ed25519 => {
436                Vigor::form_authentication_ed25519(self)
437            },
438            AuthMode::Password => {
439                Vigor::form_authentication_password(self)
440            },
441            AuthMode::Auto => {
442                if self.config.ed25519.enabled {
443                    match Vigor::form_authentication_ed25519(self) {
444                        Ok(payload) => {
445                            return Ok(payload)
446                        },
447                        Err(_) => {}
448                    };
449                }
450                match Vigor::form_authentication_password(self) {
451                    Ok(payload) => {
452                        return Ok(payload)
453                    },
454                    Err(_) => {
455                        return Err(Error::new("No authentication modes available that aren't disabled or erroneous.", ErrorKinds::AuthModeUnresolved));
456                    }
457                };
458            }
459        }
460    }
461
462    /// Performs token retrieval to a Vigor host.
463    ///
464    /// # Examples
465    /// ```no_run
466    /// # let mut agent = vigor_agent::Vigor::new().unwrap();
467    /// # agent.init().unwrap();
468    /// // assuming you already have an instance called "agent"
469    /// agent.get("http://example.com/claims/", vigor_agent::AuthMode::Auto).unwrap();
470    /// ```
471    pub fn get(&self, host: &str, mode: AuthMode) -> Result<String, Error> {
472        match Vigor::form_authentication(self, mode) {
473            Ok(payload) => {
474                match Vigor::process_request_response(ureq::get(&Vigor::host_finalize(self, &host)).send_json(payload)) {
475                    Ok(response) => {
476                        match response.into_json::<TokenResponse>() {
477                            Ok(payload) => Ok(payload.jwt),
478                            Err(error) => Err(Error::new(&error.to_string(), ErrorKinds::ResponseInvalid))
479                        }
480                    },
481                    Err(error) => Err(error)
482                }
483            },
484            Err(error) => Err(error)
485        }
486    }
487
488
489    /// Performs account deletion to a Vigor host.
490    ///
491    /// # Examples
492    ///
493    /// ```no_run
494    /// # let mut agent = vigor_agent::Vigor::new().unwrap();
495    /// # agent.init().unwrap();
496    /// // assuming you already have an instance called "agent"
497    /// agent.delete("http://example.com/claims/", vigor_agent::AuthMode::Auto).unwrap();
498    /// ```
499    pub fn delete(&self, host: &str, mode: AuthMode) -> Result<(), Error> {
500        match Vigor::form_authentication(self, mode) {
501            Ok(payload) => {
502                match Vigor::process_request_response(ureq::delete(&Vigor::host_finalize(self, &host)).send_json(payload)) {
503                    Ok(_) => Ok(()),
504                    Err(error) => Err(error)
505                }
506            },
507            Err(error) => Err(error)
508        }
509    }
510
511    /// Performs account modification to a Vigor host.
512    ///
513    /// This method expects three booleans after the host argument for whether email, password, and/or Ed25519 should be updated, respectively.
514    ///
515    /// # Examples
516    ///
517    /// ```no_run
518    /// # let mut agent = vigor_agent::Vigor::new().unwrap();
519    /// # agent.init().unwrap();
520    /// // assuming you already have an instance called "agent"
521    /// agent.patch("http://example.com/claims/", vigor_agent::AuthMode::Auto, true, true, true).unwrap();
522    /// ```
523    pub fn patch(&self, host: &str, mode: AuthMode, share_email: bool, use_password: bool, use_ed25519: bool) -> Result<(), Error> {
524        if !share_email && !use_password && !use_ed25519 {
525            return Err(Error::new("At least one account property needs to be updated.", ErrorKinds::IllegalOperation));
526        }
527        match Vigor::form_authentication(self, mode) {
528            Ok(payload) => {
529                let mut payload_mod: serde_json::Map<String, serde_json::Value> = serde_json::to_value(payload).unwrap().as_object().unwrap().clone();
530                match Vigor::form_account_payload(self, share_email, use_password, use_ed25519) {
531                    Ok(changes) => {
532                        payload_mod.insert("new".to_string(), serde_json::Value::Object(changes));
533                        match Vigor::process_request_response(ureq::patch(&Vigor::host_finalize(self, &host)).send_json(&payload_mod)) {
534                            Ok(_) => Ok(()),
535                            Err(error) => Err(error)
536                        }
537                    },
538                    Err(error) => Err(error)
539                }
540            },
541            Err(error) => Err(error)
542        }
543    }
544}