Skip to content

Commit 92b82c6

Browse files
feat(custodian): add support for authorization for accessing keys (#24)
1 parent ece4d7c commit 92b82c6

File tree

24 files changed

+274
-18
lines changed

24 files changed

+274
-18
lines changed

Cargo.lock

Lines changed: 43 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ rustc-hash = "1.1.0"
5353
rayon = "1.10.0"
5454
once_cell = "1.19.0"
5555
hyper = "1.3.1"
56+
blake3 = "1.5.4"
5657

5758
[build-dependencies]
5859
cargo_metadata = "0.18.1"

config/development.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ log_format = "console"
2424

2525
[secrets]
2626
master_key = "6d761d32f1b14ef34cf016d726b29b02b5cfce92a8959f1bfb65995c8100925e"
27-
27+
access_token = "secret123"
28+
hash_context = "keymanager:hyperswitch"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- This file should undo anything in `up.sql`
2+
ALTER TABLE data_key_store DROP COLUMN IF EXISTS token;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Your SQL goes here
2+
ALTER TABLE data_key_store ADD COLUMN IF NOT EXISTS token VARCHAR(255);

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ pub struct Secrets {
128128
pub master_key: GcmAes256,
129129
#[cfg(feature = "aws")]
130130
pub kms_config: AwsKmsConfig,
131+
pub access_token: masking::Secret<String>,
132+
pub hash_context: masking::Secret<String>,
131133
}
132134

133135
#[derive(Deserialize, Debug)]

src/core/crypto.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod crux;
2+
pub(crate) mod custodian;
23
mod decryption;
34
mod encryption;
45

@@ -17,14 +18,17 @@ use axum::extract::{Json, State};
1718
use opentelemetry::KeyValue;
1819
use std::sync::Arc;
1920

21+
use self::custodian::Custodian;
22+
2023
pub async fn encrypt_data(
2124
State(state): State<Arc<AppState>>,
25+
custodian: Custodian,
2226
Json(req): Json<EncryptDataRequest>,
2327
) -> errors::ApiResponseResult<Json<EncryptionResponse>> {
2428
let (data_identifier, key_identifier) = req.identifier.get_identifier();
2529

2630
utils::record_api_operation(
27-
encryption::encryption(state, req),
31+
encryption::encryption(state, custodian, req),
2832
&metrics::ENCRYPTION_API_LATENCY,
2933
&[
3034
KeyValue::new("data_identifier", data_identifier),
@@ -36,12 +40,13 @@ pub async fn encrypt_data(
3640

3741
pub async fn decrypt_data(
3842
State(state): State<Arc<AppState>>,
43+
custodian: Custodian,
3944
Json(req): Json<DecryptionRequest>,
4045
) -> errors::ApiResponseResult<Json<DecryptionResponse>> {
4146
let (data_identifier, key_identifier) = req.identifier.get_identifier();
4247

4348
utils::record_api_operation(
44-
decryption::decryption(state, req),
49+
decryption::decryption(state, custodian, req),
4550
&metrics::DECRYPTION_API_LATENCY,
4651
&[
4752
KeyValue::new("data_identifier", data_identifier),

src/core/crypto/crux.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use error_stack::ensure;
12
use masking::PeekInterface;
23
use rayon::prelude::*;
34

@@ -15,6 +16,8 @@ use crate::{
1516
},
1617
};
1718

19+
use super::custodian::Custodian;
20+
1821
#[async_trait::async_trait]
1922
pub trait KeyEncrypter<ToType> {
2023
async fn encrypt(self, state: &AppState) -> errors::CustomResult<ToType, errors::CryptoError>;
@@ -47,6 +50,7 @@ impl KeyEncrypter<DataKeyNew> for Key {
4750
time::OffsetDateTime::now_utc().date(),
4851
time::OffsetDateTime::now_utc().time(),
4952
),
53+
token: self.token,
5054
})
5155
}
5256
}
@@ -71,6 +75,7 @@ impl KeyDecrypter<Key> for DataKey {
7175
version: self.version,
7276
key: decrypted_key.into(),
7377
source,
78+
token: self.token,
7479
})
7580
}
7681
}
@@ -81,6 +86,7 @@ pub trait DataEncrypter<ToType> {
8186
self,
8287
state: &AppState,
8388
identifier: &Identifier,
89+
custodian: Custodian,
8490
) -> errors::CustomResult<ToType, errors::CryptoError>;
8591
}
8692

