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