refactor(web): updated web to use async sdk
parent
4fc36d6474
commit
2871fe13ce
@ -1,23 +0,0 @@
|
||||
use base64::Engine;
|
||||
|
||||
use crate::types::{Error, ErrorKind, Result};
|
||||
|
||||
const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
|
||||
&base64::alphabet::URL_SAFE,
|
||||
base64::engine::general_purpose::NO_PAD,
|
||||
);
|
||||
|
||||
/// Encode an array of bytes to a base64-encoded string
|
||||
pub fn encode(data: &[u8]) -> String {
|
||||
BASE64_ENGINE.encode(data)
|
||||
}
|
||||
|
||||
/// Decode a base64-encoded array of bytes to an array of utf-8 encoded bytes
|
||||
pub fn decode(data: &[u8]) -> Result<Vec<u8>> {
|
||||
BASE64_ENGINE.decode(data).map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::DecodeBase64,
|
||||
format!("Failed to decode base64: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
use chacha20poly1305::{
|
||||
aead::{rand_core::RngCore, Aead, OsRng},
|
||||
KeyInit, XChaCha20Poly1305,
|
||||
};
|
||||
|
||||
use crate::types::{Error, ErrorKind, Result};
|
||||
|
||||
pub fn generate_key() -> [u8; 32] {
|
||||
let mut key = [0u8; 32];
|
||||
|
||||
OsRng.fill_bytes(&mut key);
|
||||
|
||||
key
|
||||
}
|
||||
|
||||
pub fn generate_nonce() -> [u8; 24] {
|
||||
let mut nonce = [0u8; 24];
|
||||
|
||||
OsRng.fill_bytes(&mut nonce);
|
||||
|
||||
nonce
|
||||
}
|
||||
|
||||
pub fn encrypt(data: &[u8], key: &[u8; 32], nonce: &[u8; 24]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
|
||||
let encrypted = cipher
|
||||
.encrypt(nonce.into(), data)
|
||||
.map_err(|e| Error::new(ErrorKind::Crypto, format!("Failed to encrypt data: {}", e)))?;
|
||||
|
||||
Ok(encrypted)
|
||||
}
|
||||
|
||||
pub fn decrypt(data: &[u8], key: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
|
||||
let decrypted = cipher
|
||||
.decrypt(nonce.into(), data)
|
||||
.map_err(|e| Error::new(ErrorKind::Crypto, format!("Failed to decrypt data: {}", e)))?;
|
||||
|
||||
Ok(decrypted)
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
use directories::ProjectDirs;
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::types::{Error, ErrorKind, Result};
|
||||
|
||||
fn parse_io_error(error: std::io::Error) -> Error {
|
||||
Error::new(ErrorKind::IoError, error.to_string())
|
||||
}
|
||||
|
||||
const APP_NAME: &str = "Dust-Mail";
|
||||
|
||||
fn ensure_cache_dir(file_name: &str) -> Result<PathBuf> {
|
||||
if let Some(project_dirs) =
|
||||
ProjectDirs::from("dev.guusvanmeerveld", "Guus van Meerveld", APP_NAME)
|
||||
{
|
||||
let cache_dir = project_dirs.cache_dir().join(APP_NAME.to_ascii_lowercase());
|
||||
|
||||
if let Some(p) = cache_dir.parent() {
|
||||
fs::create_dir_all(p).map_err(parse_io_error)?
|
||||
};
|
||||
|
||||
return Ok(cache_dir.with_file_name(file_name));
|
||||
};
|
||||
|
||||
Err(Error::new(
|
||||
ErrorKind::NoCacheDir,
|
||||
"Could locate a valid cache directory",
|
||||
))
|
||||
}
|
||||
|
||||
pub struct CacheFile(String);
|
||||
|
||||
impl CacheFile {
|
||||
pub fn from_session_name<S: Into<String>>(session_name: S) -> Self {
|
||||
Self(format!("{}.session", session_name.into()))
|
||||
}
|
||||
|
||||
pub fn new<S: Into<String>>(file_name: S) -> Self {
|
||||
Self(file_name.into())
|
||||
}
|
||||
|
||||
/// Read a file with a given filename from the application's cache directory.
|
||||
pub fn read(&self, buf: &mut Vec<u8>) -> Result<()> {
|
||||
let cache_file = ensure_cache_dir(&self.0)?;
|
||||
|
||||
let mut file = File::open(cache_file).map_err(parse_io_error)?;
|
||||
|
||||
file.read_to_end(buf).map_err(parse_io_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(&self) -> Result<()> {
|
||||
let cache_file = ensure_cache_dir(&self.0)?;
|
||||
|
||||
fs::remove_file(cache_file).map_err(parse_io_error)
|
||||
}
|
||||
|
||||
/// Write a file with a given filename to the applications cache directory.
|
||||
pub fn write(&self, data: &[u8]) -> Result<()> {
|
||||
let cache_file = ensure_cache_dir(&self.0)?;
|
||||
|
||||
fs::write(cache_file, data).map_err(parse_io_error)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
use std::io::Read;
|
||||
|
||||
use crate::types::Result;
|
||||
|
||||
use data_encoding::HEXUPPER;
|
||||
use ring::digest::{Context, Digest, SHA256};
|
||||
|
||||
fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
|
||||
let mut context = Context::new(&SHA256);
|
||||
let mut buffer = [0; 1024];
|
||||
|
||||
loop {
|
||||
let count = reader.read(&mut buffer)?;
|
||||
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
context.update(&buffer[..count]);
|
||||
}
|
||||
|
||||
Ok(context.finish())
|
||||
}
|
||||
|
||||
pub fn sha256_hex<R: Read>(reader: R) -> Result<String> {
|
||||
let digest = sha256_digest(reader)?;
|
||||
|
||||
let hex = HEXUPPER.encode(digest.as_ref());
|
||||
|
||||
Ok(hex)
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
use sdk::session::{FullLoginOptions, LoginType};
|
||||
|
||||
use crate::{hash::sha256_hex, types::Result};
|
||||
|
||||
pub struct Identifier {
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl Identifier {
|
||||
/// Hash the currently stored identifier
|
||||
pub fn hash(&mut self) -> Result<()> {
|
||||
self.id = sha256_hex(self.id.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for Identifier {
|
||||
fn into(self) -> String {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FullLoginOptions> for Identifier {
|
||||
fn from(login_options: &FullLoginOptions) -> Self {
|
||||
// TODO: Outgoing support
|
||||
let incoming = login_options.incoming_options();
|
||||
|
||||
let (username, password) = match incoming.login_type() {
|
||||
LoginType::PasswordBased(creds) => (creds.username(), creds.password()),
|
||||
LoginType::OAuthBased(creds) => (creds.username(), creds.access_token()),
|
||||
};
|
||||
|
||||
// A string that is unique to these login options.
|
||||
let credentials_string = format!(
|
||||
"{}:{}@{}:{}",
|
||||
username,
|
||||
password,
|
||||
incoming.domain(),
|
||||
incoming.port()
|
||||
);
|
||||
|
||||
Self {
|
||||
id: credentials_string,
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
use crate::types::Result;
|
||||
|
||||
use keyring::Entry;
|
||||
|
||||
const APPLICATION_NAME: &str = "Dust-Mail";
|
||||
|
||||
fn build_entry_from_identifier<T: AsRef<str>>(identifier: T) -> Result<Entry> {
|
||||
let username = whoami::username();
|
||||
|
||||
let name = format!("{}:{}", APPLICATION_NAME, identifier.as_ref());
|
||||
|
||||
let entry = Entry::new(&name, &username)?;
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
pub fn get<T: AsRef<str>>(identifier: T) -> Result<String> {
|
||||
let entry = build_entry_from_identifier(identifier)?;
|
||||
|
||||
let password = entry.get_password()?;
|
||||
|
||||
Ok(password)
|
||||
}
|
||||
|
||||
pub fn set<T: AsRef<str>, S: AsRef<str>>(identifier: T, value: S) -> Result<()> {
|
||||
let entry = build_entry_from_identifier(identifier)?;
|
||||
|
||||
entry.set_password(value.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
use dashmap::DashMap;
|
||||
use serde_json::from_str;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use sdk::session::{MailSessions, ThreadSafeIncomingSession};
|
||||
|
||||
use crate::{keyring, types::Result};
|
||||
|
||||
pub struct Sessions {
|
||||
sessions_map: DashMap<String, Arc<MailSessions>>,
|
||||
}
|
||||
|
||||
impl Sessions {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sessions_map: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_session<S: Into<String>>(
|
||||
&self,
|
||||
identifier: S,
|
||||
sessions: MailSessions,
|
||||
) -> Result<()> {
|
||||
let identifier = identifier.into();
|
||||
|
||||
self.sessions_map.insert(identifier, Arc::new(sessions));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_incoming_session<S: AsRef<str>>(
|
||||
&self,
|
||||
identifier: S,
|
||||
) -> Result<ThreadSafeIncomingSession> {
|
||||
let mail_sessions = self.get_session(identifier.as_ref()).await?;
|
||||
|
||||
Ok(mail_sessions.incoming().clone())
|
||||
}
|
||||
|
||||
pub async fn get_session<S: Into<String>>(&self, identifier: S) -> Result<Arc<MailSessions>> {
|
||||
let identifier = identifier.into();
|
||||
|
||||
match self.sessions_map.get(&identifier) {
|
||||
Some(sessions) => Ok(sessions.clone()),
|
||||
None => {
|
||||
// If we don't have a session stored, we try to get it from the credentials stored in the keyring.
|
||||
let credentials_json = keyring::get(&identifier)?;
|
||||
|
||||
let credentials = from_str(&credentials_json)?;
|
||||
|
||||
let mail_sessions = sdk::session::create_sessions(&credentials).await?;
|
||||
|
||||
self.insert_session(identifier.clone(), mail_sessions)?;
|
||||
|
||||
match self.sessions_map.get(&identifier) {
|
||||
Some(sessions) => Ok(sessions.clone()),
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
mod utils;
|
||||
|
||||
use dashmap::DashMap;
|
||||
pub use utils::{generate_token, get_nonce_and_key_from_token};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use sdk::session::{MailSessions, create_sessions, ThreadSafeIncomingSession};
|
||||
|
||||
use crate::types::{Error, ErrorKind, Result};
|
||||
|
||||
pub struct Sessions(DashMap<String, Arc<MailSessions>>);
|
||||
|
||||
impl Sessions {
|
||||
pub fn new() -> Self {
|
||||
Self(DashMap::new())
|
||||
}
|
||||
|
||||
pub fn insert_session(&self, token: &str, session: MailSessions) -> Result<()> {
|
||||
let (_, nonce_base64) = get_nonce_and_key_from_token(token)?;
|
||||
|
||||
let key = format!("{}-incoming", nonce_base64);
|
||||
|
||||
let thread_safe_session = Arc::new(session);
|
||||
|
||||
self.0.insert(key, thread_safe_session);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_incoming_session(&self, token: &str) -> Result<ThreadSafeIncomingSession> {
|
||||
let mail_sessions = self.get_sessions(token).await?;
|
||||
|
||||
Ok(mail_sessions.incoming())
|
||||
}
|
||||
|
||||
pub async fn get_sessions(&self, token: &str) -> Result<Arc<MailSessions>> {
|
||||
let (_, nonce_base64) = get_nonce_and_key_from_token(token)?;
|
||||
|
||||
let key = format!("{}-incoming", nonce_base64);
|
||||
|
||||
match self.0.get(&key) {
|
||||
// Return the current session
|
||||
Some(session) => Ok(session.clone()),
|
||||
None => {
|
||||
let credentials = utils::get_credentials(token)?;
|
||||
|
||||
let mail_sessions = create_sessions(&credentials).await?;
|
||||
|
||||
self.0.insert(key.clone(), Arc::new(mail_sessions));
|
||||
|
||||
match self.0.get(&key) {
|
||||
Some(session) => Ok(session.clone()),
|
||||
None => Err(Error::new(
|
||||
ErrorKind::NotLoggedIn,
|
||||
"Could not find incoming session",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
use sdk::session::FullLoginOptions;
|
||||
|
||||
use crate::{
|
||||
base64, cryptography,
|
||||
files::CacheFile,
|
||||
parse,
|
||||
types::{Error, ErrorKind, Result},
|
||||
};
|
||||
|
||||
/// Given a token, get the encryption nonce and key.
|
||||
pub fn get_nonce_and_key_from_token(token: &str) -> Result<(String, String)> {
|
||||
let mut split = token.split(':');
|
||||
|
||||
let nonce_base64 = match split.next() {
|
||||
Some(key) => key.to_string(),
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"Token is missing a nonce",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let key_base64 = match split.next() {
|
||||
Some(key) => key.to_string(),
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidToken,
|
||||
"Token is missing a key",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok((key_base64, nonce_base64))
|
||||
}
|
||||
|
||||
/// Given a token, find the corresponding encrypted login credentials in the cache dir and return them.
|
||||
pub fn get_credentials(token: &str) -> Result<FullLoginOptions> {
|
||||
let (key_base64, nonce_base64) = get_nonce_and_key_from_token(token)?;
|
||||
|
||||
let key = base64::decode(key_base64.as_bytes())?;
|
||||
let nonce = base64::decode(nonce_base64.as_bytes())?;
|
||||
|
||||
let mut encrypted: Vec<u8> = Vec::new();
|
||||
|
||||
let cache = CacheFile::from_session_name(nonce_base64);
|
||||
|
||||
match cache.read(&mut encrypted) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::NotLoggedIn,
|
||||
"Could not find login credentials, please login",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted = cryptography::decrypt(&encrypted, &key, &nonce)?;
|
||||
|
||||
let login_options: FullLoginOptions = serde_json::from_slice(&decrypted).map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::DeserializeJSON,
|
||||
format!("Could not deserialize encrypted login info {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(login_options)
|
||||
}
|
||||
|
||||
pub fn generate_token(credentials: &FullLoginOptions) -> Result<String> {
|
||||
// Serialize the given options to json
|
||||
let options_json = parse::to_json(credentials)?;
|
||||
|
||||
// Generate a key and nonce to sign the options
|
||||
let key = cryptography::generate_key();
|
||||
let nonce = cryptography::generate_nonce();
|
||||
|
||||
// Crypographically sign the login options.
|
||||
let encrypted = cryptography::encrypt(&options_json, &key, &nonce)?;
|
||||
|
||||
// Convert the key and nonce to base64
|
||||
let nonce_base64 = base64::encode(&nonce);
|
||||
let key_base64 = base64::encode(&key);
|
||||
|
||||
let cache = CacheFile::from_session_name(&nonce_base64);
|
||||
|
||||
// Write to file cache so the credentials are still available when the program restarts.
|
||||
cache.write(&encrypted)?;
|
||||
|
||||
let token: String = format!("{}:{}", nonce_base64, key_base64);
|
||||
|
||||
Ok(token)
|
||||
}
|
Loading…
Reference in new issue