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}