Skip to content

Commit

Permalink
feat: make crypto logic generic
Browse files Browse the repository at this point in the history
  • Loading branch information
Sakilmostak committed Feb 23, 2025
1 parent 0688972 commit dbf36d1
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 201 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/common_utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ error-stack = "0.4.1"
futures = { version = "0.3.30", optional = true }
globset = "0.4.14"
hex = "0.4.3"
hkdf = "0.12.4"
http = "0.2.12"
md5 = "0.7.0"
nanoid = "0.4.0"
nutype = { version = "0.4.2", features = ["serde"] }
once_cell = "1.19.0"
openssl = "0.10.64"
phonenumber = "0.3.3"
quick-xml = { version = "0.31.0", features = ["serialize"] }
rand = "0.8.5"
Expand All @@ -51,6 +53,7 @@ semver = { version = "1.0.22", features = ["serde"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
serde_urlencoded = "0.7.1"
sha2 = "0.10.8"
signal-hook = { version = "0.3.17", optional = true }
strum = { version = "0.26.2", features = ["derive"] }
thiserror = "1.0.58"
Expand Down
164 changes: 160 additions & 4 deletions crates/common_utils/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! Utilities for cryptographic algorithms
use std::ops::Deref;

use error_stack::ResultExt;
use error_stack::{report, ResultExt};
use masking::{ExposeInterface, Secret};
use md5;
use ring::{
aead::{self, BoundKey, OpeningKey, SealingKey, UnboundKey},
hmac,
};
#[cfg(feature = "logs")]
use router_env::logger;

use crate::{
errors::{self, CustomResult},
Expand Down Expand Up @@ -105,6 +107,29 @@ pub trait DecodeMessage {
&self,
_secret: &[u8],
_msg: Secret<Vec<u8>, EncryptionStrategy>,
_iv: Option<&[u8]>,
) -> CustomResult<Vec<u8>, errors::CryptoError>;
}

/// Trait for generating shared key using public and private keys
pub trait GetSharedKey {
/// Takes in a public key and a private key and returns the shared key
fn get_shared_key(
&self,
_public_key: &[u8],
_private_key: &[u8],
) -> CustomResult<Vec<u8>, errors::CryptoError>;
}

/// Trait for deriving key (Symmetric Key + MAC Key) from a secret
pub trait DeriveKey {
/// Takes in a key, a shared secret and an optional salt and returns the derived key
fn derive_key(
&self,
_key: &[u8],
_shared_secret: &[u8],
_info: &[u8],
_salt: Option<&[u8]>,
) -> CustomResult<Vec<u8>, errors::CryptoError>;
}

Expand Down Expand Up @@ -149,6 +174,7 @@ impl DecodeMessage for NoAlgorithm {
&self,
_secret: &[u8],
msg: Secret<Vec<u8>, EncryptionStrategy>,
_iv: Option<&[u8]>,
) -> CustomResult<Vec<u8>, errors::CryptoError> {
Ok(msg.expose())
}
Expand Down Expand Up @@ -305,6 +331,7 @@ impl DecodeMessage for GcmAes256 {
&self,
secret: &[u8],
msg: Secret<Vec<u8>, EncryptionStrategy>,
_iv: Option<&[u8]>,
) -> CustomResult<Vec<u8>, errors::CryptoError> {
let msg = msg.expose();
let key = UnboundKey::new(&aead::AES_256_GCM, secret)
Expand Down Expand Up @@ -416,6 +443,135 @@ impl VerifySignature for Sha256 {
}
}

/// Advanceed Encrypption Standard 256 with Counter(CTR) mode
#[derive(Debug)]
pub struct CtrAes256;

impl DecodeMessage for CtrAes256 {
fn decode_message(
&self,
secret: &[u8],
msg: Secret<Vec<u8>, EncryptionStrategy>,
iv: Option<&[u8]>,
) -> CustomResult<Vec<u8>, errors::CryptoError> {
let encrypted_message = msg.expose();
// extract the tag from the end of the encrypted message
let tag = encrypted_message
.get(encrypted_message.len() - 16..)
.ok_or(errors::CryptoError::DecodingFailed)?;

let cipher = openssl::symm::Cipher::aes_256_ctr();
let decrypted_data =
openssl::symm::decrypt_aead(cipher, secret, iv, &[], &encrypted_message, tag)
.change_context(errors::CryptoError::DecodingFailed)?;

Ok(decrypted_data)
}
}

/// Elliptic Curve Digital Signature Algorithm
#[derive(Debug)]
pub struct ECDSA;

impl VerifySignature for ECDSA {
fn verify_signature(
&self,
secret: &[u8],
signature: &[u8],
msg: &[u8],
) -> CustomResult<bool, errors::CryptoError> {
// parse the DER-encoded data as an EC public key
let ec_key = openssl::ec::EcKey::public_key_from_der(secret)
.change_context(errors::CryptoError::DerivingEcKeyFailed)?;

// parse the signature using ECDSA
let ecdsa_signature = openssl::ecdsa::EcdsaSig::from_der(signature)
.change_context(errors::CryptoError::EcdsaSignatureFailed)?;

// hash the signed data
let message_hash = openssl::sha::sha256(msg);

// verify the signature
ecdsa_signature
.verify(&message_hash, &ec_key)
.change_context(errors::CryptoError::SignatureVerificationFailed)
}
}

impl GetSharedKey for ECDSA {
fn get_shared_key(
&self,
public_key: &[u8],
private_key: &[u8],
) -> CustomResult<Vec<u8>, errors::CryptoError> {
let private_key = openssl::pkey::PKey::private_key_from_pkcs8(private_key)
.change_context(errors::CryptoError::DerivingPrivateKeyFailed)
.attach_printable("cannot convert private key from decode_key")?;

let group = openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)
.change_context(errors::CryptoError::DerivingEcGroupFailed)?;

let mut big_num_context = openssl::bn::BigNumContext::new()
.change_context(errors::CryptoError::BigNumAllocationFailed)?;

let ec_key = openssl::ec::EcPoint::from_bytes(&group, public_key, &mut big_num_context)
.change_context(errors::CryptoError::DerivingEcKeyFailed)?;

// create an ephemeral public key from the given bytes
let ephemeral_public_key = openssl::ec::EcKey::from_public_key(&group, &ec_key)
.change_context(errors::CryptoError::DerivingPublicKeyFailed)?;

// wrap the public key in a PKey
let ephemeral_pkey = openssl::pkey::PKey::from_ec_key(ephemeral_public_key)
.change_context(errors::CryptoError::DerivingPublicKeyFailed)?;

// perform ECDH to derive the shared key
let mut deriver = openssl::derive::Deriver::new(&private_key)
.change_context(errors::CryptoError::DerivingSharedSecretKeyFailed)?;

deriver
.set_peer(&ephemeral_pkey)
.change_context(errors::CryptoError::DerivingSharedSecretKeyFailed)?;

let shared_key = deriver
.derive_to_vec()
.change_context(errors::CryptoError::DerivingSharedSecretKeyFailed)?;

Ok(shared_key)
}
}

/// HMAC-based Key Derivation Function with SHA-256
#[derive(Debug)]
pub struct HkdfSha256;

impl DeriveKey for HkdfSha256 {
fn derive_key(
&self,
key: &[u8],
shared_secret: &[u8],
info: &[u8],
salt: Option<&[u8]>,
) -> CustomResult<Vec<u8>, errors::CryptoError> {
// concatenate key and shared secret
let input_key_material = [key, shared_secret].concat();

// initialize HKDF with salt and SHA-256 as the hash function
let hkdf: ::hkdf::Hkdf<sha2::Sha256> = ::hkdf::Hkdf::new(salt, &input_key_material);

// derive 64 bytes for the output key (symmetric encryption + MAC key)
let mut output_key = vec![0u8; 64];

hkdf.expand(info, &mut output_key).map_err(|_| {
#[cfg(feature = "logs")]
logger::error!("Failed to derive the shared secret");
report!(errors::CryptoError::DerivingSharedSecretFailed)
})?;

Ok(output_key)
}
}

/// Generate a random string using a cryptographically secure pseudo-random number generator
/// (CSPRNG). Typically used for generating (readable) keys and passwords.
#[inline]
Expand Down Expand Up @@ -666,7 +822,7 @@ mod crypto_tests {

assert_eq!(
algorithm
.decode_message(&secret, encoded_message.into())
.decode_message(&secret, encoded_message.into(), None)
.expect("Decode Failed"),
message
);
Expand Down Expand Up @@ -694,7 +850,7 @@ mod crypto_tests {
let algorithm = super::GcmAes256;

let decoded = algorithm
.decode_message(&right_secret, message.clone().into())
.decode_message(&right_secret, message.clone().into(), None)
.expect("Decoded message");

assert_eq!(
Expand All @@ -703,7 +859,7 @@ mod crypto_tests {
.expect("Decoded plaintext message")
);

let err_decoded = algorithm.decode_message(&wrong_secret, message.into());
let err_decoded = algorithm.decode_message(&wrong_secret, message.into(), None);

assert!(err_decoded.is_err());
}
Expand Down
24 changes: 24 additions & 0 deletions crates/common_utils/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ pub enum CryptoError {
/// The cryptographic algorithm was unable to verify the given signature
#[error("Failed to verify signature")]
SignatureVerificationFailed,
/// The cryptographic algorithm was unable to generate a elliptic curve key
#[error("Failed to derive Elliptic Curve key")]
DerivingEcKeyFailed,
/// The cryptographic algorithm was unable to generate ECDSA signature
#[error("Failed to get the ECDSA signature")]
EcdsaSignatureFailed,
/// The cryptographic algorithm was unable to derive Elliptic Curve group
#[error("Failed to Derive Elliptic Curve group")]
DerivingEcGroupFailed,
/// The cryptographic algorithm failed to generate a random number
#[error("Failed to allocate memory for big number")]
BigNumAllocationFailed,
/// The cryptographic algorithm failed to derive a shared secret key
#[error("Failed to derive a shared secret key")]
DerivingSharedSecretKeyFailed,
/// The cryptographic algorithm failed to derive a public key
#[error("Failed to Derive Public key")]
DerivingPublicKeyFailed,
/// The cryptographic algorithm failed to derive a private key
#[error("Failed to Derive Private key")]
DerivingPrivateKeyFailed,
/// The cryptographic algorithm failed to derive a shared secret
#[error("Failed to derive a shared ephemeral key")]
DerivingSharedSecretFailed,
}

/// Errors for Qr code handling
Expand Down
13 changes: 7 additions & 6 deletions crates/hyperswitch_domain_models/src/type_encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ mod encrypt {
) -> CustomResult<Self, errors::CryptoError> {
metrics::APPLICATION_DECRYPTION_COUNT.add(1, &[]);
let encrypted = encrypted_data.into_inner();
let data = crypt_algo.decode_message(key, encrypted.clone())?;
let data = crypt_algo.decode_message(key, encrypted.clone(), None)?;

let value: String = std::str::from_utf8(&data)
.change_context(errors::CryptoError::DecodingFailed)?
Expand Down Expand Up @@ -336,7 +336,7 @@ mod encrypt {
encrypted_data
.into_iter()
.map(|(k, v)| {
let data = crypt_algo.decode_message(key, v.clone().into_inner())?;
let data = crypt_algo.decode_message(key, v.clone().into_inner(), None)?;
let value: String = std::str::from_utf8(&data)
.change_context(errors::CryptoError::DecodingFailed)?
.to_string();
Expand Down Expand Up @@ -454,7 +454,7 @@ mod encrypt {
) -> CustomResult<Self, errors::CryptoError> {
metrics::APPLICATION_DECRYPTION_COUNT.add(1, &[]);
let encrypted = encrypted_data.into_inner();
let data = crypt_algo.decode_message(key, encrypted.clone())?;
let data = crypt_algo.decode_message(key, encrypted.clone(), None)?;

let value: serde_json::Value = serde_json::from_slice(&data)
.change_context(errors::CryptoError::DecodingFailed)?;
Expand Down Expand Up @@ -571,7 +571,8 @@ mod encrypt {
encrypted_data
.into_iter()
.map(|(k, v)| {
let data = crypt_algo.decode_message(key, v.clone().into_inner().clone())?;
let data =
crypt_algo.decode_message(key, v.clone().into_inner().clone(), None)?;

let value: serde_json::Value = serde_json::from_slice(&data)
.change_context(errors::CryptoError::DecodingFailed)?;
Expand Down Expand Up @@ -923,7 +924,7 @@ mod encrypt {
) -> CustomResult<Self, errors::CryptoError> {
metrics::APPLICATION_DECRYPTION_COUNT.add(1, &[]);
let encrypted = encrypted_data.into_inner();
let data = crypt_algo.decode_message(key, encrypted.clone())?;
let data = crypt_algo.decode_message(key, encrypted.clone(), None)?;
Ok(Self::new(data.into(), encrypted))
}

Expand Down Expand Up @@ -1039,7 +1040,7 @@ mod encrypt {
k,
Self::new(
crypt_algo
.decode_message(key, v.clone().into_inner().clone())?
.decode_message(key, v.clone().into_inner().clone(), None)?
.into(),
v.into_inner(),
),
Expand Down
2 changes: 1 addition & 1 deletion crates/hyperswitch_interfaces/src/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

algorithm
.decode_message(&secret.secret, message.into())
.decode_message(&secret.secret, message.into(), None)
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)
}

Expand Down
2 changes: 0 additions & 2 deletions crates/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ encoding_rs = "0.8.33"
error-stack = "0.4.1"
futures = "0.3.30"
hex = "0.4.3"
hkdf = "0.12.4"
http = "0.2.12"
hyper = "0.14.28"
infer = "0.15.0"
Expand Down Expand Up @@ -106,7 +105,6 @@ serde_repr = "0.1.19"
serde_urlencoded = "0.7.1"
serde_with = "3.7.0"
sha1 = { version = "0.10.6" }
sha2 = "0.10.8"
strum = { version = "0.26", features = ["derive"] }
tera = "1.19.1"
thiserror = "1.0.58"
Expand Down
Loading

0 comments on commit dbf36d1

Please sign in to comment.