it compiles again!
This commit is contained in:
parent
47e25cd7f7
commit
064a1077c8
7 changed files with 152 additions and 405 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -34,7 +34,7 @@ dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"base64 0.13.1",
|
"base64 0.21.2",
|
||||||
"boitalettres",
|
"boitalettres",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
|
@ -11,7 +11,7 @@ anyhow = "1.0.28"
|
||||||
argon2 = "0.3"
|
argon2 = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
base64 = "0.13"
|
base64 = "0.21"
|
||||||
clap = { version = "3.1.18", features = ["derive", "env"] }
|
clap = { version = "3.1.18", features = ["derive", "env"] }
|
||||||
duplexify = "1.1.0"
|
duplexify = "1.1.0"
|
||||||
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
|
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize, Serializer, Deserializer};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct CompanionConfig {
|
pub struct CompanionConfig {
|
||||||
|
@ -79,6 +79,8 @@ pub struct LoginLdapConfig {
|
||||||
pub username_attr: String,
|
pub username_attr: String,
|
||||||
#[serde(default = "default_mail_attr")]
|
#[serde(default = "default_mail_attr")]
|
||||||
pub mail_attr: String,
|
pub mail_attr: String,
|
||||||
|
|
||||||
|
// The field that will contain the crypto root thingy
|
||||||
pub crypto_root_attr: String,
|
pub crypto_root_attr: String,
|
||||||
|
|
||||||
// Storage related thing
|
// Storage related thing
|
||||||
|
@ -108,27 +110,12 @@ pub struct StaticGarageConfig {
|
||||||
|
|
||||||
pub type UserList = HashMap<String, UserEntry>;
|
pub type UserList = HashMap<String, UserEntry>;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(tag = "crypto_root")]
|
|
||||||
pub enum CryptographyRoot {
|
|
||||||
PasswordProtected {
|
|
||||||
root_blob: String,
|
|
||||||
},
|
|
||||||
Keyring,
|
|
||||||
ClearText {
|
|
||||||
master_key: String,
|
|
||||||
secret_key: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct UserEntry {
|
pub struct UserEntry {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub email_addresses: Vec<String>,
|
pub email_addresses: Vec<String>,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub crypto_root: String,
|
||||||
#[serde(flatten)]
|
|
||||||
pub crypto_root: CryptographyRoot,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub storage: StaticStorage,
|
pub storage: StaticStorage,
|
||||||
|
@ -178,19 +165,3 @@ pub fn write_config<T: Serialize>(config_file: PathBuf, config: &T) -> Result<()
|
||||||
fn default_mail_attr() -> String {
|
fn default_mail_attr() -> String {
|
||||||
"mail".into()
|
"mail".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_base64<T, S>(val: &T, serializer: &mut S) -> Result<(), S::Error>
|
|
||||||
where T: AsRef<[u8]>,
|
|
||||||
S: Serializer<Ok = ()>
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&base64::encode(val.as_ref()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_base64<D>(deserializer: &mut D) -> Result<Vec<u8>, D::Error>
|
|
||||||
where D: Deserializer
|
|
||||||
{
|
|
||||||
use serde::de::Error;
|
|
||||||
String::deserialize(deserializer)
|
|
||||||
.and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string())))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub struct LdapLoginProvider {
|
||||||
attrs_to_retrieve: Vec<String>,
|
attrs_to_retrieve: Vec<String>,
|
||||||
username_attr: String,
|
username_attr: String,
|
||||||
mail_attr: String,
|
mail_attr: String,
|
||||||
|
crypto_root_attr: String,
|
||||||
|
|
||||||
storage_specific: StorageSpecific,
|
storage_specific: StorageSpecific,
|
||||||
}
|
}
|
||||||
|
@ -48,6 +49,7 @@ impl LdapLoginProvider {
|
||||||
let mut attrs_to_retrieve = vec![
|
let mut attrs_to_retrieve = vec![
|
||||||
config.username_attr.clone(),
|
config.username_attr.clone(),
|
||||||
config.mail_attr.clone(),
|
config.mail_attr.clone(),
|
||||||
|
config.crypto_root_attr.clone(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// storage specific
|
// storage specific
|
||||||
|
@ -78,6 +80,7 @@ impl LdapLoginProvider {
|
||||||
attrs_to_retrieve,
|
attrs_to_retrieve,
|
||||||
username_attr: config.username_attr,
|
username_attr: config.username_attr,
|
||||||
mail_attr: config.mail_attr,
|
mail_attr: config.mail_attr,
|
||||||
|
crypto_root_attr: config.crypto_root_attr,
|
||||||
storage_specific: specific,
|
storage_specific: specific,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -155,10 +158,16 @@ impl LoginProvider for LdapLoginProvider {
|
||||||
.context("Invalid password")?;
|
.context("Invalid password")?;
|
||||||
debug!("Ldap login with user name {} successfull", username);
|
debug!("Ldap login with user name {} successfull", username);
|
||||||
|
|
||||||
|
// cryptography
|
||||||
|
let crstr = get_attr(&user, &self.crypto_root_attr)?;
|
||||||
|
let cr = CryptoRoot(crstr);
|
||||||
|
let keys = cr.crypto_keys(password)?;
|
||||||
|
|
||||||
|
// storage
|
||||||
let storage = self.storage_creds_from_ldap_user(&user)?;
|
let storage = self.storage_creds_from_ldap_user(&user)?;
|
||||||
|
|
||||||
drop(ldap);
|
drop(ldap);
|
||||||
|
|
||||||
let keys = CryptoKeys::open(&storage, password).await?;
|
|
||||||
|
|
||||||
Ok(Credentials { storage, keys })
|
Ok(Credentials { storage, keys })
|
||||||
}
|
}
|
||||||
|
@ -197,12 +206,15 @@ impl LoginProvider for LdapLoginProvider {
|
||||||
let user = SearchEntry::construct(matches.into_iter().next().unwrap());
|
let user = SearchEntry::construct(matches.into_iter().next().unwrap());
|
||||||
debug!("Found matching LDAP user for email {}: {}", email, user.dn);
|
debug!("Found matching LDAP user for email {}: {}", email, user.dn);
|
||||||
|
|
||||||
|
// cryptography
|
||||||
|
let crstr = get_attr(&user, &self.crypto_root_attr)?;
|
||||||
|
let cr = CryptoRoot(crstr);
|
||||||
|
let public_key = cr.public_key()?;
|
||||||
|
|
||||||
|
// storage
|
||||||
let storage = self.storage_creds_from_ldap_user(&user)?;
|
let storage = self.storage_creds_from_ldap_user(&user)?;
|
||||||
drop(ldap);
|
drop(ldap);
|
||||||
|
|
||||||
let k2v_client = storage.row_store()?;
|
|
||||||
let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?;
|
|
||||||
|
|
||||||
Ok(PublicCredentials {
|
Ok(PublicCredentials {
|
||||||
storage,
|
storage,
|
||||||
public_key,
|
public_key,
|
||||||
|
|
454
src/login/mod.rs
454
src/login/mod.rs
|
@ -2,7 +2,7 @@ pub mod ldap_provider;
|
||||||
pub mod static_provider;
|
pub mod static_provider;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use futures::try_join;
|
use base64::Engine;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -37,6 +37,14 @@ pub struct Credentials {
|
||||||
/// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V
|
/// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V
|
||||||
pub keys: CryptoKeys,
|
pub keys: CryptoKeys,
|
||||||
}
|
}
|
||||||
|
impl Credentials {
|
||||||
|
pub fn row_client(&self) -> Result<RowStore> {
|
||||||
|
Ok(self.storage.row_store()?)
|
||||||
|
}
|
||||||
|
pub fn blob_client(&self) -> Result<BlobStore> {
|
||||||
|
Ok(self.storage.blob_store()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PublicCredentials {
|
pub struct PublicCredentials {
|
||||||
|
@ -45,20 +53,81 @@ pub struct PublicCredentials {
|
||||||
pub public_key: PublicKey,
|
pub public_key: PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
use serde::{Serialize, Deserialize};
|
||||||
/// The struct UserSecrets represents intermediary secrets that are mixed in with the user's
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
/// password when decrypting the cryptographic keys that are stored in their bucket.
|
pub struct CryptoRoot(pub String);
|
||||||
/// These secrets should be stored somewhere else (e.g. in the LDAP server or in the
|
|
||||||
/// local config file), as an additionnal authentification factor so that the password
|
impl CryptoRoot {
|
||||||
/// isn't enough just alone to decrypt the content of a user's bucket.
|
pub fn create_pass(password: &str, k: &CryptoKeys) -> Result<Self> {
|
||||||
pub struct UserSecrets {
|
let bytes = k.password_seal(password)?;
|
||||||
/// The main user secret that will be used to encrypt keys when a new password is added
|
let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes);
|
||||||
pub user_secret: String,
|
let cr = format!("aero:cryptoroot:pass:{}", b64);
|
||||||
/// Alternative user secrets that will be tried when decrypting keys that were encrypted
|
Ok(Self(cr))
|
||||||
/// with old passwords
|
}
|
||||||
pub alternate_user_secrets: Vec<String>,
|
|
||||||
|
pub fn create_cleartext(k: &CryptoKeys) -> Self {
|
||||||
|
let bytes = k.serialize();
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes);
|
||||||
|
let cr = format!("aero:cryptoroot:cleartext:{}", b64);
|
||||||
|
Self(cr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_incoming(pk: &PublicKey) -> Self {
|
||||||
|
let bytes: &[u8] = &pk[..];
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes);
|
||||||
|
let cr = format!("aero:cryptoroot:incoming:{}", b64);
|
||||||
|
Self(cr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&self) -> Result<PublicKey> {
|
||||||
|
match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] {
|
||||||
|
[ "aero", "cryptoroot", "pass", b64blob ] => {
|
||||||
|
let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?;
|
||||||
|
if blob.len() < 32 {
|
||||||
|
bail!("Decoded data is {} bytes long, expect at least 32 bytes", blob.len());
|
||||||
|
}
|
||||||
|
PublicKey::from_slice(&blob[..32]).context("must be a valid public key")
|
||||||
|
},
|
||||||
|
[ "aero", "cryptoroot", "cleartext", b64blob ] => {
|
||||||
|
let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?;
|
||||||
|
Ok(CryptoKeys::deserialize(&blob)?.public)
|
||||||
|
},
|
||||||
|
[ "aero", "cryptoroot", "incoming", b64blob ] => {
|
||||||
|
let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?;
|
||||||
|
if blob.len() < 32 {
|
||||||
|
bail!("Decoded data is {} bytes long, expect at least 32 bytes", blob.len());
|
||||||
|
}
|
||||||
|
PublicKey::from_slice(&blob[..32]).context("must be a valid public key")
|
||||||
|
},
|
||||||
|
[ "aero", "cryptoroot", "keyring", _ ] => {
|
||||||
|
bail!("keyring is not yet implemented!")
|
||||||
|
},
|
||||||
|
_ => bail!(format!("passed string '{}' is not a valid cryptoroot", self.0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn crypto_keys(&self, password: &str) -> Result<CryptoKeys> {
|
||||||
|
match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] {
|
||||||
|
[ "aero", "cryptoroot", "pass", b64blob ] => {
|
||||||
|
let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?;
|
||||||
|
if blob.len() < 32 {
|
||||||
|
bail!("Decoded data is {} bytes long, expect at least 32 bytes", blob.len());
|
||||||
|
}
|
||||||
|
CryptoKeys::password_open(password, &blob[32..])
|
||||||
|
},
|
||||||
|
[ "aero", "cryptoroot", "cleartext", b64blob ] => {
|
||||||
|
let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?;
|
||||||
|
CryptoKeys::deserialize(&blob)
|
||||||
|
},
|
||||||
|
[ "aero", "cryptoroot", "incoming", b64blob ] => {
|
||||||
|
bail!("incoming cryptoroot does not contain a crypto key!")
|
||||||
|
},
|
||||||
|
[ "aero", "cryptoroot", "keyring", _ ] =>{
|
||||||
|
bail!("keyring is not yet implemented!")
|
||||||
|
},
|
||||||
|
_ => bail!(format!("passed string '{}' is not a valid cryptoroot", self.0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt
|
/// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt
|
||||||
/// data in a user's mailbox.
|
/// data in a user's mailbox.
|
||||||
|
@ -75,337 +144,22 @@ pub struct CryptoKeys {
|
||||||
// ----
|
// ----
|
||||||
|
|
||||||
|
|
||||||
impl Credentials {
|
|
||||||
pub fn row_client(&self) -> Result<RowStore> {
|
|
||||||
Ok(self.storage.row_store()?)
|
|
||||||
}
|
|
||||||
pub fn blob_client(&self) -> Result<BlobStore> {
|
|
||||||
Ok(self.storage.blob_store()?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CryptoKeys {
|
impl CryptoKeys {
|
||||||
pub async fn init(
|
/// Initialize a new cryptography root
|
||||||
storage: &Builders,
|
pub fn init() -> Self {
|
||||||
password: &str,
|
|
||||||
) -> Result<Self> {
|
|
||||||
// Check that salt and public don't exist already
|
|
||||||
let k2v = storage.row_store()?;
|
|
||||||
let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?;
|
|
||||||
|
|
||||||
// Generate salt for password identifiers
|
|
||||||
let mut ident_salt = [0u8; 32];
|
|
||||||
thread_rng().fill(&mut ident_salt);
|
|
||||||
|
|
||||||
// Generate (public, private) key pair and master key
|
|
||||||
let (public, secret) = gen_keypair();
|
let (public, secret) = gen_keypair();
|
||||||
let master = gen_key();
|
let master = gen_key();
|
||||||
let keys = CryptoKeys {
|
CryptoKeys {
|
||||||
master,
|
master,
|
||||||
secret,
|
secret,
|
||||||
public,
|
public,
|
||||||
};
|
}
|
||||||
|
|
||||||
// Generate short password digest (= password identity)
|
|
||||||
let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?;
|
|
||||||
|
|
||||||
// Generate salt for KDF
|
|
||||||
let mut kdf_salt = [0u8; 32];
|
|
||||||
thread_rng().fill(&mut kdf_salt);
|
|
||||||
|
|
||||||
// Calculate key for password secret box
|
|
||||||
let password_key = derive_password_key(&kdf_salt, password)?;
|
|
||||||
|
|
||||||
// Seal a secret box that contains our crypto keys
|
|
||||||
let password_sealed = seal(&keys.serialize(), &password_key)?;
|
|
||||||
|
|
||||||
let password_sortkey = format!("password:{}", hex::encode(&ident));
|
|
||||||
let password_blob = [&kdf_salt[..], &password_sealed].concat();
|
|
||||||
|
|
||||||
// Write values to storage
|
|
||||||
// @FIXME Implement insert batch in the storage API
|
|
||||||
let (salt, public, passwd) = (
|
|
||||||
salt_ct.set_value(&ident_salt),
|
|
||||||
public_ct.set_value(keys.public.as_ref()),
|
|
||||||
k2v.row("keys", &password_sortkey).set_value(&password_blob)
|
|
||||||
);
|
|
||||||
try_join!(salt.push(), public.push(), passwd.push())
|
|
||||||
.context("InsertBatch for salt, public, and password")?;
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_without_password(
|
|
||||||
storage: &Builders,
|
|
||||||
master: &Key,
|
|
||||||
secret: &SecretKey,
|
|
||||||
) -> Result<Self> {
|
|
||||||
// Check that salt and public don't exist already
|
|
||||||
let k2v = storage.row_store()?;
|
|
||||||
let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?;
|
|
||||||
|
|
||||||
// Generate salt for password identifiers
|
|
||||||
let mut ident_salt = [0u8; 32];
|
|
||||||
thread_rng().fill(&mut ident_salt);
|
|
||||||
|
|
||||||
// Create CryptoKeys struct from given keys
|
|
||||||
let public = secret.public_key();
|
|
||||||
let keys = CryptoKeys {
|
|
||||||
master: master.clone(),
|
|
||||||
secret: secret.clone(),
|
|
||||||
public,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write values to storage
|
|
||||||
// @FIXME implement insert batch in the storage API
|
|
||||||
let (salt, public) = (
|
|
||||||
salt_ct.set_value(&ident_salt),
|
|
||||||
public_ct.set_value(keys.public.as_ref()),
|
|
||||||
);
|
|
||||||
|
|
||||||
try_join!(salt.push(), public.push()).context("InsertBatch for salt and public")?;
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open(
|
|
||||||
password: &str,
|
|
||||||
root_blob: &str,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let kdf_salt = &password_blob[..32];
|
|
||||||
let password_openned = try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?;
|
|
||||||
|
|
||||||
let keys = Self::deserialize(&password_openned)?;
|
|
||||||
if keys.public != expected_public {
|
|
||||||
bail!("Password public key doesn't match stored public key");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
|
|
||||||
/*
|
|
||||||
let k2v = storage.row_store()?;
|
|
||||||
let (ident_salt, expected_public) = Self::load_salt_and_public(&k2v).await?;
|
|
||||||
|
|
||||||
// Generate short password digest (= password identity)
|
|
||||||
let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?;
|
|
||||||
|
|
||||||
// Lookup password blob
|
|
||||||
let password_sortkey = format!("password:{}", hex::encode(&ident));
|
|
||||||
let password_ref = k2v.row("keys", &password_sortkey);
|
|
||||||
|
|
||||||
let password_blob = {
|
|
||||||
let val = match password_ref.fetch().await {
|
|
||||||
Err(StorageError::NotFound) => {
|
|
||||||
bail!("invalid password")
|
|
||||||
}
|
|
||||||
x => x?,
|
|
||||||
};
|
|
||||||
if val.content().len() != 1 {
|
|
||||||
bail!("multiple values for password in storage");
|
|
||||||
}
|
|
||||||
match val.content().pop().unwrap() {
|
|
||||||
Alternative::Value(v) => v,
|
|
||||||
Alternative::Tombstone => bail!("invalid password"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to open blob
|
|
||||||
let kdf_salt = &password_blob[..32];
|
|
||||||
let password_openned = try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?;
|
|
||||||
|
|
||||||
let keys = Self::deserialize(&password_openned)?;
|
|
||||||
if keys.public != expected_public {
|
|
||||||
bail!("Password public key doesn't match stored public key");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open_without_password(
|
|
||||||
storage: &Builders,
|
|
||||||
master: &Key,
|
|
||||||
secret: &SecretKey,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let k2v = storage.row_store()?;
|
|
||||||
let (_ident_salt, expected_public) = Self::load_salt_and_public(&k2v).await?;
|
|
||||||
|
|
||||||
// Create CryptoKeys struct from given keys
|
|
||||||
let public = secret.public_key();
|
|
||||||
let keys = CryptoKeys {
|
|
||||||
master: master.clone(),
|
|
||||||
secret: secret.clone(),
|
|
||||||
public,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check public key matches
|
|
||||||
if keys.public != expected_public {
|
|
||||||
bail!("Given public key doesn't match stored public key");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_password(
|
|
||||||
&self,
|
|
||||||
storage: &Builders,
|
|
||||||
password: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let k2v = storage.row_store()?;
|
|
||||||
let (ident_salt, _public) = Self::load_salt_and_public(&k2v).await?;
|
|
||||||
|
|
||||||
// Generate short password digest (= password identity)
|
|
||||||
let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?;
|
|
||||||
|
|
||||||
// Generate salt for KDF
|
|
||||||
let mut kdf_salt = [0u8; 32];
|
|
||||||
thread_rng().fill(&mut kdf_salt);
|
|
||||||
|
|
||||||
// Calculate key for password secret box
|
|
||||||
let password_key = derive_password_key(&kdf_salt, password)?;
|
|
||||||
|
|
||||||
// Seal a secret box that contains our crypto keys
|
|
||||||
let password_sealed = seal(&self.serialize(), &password_key)?;
|
|
||||||
|
|
||||||
let password_sortkey = format!("password:{}", hex::encode(&ident));
|
|
||||||
let password_blob = [&kdf_salt[..], &password_sealed].concat();
|
|
||||||
|
|
||||||
// List existing passwords to overwrite existing entry if necessary
|
|
||||||
let pass_key = k2v.row("keys", &password_sortkey);
|
|
||||||
let passwd = match pass_key.fetch().await {
|
|
||||||
Err(StorageError::NotFound) => pass_key,
|
|
||||||
v => {
|
|
||||||
let entry = v?;
|
|
||||||
if entry.content().iter().any(|x| matches!(x, Alternative::Value(_))) {
|
|
||||||
bail!("password already exists");
|
|
||||||
}
|
|
||||||
entry.to_ref()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write values to storage
|
|
||||||
passwd
|
|
||||||
.set_value(&password_blob)
|
|
||||||
.push()
|
|
||||||
.await
|
|
||||||
.context("InsertBatch for new password")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_password(
|
|
||||||
storage: &Builders,
|
|
||||||
password: &str,
|
|
||||||
allow_delete_all: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let k2v = storage.row_store()?;
|
|
||||||
let (ident_salt, _public) = Self::load_salt_and_public(&k2v).await?;
|
|
||||||
|
|
||||||
// Generate short password digest (= password identity)
|
|
||||||
let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?;
|
|
||||||
let password_sortkey = format!("password:{}", hex::encode(&ident));
|
|
||||||
|
|
||||||
// List existing passwords
|
|
||||||
let existing_passwords = Self::list_existing_passwords(&k2v).await?;
|
|
||||||
|
|
||||||
// Check password is there
|
|
||||||
let pw = existing_passwords
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.to_ref())
|
|
||||||
.find(|x| x.key().1 == &password_sortkey)
|
|
||||||
//.get(&password_sortkey)
|
|
||||||
.ok_or(anyhow!("password does not exist"))?;
|
|
||||||
|
|
||||||
if !allow_delete_all && existing_passwords.len() < 2 {
|
|
||||||
bail!("No other password exists, not deleting last password.");
|
|
||||||
}
|
|
||||||
|
|
||||||
pw.rm().await.context("DeleteItem for password")?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- STORAGE UTIL ----
|
|
||||||
//
|
|
||||||
async fn check_uninitialized(
|
|
||||||
k2v: &RowStore,
|
|
||||||
) -> Result<(RowRef, RowRef)> {
|
|
||||||
let params = k2v
|
|
||||||
.select(Selector::List(vec![
|
|
||||||
("keys", "salt"),
|
|
||||||
("keys", "public"),
|
|
||||||
]))
|
|
||||||
.await
|
|
||||||
.context("ReadBatch for salt and public in check_uninitialized")?;
|
|
||||||
|
|
||||||
if params.len() != 2 {
|
|
||||||
bail!(
|
|
||||||
"Invalid response from k2v storage: {:?} (expected two items)",
|
|
||||||
params
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let salt_ct = params[0].to_ref();
|
|
||||||
if params[0].content().iter().any(|x| matches!(x, Alternative::Value(_))) {
|
|
||||||
bail!("key storage already initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
let public_ct = params[1].to_ref();
|
|
||||||
if params[1].content().iter().any(|x| matches!(x, Alternative::Value(_))) {
|
|
||||||
bail!("key storage already initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((salt_ct, public_ct))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_salt_and_public(k2v: &RowStore) -> Result<([u8; 32], PublicKey)> {
|
|
||||||
let params = k2v
|
|
||||||
.select(Selector::List(vec![
|
|
||||||
("keys", "salt"),
|
|
||||||
("keys", "public"),
|
|
||||||
]))
|
|
||||||
.await
|
|
||||||
.context("ReadBatch for salt and public in load_salt_and_public")?;
|
|
||||||
|
|
||||||
if params.len() != 2 {
|
|
||||||
bail!(
|
|
||||||
"Invalid response from k2v storage: {:?} (expected two items)",
|
|
||||||
params
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if params[0].content().len() != 1 || params[1].content().len() != 1 {
|
|
||||||
bail!("cryptographic keys not initialized for user");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve salt from given response
|
|
||||||
let salt: Vec<u8> = match &mut params[0].content().iter_mut().next().unwrap() {
|
|
||||||
Alternative::Value(v) => std::mem::take(v),
|
|
||||||
Alternative::Tombstone => bail!("salt is a tombstone"),
|
|
||||||
};
|
|
||||||
if salt.len() != 32 {
|
|
||||||
bail!("`salt` is not 32 bytes long");
|
|
||||||
}
|
|
||||||
let mut salt_constlen = [0u8; 32];
|
|
||||||
salt_constlen.copy_from_slice(&salt);
|
|
||||||
|
|
||||||
// Retrieve public from given response
|
|
||||||
let public: Vec<u8> = match &mut params[1].content().iter_mut().next().unwrap() {
|
|
||||||
Alternative::Value(v) => std::mem::take(v),
|
|
||||||
Alternative::Tombstone => bail!("public is a tombstone"),
|
|
||||||
};
|
|
||||||
let public = PublicKey::from_slice(&public).ok_or(anyhow!("Invalid public key length"))?;
|
|
||||||
|
|
||||||
Ok((salt_constlen, public))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_existing_passwords(k2v: &RowStore) -> Result<Vec<RowValue>> {
|
|
||||||
let res = k2v.select(Selector::Prefix { shard_key: "keys", prefix: "password:" })
|
|
||||||
.await
|
|
||||||
.context("ReadBatch for prefix password: in list_existing_passwords")?;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear text serialize/deserialize
|
||||||
|
/// Serialize the root as bytes without encryption
|
||||||
fn serialize(&self) -> [u8; 64] {
|
fn serialize(&self) -> [u8; 64] {
|
||||||
let mut res = [0u8; 64];
|
let mut res = [0u8; 64];
|
||||||
res[..32].copy_from_slice(self.master.as_ref());
|
res[..32].copy_from_slice(self.master.as_ref());
|
||||||
|
@ -413,6 +167,7 @@ impl CryptoKeys {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialize a clear text crypto root without encryption
|
||||||
fn deserialize(bytes: &[u8]) -> Result<Self> {
|
fn deserialize(bytes: &[u8]) -> Result<Self> {
|
||||||
if bytes.len() != 64 {
|
if bytes.len() != 64 {
|
||||||
bail!("Invalid length: {}, expected 64", bytes.len());
|
bail!("Invalid length: {}, expected 64", bytes.len());
|
||||||
|
@ -426,6 +181,31 @@ impl CryptoKeys {
|
||||||
public,
|
public,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password sealed keys serialize/deserialize
|
||||||
|
pub fn password_open(password: &str, blob: &[u8]) -> Result<Self> {
|
||||||
|
let kdf_salt = &blob[0..32];
|
||||||
|
let password_openned = try_open_encrypted_keys(kdf_salt, password, &blob[32..])?;
|
||||||
|
|
||||||
|
let keys = Self::deserialize(&password_openned)?;
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password_seal(&self, password: &str) -> Result<Vec<u8>> {
|
||||||
|
let mut kdf_salt = [0u8; 32];
|
||||||
|
thread_rng().fill(&mut kdf_salt);
|
||||||
|
|
||||||
|
// Calculate key for password secret box
|
||||||
|
let password_key = derive_password_key(&kdf_salt, password)?;
|
||||||
|
|
||||||
|
// Seal a secret box that contains our crypto keys
|
||||||
|
let password_sealed = seal(&self.serialize(), &password_key)?;
|
||||||
|
|
||||||
|
// Create blob
|
||||||
|
let password_blob = [&self.public[..], &kdf_salt[..], &password_sealed].concat();
|
||||||
|
|
||||||
|
Ok(password_blob)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_password_key(kdf_salt: &[u8], password: &str) -> Result<Key> {
|
fn derive_password_key(kdf_salt: &[u8], password: &str) -> Result<Key> {
|
||||||
|
@ -452,7 +232,7 @@ pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result<Vec
|
||||||
.map_err(|e| anyhow!("Invalid argon2 params: {}", e))?;
|
.map_err(|e| anyhow!("Invalid argon2 params: {}", e))?;
|
||||||
let argon2 = Argon2::new(Algorithm::default(), Version::default(), params);
|
let argon2 = Argon2::new(Algorithm::default(), Version::default(), params);
|
||||||
|
|
||||||
let salt = base64::encode_config(salt, base64::STANDARD_NO_PAD);
|
let salt = base64::engine::general_purpose::STANDARD_NO_PAD.encode(salt);
|
||||||
let hash = argon2
|
let hash = argon2
|
||||||
.hash_password(password, &salt)
|
.hash_password(password, &salt)
|
||||||
.map_err(|e| anyhow!("Unable to hash: {}", e))?;
|
.map_err(|e| anyhow!("Unable to hash: {}", e))?;
|
||||||
|
|
|
@ -6,7 +6,6 @@ use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::cryptoblob::{Key, SecretKey};
|
|
||||||
use crate::login::*;
|
use crate::login::*;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
|
||||||
|
@ -82,19 +81,8 @@ impl LoginProvider for StaticLoginProvider {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let keys = match &user.crypto_root { /*(&user.master_key, &user.secret_key) {*/
|
let cr = CryptoRoot(user.crypto_root);
|
||||||
CryptographyRoot::ClearText { master_key: m, secret_key: s } => {
|
let keys = cr.crypto_keys(password)?;
|
||||||
let master_key =
|
|
||||||
Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?;
|
|
||||||
let secret_key = SecretKey::from_slice(&base64::decode(s)?)
|
|
||||||
.ok_or(anyhow!("Invalid secret key"))?;
|
|
||||||
CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await?
|
|
||||||
}
|
|
||||||
CryptographyRoot::PasswordProtected { root_blob } => {
|
|
||||||
CryptoKeys::open(password, root_blob).await?
|
|
||||||
}
|
|
||||||
CryptographyRoot::Keyring => unimplemented!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!(user=%username, "logged");
|
tracing::debug!(user=%username, "logged");
|
||||||
Ok(Credentials { storage, keys })
|
Ok(Credentials { storage, keys })
|
||||||
|
@ -118,8 +106,8 @@ impl LoginProvider for StaticLoginProvider {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let k2v_client = storage.row_store()?;
|
let cr = CryptoRoot(user.crypto_root);
|
||||||
let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?;
|
let public_key = cr.public_key()?;
|
||||||
|
|
||||||
Ok(PublicCredentials {
|
Ok(PublicCredentials {
|
||||||
storage,
|
storage,
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -156,17 +156,6 @@ fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -
|
||||||
tracing::debug!(user=login, "will-create");
|
tracing::debug!(user=login, "will-create");
|
||||||
let stp: SetupEntry = read_config(setup.clone())?;
|
let stp: SetupEntry = read_config(setup.clone())?;
|
||||||
tracing::debug!(user=login, "loaded setup entry");
|
tracing::debug!(user=login, "loaded setup entry");
|
||||||
let crypto_root = match root {
|
|
||||||
Command::Provider(_) => CryptographyRoot::PasswordProtected,
|
|
||||||
Command::Companion(_) => {
|
|
||||||
// @TODO use keyring by default instead of inplace in the future
|
|
||||||
// @TODO generate keys
|
|
||||||
CryptographyRoot::InPlace {
|
|
||||||
master_key: "".to_string(),
|
|
||||||
secret_key: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let password = match stp.clear_password {
|
let password = match stp.clear_password {
|
||||||
Some(pwd) => pwd,
|
Some(pwd) => pwd,
|
||||||
|
@ -179,12 +168,19 @@ fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -
|
||||||
password
|
password
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let crypto_keys = CryptoKeys::init();
|
||||||
|
let crypto_root = match root {
|
||||||
|
Command::Provider(_) => CryptoRoot::create_pass(&password, &crypto_keys)?,
|
||||||
|
Command::Companion(_) => CryptoRoot::create_cleartext(&crypto_keys),
|
||||||
|
};
|
||||||
|
|
||||||
let hash = hash_password(password.as_str()).context("unable to hash password")?;
|
let hash = hash_password(password.as_str()).context("unable to hash password")?;
|
||||||
|
|
||||||
ulist.insert(login.clone(), UserEntry {
|
ulist.insert(login.clone(), UserEntry {
|
||||||
email_addresses: stp.email_addresses,
|
email_addresses: stp.email_addresses,
|
||||||
password: hash,
|
password: hash,
|
||||||
crypto_root,
|
crypto_root: crypto_root.0,
|
||||||
storage: stp.storage,
|
storage: stp.storage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -192,7 +188,7 @@ fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -
|
||||||
},
|
},
|
||||||
AccountManagement::Delete { login } => {
|
AccountManagement::Delete { login } => {
|
||||||
tracing::debug!(user=login, "will-delete");
|
tracing::debug!(user=login, "will-delete");
|
||||||
ulist.remove(&login);
|
ulist.remove(login);
|
||||||
write_config(users.clone(), &ulist)?;
|
write_config(users.clone(), &ulist)?;
|
||||||
},
|
},
|
||||||
AccountManagement::ChangePassword { login } => {
|
AccountManagement::ChangePassword { login } => {
|
||||||
|
|
Loading…
Reference in a new issue