use crate::secrets_store_capnp::{self, secret_entry, secret_version_ref};
use capnp::text_list;
use chrono::{TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fmt;
use zeroize::Zeroize;
mod command;
mod config;
mod event;
mod zeroize_datetime;
#[cfg(test)]
mod tests;
pub use command::*;
pub use config::*;
pub use event::*;
pub use zeroize_datetime::*;
pub const PROPERTY_USERNAME: &str = "username";
pub const PROPERTY_PASSWORD: &str = "password";
pub const PROPERTY_TOTP_URL: &str = "totpUrl";
pub const PROPERTY_NOTES: &str = "notes";
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct Status {
pub locked: bool,
pub unlocked_by: Option<Identity>,
pub autolock_at: Option<ZeroizeDateTime>,
pub version: String,
pub autolock_timeout: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct Identity {
pub id: String,
pub name: String,
pub email: String,
pub hidden: bool,
}
impl std::fmt::Display for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[serde(rename_all = "lowercase")]
pub enum SecretType {
Login,
Note,
Licence,
Wlan,
Password,
#[serde(other)]
Other,
}
impl Zeroize for SecretType {
fn zeroize(&mut self) {
*self = SecretType::Other
}
}
impl SecretType {
pub fn password_properties(&self) -> &[&str] {
match self {
SecretType::Login => &[PROPERTY_PASSWORD],
SecretType::Note => &[],
SecretType::Licence => &[],
SecretType::Wlan => &[PROPERTY_PASSWORD],
SecretType::Password => &[PROPERTY_PASSWORD],
SecretType::Other => &[],
}
}
pub fn from_reader(api: secrets_store_capnp::SecretType) -> Self {
match api {
secrets_store_capnp::SecretType::Login => SecretType::Login,
secrets_store_capnp::SecretType::Licence => SecretType::Licence,
secrets_store_capnp::SecretType::Wlan => SecretType::Wlan,
secrets_store_capnp::SecretType::Note => SecretType::Note,
secrets_store_capnp::SecretType::Password => SecretType::Password,
secrets_store_capnp::SecretType::Other => SecretType::Other,
}
}
pub fn to_builder(self) -> secrets_store_capnp::SecretType {
match self {
SecretType::Login => secrets_store_capnp::SecretType::Login,
SecretType::Licence => secrets_store_capnp::SecretType::Licence,
SecretType::Note => secrets_store_capnp::SecretType::Note,
SecretType::Wlan => secrets_store_capnp::SecretType::Wlan,
SecretType::Password => secrets_store_capnp::SecretType::Password,
SecretType::Other => secrets_store_capnp::SecretType::Other,
}
}
}
impl fmt::Display for SecretType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SecretType::Login => write!(f, "Login"),
SecretType::Note => write!(f, "Note"),
SecretType::Licence => write!(f, "Licence"),
SecretType::Wlan => write!(f, "WLAN"),
SecretType::Password => write!(f, "Password"),
SecretType::Other => write!(f, "Other"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretListFilter {
pub url: Option<String>,
pub tag: Option<String>,
#[serde(rename = "type")]
pub secret_type: Option<SecretType>,
pub name: Option<String>,
#[serde(default)]
pub deleted: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretEntry {
pub id: String,
pub name: String,
#[serde(rename = "type")]
pub secret_type: SecretType,
pub tags: Vec<String>,
pub urls: Vec<String>,
pub timestamp: ZeroizeDateTime,
pub deleted: bool,
}
impl SecretEntry {
pub fn from_reader(reader: secret_entry::Reader) -> capnp::Result<Self> {
Ok(SecretEntry {
id: reader.get_id()?.to_string()?,
timestamp: Utc.timestamp_millis_opt(reader.get_timestamp()).unwrap().into(),
name: reader.get_name()?.to_string()?,
secret_type: SecretType::from_reader(reader.get_type()?),
tags: reader
.get_tags()?
.into_iter()
.map(|t| t.and_then(|t| Ok(t.to_string()?)))
.collect::<capnp::Result<Vec<String>>>()?,
urls: reader
.get_urls()?
.into_iter()
.map(|u| u.and_then(|u| Ok(u.to_string()?)))
.collect::<capnp::Result<Vec<String>>>()?,
deleted: reader.get_deleted(),
})
}
pub fn to_builder(&self, mut builder: secret_entry::Builder) {
builder.set_id(&self.id);
builder.set_timestamp(self.timestamp.timestamp_millis());
builder.set_name(&self.name);
builder.set_type(self.secret_type.to_builder());
let mut tags = builder.reborrow().init_tags(self.tags.len() as u32);
for (idx, tag) in self.tags.iter().enumerate() {
tags.set(idx as u32, tag)
}
let mut urls = builder.reborrow().init_urls(self.urls.len() as u32);
for (idx, url) in self.urls.iter().enumerate() {
urls.set(idx as u32, url)
}
builder.set_deleted(self.deleted);
}
}
impl Ord for SecretEntry {
fn cmp(&self, other: &Self) -> Ordering {
match self.name.cmp(&other.name) {
Ordering::Equal => self.id.cmp(&other.id),
ord => ord,
}
}
}
impl PartialOrd for SecretEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for SecretEntry {
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretEntryMatch {
pub entry: SecretEntry,
pub name_score: isize,
pub name_highlights: Vec<usize>,
pub url_highlights: Vec<usize>,
pub tags_highlights: Vec<usize>,
}
impl Ord for SecretEntryMatch {
fn cmp(&self, other: &Self) -> Ordering {
match other.name_score.cmp(&self.name_score) {
Ordering::Equal => self.entry.cmp(&other.entry),
ord => ord,
}
}
}
impl PartialOrd for SecretEntryMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for SecretEntryMatch {
fn eq(&self, other: &Self) -> bool {
self.entry.eq(&other.entry)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretList {
pub all_tags: Vec<String>,
pub entries: Vec<SecretEntryMatch>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[serde(transparent)]
pub struct SecretProperties(BTreeMap<String, String>);
impl SecretProperties {
pub fn new(properties: BTreeMap<String, String>) -> Self {
SecretProperties(properties)
}
pub fn has_non_empty(&self, name: &str) -> bool {
matches!(self.0.get(name), Some(value) if !value.is_empty())
}
pub fn get(&self, name: &str) -> Option<&String> {
self.0.get(name)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
}
impl Drop for SecretProperties {
fn drop(&mut self) {
self.zeroize()
}
}
impl Zeroize for SecretProperties {
fn zeroize(&mut self) {
self.0.values_mut().for_each(Zeroize::zeroize);
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretAttachment {
name: String,
mime_type: String,
content: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretVersion {
pub secret_id: String,
#[serde(rename = "type")]
pub secret_type: SecretType,
pub timestamp: ZeroizeDateTime,
pub name: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub urls: Vec<String>,
pub properties: SecretProperties,
#[serde(default)]
pub attachments: Vec<SecretAttachment>,
#[serde(default)]
pub deleted: bool,
#[serde(default)]
pub recipients: Vec<String>,
}
impl SecretVersion {
pub fn to_entry_builder(&self, mut builder: secret_entry::Builder) -> capnp::Result<()> {
builder.set_id(&self.secret_id);
builder.set_timestamp(self.timestamp.timestamp_millis());
builder.set_name(&self.name);
builder.set_type(self.secret_type.to_builder());
set_text_list(builder.reborrow().init_tags(self.tags.len() as u32), &self.tags)?;
set_text_list(builder.reborrow().init_urls(self.urls.len() as u32), &self.urls)?;
builder.set_deleted(self.deleted);
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct PasswordEstimate {
pub password: String,
pub inputs: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct PasswordStrength {
pub entropy: f64,
pub crack_time: f64,
pub crack_time_display: String,
pub score: u8,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct SecretVersionRef {
pub block_id: String,
pub timestamp: ZeroizeDateTime,
}
impl SecretVersionRef {
pub fn from_reader(reader: secret_version_ref::Reader) -> capnp::Result<Self> {
Ok(SecretVersionRef {
block_id: reader.get_block_id()?.to_string()?,
timestamp: Utc.timestamp_millis_opt(reader.get_timestamp()).unwrap().into(),
})
}
pub fn to_builder(&self, mut builder: secret_version_ref::Builder) {
builder.set_block_id(&self.block_id);
builder.set_timestamp(self.timestamp.timestamp_millis());
}
}
impl std::fmt::Display for SecretVersionRef {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.timestamp.format("%Y-%m-%d %H:%M:%S"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
pub struct Secret {
pub id: String,
#[serde(rename = "type")]
pub secret_type: SecretType,
pub current: SecretVersion,
pub current_block_id: String,
pub versions: Vec<SecretVersionRef>,
pub password_strengths: HashMap<String, PasswordStrength>,
}
impl Zeroize for Secret {
fn zeroize(&mut self) {
self.id.zeroize();
self.secret_type.zeroize();
self.current.zeroize();
self.current_block_id.zeroize();
self.versions.zeroize();
self.password_strengths.values_mut().for_each(Zeroize::zeroize);
}
}
impl Drop for Secret {
fn drop(&mut self) {
self.zeroize();
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct PasswordGeneratorCharsParam {
pub num_chars: u8,
pub include_uppers: bool,
pub include_numbers: bool,
pub include_symbols: bool,
pub require_upper: bool,
pub require_number: bool,
pub require_symbol: bool,
pub exclude_similar: bool,
pub exclude_ambiguous: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct PasswordGeneratorWordsParam {
pub num_words: u8,
pub delim: char,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[serde(rename_all = "lowercase")]
#[zeroize(drop)]
pub enum PasswordGeneratorParam {
Chars(PasswordGeneratorCharsParam),
Words(PasswordGeneratorWordsParam),
}
pub fn set_text_list<I, S>(mut text_list: text_list::Builder, texts: I) -> capnp::Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for (idx, text) in texts.into_iter().enumerate() {
text_list.set(idx as u32, text.as_ref());
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Zeroize)]
#[cfg_attr(feature = "with_specta", derive(specta::Type))]
#[zeroize(drop)]
pub struct ClipboardProviding {
pub store_name: String,
pub block_id: String,
pub secret_name: String,
pub property: String,
}