use serde::{Deserialize, Serialize};
use async_trait::async_trait;
use binary_stream::futures::{BinaryReader, Decodable};
use futures::io::{AsyncReadExt, AsyncSeek};
use futures::io::{BufReader, Cursor};
use tokio_util::compat::TokioAsyncReadCompatExt;
use age::x25519::{Identity, Recipient};
use bitflags::bitflags;
use indexmap::IndexMap;
use secrecy::SecretString;
use sha2::{Digest, Sha256};
use std::{
borrow::Cow, cmp::Ordering, collections::HashMap, fmt, path::Path,
str::FromStr,
};
use urn::Urn;
use uuid::Uuid;
use crate::{
commit::CommitHash,
constants::{DEFAULT_VAULT_NAME, VAULT_IDENTITY, VAULT_NSS},
crypto::{
AccessKey, AeadPack, Cipher, Deriver, KeyDerivation, PrivateKey, Seed,
},
decode, encode,
encoding::{encoding_options, VERSION},
events::{ReadEvent, WriteEvent},
formats::FileIdentity,
vault::secret::SecretId,
vfs::File,
Error, Result, UtcDateTime,
};
pub type VaultId = Uuid;
bitflags! {
#[derive(Default, Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
#[serde(transparent)]
pub struct VaultFlags: u64 {
const DEFAULT = 0b0000000000000001;
const IDENTITY = 0b0000000000000010;
const ARCHIVE = 0b0000000000000100;
const AUTHENTICATOR = 0b0000000000001000;
const CONTACT = 0b0000000000010000;
const SYSTEM = 0b0000000000100000;
const DEVICE = 0b0000000001000000;
const NO_SYNC_SELF = 0b0000000010000000;
const NO_SYNC_OTHER = 0b0000000100000000;
const SHARED = 0b0000001000000000;
}
}
impl VaultFlags {
pub fn is_default(&self) -> bool {
self.contains(VaultFlags::DEFAULT)
}
pub fn is_identity(&self) -> bool {
self.contains(VaultFlags::IDENTITY)
}
pub fn is_archive(&self) -> bool {
self.contains(VaultFlags::ARCHIVE)
}
pub fn is_authenticator(&self) -> bool {
self.contains(VaultFlags::AUTHENTICATOR)
}
pub fn is_contact(&self) -> bool {
self.contains(VaultFlags::CONTACT)
}
pub fn is_system(&self) -> bool {
self.contains(VaultFlags::SYSTEM)
}
pub fn is_device(&self) -> bool {
self.contains(VaultFlags::DEVICE)
}
pub fn is_no_sync_self(&self) -> bool {
self.contains(VaultFlags::NO_SYNC_SELF)
}
pub fn is_no_sync_other(&self) -> bool {
self.contains(VaultFlags::NO_SYNC_OTHER)
}
pub fn is_shared(&self) -> bool {
self.contains(VaultFlags::SHARED)
}
}
#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultMeta {
pub(crate) date_created: UtcDateTime,
#[serde(skip_serializing_if = "String::is_empty")]
pub(crate) description: String,
}
impl VaultMeta {
pub fn description(&self) -> &str {
&self.description
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn date_created(&self) -> &UtcDateTime {
&self.date_created
}
}
#[derive(Debug, Clone)]
pub enum FolderRef {
Id(VaultId),
Name(String),
}
impl fmt::Display for FolderRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Id(id) => write!(f, "{}", id),
Self::Name(name) => write!(f, "{}", name),
}
}
}
impl FromStr for FolderRef {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Ok(id) = Uuid::parse_str(s) {
Ok(Self::Id(id))
} else {
Ok(Self::Name(s.to_string()))
}
}
}
impl From<VaultId> for FolderRef {
fn from(value: VaultId) -> Self {
Self::Id(value)
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct VaultEntry(pub AeadPack, pub AeadPack);
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct VaultCommit(pub CommitHash, pub VaultEntry);
#[async_trait]
pub trait VaultAccess {
async fn summary(&self) -> Result<Summary>;
async fn vault_name(&self) -> Result<Cow<'_, str>>;
async fn set_vault_name(&mut self, name: String) -> Result<WriteEvent>;
async fn set_vault_meta(
&mut self,
meta_data: AeadPack,
) -> Result<WriteEvent>;
async fn create(
&mut self,
commit: CommitHash,
secret: VaultEntry,
) -> Result<WriteEvent>;
#[doc(hidden)]
async fn insert(
&mut self,
id: SecretId,
commit: CommitHash,
secret: VaultEntry,
) -> Result<WriteEvent>;
async fn read<'a>(
&'a self,
id: &SecretId,
) -> Result<(Option<Cow<'a, VaultCommit>>, ReadEvent)>;
async fn update(
&mut self,
id: &SecretId,
commit: CommitHash,
secret: VaultEntry,
) -> Result<Option<WriteEvent>>;
async fn delete(&mut self, id: &SecretId) -> Result<Option<WriteEvent>>;
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Auth {
pub(crate) salt: Option<String>,
pub(crate) seed: Option<Seed>,
}
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
pub struct Summary {
pub(crate) version: u16,
pub(crate) id: VaultId,
pub(crate) name: String,
pub(crate) cipher: Cipher,
pub(crate) kdf: KeyDerivation,
pub(crate) flags: VaultFlags,
}
impl Ord for Summary {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for Summary {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Summary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Version {} using {} with {}\n{} {}",
self.version, self.cipher, self.kdf, self.name, self.id
)
}
}
impl Default for Summary {
fn default() -> Self {
Self {
version: VERSION,
cipher: Default::default(),
kdf: Default::default(),
id: Uuid::new_v4(),
name: DEFAULT_VAULT_NAME.to_string(),
flags: Default::default(),
}
}
}
impl Summary {
pub fn new(
id: VaultId,
name: String,
cipher: Cipher,
kdf: KeyDerivation,
flags: VaultFlags,
) -> Self {
Self {
version: VERSION,
cipher,
kdf,
id,
name,
flags,
}
}
pub fn version(&self) -> &u16 {
&self.version
}
pub fn cipher(&self) -> &Cipher {
&self.cipher
}
pub fn kdf(&self) -> &KeyDerivation {
&self.kdf
}
pub fn id(&self) -> &VaultId {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: String) {
self.name = name;
}
pub fn flags(&self) -> &VaultFlags {
&self.flags
}
pub fn flags_mut(&mut self) -> &mut VaultFlags {
&mut self.flags
}
}
impl From<Summary> for VaultId {
fn from(value: Summary) -> Self {
value.id
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct Header {
pub(crate) summary: Summary,
pub(crate) meta: Option<AeadPack>,
pub(crate) auth: Auth,
pub(crate) shared_access: SharedAccess,
}
impl Header {
pub fn new(
id: VaultId,
name: String,
cipher: Cipher,
kdf: KeyDerivation,
flags: VaultFlags,
) -> Self {
Self {
summary: Summary::new(id, name, cipher, kdf, flags),
meta: None,
auth: Default::default(),
shared_access: Default::default(),
}
}
pub fn id(&self) -> &VaultId {
self.summary.id()
}
pub(crate) fn clear_salt(&mut self) {
self.auth.salt = None;
}
pub fn name(&self) -> &str {
&self.summary.name
}
pub fn set_name(&mut self, name: String) {
self.summary.set_name(name);
}
pub fn meta(&self) -> Option<&AeadPack> {
self.meta.as_ref()
}
pub fn set_meta(&mut self, meta: Option<AeadPack>) {
self.meta = meta;
}
pub async fn read_content_offset<P: AsRef<Path>>(path: P) -> Result<u64> {
let mut stream = File::open(path.as_ref()).await?.compat();
Header::read_content_offset_stream(&mut stream).await
}
pub async fn read_content_offset_slice(buffer: &[u8]) -> Result<u64> {
let mut stream = BufReader::new(Cursor::new(buffer));
Header::read_content_offset_stream(&mut stream).await
}
pub async fn read_content_offset_stream<
R: AsyncReadExt + AsyncSeek + Unpin + Send,
>(
stream: R,
) -> Result<u64> {
let mut reader = BinaryReader::new(stream, encoding_options());
let identity = reader.read_bytes(VAULT_IDENTITY.len()).await?;
FileIdentity::read_slice(&identity, &VAULT_IDENTITY)?;
let header_len = reader.read_u32().await? as u64;
let content_offset = VAULT_IDENTITY.len() as u64 + 4 + header_len;
Ok(content_offset)
}
pub async fn read_summary_file<P: AsRef<Path>>(
file: P,
) -> Result<Summary> {
let mut stream = File::open(file.as_ref()).await?.compat();
Header::read_summary_stream(&mut stream).await
}
pub async fn read_summary_slice(buffer: &[u8]) -> Result<Summary> {
let mut stream = BufReader::new(Cursor::new(buffer));
Header::read_summary_stream(&mut stream).await
}
async fn read_summary_stream<
R: AsyncReadExt + AsyncSeek + Unpin + Send,
>(
stream: R,
) -> Result<Summary> {
let mut reader = BinaryReader::new(stream, encoding_options());
FileIdentity::read_identity(&mut reader, &VAULT_IDENTITY).await?;
let _ = reader.read_u32().await?;
let mut summary: Summary = Default::default();
summary.decode(&mut reader).await?;
Ok(summary)
}
pub async fn read_header_file<P: AsRef<Path>>(file: P) -> Result<Header> {
let mut stream = File::open(file.as_ref()).await?.compat();
Header::read_header_stream(&mut stream).await
}
pub(crate) async fn read_header_stream<
R: AsyncReadExt + AsyncSeek + Unpin + Send,
>(
stream: R,
) -> Result<Header> {
let mut reader = BinaryReader::new(stream, encoding_options());
let mut header: Header = Default::default();
header.decode(&mut reader).await?;
Ok(header)
}
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SharedAccess {
WriteAccess(Vec<String>),
ReadOnly(AeadPack),
}
impl Default for SharedAccess {
fn default() -> Self {
Self::WriteAccess(vec![])
}
}
impl SharedAccess {
fn parse_recipients(access: &Vec<String>) -> Result<Vec<Recipient>> {
let mut recipients = Vec::new();
for recipient in access {
let recipient = recipient.parse().map_err(|s: &str| {
Error::InvalidX25519Identity(s.to_owned())
})?;
recipients.push(recipient);
}
Ok(recipients)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Contents {
pub(crate) data: IndexMap<SecretId, VaultCommit>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Vault {
pub(crate) header: Header,
pub(crate) contents: Contents,
}
impl Vault {
pub fn new(
id: VaultId,
name: String,
cipher: Cipher,
kdf: KeyDerivation,
flags: VaultFlags,
) -> Self {
Self {
header: Header::new(id, name, cipher, kdf, flags),
contents: Default::default(),
}
}
pub fn shared_access(&self) -> &SharedAccess {
&self.header.shared_access
}
pub fn vault_urn(id: &VaultId) -> Result<Urn> {
let vault_urn = format!("urn:sos:{}{}", VAULT_NSS, id);
Ok(vault_urn.parse()?)
}
pub(crate) async fn symmetric(
&mut self,
password: SecretString,
seed: Option<Seed>,
) -> Result<PrivateKey> {
if self.header.auth.salt.is_none() {
let salt = KeyDerivation::generate_salt();
let deriver = self.deriver();
let derived_private_key =
deriver.derive(&password, &salt, seed.as_ref())?;
let private_key = PrivateKey::Symmetric(derived_private_key);
self.header.auth.salt = Some(salt.to_string());
self.header.auth.seed = seed;
Ok(private_key)
} else {
Err(Error::VaultAlreadyInit)
}
}
pub(crate) async fn asymmetric(
&mut self,
owner: &Identity,
mut recipients: Vec<Recipient>,
read_only: bool,
) -> Result<PrivateKey> {
if self.header.auth.salt.is_none() {
let owner_public = owner.to_public();
if !recipients
.iter()
.any(|r| r.to_string() == owner_public.to_string())
{
recipients.push(owner_public);
}
self.flags_mut().set(VaultFlags::SHARED, true);
let salt = KeyDerivation::generate_salt();
let private_key = PrivateKey::Asymmetric(owner.clone());
self.header.summary.cipher = Cipher::X25519;
let recipients: Vec<_> =
recipients.into_iter().map(|r| r.to_string()).collect();
self.header.shared_access = if read_only {
let access = SharedAccess::WriteAccess(recipients);
let buffer = encode(&access).await?;
let private_key = PrivateKey::Asymmetric(owner.clone());
let cipher = self.header.summary.cipher.clone();
let owner_recipients = vec![owner.to_public()];
let aead = cipher
.encrypt_asymmetric(
&private_key,
&buffer,
owner_recipients,
)
.await?;
SharedAccess::ReadOnly(aead)
} else {
SharedAccess::WriteAccess(recipients)
};
self.header.auth.salt = Some(salt.to_string());
Ok(private_key)
} else {
Err(Error::VaultAlreadyInit)
}
}
pub fn deriver(&self) -> Box<dyn Deriver<Sha256> + Send + 'static> {
self.header.summary.kdf.deriver()
}
pub fn set_default_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::DEFAULT, value);
}
pub fn set_archive_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::ARCHIVE, value);
}
pub fn set_authenticator_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::AUTHENTICATOR, value);
}
pub fn set_contact_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::CONTACT, value);
}
pub fn set_system_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::SYSTEM, value);
}
pub fn set_device_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::DEVICE, value);
}
pub fn set_no_sync_self_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::NO_SYNC_SELF, value);
}
pub fn set_no_sync_other_flag(&mut self, value: bool) {
self.flags_mut().set(VaultFlags::NO_SYNC_OTHER, value);
}
pub(crate) fn insert_entry(&mut self, id: SecretId, entry: VaultCommit) {
self.contents.data.insert(id, entry);
}
pub fn get(&self, id: &SecretId) -> Option<&VaultCommit> {
self.contents.data.get(id)
}
pub async fn encrypt(
&self,
key: &PrivateKey,
plaintext: &[u8],
) -> Result<AeadPack> {
match self.cipher() {
Cipher::XChaCha20Poly1305 | Cipher::AesGcm256 => {
self.cipher().encrypt_symmetric(key, plaintext, None).await
}
Cipher::X25519 => {
let recipients = match &self.header.shared_access {
SharedAccess::WriteAccess(access) => {
SharedAccess::parse_recipients(access)?
}
SharedAccess::ReadOnly(aead) => {
let buffer = self
.decrypt(key, aead)
.await
.map_err(|_| Error::PermissionDenied)?;
let shared_access: SharedAccess =
decode(&buffer).await?;
if let SharedAccess::WriteAccess(access) =
&shared_access
{
SharedAccess::parse_recipients(access)?
} else {
return Err(Error::PermissionDenied);
}
}
};
self.cipher()
.encrypt_asymmetric(key, plaintext, recipients)
.await
}
}
}
pub async fn decrypt(
&self,
key: &PrivateKey,
aead: &AeadPack,
) -> Result<Vec<u8>> {
match self.cipher() {
Cipher::XChaCha20Poly1305 | Cipher::AesGcm256 => {
self.cipher().decrypt_symmetric(key, aead).await
}
Cipher::X25519 => {
self.cipher().decrypt_asymmetric(key, aead).await
}
}
}
pub fn rotate_identifier(&mut self) {
self.header.summary.id = Uuid::new_v4();
}
pub async fn verify(&self, key: &AccessKey) -> Result<()> {
let salt = self.salt().ok_or(Error::VaultNotInit)?;
let meta_aead = self.header().meta().ok_or(Error::VaultNotInit)?;
let private_key = match key {
AccessKey::Password(password) => {
let salt = KeyDerivation::parse_salt(salt)?;
let deriver = self.deriver();
PrivateKey::Symmetric(deriver.derive(
password,
&salt,
self.seed(),
)?)
}
AccessKey::Identity(id) => PrivateKey::Asymmetric(id.clone()),
};
let _ = self
.decrypt(&private_key, meta_aead)
.await
.map_err(|_| Error::PassphraseVerification)?;
Ok(())
}
pub fn iter(&self) -> impl Iterator<Item = (&Uuid, &VaultCommit)> {
self.contents.data.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &Uuid> {
self.contents.data.keys()
}
pub fn values(&self) -> impl Iterator<Item = &VaultCommit> {
self.contents.data.values()
}
pub fn len(&self) -> usize {
self.contents.data.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub async fn into_event(&self) -> Result<WriteEvent> {
let buffer = if self.is_empty() {
encode(self).await?
} else {
let header = self.header.clone();
let vault: Vault = header.into();
encode(&vault).await?
};
Ok(WriteEvent::CreateVault(buffer))
}
pub fn commits(&self) -> impl Iterator<Item = (&Uuid, &CommitHash)> {
self.contents
.data
.keys()
.zip(self.contents.data.values().map(|v| &v.0))
}
pub fn salt(&self) -> Option<&String> {
self.header.auth.salt.as_ref()
}
pub fn seed(&self) -> Option<&Seed> {
self.header.auth.seed.as_ref()
}
pub fn summary(&self) -> &Summary {
&self.header.summary
}
pub fn flags(&self) -> &VaultFlags {
self.header.summary.flags()
}
pub fn flags_mut(&mut self) -> &mut VaultFlags {
self.header.summary.flags_mut()
}
pub fn id(&self) -> &VaultId {
&self.header.summary.id
}
pub fn name(&self) -> &str {
self.header.name()
}
pub fn set_name(&mut self, name: String) {
self.header.set_name(name);
}
pub fn cipher(&self) -> &Cipher {
&self.header.summary.cipher
}
pub fn kdf(&self) -> &KeyDerivation {
&self.header.summary.kdf
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn header_mut(&mut self) -> &mut Header {
&mut self.header
}
pub fn meta_data(&self) -> HashMap<&Uuid, &AeadPack> {
self.contents
.data
.iter()
.map(|(k, v)| (k, &v.1 .0))
.collect::<HashMap<_, _>>()
}
pub async fn write_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
use tokio::io::AsyncWriteExt;
let mut stream = File::create(path).await?;
let buffer = encode(self).await?;
stream.write_all(&buffer).await?;
stream.flush().await?;
Ok(())
}
pub async fn commit_hash(
meta_aead: &AeadPack,
secret_aead: &AeadPack,
) -> Result<(CommitHash, Vec<u8>)> {
let encoded_meta = encode(meta_aead).await?;
let encoded_data = encode(secret_aead).await?;
let mut hash_bytes =
Vec::with_capacity(encoded_meta.len() + encoded_data.len());
hash_bytes.extend_from_slice(&encoded_meta);
hash_bytes.extend_from_slice(&encoded_data);
let commit = CommitHash(
Sha256::digest(hash_bytes.as_slice())
.as_slice()
.try_into()?,
);
Ok((commit, hash_bytes))
}
}
impl From<Header> for Vault {
fn from(header: Header) -> Self {
Vault {
header,
contents: Default::default(),
}
}
}
impl From<Vault> for Header {
fn from(value: Vault) -> Self {
value.header
}
}
impl IntoIterator for Vault {
type Item = (SecretId, VaultCommit);
type IntoIter = indexmap::map::IntoIter<SecretId, VaultCommit>;
fn into_iter(self) -> Self::IntoIter {
self.contents.data.into_iter()
}
}
#[async_trait]
impl VaultAccess for Vault {
async fn summary(&self) -> Result<Summary> {
Ok(self.header.summary.clone())
}
async fn vault_name(&self) -> Result<Cow<'_, str>> {
Ok(Cow::Borrowed(self.name()))
}
async fn set_vault_name(&mut self, name: String) -> Result<WriteEvent> {
self.set_name(name.clone());
Ok(WriteEvent::SetVaultName(name))
}
async fn set_vault_meta(
&mut self,
meta_data: AeadPack,
) -> Result<WriteEvent> {
self.header.set_meta(Some(meta_data.clone()));
Ok(WriteEvent::SetVaultMeta(meta_data))
}
async fn create(
&mut self,
commit: CommitHash,
secret: VaultEntry,
) -> Result<WriteEvent> {
let id = Uuid::new_v4();
self.insert(id, commit, secret).await
}
async fn insert(
&mut self,
id: SecretId,
commit: CommitHash,
secret: VaultEntry,
) -> Result<WriteEvent> {
let value = self
.contents
.data
.entry(id)
.or_insert(VaultCommit(commit, secret));
Ok(WriteEvent::CreateSecret(id, value.clone()))
}
async fn read<'a>(
&'a self,
id: &SecretId,
) -> Result<(Option<Cow<'a, VaultCommit>>, ReadEvent)> {
let result = self.contents.data.get(id).map(Cow::Borrowed);
Ok((result, ReadEvent::ReadSecret(*id)))
}
async fn update(
&mut self,
id: &SecretId,
commit: CommitHash,
secret: VaultEntry,
) -> Result<Option<WriteEvent>> {
let _vault_id = *self.id();
if let Some(value) = self.contents.data.get_mut(id) {
*value = VaultCommit(commit, secret);
Ok(Some(WriteEvent::UpdateSecret(*id, value.clone())))
} else {
Ok(None)
}
}
async fn delete(&mut self, id: &SecretId) -> Result<Option<WriteEvent>> {
let entry = self.contents.data.shift_remove(id);
if entry.is_some() {
Ok(Some(WriteEvent::DeleteSecret(*id)))
} else {
Ok(None)
}
}
}