1use crate::error::{IOMode, VaultError};
2use crate::spec::WriteMode;
3use crate::util::{strip_ext, write_at, FingerprintUserId, ResetCWD};
4use failure::{err_msg, Error, ResultExt};
5use glob::glob;
6use gpgme;
7use serde_yaml;
8use std::collections::HashSet;
9use std::fs::create_dir_all;
10use std::fs::File;
11use std::io;
12use std::io::{stdin, BufRead, BufReader, Read, Write};
13use std::iter::once;
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16
17pub const GPG_GLOB: &str = "**/*.gpg";
18pub fn recipients_default() -> PathBuf {
19 PathBuf::from(".gpg-id")
20}
21
22pub fn secrets_default() -> PathBuf {
23 PathBuf::from(".")
24}
25
26#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash)]
27pub enum VaultKind {
28 Leader,
29 Partition,
30}
31
32impl Default for VaultKind {
33 fn default() -> Self {
34 VaultKind::Leader
35 }
36}
37
38#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash, Ord, PartialOrd)]
39#[serde(rename_all = "kebab-case")]
40pub enum TrustModel {
41 GpgWebOfTrust,
42 Always,
43}
44
45impl Default for TrustModel {
46 fn default() -> Self {
47 TrustModel::GpgWebOfTrust
48 }
49}
50
51impl FromStr for TrustModel {
52 type Err = String;
53
54 fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
55 Ok(match s {
56 "web-of-trust" => TrustModel::GpgWebOfTrust,
57 "always" => TrustModel::Always,
58 _ => return Err(format!("Unknown trust model: '{}'", s)),
59 })
60 }
61}
62
63#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Hash)]
64pub struct Vault {
65 pub name: Option<String>,
66 #[serde(skip)]
67 pub kind: VaultKind,
68 #[serde(skip)]
69 pub index: usize,
70 #[serde(skip)]
71 pub partitions: Vec<Vault>,
72 #[serde(skip)]
73 pub resolved_at: PathBuf,
74 #[serde(skip)]
75 pub vault_path: Option<PathBuf>,
76 #[serde(default)]
77 pub auto_import: Option<bool>,
78 #[serde(default)]
79 pub trust_model: Option<TrustModel>,
80 #[serde(default = "secrets_default")]
81 pub secrets: PathBuf,
82 pub gpg_keys: Option<PathBuf>,
83 #[serde(default = "recipients_default")]
84 pub recipients: PathBuf,
85}
86
87impl Default for Vault {
88 fn default() -> Self {
89 Vault {
90 kind: VaultKind::default(),
91 index: 0,
92 partitions: Default::default(),
93 trust_model: Default::default(),
94 auto_import: Some(true),
95 vault_path: None,
96 name: None,
97 secrets: secrets_default(),
98 resolved_at: secrets_default(),
99 gpg_keys: None,
100 recipients: recipients_default(),
101 }
102 }
103}
104
105impl Vault {
106 pub fn from_file(path: &Path) -> Result<Vec<Vault>, Error> {
107 let path_is_stdin = path == Path::new("-");
108 let reader: Box<dyn Read> = if path_is_stdin {
109 Box::new(stdin())
110 } else {
111 if !path.exists() {
112 if let Some(recipients_path) = path.parent().map(|p| p.join(recipients_default())) {
113 if recipients_path.is_file() {
114 let mut vault = Vault {
115 name: None,
116 kind: VaultKind::Leader,
117 index: 0,
118 partitions: Vec::new(),
119 resolved_at: path.to_owned(),
120 vault_path: None,
121 secrets: PathBuf::from("."),
122 gpg_keys: None,
123 recipients: recipients_default(),
124 auto_import: Some(false),
125 trust_model: Some(TrustModel::GpgWebOfTrust),
126 };
127 vault = vault.set_resolved_at(
128 &recipients_path
129 .parent()
130 .expect("parent dir for recipient path which was joined before")
131 .join("sy-vault.yml"),
132 )?;
133 return Ok(vec![vault]);
134 }
135 }
136 }
137 Box::new(File::open(path).map_err(|cause| VaultError::from_io_err(cause, path, &IOMode::Read))?)
138 };
139 let vaults: Vec<_> = split_documents(reader)?
140 .iter()
141 .enumerate()
142 .map(|(index, s)| {
143 serde_yaml::from_str(s)
144 .map_err(|cause| VaultError::Deserialization {
145 cause,
146 path: path.to_owned(),
147 })
148 .map_err(Into::into)
149 .and_then(|v: Vault| match v.set_resolved_at(path) {
150 Ok(mut v) => {
151 v.index = index;
152 Ok(v)
153 }
154 err => err,
155 })
156 })
157 .collect::<Result<_, _>>()?;
158 if !vaults.is_empty() {
159 vaults[0].validate()?;
160 }
161 Ok(vaults)
162 }
163
164 pub fn set_resolved_at(mut self, vault_file: &Path) -> Result<Self, Error> {
165 self.resolved_at = normalize(
166 vault_file
167 .parent()
168 .ok_or_else(|| format_err!("The vault file path '{}' is invalid.", vault_file.display()))?,
169 );
170 self.vault_path = Some(vault_file.to_owned());
171 Ok(self)
172 }
173
174 pub fn validate(&self) -> Result<(), Error> {
175 if self.partitions.is_empty() {
176 return Ok(());
177 }
178 {
179 let all_secrets_paths: Vec<_> = self
180 .partitions
181 .iter()
182 .map(|v| v.secrets_path())
183 .chain(once(self.secrets_path()))
184 .map(|mut p| {
185 if p.is_relative() {
186 p = Path::new(".").join(p);
187 }
188 p
189 })
190 .collect();
191 for (sp, dp) in iproduct!(
192 all_secrets_paths.iter().enumerate(),
193 all_secrets_paths.iter().enumerate()
194 )
195 .filter_map(|((si, s), (di, d))| if si == di { None } else { Some((s, d)) })
196 {
197 if sp.starts_with(&dp) {
198 bail!(
199 "Partition at '{}' is contained in another partitions resources directory at '{}'",
200 sp.display(),
201 dp.display()
202 );
203 }
204 }
205 }
206 {
207 let mut seen: HashSet<_> = Default::default();
208 for path in self
209 .partitions
210 .iter()
211 .map(|v| v.recipients_path())
212 .chain(once(self.recipients_path()))
213 {
214 if seen.contains(&path) {
215 bail!(
216 "Recipients path '{}' is already used, but must be unique across all partitions",
217 path.display()
218 );
219 }
220 seen.insert(path);
221 }
222 }
223
224 Ok(())
225 }
226
227 pub fn to_file(&self, path: &Path, mode: WriteMode) -> Result<(), VaultError> {
228 if let WriteMode::RefuseOverwrite = mode {
229 if path.exists() {
230 return Err(VaultError::ConfigurationFileExists(path.to_owned()));
231 }
232 }
233 self.validate().map_err(VaultError::Validation)?;
234
235 match self.kind {
236 VaultKind::Partition => return Err(VaultError::PartitionUnsupported),
237 VaultKind::Leader => {
238 let mut file = write_at(path).map_err(|cause| VaultError::from_io_err(cause, path, &IOMode::Write))?;
239 let all_vaults = self.all_in_order();
240 for vault in &all_vaults {
241 serde_yaml::to_writer(&mut file, vault)
242 .map_err(|cause| VaultError::Serialization {
243 cause,
244 path: path.to_owned(),
245 })
246 .and_then(|_| {
247 writeln!(file).map_err(|cause| VaultError::from_io_err(cause, path, &IOMode::Write))
248 })?;
249 }
250 }
251 }
252 Ok(())
253 }
254
255 pub fn absolute_path(&self, path: &Path) -> PathBuf {
256 normalize(&self.resolved_at.join(path))
257 }
258
259 pub fn secrets_path(&self) -> PathBuf {
260 normalize(&self.absolute_path(&self.secrets))
261 }
262 pub fn url(&self) -> String {
263 format!(
264 "syv://{}{}",
265 self.name
266 .as_ref()
267 .map(|s| format!("{}@", s))
268 .unwrap_or_else(String::new),
269 self.secrets_path().display()
270 )
271 }
272
273 pub fn print_resources(&self, w: &mut dyn Write) -> Result<(), Error> {
274 let has_multiple_partitions = !self.partitions.is_empty();
275 for partition in once(self).chain(self.partitions.iter()) {
276 writeln!(w, "{}", partition.url())?;
277 let dir = partition.secrets_path();
278 if !dir.is_dir() {
279 continue;
280 }
281 let _change_cwd = ResetCWD::from_path(&dir)?;
282 for entry in glob(GPG_GLOB).expect("valid pattern").filter_map(Result::ok) {
283 if has_multiple_partitions {
284 writeln!(w, "{}", dir.join(strip_ext(&entry)).display())?;
285 } else {
286 writeln!(w, "{}", strip_ext(&entry).display())?;
287 }
288 }
289 }
290 Ok(())
291 }
292
293 pub fn write_recipients_list(&self, recipients: &mut Vec<String>) -> Result<PathBuf, Error> {
294 recipients.sort();
295 recipients.dedup();
296
297 let recipients_path = self.recipients_path();
298 if let Some(recipients_parent_dir) = recipients_path.parent() {
299 if !recipients_parent_dir.is_dir() {
300 create_dir_all(recipients_parent_dir).context(format!(
301 "Failed to create directory leading to recipients file at '{}'",
302 recipients_path.display()
303 ))?;
304 }
305 }
306 let mut writer = write_at(&recipients_path).context(format!(
307 "Failed to open recipients at '{}' file for writing",
308 recipients_path.display()
309 ))?;
310 for recipient in recipients {
311 writeln!(&mut writer, "{}", recipient).context(format!(
312 "Failed to write recipient '{}' to file at '{}'",
313 recipient,
314 recipients_path.display()
315 ))?
316 }
317 Ok(recipients_path)
318 }
319
320 pub fn recipients_path(&self) -> PathBuf {
321 self.absolute_path(&self.recipients)
322 }
323
324 pub fn recipients_list(&self) -> Result<Vec<String>, Error> {
325 let recipients_file_path = self.recipients_path();
326 let rfile = File::open(&recipients_file_path).map(BufReader::new).context(format!(
327 "Could not open recipients file at '{}' for reading",
328 recipients_file_path.display()
329 ))?;
330 Ok(rfile.lines().collect::<Result<_, _>>().context(format!(
331 "Could not read all recipients from file at '{}'",
332 recipients_file_path.display()
333 ))?)
334 }
335
336 pub fn keys_by_ids(
337 &self,
338 ctx: &mut gpgme::Context,
339 ids: &[String],
340 type_of_ids_for_errors: &str,
341 gpg_keys_dir: Option<&Path>,
342 output: &mut dyn io::Write,
343 ) -> Result<Vec<gpgme::Key>, Error> {
344 ctx.find_keys(ids)
345 .context(format!("Could not iterate keys for given {}s", type_of_ids_for_errors))?;
346 let (keys, missing): (Vec<gpgme::Key>, Vec<String>) = ids.iter().map(|id| (ctx.get_key(id), id)).fold(
347 (Vec::new(), Vec::new()),
348 |(mut keys, mut missing), (r, id)| {
349 match r {
350 Ok(k) => keys.push(k),
351 Err(_) => missing.push(id.to_owned()),
352 };
353 (keys, missing)
354 },
355 );
356 if keys.len() == ids.len() {
357 assert_eq!(missing.len(), 0);
358 return Ok(keys);
359 }
360 let diff: isize = ids.len() as isize - keys.len() as isize;
361 let mut msg = vec![if diff > 0 {
362 if let Some(dir) = gpg_keys_dir {
363 self.import_keys(ctx, dir, &missing, output)
364 .context("Could not auto-import all required keys")?;
365 return self.keys_by_ids(ctx, ids, type_of_ids_for_errors, None, output);
366 }
367
368 let mut msg = format!(
369 "Didn't find the key for {} {}(s) in the gpg database.{}",
370 diff,
371 type_of_ids_for_errors,
372 match self.gpg_keys.as_ref() {
373 Some(dir) => format!(
374 " This might mean it wasn't imported yet from the '{}' directory.",
375 self.absolute_path(dir).display()
376 ),
377 None => String::new(),
378 }
379 );
380 msg.push_str(&format!(
381 "\nThe following {}(s) could not be found in the gpg key database:",
382 type_of_ids_for_errors
383 ));
384 for fpr in missing {
385 msg.push_str("\n");
386 let key_path_info = match self.gpg_keys.as_ref() {
387 Some(dir) => {
388 let key_path = self.absolute_path(dir).join(&fpr);
389 format!(
390 "{}'{}'",
391 if key_path.is_file() {
392 "Import key-file using 'gpg --import "
393 } else {
394 "Key-file does not exist at "
395 },
396 key_path.display()
397 )
398 }
399 None => "No GPG keys directory".into(),
400 };
401 msg.push_str(&format!("{} ({})", &fpr, key_path_info));
402 }
403 msg
404 } else {
405 format!(
406 "Found {} additional keys to encrypt for, which may indicate an unusual \
407 {}s specification in the recipients file at '{}'",
408 diff,
409 type_of_ids_for_errors,
410 self.recipients_path().display()
411 )
412 }];
413 if !keys.is_empty() {
414 msg.push(format!("All {}s found in gpg database:", type_of_ids_for_errors));
415 msg.extend(keys.iter().map(|k| format!("{}", FingerprintUserId(k))));
416 }
417 Err(err_msg(msg.join("\n")))
418 }
419
420 pub fn recipient_keys(
421 &self,
422 ctx: &mut gpgme::Context,
423 gpg_keys_dir: Option<&Path>,
424 output: &mut dyn io::Write,
425 ) -> Result<Vec<gpgme::Key>, Error> {
426 let recipients_fprs = self.recipients_list()?;
427 if recipients_fprs.is_empty() {
428 return Err(format_err!(
429 "No recipients found in recipients file at '{}'.",
430 self.recipients.display()
431 ));
432 }
433 self.keys_by_ids(ctx, &recipients_fprs, "recipient", gpg_keys_dir, output)
434 }
435
436 fn vault_path_for_display(&self) -> String {
437 self.vault_path
438 .as_ref()
439 .map(|p| p.to_string_lossy().into_owned())
440 .unwrap_or_else(|| String::from("<unknown>"))
441 }
442
443 pub fn find_gpg_keys_dir(&self) -> Result<PathBuf, Error> {
446 once(self.gpg_keys_dir())
447 .chain(self.partitions.iter().map(|p| p.gpg_keys_dir()))
448 .filter_map(Result::ok)
449 .next()
450 .ok_or_else(|| {
451 format_err!(
452 "The vault at '{}' does not have a gpg_keys directory configured.",
453 self.vault_path_for_display()
454 )
455 })
456 }
457
458 pub fn gpg_keys_dir_for_auto_import(&self, partition: &Vault) -> Option<PathBuf> {
459 let auto_import = partition.auto_import.or(self.auto_import).unwrap_or(false);
460 if auto_import {
461 self.find_gpg_keys_dir().ok()
462 } else {
463 None
464 }
465 }
466
467 pub fn gpg_keys_dir(&self) -> Result<PathBuf, Error> {
468 self.gpg_keys.as_ref().map(|p| self.absolute_path(p)).ok_or_else(|| {
469 format_err!(
470 "The vault at '{}' does not have a gpg_keys directory configured.",
471 self.vault_path_for_display()
472 )
473 })
474 }
475}
476
477pub trait VaultExt {
478 fn select(self, vault_id: &str) -> Result<Vault, Error>;
479}
480
481impl VaultExt for Vec<Vault> {
482 fn select(mut self, selector: &str) -> Result<Vault, Error> {
483 let leader_index = Vault::partition_index(selector, self.iter(), None)?;
484 for (_, vault) in self.iter_mut().enumerate().filter(|&(vid, _)| vid != leader_index) {
485 vault.kind = VaultKind::Partition;
486 }
487
488 let mut vault = self[leader_index].clone();
489 vault.kind = VaultKind::Leader;
490
491 self.retain(|v| match v.kind {
492 VaultKind::Partition => true,
493 VaultKind::Leader => false,
494 });
495
496 vault.partitions = self;
497 Ok(vault)
498 }
499}
500
501fn normalize(p: &Path) -> PathBuf {
502 use std::path::Component;
503 let mut p = p.components().fold(PathBuf::new(), |mut p, c| {
504 match c {
505 Component::CurDir => {}
506 _ => p.push(c.as_os_str()),
507 }
508 p
509 });
510 if p.components().count() == 0 {
511 p = PathBuf::from(".");
512 }
513 p
514}
515
516fn split_documents<R: Read>(mut r: R) -> Result<Vec<String>, Error> {
517 use yaml_rust::{YamlEmitter, YamlLoader};
518
519 let mut buf = String::new();
520 r.read_to_string(&mut buf)?;
521
522 let docs = YamlLoader::load_from_str(&buf).context("YAML deserialization failed")?;
523 Ok(docs
524 .iter()
525 .map(|d| {
526 let mut out_str = String::new();
527 {
528 let mut emitter = YamlEmitter::new(&mut out_str);
529 emitter.dump(d).expect("dumping a valid yaml into a string to work");
530 }
531 out_str
532 })
533 .collect())
534}
535
536#[cfg(test)]
537mod tests_vault_ext {
538 use super::*;
539
540 #[test]
541 fn it_selects_by_name() {
542 let vault = Vault {
543 name: Some("foo".into()),
544 ..Default::default()
545 };
546 let v = vec![vault.clone()];
547 assert_eq!(v.select("foo").unwrap(), vault)
548 }
549
550 #[test]
551 fn it_selects_by_secrets_dir() {
552 let vault = Vault {
553 secrets: PathBuf::from("../dir"),
554 ..Default::default()
555 };
556 let v = vec![vault.clone()];
557 assert_eq!(v.select("../dir").unwrap(), vault)
558 }
559
560 #[test]
561 fn it_selects_by_index() {
562 let v = vec![Vault::default()];
563 assert!(v.select("0").is_ok())
564 }
565
566 #[test]
567 fn it_errors_if_name_is_unknown() {
568 let v = Vec::<Vault>::new();
569 assert_eq!(
570 format!("{}", v.select("foo").unwrap_err()),
571 "No partition matched the given selector 'foo'"
572 )
573 }
574 #[test]
575 fn it_errors_if_index_is_out_of_bounds() {
576 let v = Vec::<Vault>::new();
577 assert_eq!(
578 format!("{}", v.select("0").unwrap_err()),
579 "Could not find partition with index 0"
580 )
581 }
582}
583
584#[cfg(test)]
585mod tests_utils {
586 use super::*;
587
588 #[test]
589 fn it_will_always_remove_current_dirs_including_the_first_one() {
590 assert_eq!(format!("{}", normalize(Path::new("./././a")).display()), "a")
591 }
592 #[test]
593 fn it_does_not_alter_parent_dirs() {
594 assert_eq!(format!("{}", normalize(Path::new("./../.././a")).display()), "../../a")
595 }
596}
597
598#[cfg(test)]
599mod tests_vault {
600 use super::*;
601
602 #[test]
603 fn it_print_the_name_in_the_url_if_there_is_none() {
604 let mut v = Vault::default();
605 v.name = Some("name".into());
606 assert_eq!(v.url(), "syv://name@.")
607 }
608
609 #[test]
610 fn it_does_not_print_the_name_in_the_url_if_there_is_none() {
611 let v = Vault::default();
612 assert_eq!(v.url(), "syv://.")
613 }
614}