t_rust_less_lib/api/
mod.rs

1use crate::secrets_store_capnp::{self, secret_entry, secret_version_ref};
2use capnp::text_list;
3use chrono::{TimeZone, Utc};
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::collections::BTreeMap;
7use std::collections::HashMap;
8use std::fmt;
9use zeroize::Zeroize;
10mod command;
11mod config;
12mod event;
13mod zeroize_datetime;
14
15#[cfg(test)]
16mod tests;
17
18pub use command::*;
19pub use config::*;
20pub use event::*;
21pub use zeroize_datetime::*;
22
23pub const PROPERTY_USERNAME: &str = "username";
24pub const PROPERTY_PASSWORD: &str = "password";
25pub const PROPERTY_TOTP_URL: &str = "totpUrl";
26pub const PROPERTY_NOTES: &str = "notes";
27
28/// Status information of a secrets store
29///
30#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
31#[zeroize(drop)]
32pub struct Status {
33  pub locked: bool,
34  pub unlocked_by: Option<Identity>,
35  pub autolock_at: Option<ZeroizeDateTime>,
36  pub version: String,
37  pub autolock_timeout: u64,
38}
39
40/// An Identity that might be able to unlock a
41/// secrets store and be a recipient of secrets.
42///
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
44#[zeroize(drop)]
45pub struct Identity {
46  pub id: String,
47  pub name: String,
48  pub email: String,
49  pub hidden: bool,
50}
51
52impl std::fmt::Display for Identity {
53  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54    write!(f, "{} <{}>", self.name, self.email)
55  }
56}
57
58/// General type of a secret.
59///
60/// This only serves as a hint for an UI.
61///
62#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "lowercase")]
64pub enum SecretType {
65  Login,
66  Note,
67  Licence,
68  Wlan,
69  Password,
70  #[serde(other)]
71  Other,
72}
73
74impl Zeroize for SecretType {
75  fn zeroize(&mut self) {
76    *self = SecretType::Other
77  }
78}
79
80impl SecretType {
81  /// Get the commonly used property name that may contain a password.
82  ///
83  /// The values of these properties are automatically estimated for the strengths.
84  pub fn password_properties(&self) -> &[&str] {
85    match self {
86      SecretType::Login => &[PROPERTY_PASSWORD],
87      SecretType::Note => &[],
88      SecretType::Licence => &[],
89      SecretType::Wlan => &[PROPERTY_PASSWORD],
90      SecretType::Password => &[PROPERTY_PASSWORD],
91      SecretType::Other => &[],
92    }
93  }
94
95  pub fn from_reader(api: secrets_store_capnp::SecretType) -> Self {
96    match api {
97      secrets_store_capnp::SecretType::Login => SecretType::Login,
98      secrets_store_capnp::SecretType::Licence => SecretType::Licence,
99      secrets_store_capnp::SecretType::Wlan => SecretType::Wlan,
100      secrets_store_capnp::SecretType::Note => SecretType::Note,
101      secrets_store_capnp::SecretType::Password => SecretType::Password,
102      secrets_store_capnp::SecretType::Other => SecretType::Other,
103    }
104  }
105
106  pub fn to_builder(self) -> secrets_store_capnp::SecretType {
107    match self {
108      SecretType::Login => secrets_store_capnp::SecretType::Login,
109      SecretType::Licence => secrets_store_capnp::SecretType::Licence,
110      SecretType::Note => secrets_store_capnp::SecretType::Note,
111      SecretType::Wlan => secrets_store_capnp::SecretType::Wlan,
112      SecretType::Password => secrets_store_capnp::SecretType::Password,
113      SecretType::Other => secrets_store_capnp::SecretType::Other,
114    }
115  }
116}
117
118impl fmt::Display for SecretType {
119  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120    match self {
121      SecretType::Login => write!(f, "Login"),
122      SecretType::Note => write!(f, "Note"),
123      SecretType::Licence => write!(f, "Licence"),
124      SecretType::Wlan => write!(f, "WLAN"),
125      SecretType::Password => write!(f, "Password"),
126      SecretType::Other => write!(f, "Other"),
127    }
128  }
129}
130
131/// A combination of filter criterias to search for a secret.
132///
133/// All criterias are supposed to be combined by AND (i.e. all criterias have
134/// to match).
135/// Match on `name` is supposed to be "fuzzy" by some fancy scheme.
136///
137#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, Zeroize)]
138#[zeroize(drop)]
139pub struct SecretListFilter {
140  pub url: Option<String>,
141  pub tag: Option<String>,
142  #[serde(rename = "type")]
143  pub secret_type: Option<SecretType>,
144  pub name: Option<String>,
145  #[serde(default)]
146  pub deleted: bool,
147}
148
149/// SecretEntry contains all the information of a secrets that should be
150/// indexed.
151///
152/// Even though a SecretEntry does no contain a password it is still supposed to
153/// be sensitive data.
154///
155/// See SecretVersion for further detail.
156///
157#[derive(Clone, Debug, Serialize, Deserialize, Eq, Zeroize)]
158#[zeroize(drop)]
159pub struct SecretEntry {
160  pub id: String,
161  pub name: String,
162  #[serde(rename = "type")]
163  pub secret_type: SecretType,
164  pub tags: Vec<String>,
165  pub urls: Vec<String>,
166  pub timestamp: ZeroizeDateTime,
167  pub deleted: bool,
168}
169
170impl SecretEntry {
171  pub fn from_reader(reader: secret_entry::Reader) -> capnp::Result<Self> {
172    Ok(SecretEntry {
173      id: reader.get_id()?.to_string(),
174      timestamp: Utc.timestamp_millis_opt(reader.get_timestamp()).unwrap().into(),
175      name: reader.get_name()?.to_string(),
176      secret_type: SecretType::from_reader(reader.get_type()?),
177      tags: reader
178        .get_tags()?
179        .into_iter()
180        .map(|t| t.map(|t| t.to_string()))
181        .collect::<capnp::Result<Vec<String>>>()?,
182      urls: reader
183        .get_urls()?
184        .into_iter()
185        .map(|u| u.map(|u| u.to_string()))
186        .collect::<capnp::Result<Vec<String>>>()?,
187      deleted: reader.get_deleted(),
188    })
189  }
190
191  pub fn to_builder(&self, mut builder: secret_entry::Builder) {
192    builder.set_id(&self.id);
193    builder.set_timestamp(self.timestamp.timestamp_millis());
194    builder.set_name(&self.name);
195    builder.set_type(self.secret_type.to_builder());
196    let mut tags = builder.reborrow().init_tags(self.tags.len() as u32);
197    for (idx, tag) in self.tags.iter().enumerate() {
198      tags.set(idx as u32, tag)
199    }
200    let mut urls = builder.reborrow().init_urls(self.urls.len() as u32);
201    for (idx, url) in self.urls.iter().enumerate() {
202      urls.set(idx as u32, url)
203    }
204    builder.set_deleted(self.deleted);
205  }
206}
207
208impl Ord for SecretEntry {
209  fn cmp(&self, other: &Self) -> Ordering {
210    match self.name.cmp(&other.name) {
211      Ordering::Equal => self.id.cmp(&other.id),
212      ord => ord,
213    }
214  }
215}
216
217impl PartialOrd for SecretEntry {
218  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
219    Some(self.cmp(other))
220  }
221}
222
223impl PartialEq for SecretEntry {
224  fn eq(&self, other: &Self) -> bool {
225    self.id.eq(&other.id)
226  }
227}
228
229/// Representation of a filter match to a SecretEntry.
230///
231/// For the most part this is just the entry itself with some additional information
232/// which parts should be highlighted in the UI
233///
234#[derive(Clone, Debug, Serialize, Deserialize, Eq, Zeroize)]
235#[zeroize(drop)]
236pub struct SecretEntryMatch {
237  pub entry: SecretEntry,
238  /// Matching score of the name
239  pub name_score: isize,
240  /// Array of positions (single chars) to highlight in the name of the entry
241  pub name_highlights: Vec<usize>,
242  /// Array of matching urls
243  pub url_highlights: Vec<usize>,
244  /// Array of matching tags
245  pub tags_highlights: Vec<usize>,
246}
247
248impl Ord for SecretEntryMatch {
249  fn cmp(&self, other: &Self) -> Ordering {
250    match other.name_score.cmp(&self.name_score) {
251      Ordering::Equal => self.entry.cmp(&other.entry),
252      ord => ord,
253    }
254  }
255}
256
257impl PartialOrd for SecretEntryMatch {
258  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
259    Some(self.cmp(other))
260  }
261}
262
263impl PartialEq for SecretEntryMatch {
264  fn eq(&self, other: &Self) -> bool {
265    self.entry.eq(&other.entry)
266  }
267}
268
269/// Convenient wrapper of a list of SecretEntryMatch'es.
270///
271/// Also contains a unique list of tags of all secrets (e.g. to support autocompletion)
272#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, Zeroize)]
273#[zeroize(drop)]
274pub struct SecretList {
275  pub all_tags: Vec<String>,
276  pub entries: Vec<SecretEntryMatch>,
277}
278
279#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
280#[serde(transparent)]
281pub struct SecretProperties(BTreeMap<String, String>);
282
283impl SecretProperties {
284  pub fn new(properties: BTreeMap<String, String>) -> Self {
285    SecretProperties(properties)
286  }
287
288  pub fn has_non_empty(&self, name: &str) -> bool {
289    matches!(self.0.get(name), Some(value) if !value.is_empty())
290  }
291
292  pub fn get(&self, name: &str) -> Option<&String> {
293    self.0.get(name)
294  }
295
296  pub fn len(&self) -> usize {
297    self.0.len()
298  }
299
300  pub fn is_empty(&self) -> bool {
301    self.0.is_empty()
302  }
303
304  pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
305    self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
306  }
307}
308
309impl Drop for SecretProperties {
310  fn drop(&mut self) {
311    self.zeroize()
312  }
313}
314
315impl Zeroize for SecretProperties {
316  fn zeroize(&mut self) {
317    self.0.values_mut().for_each(Zeroize::zeroize);
318  }
319}
320
321/// Some short of attachment to a secret.
322///
323/// Be aware that t-rust-less is supposed to be a password store, do not misuse it as a
324/// secure document store. Nevertheless, sometimes it might be convenient added some
325/// sort of (small) document to a password.
326///
327#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
328#[zeroize(drop)]
329pub struct SecretAttachment {
330  name: String,
331  mime_type: String,
332  content: Vec<u8>,
333}
334
335/// SecretVersion holds all information of a specific version of a secret.
336///
337/// Under the hood t-rust-less only stores SecretVersion's, a Secret is no more (or less)
338/// than a group-by view over all SecretVersion's. As a rule a SecretVersion shall never be
339/// overwritten or modified once stored. To change a Secret just add a new SecretVersion for it.
340///
341#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
342#[zeroize(drop)]
343pub struct SecretVersion {
344  /// Identifier of the secret this version belongs to.
345  /// This should be opaque (i.e. not reveal anything about the content whatsoever), e.g. a
346  /// random string of sufficient length or some sort of UUID will do fine.
347  ///
348  /// By the way, as UUID was mentioned: A time-based UUID will reveal the MAC address of the
349  /// creator of the Secret as well as when it was created. If you are fine was that, ok,
350  /// otherwise do not use this kind of UUID.
351  pub secret_id: String,
352  /// General type of the Secret (in this version)
353  #[serde(rename = "type")]
354  pub secret_type: SecretType,
355  /// Timestamp of this version. All SecretVersion's of a Secret a sorted by their timestamps,
356  /// the last one will be considered the current version.
357  pub timestamp: ZeroizeDateTime,
358  /// Name/title of the Secret (in this version)
359  pub name: String,
360  /// List or arbitrary tags for filtering (or just displaying)
361  #[serde(default)]
362  pub tags: Vec<String>,
363  /// List of URLs the Secret might be associated with (most commonly the login page where
364  /// the Secret is needed)
365  #[serde(default)]
366  pub urls: Vec<String>,
367  /// Generic list of secret properties. The `secret_type` defines a list of commonly used
368  /// property-names for that type.
369  pub properties: SecretProperties,
370  /// List of attachments.
371  #[serde(default)]
372  pub attachments: Vec<SecretAttachment>,
373  /// If this version of the Secret should be marked as deleted.
374  /// As a rule of thumb it is a very bad idea to just delete secret. Maybe it was deleted by
375  /// accident, or you might need it for other reasons you have not thought of. Also just
376  /// deleting a Secret does not make it unseen. The information that someone (or yourself) has
377  /// once seen this secret might be as valuable as the secret itself.
378  #[serde(default)]
379  pub deleted: bool,
380  /// List of recipients that may see this version of the Secret.
381  /// Again: Once published, it cannot be made unseen. The only safe way to remove a recipient is
382  /// to change the Secret and create a new version without the recipient.
383  #[serde(default)]
384  pub recipients: Vec<String>,
385}
386
387impl SecretVersion {
388  pub fn to_entry_builder(&self, mut builder: secret_entry::Builder) -> capnp::Result<()> {
389    builder.set_id(&self.secret_id);
390    builder.set_timestamp(self.timestamp.timestamp_millis());
391    builder.set_name(&self.name);
392    builder.set_type(self.secret_type.to_builder());
393    set_text_list(builder.reborrow().init_tags(self.tags.len() as u32), &self.tags)?;
394    set_text_list(builder.reborrow().init_urls(self.urls.len() as u32), &self.urls)?;
395    builder.set_deleted(self.deleted);
396    Ok(())
397  }
398}
399
400#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)]
401#[zeroize(drop)]
402pub struct PasswordEstimate {
403  pub password: String,
404  pub inputs: Vec<String>,
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Zeroize)]
408#[zeroize(drop)]
409pub struct PasswordStrength {
410  pub entropy: f64,
411  pub crack_time: f64,
412  pub crack_time_display: String,
413  pub score: u8,
414}
415
416#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
417#[zeroize(drop)]
418pub struct SecretVersionRef {
419  pub block_id: String,
420  pub timestamp: ZeroizeDateTime,
421}
422
423impl SecretVersionRef {
424  pub fn from_reader(reader: secret_version_ref::Reader) -> capnp::Result<Self> {
425    Ok(SecretVersionRef {
426      block_id: reader.get_block_id()?.to_string(),
427      timestamp: Utc.timestamp_millis_opt(reader.get_timestamp()).unwrap().into(),
428    })
429  }
430
431  pub fn to_builder(&self, mut builder: secret_version_ref::Builder) {
432    builder.set_block_id(&self.block_id);
433    builder.set_timestamp(self.timestamp.timestamp_millis());
434  }
435}
436
437impl std::fmt::Display for SecretVersionRef {
438  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
439    write!(f, "{}", self.timestamp.format("%Y-%m-%d %H:%M:%S"))
440  }
441}
442
443/// Representation of a secret with all its versions.
444///
445/// The is the default view when retrieving a specific secret.
446#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
447pub struct Secret {
448  pub id: String,
449  #[serde(rename = "type")]
450  pub secret_type: SecretType,
451  pub current: SecretVersion,
452  pub current_block_id: String,
453  pub versions: Vec<SecretVersionRef>,
454  pub password_strengths: HashMap<String, PasswordStrength>,
455}
456
457impl Zeroize for Secret {
458  fn zeroize(&mut self) {
459    self.id.zeroize();
460    self.secret_type.zeroize();
461    self.current.zeroize();
462    self.current_block_id.zeroize();
463    self.versions.zeroize();
464    self.password_strengths.values_mut().for_each(Zeroize::zeroize);
465  }
466}
467
468impl Drop for Secret {
469  fn drop(&mut self) {
470    self.zeroize();
471  }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
475#[zeroize(drop)]
476pub struct PasswordGeneratorCharsParam {
477  pub num_chars: u8,
478  pub include_uppers: bool,
479  pub include_numbers: bool,
480  pub include_symbols: bool,
481  pub require_upper: bool,
482  pub require_number: bool,
483  pub require_symbol: bool,
484  pub exclude_similar: bool,
485  pub exclude_ambiguous: bool,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
489#[zeroize(drop)]
490pub struct PasswordGeneratorWordsParam {
491  pub num_words: u8,
492  pub delim: char,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
496#[serde(rename_all = "lowercase")]
497#[zeroize(drop)]
498pub enum PasswordGeneratorParam {
499  Chars(PasswordGeneratorCharsParam),
500  Words(PasswordGeneratorWordsParam),
501}
502
503pub fn set_text_list<I, S>(mut text_list: text_list::Builder, texts: I) -> capnp::Result<()>
504where
505  I: IntoIterator<Item = S>,
506  S: AsRef<str>,
507{
508  for (idx, text) in texts.into_iter().enumerate() {
509    text_list.set(idx as u32, capnp::text::new_reader(text.as_ref().as_bytes())?);
510  }
511  Ok(())
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
515#[zeroize(drop)]
516pub struct ClipboardProviding {
517  pub store_name: String,
518  pub block_id: String,
519  pub secret_name: String,
520  pub property: String,
521}