mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 09:38:33 +08:00
refactor(api_keys): use a KMS encrypted API key hashing key and remove key ID prefix from plaintext API keys (#639)
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
@ -76,6 +76,9 @@ outgoing_enabled = true
|
|||||||
[eph_key]
|
[eph_key]
|
||||||
validity = 1
|
validity = 1
|
||||||
|
|
||||||
|
[api_keys]
|
||||||
|
hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
[connectors.aci]
|
[connectors.aci]
|
||||||
base_url = "https://eu-test.oppwa.com/"
|
base_url = "https://eu-test.oppwa.com/"
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,16 @@ outgoing_enabled = true
|
|||||||
[eph_key]
|
[eph_key]
|
||||||
validity = 1
|
validity = 1
|
||||||
|
|
||||||
|
[api_keys]
|
||||||
|
# Key ID for the KMS managed key used to decrypt the API key hashing key
|
||||||
|
aws_key_id = ""
|
||||||
|
# The AWS region for the KMS managed key used to decrypt the API key hashing key
|
||||||
|
aws_region = ""
|
||||||
|
# Base64-encoded (KMS encrypted) ciphertext of the API key hashing key
|
||||||
|
kms_encrypted_hash_key = ""
|
||||||
|
# Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating hashes of API keys
|
||||||
|
hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
# Connector configuration, provided attributes will be used to fulfill API requests.
|
# Connector configuration, provided attributes will be used to fulfill API requests.
|
||||||
# Examples provided here are sandbox/test base urls, can be replaced by live or mock
|
# Examples provided here are sandbox/test base urls, can be replaced by live or mock
|
||||||
# base urls based on your need.
|
# base urls based on your need.
|
||||||
|
|||||||
@ -63,6 +63,9 @@ cluster_urls = ["redis-queue:6379"]
|
|||||||
max_attempts = 10
|
max_attempts = 10
|
||||||
max_age = 365
|
max_age = 365
|
||||||
|
|
||||||
|
[api_keys]
|
||||||
|
hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
[connectors.aci]
|
[connectors.aci]
|
||||||
base_url = "https://eu-test.oppwa.com/"
|
base_url = "https://eu-test.oppwa.com/"
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ build = "src/build.rs"
|
|||||||
[features]
|
[features]
|
||||||
default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache"]
|
default = ["kv_store", "stripe", "oltp", "olap", "accounts_cache"]
|
||||||
kms = ["aws-config", "aws-sdk-kms"]
|
kms = ["aws-config", "aws-sdk-kms"]
|
||||||
basilisk = ["josekit"]
|
basilisk = ["josekit", "kms"]
|
||||||
stripe = ["dep:serde_qs"]
|
stripe = ["dep:serde_qs"]
|
||||||
sandbox = ["kms", "stripe", "basilisk"]
|
sandbox = ["kms", "stripe", "basilisk"]
|
||||||
olap = []
|
olap = []
|
||||||
|
|||||||
@ -57,6 +57,7 @@ pub struct Settings {
|
|||||||
pub webhooks: WebhooksSettings,
|
pub webhooks: WebhooksSettings,
|
||||||
pub pm_filters: ConnectorFilters,
|
pub pm_filters: ConnectorFilters,
|
||||||
pub bank_config: BankRedirectConfig,
|
pub bank_config: BankRedirectConfig,
|
||||||
|
pub api_keys: ApiKeys,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
@ -304,6 +305,26 @@ pub struct WebhooksSettings {
|
|||||||
pub outgoing_enabled: bool,
|
pub outgoing_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ApiKeys {
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
pub aws_key_id: String,
|
||||||
|
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
pub aws_region: String,
|
||||||
|
|
||||||
|
/// Base64-encoded (KMS encrypted) ciphertext of the key used for calculating hashes of API
|
||||||
|
/// keys
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
pub kms_encrypted_hash_key: String,
|
||||||
|
|
||||||
|
/// Hex-encoded 32-byte long (64 characters long when hex-encoded) key used for calculating
|
||||||
|
/// hashes of API keys
|
||||||
|
#[cfg(not(feature = "kms"))]
|
||||||
|
pub hash_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
pub fn new() -> ApplicationResult<Self> {
|
pub fn new() -> ApplicationResult<Self> {
|
||||||
Self::with_config_path(None)
|
Self::with_config_path(None)
|
||||||
@ -378,6 +399,7 @@ impl Settings {
|
|||||||
#[cfg(feature = "kv_store")]
|
#[cfg(feature = "kv_store")]
|
||||||
self.drainer.validate()?;
|
self.drainer.validate()?;
|
||||||
self.jwekey.validate()?;
|
self.jwekey.validate()?;
|
||||||
|
self.api_keys.validate()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,3 +184,37 @@ impl super::settings::DrainerSettings {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl super::settings::ApiKeys {
|
||||||
|
pub fn validate(&self) -> Result<(), ApplicationError> {
|
||||||
|
use common_utils::fp_utils::when;
|
||||||
|
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
{
|
||||||
|
when(self.aws_key_id.is_default_or_empty(), || {
|
||||||
|
Err(ApplicationError::InvalidConfigurationValueError(
|
||||||
|
"API key AWS key ID must not be empty when KMS feature is enabled".into(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
when(self.aws_region.is_default_or_empty(), || {
|
||||||
|
Err(ApplicationError::InvalidConfigurationValueError(
|
||||||
|
"API key AWS region must not be empty when KMS feature is enabled".into(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
when(self.kms_encrypted_hash_key.is_default_or_empty(), || {
|
||||||
|
Err(ApplicationError::InvalidConfigurationValueError(
|
||||||
|
"API key hashing key must not be empty when KMS feature is enabled".into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "kms"))]
|
||||||
|
when(self.hash_key.is_empty(), || {
|
||||||
|
Err(ApplicationError::InvalidConfigurationValueError(
|
||||||
|
"API key hashing key must not be empty".into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
use common_utils::{date_time, errors::CustomResult, fp_utils};
|
use common_utils::{date_time, errors::CustomResult, fp_utils};
|
||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
use masking::{PeekInterface, Secret};
|
use masking::{PeekInterface, Secret, StrongSecret};
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
use crate::services::kms;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
configs::settings,
|
||||||
consts,
|
consts,
|
||||||
core::errors::{self, RouterResponse, StorageErrorExt},
|
core::errors::{self, RouterResponse, StorageErrorExt},
|
||||||
db::StorageInterface,
|
db::StorageInterface,
|
||||||
@ -12,23 +15,52 @@ use crate::{
|
|||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static HASH_KEY: tokio::sync::OnceCell<StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> =
|
||||||
|
tokio::sync::OnceCell::const_new();
|
||||||
|
|
||||||
|
pub async fn get_hash_key(
|
||||||
|
api_key_config: &settings::ApiKeys,
|
||||||
|
) -> errors::RouterResult<StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> {
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
let hash_key = kms::KeyHandler::get_kms_decrypted_key(
|
||||||
|
&api_key_config.aws_region,
|
||||||
|
&api_key_config.aws_key_id,
|
||||||
|
api_key_config.kms_encrypted_hash_key.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to KMS decrypt API key hashing key")?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "kms"))]
|
||||||
|
let hash_key = &api_key_config.hash_key;
|
||||||
|
|
||||||
|
<[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from(
|
||||||
|
hex::decode(hash_key)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("API key hash key has invalid hexadecimal data")?
|
||||||
|
.as_slice(),
|
||||||
|
)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("The API hashing key has incorrect length")
|
||||||
|
.map(StrongSecret::new)
|
||||||
|
}
|
||||||
|
|
||||||
// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
|
// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
|
||||||
// of plaintext API key being stored in the data store.
|
// of plaintext API key being stored in the data store.
|
||||||
pub struct PlaintextApiKey(Secret<String>);
|
pub struct PlaintextApiKey(Secret<String>);
|
||||||
pub struct HashedApiKey(String);
|
pub struct HashedApiKey(String);
|
||||||
|
|
||||||
impl PlaintextApiKey {
|
impl PlaintextApiKey {
|
||||||
const HASH_KEY_LEN: usize = 32;
|
pub const HASH_KEY_LEN: usize = 32;
|
||||||
|
|
||||||
const PREFIX_LEN: usize = 8;
|
const PREFIX_LEN: usize = 12;
|
||||||
|
|
||||||
pub fn new(length: usize) -> Self {
|
pub fn new(length: usize) -> Self {
|
||||||
|
let env = router_env::env::prefix_for_env();
|
||||||
let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
|
let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
|
||||||
Self(key.into())
|
Self(format!("{env}_{key}").into())
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_hash_key() -> [u8; Self::HASH_KEY_LEN] {
|
|
||||||
common_utils::crypto::generate_cryptographically_secure_random_bytes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_key_id() -> String {
|
pub fn new_key_id() -> String {
|
||||||
@ -96,18 +128,20 @@ impl PlaintextApiKey {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn create_api_key(
|
pub async fn create_api_key(
|
||||||
store: &dyn StorageInterface,
|
store: &dyn StorageInterface,
|
||||||
|
api_key_config: &settings::ApiKeys,
|
||||||
api_key: api::CreateApiKeyRequest,
|
api_key: api::CreateApiKeyRequest,
|
||||||
merchant_id: String,
|
merchant_id: String,
|
||||||
) -> RouterResponse<api::CreateApiKeyResponse> {
|
) -> RouterResponse<api::CreateApiKeyResponse> {
|
||||||
let hash_key = PlaintextApiKey::new_hash_key();
|
let hash_key = HASH_KEY
|
||||||
|
.get_or_try_init(|| get_hash_key(api_key_config))
|
||||||
|
.await?;
|
||||||
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
|
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
|
||||||
let api_key = storage::ApiKeyNew {
|
let api_key = storage::ApiKeyNew {
|
||||||
key_id: PlaintextApiKey::new_key_id(),
|
key_id: PlaintextApiKey::new_key_id(),
|
||||||
merchant_id,
|
merchant_id,
|
||||||
name: api_key.name,
|
name: api_key.name,
|
||||||
description: api_key.description,
|
description: api_key.description,
|
||||||
hash_key: Secret::from(hex::encode(hash_key)),
|
hashed_api_key: plaintext_api_key.keyed_hash(hash_key.peek()).into(),
|
||||||
hashed_api_key: plaintext_api_key.keyed_hash(&hash_key).into(),
|
|
||||||
prefix: plaintext_api_key.prefix(),
|
prefix: plaintext_api_key.prefix(),
|
||||||
created_at: date_time::now(),
|
created_at: date_time::now(),
|
||||||
expires_at: api_key.expiration.into(),
|
expires_at: api_key.expiration.into(),
|
||||||
@ -198,14 +232,19 @@ impl From<HashedApiKey> for storage::HashedApiKey {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_hashing_and_verification() {
|
async fn test_hashing_and_verification() {
|
||||||
|
let settings = settings::Settings::new().expect("invalid settings");
|
||||||
|
|
||||||
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
|
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
|
||||||
let hash_key = PlaintextApiKey::new_hash_key();
|
let hash_key = HASH_KEY
|
||||||
let hashed_api_key = plaintext_api_key.keyed_hash(&hash_key);
|
.get_or_try_init(|| get_hash_key(&settings.api_keys))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek());
|
||||||
|
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
plaintext_api_key.0.peek().as_bytes(),
|
plaintext_api_key.0.peek().as_bytes(),
|
||||||
@ -213,7 +252,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
plaintext_api_key
|
plaintext_api_key
|
||||||
.verify_hash(&hash_key, &hashed_api_key)
|
.verify_hash(hash_key.peek(), &hashed_api_key)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,13 @@ pub async fn api_key_create(
|
|||||||
&req,
|
&req,
|
||||||
payload,
|
payload,
|
||||||
|state, _, payload| async {
|
|state, _, payload| async {
|
||||||
api_keys::create_api_key(&*state.store, payload, merchant_id.clone()).await
|
api_keys::create_api_key(
|
||||||
|
&*state.store,
|
||||||
|
&state.conf.api_keys,
|
||||||
|
payload,
|
||||||
|
merchant_id.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
},
|
},
|
||||||
&auth::AdminApiAuth,
|
&auth::AdminApiAuth,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ pub mod api;
|
|||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
#[cfg(feature = "basilisk")]
|
#[cfg(feature = "basilisk")]
|
||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
|
pub mod kms;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
|
||||||
use std::sync::{atomic, Arc};
|
use std::sync::{atomic, Arc};
|
||||||
|
|||||||
@ -9,6 +9,7 @@ use ring::{aead::*, error::Unspecified};
|
|||||||
use crate::{
|
use crate::{
|
||||||
configs::settings::Jwekey,
|
configs::settings::Jwekey,
|
||||||
core::errors::{self, CustomResult},
|
core::errors::{self, CustomResult},
|
||||||
|
services::kms::KeyHandler,
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,72 +51,6 @@ impl NonceSequence for NonceGen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KeyHandler;
|
|
||||||
|
|
||||||
#[cfg(feature = "kms")]
|
|
||||||
mod kms {
|
|
||||||
use aws_config::meta::region::RegionProviderChain;
|
|
||||||
use aws_sdk_kms::{types::Blob, Client, Region};
|
|
||||||
use base64::Engine;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::consts;
|
|
||||||
|
|
||||||
impl KeyHandler {
|
|
||||||
// Fetching KMS decrypted key
|
|
||||||
// | Amazon KMS decryption
|
|
||||||
// This expect a base64 encoded input but we values are set via aws cli in env than cli
|
|
||||||
// already does that so we don't need to
|
|
||||||
pub async fn get_kms_decrypted_key(
|
|
||||||
aws_keys: &Jwekey,
|
|
||||||
kms_enc_key: String,
|
|
||||||
) -> CustomResult<String, errors::EncryptionError> {
|
|
||||||
let region = aws_keys.aws_region.to_string();
|
|
||||||
let key_id = aws_keys.aws_key_id.clone();
|
|
||||||
let region_provider = RegionProviderChain::first_try(Region::new(region));
|
|
||||||
let shared_config = aws_config::from_env().region(region_provider).load().await;
|
|
||||||
let client = Client::new(&shared_config);
|
|
||||||
let data = consts::BASE64_ENGINE
|
|
||||||
.decode(kms_enc_key)
|
|
||||||
.into_report()
|
|
||||||
.change_context(errors::EncryptionError)
|
|
||||||
.attach_printable("Error decoding from base64")?;
|
|
||||||
let blob = Blob::new(data);
|
|
||||||
let resp = client
|
|
||||||
.decrypt()
|
|
||||||
.key_id(key_id)
|
|
||||||
.ciphertext_blob(blob)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.into_report()
|
|
||||||
.change_context(errors::EncryptionError)
|
|
||||||
.attach_printable("Error decrypting kms encrypted data")?;
|
|
||||||
match resp.plaintext() {
|
|
||||||
Some(inner) => {
|
|
||||||
let bytes = inner.as_ref().to_vec();
|
|
||||||
let res = String::from_utf8(bytes)
|
|
||||||
.into_report()
|
|
||||||
.change_context(errors::EncryptionError)
|
|
||||||
.attach_printable("Could not convert to UTF-8")?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
None => Err(report!(errors::EncryptionError)
|
|
||||||
.attach_printable("Missing plaintext in response")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "kms"))]
|
|
||||||
impl KeyHandler {
|
|
||||||
pub async fn get_kms_decrypted_key(
|
|
||||||
_aws_keys: &Jwekey,
|
|
||||||
key: String,
|
|
||||||
) -> CustomResult<String, errors::EncryptionError> {
|
|
||||||
Ok(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encrypt(msg: &String, key: &[u8]) -> CustomResult<Vec<u8>, errors::EncryptionError> {
|
pub fn encrypt(msg: &String, key: &[u8]) -> CustomResult<Vec<u8>, errors::EncryptionError> {
|
||||||
let nonce_seed = rand::random();
|
let nonce_seed = rand::random();
|
||||||
let mut sealing_key = {
|
let mut sealing_key = {
|
||||||
@ -184,9 +119,19 @@ pub async fn encrypt_jwe(
|
|||||||
let alg = jwe::RSA_OAEP_256;
|
let alg = jwe::RSA_OAEP_256;
|
||||||
let key_id = get_key_id(keys);
|
let key_id = get_key_id(keys);
|
||||||
let public_key = if key_id == keys.locker_key_identifier1 {
|
let public_key = if key_id == keys.locker_key_identifier1 {
|
||||||
KeyHandler::get_kms_decrypted_key(keys, keys.locker_encryption_key1.to_string()).await?
|
KeyHandler::get_kms_decrypted_key(
|
||||||
|
&keys.aws_region,
|
||||||
|
&keys.aws_key_id,
|
||||||
|
keys.locker_encryption_key1.to_string(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
} else {
|
} else {
|
||||||
KeyHandler::get_kms_decrypted_key(keys, keys.locker_encryption_key2.to_string()).await?
|
KeyHandler::get_kms_decrypted_key(
|
||||||
|
&keys.aws_region,
|
||||||
|
&keys.aws_key_id,
|
||||||
|
keys.locker_encryption_key2.to_string(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
};
|
};
|
||||||
let payload = msg.as_bytes();
|
let payload = msg.as_bytes();
|
||||||
let enc = "A256GCM";
|
let enc = "A256GCM";
|
||||||
@ -213,9 +158,19 @@ pub async fn decrypt_jwe(
|
|||||||
let alg = jwe::RSA_OAEP_256;
|
let alg = jwe::RSA_OAEP_256;
|
||||||
let key_id = get_key_id(keys);
|
let key_id = get_key_id(keys);
|
||||||
let private_key = if key_id == keys.locker_key_identifier1 {
|
let private_key = if key_id == keys.locker_key_identifier1 {
|
||||||
KeyHandler::get_kms_decrypted_key(keys, keys.locker_decryption_key1.to_string()).await?
|
KeyHandler::get_kms_decrypted_key(
|
||||||
|
&keys.aws_region,
|
||||||
|
&keys.aws_key_id,
|
||||||
|
keys.locker_decryption_key1.to_string(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
} else {
|
} else {
|
||||||
KeyHandler::get_kms_decrypted_key(keys, keys.locker_decryption_key2.to_string()).await?
|
KeyHandler::get_kms_decrypted_key(
|
||||||
|
&keys.aws_region,
|
||||||
|
&keys.aws_key_id,
|
||||||
|
keys.locker_decryption_key2.to_string(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let decrypter = alg
|
let decrypter = alg
|
||||||
|
|||||||
69
crates/router/src/services/kms.rs
Normal file
69
crates/router/src/services/kms.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use crate::core::errors::{self, CustomResult};
|
||||||
|
|
||||||
|
pub struct KeyHandler;
|
||||||
|
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
mod aws_kms {
|
||||||
|
use aws_config::meta::region::RegionProviderChain;
|
||||||
|
use aws_sdk_kms::{types::Blob, Client, Region};
|
||||||
|
use base64::Engine;
|
||||||
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::consts;
|
||||||
|
|
||||||
|
impl KeyHandler {
|
||||||
|
// Fetching KMS decrypted key
|
||||||
|
// | Amazon KMS decryption
|
||||||
|
// This expect a base64 encoded input but we values are set via aws cli in env than cli
|
||||||
|
// already does that so we don't need to
|
||||||
|
pub async fn get_kms_decrypted_key(
|
||||||
|
aws_region: &str,
|
||||||
|
aws_key_id: &str,
|
||||||
|
kms_enc_key: String,
|
||||||
|
) -> CustomResult<String, errors::EncryptionError> {
|
||||||
|
let region_provider =
|
||||||
|
RegionProviderChain::first_try(Region::new(aws_region.to_owned()));
|
||||||
|
let shared_config = aws_config::from_env().region(region_provider).load().await;
|
||||||
|
let client = Client::new(&shared_config);
|
||||||
|
let data = consts::BASE64_ENGINE
|
||||||
|
.decode(kms_enc_key)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::EncryptionError)
|
||||||
|
.attach_printable("Error decoding from base64")?;
|
||||||
|
let blob = Blob::new(data);
|
||||||
|
let resp = client
|
||||||
|
.decrypt()
|
||||||
|
.key_id(aws_key_id)
|
||||||
|
.ciphertext_blob(blob)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::EncryptionError)
|
||||||
|
.attach_printable("Error decrypting kms encrypted data")?;
|
||||||
|
match resp.plaintext() {
|
||||||
|
Some(inner) => {
|
||||||
|
let bytes = inner.as_ref().to_vec();
|
||||||
|
let res = String::from_utf8(bytes)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::EncryptionError)
|
||||||
|
.attach_printable("Could not convert to UTF-8")?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
None => Err(report!(errors::EncryptionError)
|
||||||
|
.attach_printable("Missing plaintext in response")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "kms"))]
|
||||||
|
impl KeyHandler {
|
||||||
|
pub async fn get_kms_decrypted_key(
|
||||||
|
_aws_region: &str,
|
||||||
|
_aws_key_id: &str,
|
||||||
|
key: String,
|
||||||
|
) -> CustomResult<String, errors::EncryptionError> {
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -392,15 +392,11 @@ impl
|
|||||||
|
|
||||||
let (api_key, plaintext_api_key) = item;
|
let (api_key, plaintext_api_key) = item;
|
||||||
Self {
|
Self {
|
||||||
key_id: api_key.key_id.clone(),
|
key_id: api_key.key_id,
|
||||||
merchant_id: api_key.merchant_id,
|
merchant_id: api_key.merchant_id,
|
||||||
name: api_key.name,
|
name: api_key.name,
|
||||||
description: api_key.description,
|
description: api_key.description,
|
||||||
api_key: StrongSecret::from(format!(
|
api_key: StrongSecret::from(plaintext_api_key.peek().to_owned()),
|
||||||
"{}-{}",
|
|
||||||
api_key.key_id,
|
|
||||||
plaintext_api_key.peek().to_owned()
|
|
||||||
)),
|
|
||||||
created: api_key.created_at,
|
created: api_key.created_at,
|
||||||
expiration: api_key.expires_at.into(),
|
expiration: api_key.expires_at.into(),
|
||||||
}
|
}
|
||||||
@ -410,14 +406,13 @@ impl
|
|||||||
impl ForeignFrom<storage_models::api_keys::ApiKey>
|
impl ForeignFrom<storage_models::api_keys::ApiKey>
|
||||||
for api_models::api_keys::RetrieveApiKeyResponse
|
for api_models::api_keys::RetrieveApiKeyResponse
|
||||||
{
|
{
|
||||||
fn foreign_from(item: storage_models::api_keys::ApiKey) -> Self {
|
fn foreign_from(api_key: storage_models::api_keys::ApiKey) -> Self {
|
||||||
let api_key = item;
|
|
||||||
Self {
|
Self {
|
||||||
key_id: api_key.key_id.clone(),
|
key_id: api_key.key_id,
|
||||||
merchant_id: api_key.merchant_id,
|
merchant_id: api_key.merchant_id,
|
||||||
name: api_key.name,
|
name: api_key.name,
|
||||||
description: api_key.description,
|
description: api_key.description,
|
||||||
prefix: format!("{}-{}", api_key.key_id, api_key.prefix).into(),
|
prefix: api_key.prefix.into(),
|
||||||
created: api_key.created_at,
|
created: api_key.created_at,
|
||||||
expiration: api_key.expires_at.into(),
|
expiration: api_key.expires_at.into(),
|
||||||
}
|
}
|
||||||
@ -427,8 +422,7 @@ impl ForeignFrom<storage_models::api_keys::ApiKey>
|
|||||||
impl ForeignFrom<api_models::api_keys::UpdateApiKeyRequest>
|
impl ForeignFrom<api_models::api_keys::UpdateApiKeyRequest>
|
||||||
for storage_models::api_keys::ApiKeyUpdate
|
for storage_models::api_keys::ApiKeyUpdate
|
||||||
{
|
{
|
||||||
fn foreign_from(item: api_models::api_keys::UpdateApiKeyRequest) -> Self {
|
fn foreign_from(api_key: api_models::api_keys::UpdateApiKeyRequest) -> Self {
|
||||||
let api_key = item;
|
|
||||||
Self::Update {
|
Self::Update {
|
||||||
name: api_key.name,
|
name: api_key.name,
|
||||||
description: api_key.description,
|
description: api_key.description,
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
use diesel::{AsChangeset, AsExpression, Identifiable, Insertable, Queryable};
|
use diesel::{AsChangeset, AsExpression, Identifiable, Insertable, Queryable};
|
||||||
use masking::Secret;
|
|
||||||
use time::PrimitiveDateTime;
|
use time::PrimitiveDateTime;
|
||||||
|
|
||||||
use crate::schema::api_keys;
|
use crate::schema::api_keys;
|
||||||
@ -11,7 +10,6 @@ pub struct ApiKey {
|
|||||||
pub merchant_id: String,
|
pub merchant_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub hash_key: Secret<String>,
|
|
||||||
pub hashed_api_key: HashedApiKey,
|
pub hashed_api_key: HashedApiKey,
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
pub created_at: PrimitiveDateTime,
|
pub created_at: PrimitiveDateTime,
|
||||||
@ -26,7 +24,6 @@ pub struct ApiKeyNew {
|
|||||||
pub merchant_id: String,
|
pub merchant_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub hash_key: Secret<String>,
|
|
||||||
pub hashed_api_key: HashedApiKey,
|
pub hashed_api_key: HashedApiKey,
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
pub created_at: PrimitiveDateTime,
|
pub created_at: PrimitiveDateTime,
|
||||||
|
|||||||
@ -34,7 +34,6 @@ diesel::table! {
|
|||||||
merchant_id -> Varchar,
|
merchant_id -> Varchar,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
description -> Nullable<Varchar>,
|
description -> Nullable<Varchar>,
|
||||||
hash_key -> Varchar,
|
|
||||||
hashed_api_key -> Varchar,
|
hashed_api_key -> Varchar,
|
||||||
prefix -> Varchar,
|
prefix -> Varchar,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
|
|||||||
@ -49,6 +49,9 @@ locker_decryption_key2 = ""
|
|||||||
[webhooks]
|
[webhooks]
|
||||||
outgoing_enabled = true
|
outgoing_enabled = true
|
||||||
|
|
||||||
|
[api_keys]
|
||||||
|
hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
[connectors.aci]
|
[connectors.aci]
|
||||||
base_url = "https://eu-test.oppwa.com/"
|
base_url = "https://eu-test.oppwa.com/"
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
We could have added the `hash_key` column with a default of the plaintext key
|
||||||
|
used for hashing API keys, but we don't do that as it is a hassle to update
|
||||||
|
this migration with the plaintext hash key.
|
||||||
|
*/
|
||||||
|
TRUNCATE TABLE api_keys;
|
||||||
|
|
||||||
|
ALTER TABLE api_keys
|
||||||
|
ADD COLUMN hash_key VARCHAR(64) NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE api_keys DROP CONSTRAINT api_keys_hashed_api_key_key;
|
||||||
11
migrations/2023-02-21-094019_api_keys_remove_hash_key/up.sql
Normal file
11
migrations/2023-02-21-094019_api_keys_remove_hash_key/up.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE api_keys DROP COLUMN hash_key;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Once we've dropped the `hash_key` column, we cannot use the existing API keys
|
||||||
|
from the `api_keys` table anymore, as the `hash_key` is a random string that
|
||||||
|
we no longer have.
|
||||||
|
*/
|
||||||
|
TRUNCATE TABLE api_keys;
|
||||||
|
|
||||||
|
ALTER TABLE api_keys
|
||||||
|
ADD CONSTRAINT api_keys_hashed_api_key_key UNIQUE (hashed_api_key);
|
||||||
Reference in New Issue
Block a user