aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2023-04-24 13:24:45 -0700
committerbenj <benj@rse8.com>2023-04-24 13:24:45 -0700
commiteb92f823c31a5e702af7005231f0d6915aad3342 (patch)
treebb624786a47accb2dfcfe95d20c00c9624c28a9c /crates/secd/src
parent176aae037400b43cb3971cd968afe59c73b3097a (diff)
downloadsecdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.gz
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.bz2
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.lz
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.xz
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.zst
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.zip
email templates, sendgrid, creds, and some experimental things
Started playing with namespace configs and integrating with zanzibar impls. Still lot's of experimenting and dead code going on.
Diffstat (limited to 'crates/secd/src')
-rw-r--r--crates/secd/src/auth/n.rs166
-rw-r--r--crates/secd/src/auth/z/graph.rs209
-rw-r--r--crates/secd/src/auth/z/mod.rs (renamed from crates/secd/src/auth/z.rs)43
-rw-r--r--crates/secd/src/client/email/mod.rs186
-rw-r--r--crates/secd/src/client/store/mod.rs78
-rw-r--r--crates/secd/src/client/store/sql_db.rs132
-rw-r--r--crates/secd/src/lib.rs183
-rw-r--r--crates/secd/src/util/crypter.rs87
-rw-r--r--crates/secd/src/util/from.rs5
-rw-r--r--crates/secd/src/util/mod.rs107
10 files changed, 1039 insertions, 157 deletions
diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs
index 1d3b2d5..1f32fd6 100644
--- a/crates/secd/src/auth/n.rs
+++ b/crates/secd/src/auth/n.rs
@@ -1,20 +1,23 @@
-use std::str::FromStr;
-
use crate::{
client::{
- email::{EmailValidationMessage, Sendable},
+ email::{
+ parse_email_template, EmailValidationMessage, Sendable, DEFAULT_SIGNIN_EMAIL,
+ DEFAULT_SIGNUP_EMAIL,
+ },
store::{
- AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError,
+ AddressLens, AddressValidationLens, CredentialLens, IdentityLens, SessionLens,
+ Storable, StoreError,
},
},
util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod,
- Credential, CredentialType, Identity, PhoneNumber, Secd, SecdError, Session, SessionToken,
+ Credential, CredentialType, Identity, IdentityId, Secd, SecdError, Session, SessionToken,
ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS,
ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION,
};
use email_address::EmailAddress;
use log::warn;
use rand::Rng;
+use std::str::FromStr;
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
@@ -22,10 +25,15 @@ impl Secd {
pub async fn validate_email(
&self,
email_address: &str,
- identity_id: Option<Uuid>,
+ identity_id: Option<IdentityId>,
) -> Result<AddressValidation, SecdError> {
let email_address = EmailAddress::from_str(email_address)?;
- // record address (idempotent operation)
+ let mut email_template = self
+ .cfg
+ .email_signup_message
+ .clone()
+ .unwrap_or(DEFAULT_SIGNUP_EMAIL.into());
+
let mut address = Address {
id: Uuid::new_v4(),
t: AddressType::Email {
@@ -50,6 +58,12 @@ impl Secd {
.into_iter()
.next()
.ok_or(SecdError::AddressValidationFailed)?;
+
+ email_template = self
+ .cfg
+ .email_signin_message
+ .clone()
+ .unwrap_or(DEFAULT_SIGNIN_EMAIL.into());
}
let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>());
@@ -76,13 +90,23 @@ impl Secd {
validation.write(self.store.clone()).await?;
- let msg =EmailValidationMessage {
+ let msg = EmailValidationMessage {
+ from_address: self
+ .cfg
+ .email_address_from
+ .clone()
+ .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()),
+ replyto_address: self
+ .cfg
+ .email_address_replyto
+ .clone()
+ .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()),
recipient: email_address.clone(),
- subject: "Confirm Your Email".into(),
- body: format!("This is an email validation message. Click this link [{:?}?s={}] or use the code [{}]", validation.id, secret, code),
+ subject: "Login Request".into(),
+ body: parse_email_template(&email_template, validation.id, Some(secret), Some(code))?,
};
- match msg.send().await {
+ match msg.send(self.email_messenger.clone()).await {
Ok(_) => { /* TODO: Write down the message*/ }
Err(e) => {
validation.revoked_at = Some(OffsetDateTime::now_utc());
@@ -95,10 +119,11 @@ impl Secd {
}
pub async fn validate_sms(
&self,
- phone_number: &PhoneNumber,
+ // phone_number: &PhoneNumber,
) -> Result<AddressValidation, SecdError> {
todo!()
}
+
pub async fn complete_address_validation(
&self,
validation_id: &AddressValidationId,
@@ -215,21 +240,83 @@ impl Secd {
Ok(session)
}
+
pub async fn create_credential(
&self,
t: CredentialType,
- key: String,
- value: Option<String>,
- ) -> Result<Credential, SecdError> {
- todo!()
+ identity_id: Option<IdentityId>,
+ ) -> Result<Identity, SecdError> {
+ let identity = match identity_id {
+ Some(id) => Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: Some(&id),
+ address_type: None,
+ validated_address: None,
+ session_token_hash: None,
+ },
+ )
+ .await?
+ .into_iter()
+ .nth(0)
+ .ok_or(SecdError::IdentityNotFound)?,
+
+ None => {
+ let id = Identity {
+ id: Uuid::new_v4(),
+ address_validations: vec![],
+ credentials: vec![],
+ rules: vec![],
+ metadata: None,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ };
+ id.write(self.store.clone()).await?;
+ id
+ }
+ };
+
+ let mut credential = match &Credential::find(
+ self.store.clone(),
+ &CredentialLens {
+ id: None,
+ identity_id: Some(identity.id),
+ t: Some(&t),
+ restrict_by_key: Some(false),
+ },
+ )
+ .await?[..]
+ {
+ [] => Credential {
+ id: Uuid::new_v4(),
+ identity_id: identity.id,
+ t,
+ created_at: OffsetDateTime::now_utc(),
+ revoked_at: None,
+ deleted_at: None,
+ },
+ _ => return Err(SecdError::CredentialAlreadyExists),
+ };
+
+ credential.hash(&self.crypter)?;
+ credential
+ .write(self.store.clone())
+ .await
+ .map_err(|err| match err {
+ StoreError::IdempotentCheckAlreadyExists => SecdError::CredentialAlreadyExists,
+ err => SecdError::StoreError(err),
+ })?;
+
+ Ok(identity)
}
pub async fn validate_credential(
&self,
- t: CredentialType,
- key: String,
- value: Option<String>,
+ // t: CredentialType,
+ // key: String,
+ // value: Option<String>,
) -> Result<Session, SecdError> {
+ // Credential::find(store, lens) use key here as unique index
todo!()
}
@@ -254,15 +341,23 @@ impl Secd {
}
}
- pub async fn get_identity(&self, i: &SessionToken) -> Result<Identity, SecdError> {
- let token_hash = util::hash(&hex::decode(i)?);
+ pub async fn get_identity(
+ &self,
+ i: Option<IdentityId>,
+ t: Option<SessionToken>,
+ ) -> Result<Identity, SecdError> {
+ let token_hash = match t {
+ Some(tok) => Some(util::hash(&hex::decode(&tok)?)),
+ None => None,
+ };
+
let mut i = Identity::find(
self.store.clone(),
&IdentityLens {
- id: None,
+ id: i.as_ref(),
address_type: None,
validated_address: None,
- session_token_hash: Some(token_hash),
+ session_token_hash: token_hash,
},
)
.await?;
@@ -279,6 +374,31 @@ impl Secd {
}
}
+ pub async fn update_identity_metadata(
+ &self,
+ i: IdentityId,
+ md: String,
+ ) -> Result<Identity, SecdError> {
+ let mut identity = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: Some(&i),
+ address_type: None,
+ validated_address: None,
+ session_token_hash: None,
+ },
+ )
+ .await?
+ .into_iter()
+ .nth(0)
+ .ok_or(SecdError::IdentityNotFound)?;
+
+ identity.metadata = Some(md);
+ identity.write(self.store.clone()).await?;
+
+ Ok(identity)
+ }
+
pub async fn revoke_session(&self, session: &mut Session) -> Result<(), SecdError> {
session.revoked_at = Some(OffsetDateTime::now_utc());
session.write(self.store.clone()).await?;
diff --git a/crates/secd/src/auth/z/graph.rs b/crates/secd/src/auth/z/graph.rs
new file mode 100644
index 0000000..9ca045b
--- /dev/null
+++ b/crates/secd/src/auth/z/graph.rs
@@ -0,0 +1,209 @@
+use std::collections::{HashSet, VecDeque};
+
+type NodeIdx = usize;
+type EdgeIdx = usize;
+
+struct RelationGraph {
+ nodes: Vec<Node>,
+ edges: Vec<Edge>,
+}
+
+#[derive(Hash, PartialEq, Eq)]
+struct Node {
+ id: u64,
+ is_namespace: bool,
+ edge_list_head: Option<EdgeIdx>,
+}
+
+struct Edge {
+ dst: NodeIdx,
+ op: Option<Operator>,
+ next_edge: Option<EdgeIdx>,
+}
+
+enum Operator {
+ And,
+ Or,
+ Difference,
+ Complement,
+ AndAll,
+ OrAll,
+ DifferenceAll,
+ ComplementAll,
+}
+
+impl RelationGraph {
+ pub fn new() -> Self {
+ // As I think about how to serialize this graph, when reading from the DB
+ // I should chunk up the namespaces so that I can read-on-demand when I
+ // encounter a namespace that has not yet been loaded, and in this way
+ // I don't need to deal with the entire graph...
+ //
+ // but that's a totally unnecessary fun optimization.
+ RelationGraph {
+ nodes: vec![],
+ edges: vec![],
+ }
+ }
+
+ pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx {
+ let idx = self.nodes.len();
+ self.nodes.push(Node {
+ id,
+ is_namespace,
+ edge_list_head: None,
+ });
+ idx
+ }
+
+ pub fn add_edge(
+ &mut self,
+ src: NodeIdx,
+ dst: NodeIdx,
+ op: Option<Operator>,
+ follow: Option<NodeIdx>,
+ ) {
+ let edge_idx = self.edges.len();
+ let node = &mut self.nodes[src];
+ // TODO: dupe check
+ self.edges.push(Edge {
+ dst,
+ op,
+ next_edge: node.edge_list_head,
+ });
+ node.edge_list_head = Some(edge_idx);
+ }
+
+ // doc:2#viewer@user:1
+ pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> {
+ let mut all_leaves = vec![];
+ // let mut and_leaves = vec![];
+ // let mut exc_leaves = vec![];
+
+ // the output should basically be leaves to look up
+ // for example
+ // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)>
+ // and since (organization, admin) has leaves (user) as well
+ // if the user is in that final set, then they are valid
+ // and those leaves would be queried as relation tuples saved in the DB
+
+ println!("HERE I AM");
+
+ let start = self.nodes.get(src);
+ let mut seen = HashSet::new();
+ let mut q = VecDeque::new();
+
+ seen.insert(start);
+ q.push_back(start);
+
+ // I need to build a chain of ops
+ // [AND, OR, OR, AND, OR, ...]
+ // at each stage, I need to apply the operator chain
+ while let Some(&Some(node)) = q.iter().next() {
+ if node.is_namespace {
+ all_leaves.push(node);
+ }
+
+ // for edge in edge list
+
+ println!("IN QUEUE\n");
+ println!("node: {:?}\n", node.id);
+ q.pop_front();
+ }
+ println!("DONE");
+
+ // task...starting at doc_viewer, return all leaf nodes
+ // to do this....
+ // check all children
+ // if child is OR
+ // if child is leaf, return child
+ // else return all of these children
+ // if child is AND
+ // if child is leaf, return if also present in all other nodes
+ // else return all children which are also present in other nodes
+ // if child is Difference
+ // if child is leaf, do not include and remove if it part of the user set
+ // else remove all of these children
+
+ // TOTAL pool of leafs
+ // Intersection leaves (i.e. ONLY leaves which MUST exist)
+ // exclusion leaves (i.e. leaves which CANNOT exist)
+
+ // bfs build query...
+ // execute query
+
+ // start at nodeIdx: doc_viewer
+ // expand until I find all leaf nodes.
+ // we are interested in leaf nodes with nodeIdx: user
+ // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx.
+
+ // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked.
+
+ // with each step of the bfs, perform the operator.
+ // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR]
+ // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR]
+ // the next step might be doc_viewer -> doc_auditor with OR
+ // then doc_auditor -> doc_editor with OR
+ // then doc_editor -> doc_user with OR
+ // then doc_editor -> doc_owner
+ // then doc_auditor -> doc_owner with Difference
+ //
+
+ todo!()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn build_graph_test() {
+ let mut g = RelationGraph::new();
+ let user = g.add_node(1, true);
+ let role = g.add_node(2, true);
+ let group = g.add_node(3, true);
+ let doc = g.add_node(4, true);
+
+ let role_member = g.add_node(5, false);
+
+ let group_member = g.add_node(6, false);
+ let group_admin = g.add_node(7, false);
+
+ let doc_owner = g.add_node(8, false);
+ let doc_editor = g.add_node(9, false);
+ let doc_viewer = g.add_node(10, false);
+ let doc_auditor = g.add_node(11, false);
+ let doc_parent = g.add_node(12, false);
+
+ g.add_edge(role, role_member, None, None);
+ g.add_edge(role_member, user, Some(Operator::Or), None);
+ g.add_edge(role_member, group_member, Some(Operator::Or), None);
+
+ g.add_edge(group, group_member, None, None);
+ g.add_edge(group, group_admin, None, None);
+
+ g.add_edge(group_member, user, Some(Operator::Or), None);
+ g.add_edge(group_member, group_admin, Some(Operator::Or), None);
+ g.add_edge(group_admin, user, Some(Operator::Or), None);
+
+ g.add_edge(doc, doc_owner, None, None);
+ g.add_edge(doc, doc_editor, None, None);
+ g.add_edge(doc, doc_viewer, None, None);
+ g.add_edge(doc, doc_auditor, None, None);
+ g.add_edge(doc, doc_parent, None, None);
+
+ g.add_edge(doc_owner, user, Some(Operator::Or), None);
+ g.add_edge(doc_editor, user, Some(Operator::Or), None);
+ g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None);
+ g.add_edge(doc_viewer, user, Some(Operator::Or), None);
+ g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None);
+ g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer));
+ g.add_edge(doc_viewer, user, Some(Operator::OrAll), None);
+ g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None);
+ g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None);
+
+ g.find_leaves(doc_viewer, None);
+ assert_eq!(1, 2);
+ }
+}
diff --git a/crates/secd/src/auth/z.rs b/crates/secd/src/auth/z/mod.rs
index 31f449c..b364583 100644
--- a/crates/secd/src/auth/z.rs
+++ b/crates/secd/src/auth/z/mod.rs
@@ -1,6 +1,8 @@
-use uuid::Uuid;
+mod graph;
-use crate::{Secd, SecdError};
+use crate::{Authorization, Secd, SecdError};
+use async_trait::async_trait;
+use uuid::Uuid;
pub type Namespace = String;
pub type Object = (Namespace, Uuid);
@@ -18,8 +20,9 @@ pub enum Subject {
UserSet { user: Object, relation: Relation },
}
-impl Secd {
- pub async fn check(&self, r: &Relationship) -> Result<bool, SecdError> {
+#[async_trait]
+impl Authorization for Secd {
+ async fn check(&self, r: &Relationship) -> Result<bool, SecdError> {
let spice = self
.spice
.clone()
@@ -27,16 +30,16 @@ impl Secd {
Ok(spice.check_permission(r).await?)
}
- pub async fn expand(&self) -> Result<(), SecdError> {
+ async fn expand(&self) -> Result<(), SecdError> {
todo!()
}
- pub async fn read(&self) -> Result<(), SecdError> {
+ async fn read(&self) -> Result<(), SecdError> {
todo!()
}
- pub async fn watch(&self) -> Result<(), SecdError> {
+ async fn watch(&self) -> Result<(), SecdError> {
unimplemented!()
}
- pub async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> {
+ async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> {
let spice = self
.spice
.clone()
@@ -59,3 +62,27 @@ impl Secd {
Ok(())
}
}
+
+enum RelationToken {
+ Start,
+ Or,
+ And,
+ Exclude,
+}
+struct RelationContainer {
+ name: Relation,
+ bins: Vec<(RelationToken, Relation)>,
+}
+
+struct NamespaceContainer {
+ relations: Vec<RelationContainer>,
+}
+
+impl Secd {
+ async fn write_namespace(&self, ns: &NamespaceContainer) -> Result<(), SecdError> {
+ todo!()
+ }
+ async fn read_namespace(&self) -> Result<NamespaceContainer, SecdError> {
+ todo!()
+ }
+}
diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs
index 915d18c..7c7b233 100644
--- a/crates/secd/src/client/email/mod.rs
+++ b/crates/secd/src/client/email/mod.rs
@@ -1,68 +1,172 @@
+use async_trait::async_trait;
use email_address::EmailAddress;
-use lettre::Transport;
-use log::error;
-use std::collections::HashMap;
+use lettre::{
+ message::{Mailbox, MultiPart},
+ Transport,
+};
+use log::{error, warn};
+use reqwest::StatusCode;
+use sendgrid::v3::{Content, Email, Personalization};
+use std::sync::Arc;
+
+use crate::AddressValidationId;
+
+pub const DEFAULT_SIGNUP_EMAIL: &str = "This email was recently used to signup. Please use the following code to complete your validation: {{secd::validation_code}}. If you did not request this signup link, you can safely ignore this email.";
+pub const DEFAULT_SIGNIN_EMAIL: &str = "An account with this email was recently used to signin. Please use the following code to complete your sign in process: {{secd::validation_code}}. If you did not request this signin link, you can safely ingore this email.";
#[derive(Debug, thiserror::Error, derive_more::Display)]
pub enum EmailMessengerError {
FailedToSendEmail,
+ LibLettreError(#[from] lettre::error::Error),
+ SendgridError(#[from] sendgrid::SendgridError),
}
pub struct EmailValidationMessage {
+ pub from_address: Mailbox,
+ pub replyto_address: Mailbox,
pub recipient: EmailAddress,
pub subject: String,
pub body: String,
}
-#[async_trait::async_trait]
-pub(crate) trait EmailMessenger {
- async fn send_email(
- &self,
- email_address: &EmailAddress,
- template: &str,
- template_vars: HashMap<&str, &str>,
- ) -> Result<(), EmailMessengerError>;
+#[async_trait]
+pub(crate) trait EmailMessenger: Send + Sync {
+ fn get_type(&self) -> MessengerType;
+ fn get_api_key(&self) -> String;
}
-pub(crate) struct LocalMailer {}
+pub enum MessengerType {
+ LocalMailer,
+ Sendgrid,
+}
-#[async_trait::async_trait]
+pub(crate) struct LocalMailer {}
+impl LocalMailer {
+ pub fn new() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ warn!("You are using the local mailer, which will not work in production!");
+ Arc::new(LocalMailer {})
+ }
+}
impl EmailMessenger for LocalMailer {
- async fn send_email(
- &self,
- email_address: &EmailAddress,
- template: &str,
- template_vars: HashMap<&str, &str>,
- ) -> Result<(), EmailMessengerError> {
- todo!()
+ fn get_type(&self) -> MessengerType {
+ MessengerType::LocalMailer
+ }
+ fn get_api_key(&self) -> String {
+ panic!("unreachable since no API key is expected for LocalMailer");
+ }
+}
+pub(crate) struct Sendgrid {
+ pub api_key: String,
+}
+impl Sendgrid {
+ pub fn new(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ Arc::new(Sendgrid { api_key })
+ }
+}
+impl EmailMessenger for Sendgrid {
+ fn get_type(&self) -> MessengerType {
+ MessengerType::Sendgrid
+ }
+ fn get_api_key(&self) -> String {
+ self.api_key.clone()
}
}
-#[async_trait::async_trait]
+#[async_trait]
pub(crate) trait Sendable {
- async fn send(&self) -> Result<(), EmailMessengerError>;
+ async fn send(&self, messenge: Arc<dyn EmailMessenger>) -> Result<(), EmailMessengerError>;
}
-#[async_trait::async_trait]
+#[async_trait]
impl Sendable for EmailValidationMessage {
- // TODO: We need to break this up as before, especially so we can feature
- // gate unwanted things like Lettre...
- async fn send(&self) -> Result<(), EmailMessengerError> {
- // TODO: Get these things from the template...
- let email = lettre::Message::builder()
- .from("BranchControl <iam@branchcontrol.com>".parse().unwrap())
- .reply_to("BranchControl <iam@branchcontrol.com>".parse().unwrap())
- .to(self.recipient.to_string().parse().unwrap())
- .subject(self.subject.clone())
- .body(self.body.clone())
- .unwrap();
-
- let mailer = lettre::SmtpTransport::unencrypted_localhost();
-
- mailer.send(&email).map_err(|e| {
- error!("failed to send email {:?}", e);
- EmailMessengerError::FailedToSendEmail
- })?;
+ async fn send(&self, messenger: Arc<dyn EmailMessenger>) -> Result<(), EmailMessengerError> {
+ match messenger.get_type() {
+ MessengerType::LocalMailer => {
+ let email = lettre::Message::builder()
+ .from(self.from_address.to_string().parse().unwrap())
+ .reply_to(self.replyto_address.to_string().parse().unwrap())
+ .to(self.recipient.to_string().parse().unwrap())
+ .subject(self.subject.clone())
+ .multipart(MultiPart::alternative_plain_html(
+ "".to_string(),
+ String::from(self.body.clone()),
+ ))?;
+
+ let mailer = lettre::SmtpTransport::unencrypted_localhost();
+
+ mailer.send(&email).map_err(|e| {
+ error!("failed to send email {:?}", e);
+ EmailMessengerError::FailedToSendEmail
+ })?;
+ }
+ MessengerType::Sendgrid => {
+ let msg = sendgrid::v3::Message::new(Email::new(self.from_address.to_string()))
+ .set_subject(&self.subject)
+ .add_content(
+ Content::new()
+ .set_content_type("text/html")
+ .set_value(&self.body),
+ )
+ .add_personalization(Personalization::new(Email::new(
+ self.recipient.to_string(),
+ )));
+
+ let sender = sendgrid::v3::Sender::new(messenger.get_api_key());
+ let resp = sender.send(&msg).await?;
+ match resp.status() {
+ StatusCode::ACCEPTED => {}
+ _ => {
+ error!(
+ "sendgrid failed to send message with status: {}",
+ resp.status()
+ )
+ }
+ }
+ }
+ };
+
Ok(())
}
}
+
+pub(crate) fn parse_email_template(
+ template: &str,
+ validation_id: AddressValidationId,
+ validation_secret: Option<String>,
+ validation_code: Option<String>,
+) -> Result<String, EmailMessengerError> {
+ let mut t = template.clone().to_string();
+ // We do not allow substutions for a variety of reasons, but mainly security ones.
+ // The only things we want to share are those which secd allows. In this case, that
+ // means we only send an email with static content as provided by the filter, except
+ // for the $$secd:request_id$$ and $$secd:request_code$$, either of which may be
+ // present in the email.
+
+ t = t.replace("{{secd::validation_id}}", &validation_id.to_string());
+ validation_secret.map(|secret| t = t.replace("{{secd::validation_secret}}", &secret));
+ validation_code.map(|code| t = t.replace("{{secd::validation_code}}", &code));
+
+ Ok(t)
+}
+
+#[cfg(test)]
+mod test {
+ use uuid::Uuid;
+
+ use super::*;
+
+ #[test]
+ fn test_parse_and_substitue() {
+ let raw = "This is an email validation message. Navigate to https://www.secd.com/auth/{secd::validation_id}?s={secd::validation_secret} or use the code [{secd::validation_code}]";
+
+ let parsed = parse_email_template(
+ raw,
+ Uuid::parse_str("90f42ba9-ed4a-4f56-b371-df05634a1626").unwrap(),
+ Some("s3cr3t".into()),
+ Some("102030".into()),
+ )
+ .unwrap();
+
+ assert_eq!(parsed, "This is an email validation message. Navigate to https://www.secd.com/auth/90f42ba9-ed4a-4f56-b371-df05634a1626?s=s3cr3t or use the code [102030]")
+ }
+}
diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs
index 8a076c4..7bf01d5 100644
--- a/crates/secd/src/client/store/mod.rs
+++ b/crates/secd/src/client/store/mod.rs
@@ -1,22 +1,28 @@
pub(crate) mod sql_db;
+use async_trait::async_trait;
use sqlx::{Postgres, Sqlite};
use std::sync::Arc;
use uuid::Uuid;
-use crate::{util, Address, AddressType, AddressValidation, Identity, IdentityId, Session};
+use crate::{
+ util, Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType,
+ Identity, IdentityId, Session,
+};
use self::sql_db::SqlClient;
#[derive(Debug, thiserror::Error, derive_more::Display)]
pub enum StoreError {
SqlClientError(#[from] sqlx::Error),
+ SerdeError(#[from] serde_json::Error),
+ ParseError(#[from] strum::ParseError),
StoreValueCannotBeParsedInvariant,
IdempotentCheckAlreadyExists,
}
-#[async_trait::async_trait(?Send)]
-pub trait Store {
+#[async_trait]
+pub trait Store: Send + Sync {
fn get_type(&self) -> StoreType;
}
@@ -25,7 +31,7 @@ pub enum StoreType {
Sqlite { c: Arc<SqlClient<Sqlite>> },
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
pub(crate) trait Storable<'a> {
type Item;
type Lens;
@@ -64,7 +70,15 @@ pub(crate) struct SessionLens<'a> {
}
impl<'a> Lens for SessionLens<'a> {}
-#[async_trait::async_trait(?Send)]
+pub(crate) struct CredentialLens<'a> {
+ pub id: Option<CredentialId>,
+ pub identity_id: Option<IdentityId>,
+ pub t: Option<&'a CredentialType>,
+ pub restrict_by_key: Option<bool>,
+}
+impl<'a> Lens for CredentialLens<'a> {}
+
+#[async_trait]
impl<'a> Storable<'a> for Address {
type Item = Address;
type Lens = AddressLens<'a>;
@@ -93,7 +107,7 @@ impl<'a> Storable<'a> for Address {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for AddressValidation {
type Item = AddressValidation;
type Lens = AddressValidationLens<'a>;
@@ -116,7 +130,7 @@ impl<'a> Storable<'a> for AddressValidation {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for Identity {
type Item = Identity;
type Lens = IdentityLens<'a>;
@@ -158,7 +172,7 @@ impl<'a> Storable<'a> for Identity {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for Session {
type Item = Session;
type Lens = SessionLens<'a>;
@@ -183,3 +197,51 @@ impl<'a> Storable<'a> for Session {
})
}
}
+
+#[async_trait]
+impl<'a> Storable<'a> for Credential {
+ type Item = Credential;
+ type Lens = CredentialLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ match store.get_type() {
+ StoreType::Postgres { c } => c.write_credential(self).await?,
+ StoreType::Sqlite { c } => c.write_credential(self).await?,
+ }
+ Ok(())
+ }
+
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> Result<Vec<Self::Item>, StoreError> {
+ Ok(match store.get_type() {
+ StoreType::Postgres { c } => {
+ c.find_credential(
+ lens.id,
+ lens.identity_id,
+ lens.t,
+ if let Some(true) = lens.restrict_by_key {
+ true
+ } else {
+ false
+ },
+ )
+ .await?
+ }
+ StoreType::Sqlite { c } => {
+ c.find_credential(
+ lens.id,
+ lens.identity_id,
+ lens.t,
+ if let Some(true) = lens.restrict_by_key {
+ true
+ } else {
+ false
+ },
+ )
+ .await?
+ }
+ })
+ }
+}
diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs
index ecb13be..3e72fe8 100644
--- a/crates/secd/src/client/store/sql_db.rs
+++ b/crates/secd/src/client/store/sql_db.rs
@@ -1,27 +1,19 @@
-use std::{str::FromStr, sync::Arc};
-
-use email_address::EmailAddress;
-use serde_json::value::RawValue;
-use sqlx::{
- database::HasArguments, types::Json, ColumnIndex, Database, Decode, Encode, Executor,
- IntoArguments, Pool, Transaction, Type,
-};
-use time::OffsetDateTime;
-use uuid::Uuid;
-
+use super::{Store, StoreError, StoreType};
use crate::{
- Address, AddressType, AddressValidation, AddressValidationMethod, Identity, Session,
- SessionToken,
+ Address, AddressType, AddressValidation, AddressValidationMethod, Credential, CredentialId,
+ CredentialType, Identity, IdentityId, Session,
};
-
+use email_address::EmailAddress;
use lazy_static::lazy_static;
+use sqlx::{
+ database::HasArguments, ColumnIndex, Database, Decode, Encode, Executor, IntoArguments, Pool,
+ Transaction, Type,
+};
use sqlx::{Postgres, Sqlite};
use std::collections::HashMap;
-
-use super::{
- AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, Store, StoreError,
- StoreType,
-};
+use std::{str::FromStr, sync::Arc};
+use time::OffsetDateTime;
+use uuid::Uuid;
const SQLITE: &str = "sqlite";
const PGSQL: &str = "pgsql";
@@ -30,6 +22,8 @@ const WRITE_ADDRESS: &str = "write_address";
const FIND_ADDRESS: &str = "find_address";
const WRITE_ADDRESS_VALIDATION: &str = "write_address_validation";
const FIND_ADDRESS_VALIDATION: &str = "find_address_validation";
+const WRITE_CREDENTIAL: &str = "write_credential";
+const FIND_CREDENTIAL: &str = "find_credential";
const WRITE_IDENTITY: &str = "write_identity";
const FIND_IDENTITY: &str = "find_identity";
const WRITE_SESSION: &str = "write_session";
@@ -72,6 +66,14 @@ lazy_static! {
FIND_SESSION,
include_str!("../../../store/sqlite/sql/find_session.sql"),
),
+ (
+ WRITE_CREDENTIAL,
+ include_str!("../../../store/sqlite/sql/write_credential.sql"),
+ ),
+ (
+ FIND_CREDENTIAL,
+ include_str!("../../../store/sqlite/sql/find_credential.sql"),
+ ),
]
.iter()
.cloned()
@@ -110,6 +112,14 @@ lazy_static! {
FIND_SESSION,
include_str!("../../../store/pg/sql/find_session.sql"),
),
+ (
+ WRITE_CREDENTIAL,
+ include_str!("../../../store/pg/sql/write_credential.sql"),
+ ),
+ (
+ FIND_CREDENTIAL,
+ include_str!("../../../store/pg/sql/find_credential.sql"),
+ ),
]
.iter()
.cloned()
@@ -131,7 +141,7 @@ pub trait SqlxResultExt<T> {
impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> {
fn extend_err(self) -> Result<T, StoreError> {
if let Err(sqlx::Error::Database(dbe)) = &self {
- if dbe.code() == Some("23505".into()) {
+ if dbe.code() == Some("23505".into()) || dbe.code() == Some("2067".into()) {
return Err(StoreError::IdempotentCheckAlreadyExists);
}
}
@@ -160,7 +170,7 @@ impl Store for PgClient {
impl PgClient {
pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/pg/migrations")
+ sqlx::migrate!("store/pg/migrations", "secd")
.run(&pool)
.await
.expect(ERR_MSG_MIGRATION_FAILED);
@@ -187,7 +197,7 @@ impl Store for SqliteClient {
impl SqliteClient {
pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/sqlite/migrations")
+ sqlx::migrate!("store/sqlite/migrations", "secd")
.run(&pool)
.await
.expect(ERR_MSG_MIGRATION_FAILED);
@@ -410,8 +420,7 @@ where
let sqls = get_sqls(&self.sqls_root, WRITE_IDENTITY);
sqlx::query(&sqls[0])
.bind(i.id)
- // TODO: validate this is actually Json somewhere way up the chain (when being deserialized)
- .bind(i.metadata.clone().unwrap_or("{}".into()))
+ .bind(i.metadata.clone())
.bind(i.created_at)
.bind(OffsetDateTime::now_utc())
.bind(i.deleted_at)
@@ -449,7 +458,7 @@ where
.extend_err()?;
let mut res = vec![];
- for (id, metadata, created_at, updated_at, deleted_at) in rs.into_iter() {
+ for (id, metadata, created_at, _, deleted_at) in rs.into_iter() {
res.push(Identity {
id,
address_validations: vec![],
@@ -509,6 +518,79 @@ where
}
Ok(res)
}
+
+ pub async fn write_credential(&self, c: &Credential) -> Result<(), StoreError> {
+ let sqls = get_sqls(&self.sqls_root, WRITE_CREDENTIAL);
+ let partial_key = match &c.t {
+ crate::CredentialType::Passphrase { key, value: _ } => Some(key.clone()),
+ _ => None,
+ };
+
+ sqlx::query(&sqls[0])
+ .bind(c.id)
+ .bind(c.identity_id)
+ .bind(partial_key)
+ .bind(c.t.to_string())
+ .bind(serde_json::to_string(&c.t)?)
+ .bind(c.created_at)
+ .bind(c.revoked_at)
+ .bind(c.deleted_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+ Ok(())
+ }
+ pub async fn find_credential(
+ &self,
+ id: Option<Uuid>,
+ identity_id: Option<Uuid>,
+ t: Option<&CredentialType>,
+ restrict_by_key: bool,
+ ) -> Result<Vec<Credential>, StoreError> {
+ let sqls = get_sqls(&self.sqls_root, FIND_CREDENTIAL);
+ let key = restrict_by_key
+ .then(|| {
+ t.map(|i| match i {
+ CredentialType::Passphrase { key, value: _ } => key.clone(),
+ _ => todo!(),
+ })
+ })
+ .flatten();
+
+ let rs = sqlx::query_as::<
+ _,
+ (
+ CredentialId,
+ IdentityId,
+ String,
+ OffsetDateTime,
+ Option<OffsetDateTime>,
+ Option<OffsetDateTime>,
+ ),
+ >(&sqls[0])
+ .bind(id.as_ref())
+ .bind(identity_id.as_ref())
+ .bind(t.map(|i| i.to_string()))
+ .bind(key)
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ let mut res = vec![];
+ for (id, identity_id, data, created_at, revoked_at, deleted_at) in rs.into_iter() {
+ let t: CredentialType = serde_json::from_str(&data)?;
+ res.push(Credential {
+ id,
+ identity_id,
+ t,
+ created_at,
+ revoked_at,
+ deleted_at,
+ })
+ }
+
+ Ok(res)
+ }
}
fn get_sqls(root: &str, file: &str) -> Vec<String> {
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 15615b2..54759a5 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -2,8 +2,10 @@ pub mod auth;
mod client;
mod util;
+use async_trait::async_trait;
+use auth::z::Relationship;
use client::{
- email::{EmailMessenger, EmailMessengerError, LocalMailer},
+ email::{EmailMessenger, EmailMessengerError, LocalMailer, Sendgrid},
spice::{Spice, SpiceError},
store::{
sql_db::{PgClient, SqliteClient},
@@ -11,28 +13,40 @@ use client::{
},
};
use email_address::EmailAddress;
-use log::{error, info};
+use lettre::message::Mailbox;
+use log::{error, info, warn};
use serde::{Deserialize, Serialize};
-use serde_with::{serde_as, DisplayFromStr};
-use std::{env::var, str::FromStr, sync::Arc};
+use serde_with::serde_as;
+use std::{
+ env::{set_var, var},
+ str::FromStr,
+ sync::Arc,
+};
use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
-use url::Url;
+use util::crypter::{Crypter, CrypterError};
use uuid::Uuid;
pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING";
+pub const ENV_CRYPTER_SECRET_KEY: &str = "SECD_CRYPTER_SECRET_KEY";
+pub const ENV_EMAIL_ADDRESS_FROM: &str = "SECD_EMAIL_ADDRESS_FROM";
+pub const ENV_EMAIL_ADDRESS_REPLYTO: &str = "SECD_EMAIL_ADDRESS_REPLYTO";
pub const ENV_EMAIL_MESSENGER: &str = "SECD_EMAIL_MESSENGER";
pub const ENV_EMAIL_MESSENGER_CLIENT_ID: &str = "SECD_EMAIL_MESSENGER_CLIENT_ID";
pub const ENV_EMAIL_MESSENGER_CLIENT_SECRET: &str = "SECD_EMAIL_MESSENGER_CLIENT_SECRET";
+pub const ENV_EMAIL_SENDGRID_API_KEY: &str = "SECD_EMAIL_SENDGRID_API_KEY";
+pub const ENV_EMAIL_SIGNIN_MESSAGE: &str = "SECD_EMAIL_SIGNIN_MESSAGE";
+pub const ENV_EMAIL_SIGNUP_MESSAGE: &str = "SECD_EMAIL_SIGNUP_MESSAGE";
pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET";
pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER";
-const SESSION_SIZE_BYTES: usize = 32;
-const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
-const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6;
const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5;
const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true;
+const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t";
+const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
+const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
+const SESSION_SIZE_BYTES: usize = 32;
pub type AddressId = Uuid;
pub type AddressValidationId = Uuid;
@@ -45,10 +59,17 @@ pub type SessionToken = String;
#[derive(Debug, derive_more::Display, thiserror::Error)]
pub enum SecdError {
+ // AuthenticationError(AuthnError);
+ // AuthorizationError(AuthzError);
+ // // InitializationError(...)
AddressValidationFailed,
AddressValidationSessionExchangeFailed,
AddressValidationExpiredOrConsumed,
+ CredentialAlreadyExists,
+
+ CrypterError(#[from] CrypterError),
+
TooManyIdentities,
IdentityNotFound,
@@ -69,9 +90,62 @@ pub enum SecdError {
}
pub struct Secd {
- store: Arc<dyn Store + Send + Sync + 'static>,
+ crypter: Crypter,
email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>,
spice: Option<Arc<Spice>>,
+ store: Arc<dyn Store + Send + Sync + 'static>,
+ cfg: Cfg,
+}
+
+struct Cfg {
+ email_address_from: Option<Mailbox>,
+ email_address_replyto: Option<Mailbox>,
+ email_signup_message: Option<String>,
+ email_signin_message: Option<String>,
+}
+
+#[async_trait]
+pub trait Authentication {
+ async fn validate_address(
+ &self,
+ address_type: AddressType,
+ identity_id: Option<IdentityId>,
+ ) -> Result<AddressValidation, SecdError>;
+
+ async fn complete_address_validation(
+ &self,
+ validation_id: &AddressValidationId,
+ plaintext_token: Option<String>,
+ plaintext_code: Option<String>,
+ ) -> Result<AddressValidation, SecdError>;
+
+ async fn create_credential(
+ &self,
+ t: &CredentialType,
+ identity_id: Option<IdentityId>,
+ ) -> Result<IdentityId, SecdError>;
+ // async fn update_credential(&self, t: &CredentialType) -> Result<(), SecdError>;
+ async fn reset_credential(
+ &self,
+ t: &CredentialType,
+ address: &AddressType,
+ ) -> Result<Credential, SecdError>;
+ async fn validate_credential(&self, t: &CredentialType) -> Result<Credential, SecdError>;
+
+ // async fn expire_session_chain(&self, t: &SessionToken) -> Result<(), SecdError>;
+ // async fn expire_sessions(&self, i: &IdentityId) -> Result<(), SecdError>;
+
+ // async fn get_identity(&self, t: &SessionToken) -> Result<Identity, SecdError>;
+ // async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError>;
+}
+
+#[async_trait]
+pub trait Authorization {
+ async fn check(&self, r: &Relationship) -> Result<bool, SecdError>;
+ async fn expand(&self) -> Result<(), SecdError>;
+ async fn read(&self) -> Result<(), SecdError>;
+ async fn watch(&self) -> Result<(), SecdError>;
+ async fn write(&self, relationships: &[Relationship]) -> Result<(), SecdError>;
}
#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)]
@@ -136,34 +210,53 @@ pub enum AddressType {
Sms { phone_number: Option<PhoneNumber> },
}
+#[serde_as]
#[derive(Debug, Serialize)]
pub struct Credential {
pub id: CredentialId,
pub identity_id: IdentityId,
pub t: CredentialType,
+ #[serde(with = "time::serde::timestamp")]
+ pub created_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub revoked_at: Option<OffsetDateTime>,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub deleted_at: Option<OffsetDateTime>,
}
#[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Debug, Display, Serialize, Deserialize, EnumString)]
pub enum CredentialType {
- Passphrase {
- key: String,
- value: String,
- },
- Oicd {
- value: String,
- },
- OneTimeCodes {
- codes: Vec<String>,
- },
- Totp {
- #[serde_as(as = "DisplayFromStr")]
- url: Url,
- code: String,
- },
- WebAuthn {
- value: String,
- },
+ Passphrase { key: String, value: String },
+ Oidc { value: String },
+ OneTimeCodes { codes: Vec<String> },
+ // Totp {
+ // #[serde_as(as = "DisplayFromStr")]
+ // url: Url,
+ // code: String,
+ // },
+ WebAuthn { value: String },
+}
+
+struct SecuredCredential {
+ pub id: CredentialId,
+ pub identity_id: IdentityId,
+ pub t: CredentialType,
+ pub created_at: OffsetDateTime,
+ pub revoked_at: Option<OffsetDateTime>,
+ pub deleted_at: Option<OffsetDateTime>,
+}
+
+enum SecuredCredentialType {
+ Passphrase { key: String, value: String },
+ Oidc { value: String },
+ OneTimeCodes { codes: Vec<String> },
+ // Totp {
+ // #[serde_as(as = "DisplayFromStr")]
+ // url: Url,
+ // code: String,
+ // },
+ WebAuthn { value: String },
}
#[serde_with::skip_serializing_none]
@@ -207,12 +300,29 @@ impl Secd {
&var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()),
)
.expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5");
- let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok();
- let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok();
+ let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| {
+ warn!(
+ "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE ENVIORNMENT VARIABLE {}. THE DEFAULT KEY IS: {}",
+ ENV_CRYPTER_SECRET_KEY,
+ CRYPTER_SECRET_KEY_DEFAULT,
+ );
+ CRYPTER_SECRET_KEY_DEFAULT.to_string()
+ });
info!("starting client with auth_store: {:?}", auth_store);
info!("starting client with email_messenger: {:?}", auth_store);
+ let cfg = Cfg {
+ email_address_from: var(ENV_EMAIL_ADDRESS_FROM)
+ .ok()
+ .and_then(|s| s.parse().ok()),
+ email_address_replyto: var(ENV_EMAIL_ADDRESS_REPLYTO)
+ .ok()
+ .and_then(|s| s.parse().ok()),
+ email_signup_message: var(ENV_EMAIL_SIGNUP_MESSAGE).ok(),
+ email_signin_message: var(ENV_EMAIL_SIGNIN_MESSAGE).ok(),
+ };
+
let store = match auth_store {
AuthStore::Sqlite { conn } => {
if z_schema.is_some() {
@@ -252,7 +362,10 @@ impl Secd {
};
let email_sender = match email_messenger {
- AuthEmailMessenger::Local => LocalMailer {},
+ AuthEmailMessenger::Local => LocalMailer::new(),
+ AuthEmailMessenger::Sendgrid => Sendgrid::new(
+ var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"),
+ ),
_ => unimplemented!(),
};
@@ -267,10 +380,14 @@ impl Secd {
None => None,
};
+ let crypter = Crypter::new(crypter_secret_key.as_bytes());
+
Ok(Secd {
- store,
- email_messenger: Arc::new(email_sender),
+ crypter,
+ email_messenger: email_sender,
spice,
+ store,
+ cfg,
})
}
}
diff --git a/crates/secd/src/util/crypter.rs b/crates/secd/src/util/crypter.rs
new file mode 100644
index 0000000..1717377
--- /dev/null
+++ b/crates/secd/src/util/crypter.rs
@@ -0,0 +1,87 @@
+use aes_gcm::{
+ aead::{Aead, KeyInit, OsRng},
+ Aes256Gcm, Nonce,
+};
+use argon2::{
+ password_hash::{self, SaltString},
+ Argon2, PasswordHasher,
+};
+use derive_more::Display;
+use rand::Rng;
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+#[derive(Debug, Display, Error)]
+pub enum CrypterError {
+ EncryptError(String),
+ DecryptError(String),
+ DecodeError(String),
+ HashError(String),
+}
+
+pub struct Crypter {
+ pub key: Vec<u8>,
+}
+
+impl Crypter {
+ pub fn new(key: &[u8]) -> Self {
+ Self { key: key.to_vec() }
+ }
+
+ pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.key);
+
+ let cipher = Aes256Gcm::new(&hasher.finalize());
+
+ let rbs = rand::thread_rng().gen::<[u8; 12]>();
+ let iv = Nonce::from_slice(&rbs);
+ let crypt = cipher
+ .encrypt(&iv, data)
+ .map_err(|e| CrypterError::EncryptError(e.to_string()))?;
+
+ let mut msg = iv.to_vec();
+ msg.extend_from_slice(&crypt);
+ Ok(msg)
+ }
+
+ pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.key);
+
+ let cipher = Aes256Gcm::new(&hasher.finalize());
+
+ let iv = Nonce::from_slice(&data[0..=11]);
+ let data = &data[12..];
+ Ok(cipher
+ .decrypt(&iv, data)
+ .map_err(|e| CrypterError::DecryptError(e.to_string()))?)
+ }
+
+ pub fn hash(&self, data: &[u8]) -> Result<String, CrypterError> {
+ let salt = SaltString::generate(&mut OsRng);
+ let hasher = Argon2::default();
+ Ok(hasher
+ .hash_password(data, &salt)
+ .map_err(|e| CrypterError::HashError(e.to_string()))?
+ .to_string())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn encrypt_data_test() {
+ let crypter = Crypter {
+ key: "testkey".to_string().into_bytes(),
+ };
+
+ let plaintext = "This is a secret.";
+ let enc = crypter.encrypt(&plaintext.as_bytes()).unwrap();
+
+ let res = crypter.decrypt(&enc).unwrap();
+ assert_eq!(plaintext, std::str::from_utf8(&res).unwrap());
+ }
+}
diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs
index bab8a25..ec5b62d 100644
--- a/crates/secd/src/util/from.rs
+++ b/crates/secd/src/util/from.rs
@@ -1,5 +1,3 @@
-use std::str::FromStr;
-
use crate::AuthStore;
impl From<Option<String>> for AuthStore {
@@ -23,7 +21,6 @@ mod test {
#[test]
fn auth_store_from_string() {
let in_conn = None;
- let a = AuthStore::from(in_conn.clone());
match AuthStore::from(in_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, "sqlite::memory:"),
r @ _ => assert!(
@@ -46,14 +43,12 @@ mod test {
}
let sqlite_conn = Some("sqlite:///path/to/db.sql".into());
- let a = AuthStore::from(sqlite_conn.clone());
match AuthStore::from(sqlite_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_conn.unwrap()),
r @ _ => assert!(false, "should have parsed as sqlite. Found: {:?}", r),
}
let sqlite_mem_conn = Some("sqlite:memory:".into());
- let a = AuthStore::from(sqlite_mem_conn.clone());
match AuthStore::from(sqlite_mem_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_mem_conn.unwrap()),
r @ _ => assert!(
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index c26986d..8676f26 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -1,21 +1,14 @@
+pub(crate) mod crypter;
pub(crate) mod from;
-use rand::{thread_rng, Rng};
+use self::crypter::{Crypter, CrypterError};
+use crate::{
+ AddressType, Credential, CredentialType, IdentityId, SecdError, Session, SESSION_DURATION,
+ SESSION_SIZE_BYTES,
+};
use sha2::{Digest, Sha256};
+use std::str::from_utf8;
use time::OffsetDateTime;
-use url::Url;
-
-use crate::{AddressType, IdentityId, SecdError, Session, SESSION_DURATION, SESSION_SIZE_BYTES};
-
-pub(crate) fn remove_trailing_slash(url: &mut Url) -> String {
- let mut u = url.to_string();
-
- if u.ends_with('/') {
- u.pop();
- }
-
- u
-}
pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
@@ -51,3 +44,89 @@ impl Session {
})
}
}
+
+impl Credential {
+ pub(crate) fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = hex::encode(crypter.encrypt(value.as_bytes())?);
+ }
+ _ => {}
+ })
+ }
+ pub(crate) fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = from_utf8(
+ &crypter.decrypt(
+ &hex::decode(value.clone())
+ .map_err(|e| CrypterError::DecodeError(e.to_string()))?,
+ )?,
+ )
+ .map_err(|e| CrypterError::DecodeError(e.to_string()))?
+ .to_string()
+ }
+ _ => {}
+ })
+ }
+
+ pub(crate) fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = crypter.hash(value.as_bytes())?;
+ }
+ _ => {}
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use uuid::Uuid;
+
+ use super::*;
+
+ #[test]
+ fn test_credential_encrypt() {
+ let c = Crypter::new("AMAZING_KEY".as_bytes());
+
+ let plaintext_secret = "super_password".to_string();
+
+ let mut credential = Credential {
+ id: Uuid::new_v4(),
+ identity_id: Uuid::new_v4(),
+ t: CredentialType::Passphrase {
+ key: "super_user".into(),
+ value: plaintext_secret.clone(),
+ },
+ created_at: OffsetDateTime::now_utc(),
+ revoked_at: None,
+ deleted_at: None,
+ };
+
+ credential.encrypt(&c).unwrap();
+ match &credential.t {
+ CredentialType::Passphrase { key: _, value } => {
+ assert_ne!(plaintext_secret.clone(), value.clone())
+ }
+ _ => {}
+ };
+
+ credential.decrypt(&c).unwrap();
+ match &credential.t {
+ CredentialType::Passphrase { key: _, value } => {
+ assert_eq!(plaintext_secret.clone(), value.clone())
+ }
+ _ => {}
+ };
+ }
+}