-
Notifications
You must be signed in to change notification settings - Fork 13
feat(custodian): add support for authorization for accessing keys #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c3efa08
3c1f4ca
ae9f554
98da93e
5ae50d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-- This file should undo anything in `up.sql` | ||
ALTER TABLE data_key_store DROP COLUMN IF EXISTS token; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-- Your SQL goes here | ||
ALTER TABLE data_key_store ADD COLUMN IF NOT EXISTS token VARCHAR(255); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,3 +1,4 @@ | ||||||
use error_stack::ensure; | ||||||
use masking::PeekInterface; | ||||||
use rayon::prelude::*; | ||||||
|
||||||
|
@@ -15,6 +16,8 @@ use crate::{ | |||||
}, | ||||||
}; | ||||||
|
||||||
use super::custodian::Custodian; | ||||||
|
||||||
#[async_trait::async_trait] | ||||||
pub trait KeyEncrypter<ToType> { | ||||||
async fn encrypt(self, state: &AppState) -> errors::CustomResult<ToType, errors::CryptoError>; | ||||||
|
@@ -47,6 +50,7 @@ impl KeyEncrypter<DataKeyNew> for Key { | |||||
time::OffsetDateTime::now_utc().date(), | ||||||
time::OffsetDateTime::now_utc().time(), | ||||||
), | ||||||
token: self.token, | ||||||
}) | ||||||
} | ||||||
} | ||||||
|
@@ -71,6 +75,7 @@ impl KeyDecrypter<Key> for DataKey { | |||||
version: self.version, | ||||||
key: decrypted_key.into(), | ||||||
source, | ||||||
token: self.token, | ||||||
}) | ||||||
} | ||||||
} | ||||||
|
@@ -81,6 +86,7 @@ pub trait DataEncrypter<ToType> { | |||||
self, | ||||||
state: &AppState, | ||||||
identifier: &Identifier, | ||||||
custodian: Custodian, | ||||||
) -> errors::CustomResult<ToType, errors::CryptoError>; | ||||||
} | ||||||
|
||||||
|
@@ -90,6 +96,7 @@ pub trait DataDecrypter<ToType> { | |||||
self, | ||||||
state: &AppState, | ||||||
identifier: &Identifier, | ||||||
custodian: Custodian, | ||||||
) -> errors::CustomResult<ToType, errors::CryptoError>; | ||||||
} | ||||||
|
||||||
|
@@ -99,11 +106,20 @@ impl DataEncrypter<EncryptedDataGroup> for DecryptedDataGroup { | |||||
self, | ||||||
state: &AppState, | ||||||
identifier: &Identifier, | ||||||
custodian: Custodian, | ||||||
) -> errors::CustomResult<EncryptedDataGroup, errors::CryptoError> { | ||||||
let version = Version::get_latest(identifier, state).await; | ||||||
let decrypted_key = Key::get_key(state, identifier, version).await.switch()?; | ||||||
let key = GcmAes256::new(decrypted_key.key)?; | ||||||
|
||||||
let stored_token = decrypted_key.token; | ||||||
let provided_token = custodian.into_access_token(state); | ||||||
|
||||||
ensure!( | ||||||
!identifier.is_entity() || (stored_token.eq(&provided_token)), | ||||||
errors::CryptoError::AuthenticationFailed | ||||||
); | ||||||
|
||||||
state.thread_pool.install(|| { | ||||||
self.0 | ||||||
.into_par_iter() | ||||||
|
@@ -125,12 +141,21 @@ impl DataDecrypter<DecryptedDataGroup> for EncryptedDataGroup { | |||||
self, | ||||||
state: &AppState, | ||||||
identifier: &Identifier, | ||||||
custodian: Custodian, | ||||||
) -> errors::CustomResult<DecryptedDataGroup, errors::CryptoError> { | ||||||
let version = FxHashSet::from_iter(self.0.values().map(|d| d.version)); | ||||||
let decrypted_keys = Key::get_multiple_keys(state, identifier, version) | ||||||
.await | ||||||
.switch()?; | ||||||
|
||||||
let mut stored_tokens = decrypted_keys.values().map(|k| &k.token); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's required, as it's a opaque type that will be acted upon: when we call |
||||||
let provided_token = custodian.into_access_token(state); | ||||||
|
||||||
ensure!( | ||||||
!identifier.is_entity() || stored_tokens.all(|t| t.eq(&provided_token)), | ||||||
errors::CryptoError::AuthenticationFailed | ||||||
); | ||||||
|
||||||
state.thread_pool.install(|| { | ||||||
self | ||||||
.0 | ||||||
|
@@ -160,9 +185,19 @@ impl DataEncrypter<EncryptedData> for DecryptedData { | |||||
self, | ||||||
state: &AppState, | ||||||
identifier: &Identifier, | ||||||
custodian: Custodian, | ||||||
) -> errors::CustomResult<EncryptedData, errors::CryptoError> { | ||||||
let version = Version::get_latest(identifier, state).await; | ||||||
let decrypted_key = Key::get_key(state, identifier, version).await.switch()?; | ||||||
|
||||||
let stored_token = decrypted_key.token; | ||||||
let provided_token = custodian.into_access_token(state); | ||||||
|
||||||
ensure!( | ||||||
!identifier.is_entity() || (stored_token.eq(&provided_token)), | ||||||
errors::CryptoError::AuthenticationFailed | ||||||
); | ||||||
|
||||||
let key = GcmAes256::new(decrypted_key.key)?; | ||||||
|
||||||
let encrypted_data = key.encrypt(self.inner())?; | ||||||
|
@@ -180,9 +215,19 @@ impl DataDecrypter<DecryptedData> for EncryptedData { | |||||
self, | ||||||
state: &AppState, | ||||||
identifier: &Identifier, | ||||||
custodian: Custodian, | ||||||
) -> errors::CustomResult<DecryptedData, errors::CryptoError> { | ||||||
let version = self.version; | ||||||
let decrypted_key = Key::get_key(state, identifier, version).await.switch()?; | ||||||
|
||||||
let stored_token = decrypted_key.token; | ||||||
let provided_token = custodian.into_access_token(state); | ||||||
|
||||||
ensure!( | ||||||
!identifier.is_entity() || (stored_token.eq(&provided_token)), | ||||||
errors::CryptoError::AuthenticationFailed | ||||||
); | ||||||
|
||||||
let key = GcmAes256::new(decrypted_key.key)?; | ||||||
|
||||||
let decrypted_data = key.decrypt(self.inner())?; | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
use std::sync::Arc; | ||
|
||
use axum::extract::FromRequestParts; | ||
use axum::http::request; | ||
use base64::Engine; | ||
use error_stack::{ensure, ResultExt}; | ||
use hyper::header; | ||
use masking::{PeekInterface, Secret, StrongSecret}; | ||
|
||
use crate::app::AppState; | ||
use crate::consts::base64::BASE64_ENGINE; | ||
use crate::errors::{ApiErrorContainer, CustomResult, ParsingError, SwitchError, ToContainerError}; | ||
|
||
pub struct Custodian { | ||
pub keys: Option<(StrongSecret<String>, StrongSecret<String>)>, | ||
} | ||
|
||
impl Custodian { | ||
fn new(keys: Option<(String, String)>) -> Self { | ||
let keys = keys.map(|(key1, key2)| (StrongSecret::new(key1), StrongSecret::new(key2))); | ||
Self { keys } | ||
} | ||
|
||
pub fn into_access_token(self, state: &AppState) -> Option<StrongSecret<String>> { | ||
self.keys | ||
.map(|(x, y)| format!("{}:{}", x.peek(), y.peek())) | ||
.map(|key| crate::crypto::blake3::Blake3::hash(state, Secret::new(key))) | ||
.map(hex::encode) | ||
.map(StrongSecret::new) | ||
} | ||
} | ||
|
||
#[axum::async_trait] | ||
impl FromRequestParts<Arc<AppState>> for Custodian { | ||
type Rejection = ApiErrorContainer; | ||
async fn from_request_parts( | ||
parts: &mut request::Parts, | ||
_state: &Arc<AppState>, | ||
) -> Result<Self, Self::Rejection> { | ||
parts | ||
.headers | ||
.get(header::AUTHORIZATION) | ||
.map(extract_credential) | ||
.transpose() | ||
.switch() | ||
.to_container_error() | ||
.map(Self::new) | ||
} | ||
} | ||
|
||
fn extract_credential( | ||
header: &header::HeaderValue, | ||
) -> CustomResult<(String, String), ParsingError> { | ||
let header = header.to_str().change_context(ParsingError::ParsingFailed( | ||
"Failed while converting header to string".to_string(), | ||
))?; | ||
|
||
let credential = header | ||
.strip_prefix("Basic ") | ||
.ok_or(ParsingError::ParsingFailed( | ||
"Authorization scheme is not basic".to_string(), | ||
))?; | ||
let credential = credential.trim(); | ||
let credential = | ||
BASE64_ENGINE | ||
.decode(credential) | ||
.change_context(ParsingError::DecodingFailed( | ||
"Failed while base64 decoding the authorization header".to_string(), | ||
))?; | ||
let credential = String::from_utf8(credential).change_context(ParsingError::DecodingFailed( | ||
"Failed while converting base64 to utf8".to_string(), | ||
))?; | ||
let mut parts = credential.split(':'); | ||
let key1 = parts.next().ok_or(ParsingError::ParsingFailed( | ||
"Failed while extracting key1 from credential".to_string(), | ||
))?; | ||
let key2 = parts.next().ok_or(ParsingError::ParsingFailed( | ||
"Failed while extracting key2 from credential".to_string(), | ||
))?; | ||
|
||
ensure!( | ||
parts.next().is_none(), | ||
ParsingError::ParsingFailed("Credential has more than 2 parts".to_string()) | ||
); | ||
|
||
Ok((key1.to_string(), key2.to_string())) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.