1pub const SSI_DIR: &str = "~/.ssi";
23
24use std::collections::{BTreeSet, HashSet};
25use std::fs;
26use std::io::{self, BufRead, Write};
27use std::os::unix::fs::PermissionsExt;
28use std::path::PathBuf;
29
30use baid64::Baid64ParseError;
31
32use crate::{Fingerprint, SecretParseError, Ssi, SsiPair, SsiParseError, SsiQuery, SsiSecret};
33
34#[derive(Debug, Display, Error, From)]
35#[display(inner)]
36pub enum Error {
37 #[from]
38 Io(io::Error),
39
40 #[from]
41 Baid64(Baid64ParseError),
42
43 #[from]
44 Secret(SecretParseError),
45
46 #[from]
47 Ssi(SsiParseError),
48}
49
50pub struct SsiRuntime {
51 pub secrets: BTreeSet<SsiSecret>,
52 pub identities: HashSet<Ssi>,
53}
54
55impl SsiRuntime {
56 pub fn load() -> Result<Self, Error> {
57 let data_dir = PathBuf::from(shellexpand::tilde(SSI_DIR).to_string());
58 fs::create_dir_all(&data_dir)?;
59
60 let mut path = data_dir.clone();
61 path.push("secrets");
62 let file = fs::OpenOptions::new()
63 .read(true)
64 .write(true)
65 .create(true)
66 .truncate(false)
67 .open(path)?;
68 let mut permissions = file.metadata()?.permissions();
69 permissions.set_mode(0o600);
70 let reader = io::BufReader::new(file);
71 let mut secrets = bset![];
72 for line in reader.lines() {
73 let line = line?;
74 secrets.insert(line.parse()?);
75 }
76
77 let mut path = data_dir.clone();
78 path.push("identities");
79 let file = fs::OpenOptions::new()
80 .read(true)
81 .write(true)
82 .create(true)
83 .truncate(false)
84 .open(path)?;
85 let mut permissions = file.metadata()?.permissions();
86 permissions.set_mode(0o600);
87 let reader = io::BufReader::new(file);
88 let mut identities = set![];
89 for line in reader.lines() {
90 let line = line?;
91 identities.insert(line.parse()?);
92 }
93
94 Ok(Self {
95 secrets,
96 identities,
97 })
98 }
99
100 pub fn store(&self) -> io::Result<()> {
101 let data_dir = PathBuf::from(shellexpand::tilde(SSI_DIR).to_string());
102 fs::create_dir_all(&data_dir)?;
103
104 let mut path = data_dir.clone();
105 path.push("secrets");
106 let mut file = fs::File::create(path)?;
107 for secret in &self.secrets {
108 writeln!(file, "{secret}")?;
109 }
110
111 let mut path = data_dir.clone();
112 path.push("identities");
113 let mut file = fs::File::create(path)?;
114 for ssi in &self.identities {
115 writeln!(file, "{ssi}")?;
116 }
117
118 Ok(())
119 }
120
121 pub fn find_identity(&self, query: impl Into<SsiQuery>) -> Option<&Ssi> {
122 let query = query.into();
123 self.identities.iter().find(|ssi| match query {
124 SsiQuery::Pub(pk) => ssi.pk == pk,
125 SsiQuery::Fp(fp) => ssi.pk.fingerprint() == fp,
126 SsiQuery::Id(ref id) => ssi.uids.iter().any(|uid| {
127 &uid.id == id ||
128 &uid.to_string() == id ||
129 &uid.name == id ||
130 &format!("{}:{}", uid.schema, uid.id) == id
131 }),
132 })
133 }
134
135 pub fn find_signer(&self, query: impl Into<SsiQuery>, passwd: &str) -> Option<SsiPair> {
136 let ssi = self.find_identity(query.into()).cloned()?;
137 let sk = self.secrets.iter().find_map(|s| {
138 let mut s = (*s).clone();
139 if !passwd.is_empty() {
140 s.decrypt(passwd);
141 }
142 if s.to_public() == ssi.pk {
143 Some(s)
144 } else {
145 None
146 }
147 })?;
148 Some(SsiPair::new(ssi, sk))
149 }
150
151 pub fn is_signing(&self, fp: Fingerprint) -> bool {
152 self.secrets.iter().any(|s| s.fingerprint() == fp)
153 }
154}