Skip to content

Conversation

akhildevelops
Copy link
Contributor

This PR provides support to connect to Hashicorp Vault as backend.

Features:

  • Setup Vault as docker container for development.
  • Feature Flags for connecting to different backends: aes, aws, vault.
  • Connect to Vault for generating keys, encrypt, decrypt data.
  • Get DB Error messages instead of overriding with DatabaseError::Others variant

P.S: cargo run will fail as feature flags aren't provided. Instead run with either cargo run --features=<aes/aws/vault>

Fixes: juspay/hyperswitch#6226

@dracarys18 dracarys18 changed the title Hashicorp Vault as Backend to Cripta feat(engine): implement hashicorp vault engine Oct 15, 2024
Copy link
Contributor

@dracarys18 dracarys18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akhildevelops Please mention me once the comments are resolved and encryption/decryption part is implemented for Data encryption

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are creating a docker-compose file we need to create a fully fledged one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/config.rs Outdated
{
// This function doesn't return result therefore unwrapping.
// TODO: Make it customresult return type
let client = init_vault(self.vault_config, self.vault_token).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The panic part can happen inside the init_vault function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there are many expect methods used through out the code that can also panic. Rewired unwrap to expect.

InternalServerError(&'static str),
#[error("Internal Server Error Occurred - {prefix} {}",message.as_ref().unwrap_or(&"".to_string()))]
InternalServerError {
prefix: &'static str,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is prefix here? I think we can just use message alone

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I observe there are many static lifetime strings used for error messages. I can simply transform it to String type but every time memory will be allocated while creating error messages at different code points.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use the message?

@akhildevelops
Copy link
Contributor Author

Cool, @dracarys18 will ping you oince changes are implemented.

@akhildevelops
Copy link
Contributor Author

Hi @dracarys18, I'm done with my changes and also fixes #30

I might not be very active for next couple of days. I'll try to respond for any comments as and when I'm free.

@akhildevelops
Copy link
Contributor Author

akhildevelops commented Oct 16, 2024

Setup vault, postgres and run migrations through

docker compose --file docker/development/docker-compose.yml --profile vault_only up

Run server with vault as backend through cargo run --no-default-features --features=vault
and running with AES backend cargo run

@dracarys18
Copy link
Contributor

@akhildevelops Hey why do we need changes in CI?

@akhildevelops
Copy link
Contributor Author

akhildevelops commented Oct 22, 2024

This PR fails with current ci configuration for cargo check --all-features --all-targets variant because there will be a clash in creating higher level types/objects (Ex: KeyManagerClient type fails).

➜  hyperswitch-encryption-service git:(vault) cargo hack check --all-features --all-targets
info: running `cargo check --all-features --all-targets` on cripta (1/1)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
error[E0428]: the name `KeyManagerClient` is defined multiple times
   --> src/crypto.rs:143:1
    |
140 | pub type KeyManagerClient = EncryptionClient<AwsKmsClient>;
    | ----------------------------------------------------------- previous definition of the type `KeyManagerClient` here
...
143 | pub type KeyManagerClient = EncryptionClient<GcmAes256>;
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `KeyManagerClient` redefined here
    |
    = note: `KeyManagerClient` must be defined only once in the type namespace of this module

As it will open up for all features

#[cfg(feature = "aws")]
pub type KeyManagerClient = EncryptionClient<AwsKmsClient>;


#[cfg(feature = "aes")]
pub type KeyManagerClient = EncryptionClient<GcmAes256>;


#[cfg(feature = "vault")]
pub type KeyManagerClient = EncryptionClient<Vault>;

https://github.com/akhildevelops/hyperswitch-encryption-service/blob/21aa1967c856215dcdf69ba0067dddd8c4132254/src/crypto.rs#L139-L145

It isn't a problem in the current main:63c6794 because of simple aws or !aws.

@dracarys18
Copy link
Contributor

@akhildevelops You can try something like conditional compilation

#[cfg(all(feature = "aws", not(feature = "aes")))]
pub type KeyManagerClient = EncryptionClient<AwsKmsClient>;

@akhildevelops
Copy link
Contributor Author

This will be bloated as and when multiple backends are added

#[cfg(all(feature = "aws", not(feature = "aes"),not(feature = "vault")))]
pub type KeyManagerClient = EncryptionClient<AwsKmsClient>;

@dracarys18
Copy link
Contributor

dracarys18 commented Oct 22, 2024

Really don't wanna do dynamic dispatch but we can do this for readability. Change the current EncryptionClient<T> to this

type Backend = dyn KeyManagement + Send + Sync;

pub struct KeyManagerClient {
    client: Arc<Backend>,
}

impl KeyManagerClient {
    pub fn new(client: Arc<Backend>) -> Self {
        Self { client }
    }
}

impl KeyManagerClient {
    pub fn client(&self) -> &Arc<Backend> {
        &self.client
    }
}

impl Deref for KeyManagerClient {
    type Target = Arc<Backend>;
    fn deref(&self) -> &Self::Target {
        self.client()
    }
}

and this in config.rs

impl Secrets {
    pub async fn create_keymanager_client(self) -> KeyManagerClient {
        #[cfg(feature = "aws")]
        {
            let client = AwsKmsClient::new(&self.kms_config).await;
            EncryptionClient::new(client)
        }

        #[cfg(not(feature = "aws"))]
        {
            let client = self.master_key;
            KeyManagerClient::new(std::sync::Arc::new(client))
        }
    }
}

@gorakhnathy7
Copy link

Hey @akhildevelops Kindly make the changes so that the PR can be merged within time.

@akhildevelops
Copy link
Contributor Author

Even with Dynamic trait objects how can the application decide to choose between vault or aes ? if not(feature="aws") is the only way to decide on aes or vault..

// Is activated for both vault and aes
// Will not work if vault is the backend
#[cfg(not(feature = "aws"))]
        {
            let client = self.master_key;
            KeyManagerClient::new(std::sync::Arc::new(client))
        }

I would ask you to check the ci.yml that doesn't have any harcoded way to run compilation checks for different backends.

Below is the cargo hack command runs checks for each individual backend and every compilation check is successful

➜  hyperswitch-encryption-service git:(vault) cargo hack check --each-feature --exclude-no-default-features --exclude-all-features --exclude-features mtls --all-targets
info: running `cargo check --all-targets --no-default-features --features aes` on cripta (1/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.78s

info: running `cargo check --all-targets --no-default-features --features aws` on cripta (2/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.91s

info: running `cargo check --all-targets --no-default-features --features default` on cripta (3/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.33s

info: running `cargo check --all-targets --no-default-features --features release` on cripta (4/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.91s

info: running `cargo check --all-targets --no-default-features --features vault` on cripta (5/5)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
###########################

➜  hyperswitch-encryption-service git:(vault) cargo hack clippy --each-feature --exclude-no-default-features --exclude-all-features --exclude-features mtls --all-targets
info: running `cargo clippy --all-targets --no-default-features --features aes` on cripta (1/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.09s

info: running `cargo clippy --all-targets --no-default-features --features aws` on cripta (2/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.82s

info: running `cargo clippy --all-targets --no-default-features --features default` on cripta (3/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.81s

info: running `cargo clippy --all-targets --no-default-features --features release` on cripta (4/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.18s

info: running `cargo clippy --all-targets --no-default-features --features vault` on cripta (5/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.77s

@dracarys18
Copy link
Contributor

Even with Dynamic trait objects how can the application decide to choose between vault or aes ? if not(feature="aws") is the only way to decide on aes or vault..

// Is activated for both vault and aes
// Will not work if vault is the backend
#[cfg(not(feature = "aws"))]
        {
            let client = self.master_key;
            KeyManagerClient::new(std::sync::Arc::new(client))
        }

I would ask you to check the ci.yml that doesn't have any harcoded way to run compilation checks for different backends.

Below is the cargo hack command runs checks for each individual backend and every compilation check is successful

➜  hyperswitch-encryption-service git:(vault) cargo hack check --each-feature --exclude-no-default-features --exclude-all-features --exclude-features mtls --all-targets
info: running `cargo check --all-targets --no-default-features --features aes` on cripta (1/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.78s

info: running `cargo check --all-targets --no-default-features --features aws` on cripta (2/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.91s

info: running `cargo check --all-targets --no-default-features --features default` on cripta (3/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.33s

info: running `cargo check --all-targets --no-default-features --features release` on cripta (4/5)
    Checking cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.91s

info: running `cargo check --all-targets --no-default-features --features vault` on cripta (5/5)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
###########################

➜  hyperswitch-encryption-service git:(vault) cargo hack clippy --each-feature --exclude-no-default-features --exclude-all-features --exclude-features mtls --all-targets
info: running `cargo clippy --all-targets --no-default-features --features aes` on cripta (1/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.09s

info: running `cargo clippy --all-targets --no-default-features --features aws` on cripta (2/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.82s

info: running `cargo clippy --all-targets --no-default-features --features default` on cripta (3/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.81s

info: running `cargo clippy --all-targets --no-default-features --features release` on cripta (4/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.18s

info: running `cargo clippy --all-targets --no-default-features --features vault` on cripta (5/5)
   Compiling cripta v0.1.0 (/home/akhil/practice/hyperswitch-encryption-service)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.77s

With dynamic dispatch we don't have to write that line anymore. @akhildevelops If you are not able to do the change let me know I will do it

@akhildevelops
Copy link
Contributor Author

@dracarys18 please go ahead

@dracarys18
Copy link
Contributor

Hi @akhildevelops I have made the changes I will thoroughly test this once and merge this PR! Thanks a lot for the contribution!!

@akhildevelops
Copy link
Contributor Author

akhildevelops commented Oct 29, 2024

@dracarys18 Thanks for the appreciation!!

If I'm correct, you're trying to avoid compilation complexities by including all features and choose feature during runtime. I have a suggestions, please discard if it doesn't help or you're ok with current design:

  • Remove features and pass backend as argument to binary i.e, cargo run -- --backend aws and then initialise KeyManagerClient.

This pattern helps to run any backend without recompilation considering current design of trait objects.

@dracarys18
Copy link
Contributor

@akhildevelops So once you compile for AWS it doesn't make sense to switch to vault at runtime, Because all the keys would be encrypted with AWS, That's why I kept it separate.

@dracarys18
Copy link
Contributor

I have removed the README.md changes since we will add another SETUP.md to add the documentation for the setup

dracarys18
dracarys18 previously approved these changes Nov 12, 2024
Copy link
Contributor

@dracarys18 dracarys18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR looks good to me! Thanks for the PR @akhildevelops , great work!

@dracarys18 dracarys18 requested review from ArjunKarthik and removed request for NishantJoshi00 November 12, 2024 06:19
src/config.rs Outdated
None,
)
.await
.expect("Failed why decrypted vault encrypted secret")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Failed while decrypting vault encrypted secret"

Copy link
Contributor

@ArjunKarthik ArjunKarthik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dracarys18 dracarys18 merged commit b54bf8f into juspay:main Nov 12, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEAT] add support for Hashicorp Vault Engine
4 participants