@@ -90,6 +96,7 @@ pub trait DataDecrypter<ToType> {
9096
self,
9197
state: &AppState,
9298
identifier: &Identifier,
99+
custodian: Custodian,
93100
) -> errors::CustomResult<ToType, errors::CryptoError>;
94101
}
95102

@@ -99,11 +106,20 @@ impl DataEncrypter<EncryptedDataGroup> for DecryptedDataGroup {
99106
self,
100107
state: &AppState,
101108
identifier: &Identifier,
109+
custodian: Custodian,
102110
) -> errors::CustomResult<EncryptedDataGroup, errors::CryptoError> {
103111
let version = Version::get_latest(identifier, state).await;
104112
let decrypted_key = Key::get_key(state, identifier, version).await.switch()?;
105113
let key = GcmAes256::new(decrypted_key.key)?;
106114

115+
let stored_token = decrypted_key.token;
116+
let provided_token = custodian.into_access_token(state);
117+
118+
ensure!(
119+
!identifier.is_entity() || (stored_token.eq(&provided_token)),
120+
errors::CryptoError::AuthenticationFailed
121+
);
122+
107123
state.thread_pool.install(|| {
108124
self.0
109125
.into_par_iter()
@@ -125,12 +141,21 @@ impl DataDecrypter<DecryptedDataGroup> for EncryptedDataGroup {
125141
self,
126142
state: &AppState,
127143
identifier: &Identifier,
144+
custodian: Custodian,
128145
) -> errors::CustomResult<DecryptedDataGroup, errors::CryptoError> {
129146
let version = FxHashSet::from_iter(self.0.values().map(|d| d.version));
130147
let decrypted_keys = Key::get_multiple_keys(state, identifier, version)
131148
.await
132149
.switch()?;
133150

151+
let mut stored_tokens = decrypted_keys.values().map(|k| &k.token);
152+
let provided_token = custodian.into_access_token(state);
153+
154+
ensure!(
155+
!identifier.is_entity() || stored_tokens.all(|t| t.eq(&provided_token)),
156+
errors::CryptoError::AuthenticationFailed
157+
);
158+
134159
state.thread_pool.install(|| {
135160
self
136161
.0
@@ -160,9 +185,19 @@ impl DataEncrypter<EncryptedData> for DecryptedData {
160185
self,
161186
state: &AppState,
162187
identifier: &Identifier,
188+
custodian: Custodian,
163189
) -> errors::CustomResult<EncryptedData, errors::CryptoError> {
164190
let version = Version::get_latest(identifier, state).await;
165191
let decrypted_key = Key::get_key(state, identifier, version).await.switch()?;
192+
193+
let stored_token = decrypted_key.token;
194+
let provided_token = custodian.into_access_token(state);
195+
196+
ensure!(
197+
!identifier.is_entity() || (stored_token.eq(&provided_token)),
198+
errors::CryptoError::AuthenticationFailed
199+
);
200+
166201
let key = GcmAes256::new(decrypted_key.key)?;
167202

168203
let encrypted_data = key.encrypt(self.inner())?;
@@ -180,9 +215,19 @@ impl DataDecrypter<DecryptedData> for EncryptedData {
180215
self,
181216
state: &AppState,
182217
identifier: &Identifier,
218+
custodian: Custodian,
183219
) -> errors::CustomResult<DecryptedData, errors::CryptoError> {
184220
let version = self.version;
185221
let decrypted_key = Key::get_key(state, identifier, version).await.switch()?;
222+
223+
let stored_token = decrypted_key.token;
224+
let provided_token = custodian.into_access_token(state);
225+
226+
ensure!(
227+
!identifier.is_entity() || (stored_token.eq(&provided_token)),
228+
errors::CryptoError::AuthenticationFailed
229+
);
230+
186231
let key = GcmAes256::new(decrypted_key.key)?;
187232

188233
let decrypted_data = key.decrypt(self.inner())?;

src/core/crypto/custodian.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use std::sync::Arc;
2+
3+
use axum::extract::FromRequestParts;
4+
use axum::http::request;
5+
use base64::Engine;
6+
use error_stack::{ensure, ResultExt};
7+
use hyper::header;
8+
use masking::{PeekInterface, Secret, StrongSecret};
9+
10+
use crate::app::AppState;
11+
use crate::consts::base64::BASE64_ENGINE;
12+
use crate::errors::{ApiErrorContainer, CustomResult, ParsingError, SwitchError, ToContainerError};
13+
14+
pub struct Custodian {
15+
pub keys: Option<(StrongSecret<String>, StrongSecret<String>)>,
16+
}
17+
18+
impl Custodian {
19+
fn new(keys: Option<(String, String)>) -> Self {
20+
let keys = keys.map(|(key1, key2)| (StrongSecret::new(key1), StrongSecret::new(key2)));
21+
Self { keys }
22+
}
23+
24+
pub fn into_access_token(self, state: &AppState) -> Option<StrongSecret<String>> {
25+
self.keys
26+
.map(|(x, y)| format!("{}:{}", x.peek(), y.peek()))
27+
.map(|key| crate::crypto::blake3::Blake3::hash(state, Secret::new(key)))
28+
.map(hex::encode)
29+
.map(StrongSecret::new)
30+
}
31+
}
32+
33+
#[axum::async_trait]
34+
impl FromRequestParts<Arc<AppState>> for Custodian {
35+
type Rejection = ApiErrorContainer;
36+
async fn from_request_parts(
37+
parts: &mut request::Parts,
38+
_state: &Arc<AppState>,
39+
) -> Result<Self, Self::Rejection> {
40+
parts
41+
.headers
42+
.get(header::AUTHORIZATION)
43+
.map(extract_credential)
44+
.transpose()
45+
.switch()
46+
.to_container_error()
47+
.map(Self::new)
48+
}
49+
}
50+
51+
fn extract_credential(
52+
header: &header::HeaderValue,
53+
) -> CustomResult<(String, String), ParsingError> {
54+
let header = header.to_str().change_context(ParsingError::ParsingFailed(
55+
"Failed while converting header to string".to_string(),
56+
))?;
57+
58+
let credential = header
59+
.strip_prefix("Basic ")
60+
.ok_or(ParsingError::ParsingFailed(
61+
"Authorization scheme is not basic".to_string(),
62+
))?;
63+
let credential = credential.trim();
64+
let credential =
65+
BASE64_ENGINE
66+
.decode(credential)
67+
.change_context(ParsingError::DecodingFailed(
68+
"Failed while base64 decoding the authorization header".to_string(),
69+
))?;
70+
let credential = String::from_utf8(credential).change_context(ParsingError::DecodingFailed(
71+
"Failed while converting base64 to utf8".to_string(),
72+
))?;
73+
let mut parts = credential.split(':');
74+
let key1 = parts.next().ok_or(ParsingError::ParsingFailed(
75+
"Failed while extracting key1 from credential".to_string(),
76+
))?;
77+
let key2 = parts.next().ok_or(ParsingError::ParsingFailed(
78+
"Failed while extracting key2 from credential".to_string(),
79+
))?;
80+
81+
ensure!(
82+
parts.next().is_none(),
83+
ParsingError::ParsingFailed("Credential has more than 2 parts".to_string())
84+
);
85+
86+
Ok((key1.to_string(), key2.to_string()))
87+
}

src/core/crypto/decryption.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ use crate::{
99
};
1010
use opentelemetry::KeyValue;
1111

12+
use super::custodian::Custodian;
13+
1214
pub(super) async fn decryption(
1315
state: Arc<AppState>,
16+
custodian: Custodian,
1417
req: DecryptionRequest,
1518
) -> errors::CustomResult<DecryptionResponse, errors::ApplicationErrorResponse> {
1619
let identifier = req.identifier.clone();
1720
let decrypted_data = req
1821
.data
19-
.decrypt(&state, &identifier)
22+
.decrypt(&state, &identifier, custodian)
2023
.await
2124
.map_err(|err| {
2225
logger::error!(encryption_error=?err);

0 commit comments

Comments
 (0)