Skip to content

Commit

Permalink
245 webhook payload encryption (#246)
Browse files Browse the repository at this point in the history
* Adds webook paylod encryption

* Updates docu.

* Fixes Readme.

* Fixes readme.

* Implements Hybrid webhook encryption.

---------

Co-authored-by: Andreas Finke <[email protected]>
  • Loading branch information
hey-johnnypark and Andreas Finke authored Sep 8, 2023
1 parent f427c99 commit 51457d8
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 1 deletion.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ SSL forcing can be disabled by setting

- `DISABLE_SSL_FORCE`

You can enable webhook payload encryption by setting

- `WEBHOOK_ENCRYPTION_KEY`

It expects to be a base64-encoded RSA public key in PEM format (see below).


you can store these in a local .env file for development.

It's done via environment variables. You can utilize a `.env` file while
Expand All @@ -72,6 +79,46 @@ generate one, you can use the following command:
ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'
```

### Generate a Webhook encryption key


If OpenSSL is not installed, please refer to the OpenSSL documentation for installation instructions specific to your operating system.

#### Step 1: Generate the Private-Public Keypair

To generate the private-public keypair, follow these steps:

1. Open your terminal or command prompt.

2. Run the following command to generate a private key file named `private_key.pem`:
```bash
openssl genpkey -algorithm RSA -out private_key.pem
```

3. You will be prompted to set a passphrase for the private key. Choose a strong passphrase and remember it for future use.

4. Run the following command to generate the corresponding public key file named `public_key.pem`:

```bash
openssl rsa -pubout -in private_key.pem -out public_key.pem
```

5. Remember to keep the private key (`private_key.pem`) secure and do not share it with anyone.

#### Step 2: Encode the Public Key in Base64

To encode the public key in Base64, follow these steps:

1. Use the following command to encode the public key in Base64:

```bash
openssl base64 -in public_key.pem -out public_key_base64.txt
```

2. The public key is now encoded in Base64 and saved as `public_key_base64.txt`. The file contains the Base64-encoded public key.

Congratulations! You have successfully generated a private-public keypair, converted the public key to a `.pem` file, and encoded it in Base64. You can now use it for `WEBHOOK_ENCRYPTION_KEY` (see above).

## Usage

see [docs.ebicsbox.apiary.io](http://docs.ebicsbox.apiary.io)
Expand Down
22 changes: 21 additions & 1 deletion box/models/webhook_delivery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

require_relative './event'
require_relative '../middleware/signer'
require_relative '../../config/configuration'

module Box
class WebhookDelivery < Sequel::Model
Expand Down Expand Up @@ -43,7 +44,8 @@ def execute_request
response = conn.post do |req|
req.url URI(event.callback_url).path
req.headers['Content-Type'] = 'application/json'
req.body = event.to_webhook_payload.to_json
payload = event.to_webhook_payload.to_json
req.body = Box.configuration.encrypt_webhooks? ? encrypt(payload) : payload
end
end
rescue Faraday::TimeoutError, Faraday::ConnectionFailed, Faraday::Error => ex
Expand All @@ -52,6 +54,24 @@ def execute_request
end
[response, execution_time]
end

def encrypt(payload)
aes_key = OpenSSL::Cipher.new('AES-256-CBC').random_key
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = aes_key
encrypted_payload_base64 = Base64.encode64(cipher.update(payload) + cipher.final)
public_key = OpenSSL::PKey::RSA.new(Base64.decode64(Box.configuration.webhook_encryption_key))
encrypted_aes_key_base64 = Base64.encode64(public_key.public_encrypt(aes_key))
transformToJsonString(encrypted_aes_key_base64, encrypted_payload_base64)
end

def transformToJsonString (encrypted_aes_key_base64, encrypted_payload_base64)
{
'encrypted_aes_key_base64' => encrypted_aes_key_base64,
'encrypted_payload_base64' => encrypted_payload_base64
}.to_json
end

class FailedResponse
def initialize(message)
Expand Down
9 changes: 9 additions & 0 deletions config/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,14 @@ def valid?

jwt_secret unless static_auth?
end

def webhook_encryption_key
ENV['WEBHOOK_ENCRYPTION_KEY']
end

def encrypt_webhooks?
webhook_encryption_key != nil
end

end
end
58 changes: 58 additions & 0 deletions spec/models/webhook_delivery_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# frozen_string_literal: true

require 'faraday'
require 'openssl'
require 'base64'
require 'json'

module Box
RSpec.describe WebhookDelivery do
Expand Down Expand Up @@ -41,6 +44,61 @@ module Box
it 'returns a response' do
expect(subject.execute_request.size).to eq(2)
end

it 'does encrypt the payload if encrypt_webhooks is true' do
# Set up Event to return consistent payload
expected_payload = {"foo": "bar"}
allow_any_instance_of(Box::Event).to receive(:to_webhook_payload).and_return(expected_payload)

# Set up public encryption key
webhook_encryption_public_key_pem = File.read('./spec/webhook_encryption_public_key_RSPEC_ONLY.pem')
webhook_encryption_public_key_base_64 = Base64.encode64(webhook_encryption_public_key_pem)
webhook_encryption_public_key = OpenSSL::PKey::RSA.new(webhook_encryption_public_key_pem)

# Set up private encryption key
webhook_encryption_private_key_pem = File.read('./spec/webhook_encryption_private_key_RSPEC_ONLY.pem')
webhook_encryption_private_key = OpenSSL::PKey::RSA.new(webhook_encryption_private_key_pem)
aes_key = OpenSSL::Cipher.new('AES-256-CBC')

# Set up configuration
allow_any_instance_of(Box::Configuration).to receive(:encrypt_webhooks?).and_return(true)
allow_any_instance_of(Box::Configuration).to receive(:webhook_encryption_key).and_return(webhook_encryption_public_key_base_64)
allow(OpenSSL::Cipher).to receive(:new).and_return(aes_key)

expected_payload_encrypted = Base64.encode64(webhook_encryption_public_key.public_encrypt(expected_payload.to_json))

allow_any_instance_of(Faraday::Request).to receive(:body=) do |instance, payload|
payload_as_json = JSON.parse(payload)

decrypted_aes_key = webhook_encryption_private_key.private_decrypt(Base64.decode64(payload_as_json['encrypted_aes_key_base64']))
encrypted_payload = Base64.decode64(payload_as_json['encrypted_payload_base64'])

cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.decrypt
cipher.key = decrypted_aes_key
decrypted_payload = cipher.update(encrypted_payload) + cipher.final

expect(decrypted_payload).to eq(expected_payload.to_json)
end

subject.execute_request
end

it 'does not encrypt payload if encrypt_webhooks is false' do
# Set up Event to return consistent payload
expected_payload = {"foo": "bar"}
allow_any_instance_of(Box::Event).to receive(:to_webhook_payload).and_return(expected_payload)

# Set up configuration
allow_any_instance_of(Box::Configuration).to receive(:encrypt_webhooks?).and_return(false)

allow_any_instance_of(Faraday::Request).to receive(:body=) do |instance, payload|
expect(payload).to eq(expected_payload.to_json)
end

subject.execute_request
end

end

context 'without auth defined' do
Expand Down
51 changes: 51 additions & 0 deletions spec/webhook_encryption_private_key_RSPEC_ONLY.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
13 changes: 13 additions & 0 deletions spec/webhook_encryption_public_key_RSPEC_ONLY.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAx+ee0Nm05q9zvGgUihLw78XPJvc359rrP6VUwdd/AUHLeMMP6BsJ
Mtc+jwum6Ia2AomzffsJDAypdS77b2Pmt+KzN0GIsI30w3xKzEvgP1Qv+Rb07SjY
TJJ7i5zKMJAa1LfX2ThO89oBqwhP8mylZFowSBmIkTHTkfNvcCa40EMPk4hOSd7M
qK0ZtmSlWO+zzkm5IgbVUlurKX4Fu34Llx6EqXJE3WlRljwFvPy+S7SJCh4kpIL+
OQ7VRShn+RtmvfyhxZt5FlypZ5OenoZOmWPoXHfQwdjxozJsAodYdnwricuXdSGI
/mqw9e5NFORQZUEtkgCjKDTkBc/7a4XQefAOhBhqVOafbMnsvb1DlPcCv1dQnFYn
jr7BI9ahA+AecFP3YIHtf2VKA0K14ogf1H4Q7VbJYFeJvlSt+yxWKlNRZz7dOT2r
MeQ2PpVM4B7B5ufBi50mo/+YgCixk2YbjQIxahxxk6uojW3nhbnWKaFU9j7hs1Z/
97T4H6NrekP6Rde5Y6iHM6apmiPby+Y1xmcWHK5yna2m7EUn5w3gGx7jmZOM477c
l7rvb/+LyIudtySr0t6bOHuhWMCRTwqio6UcPU4z4DJtkxgd9PglfIEKLyqvouNQ
Q5M5k8g1kHKUpuT3vVckvMz/ruPy3IvER5IhVzKtmyjfUhE+ixczNlcCAwEAAQ==
-----END RSA PUBLIC KEY-----

0 comments on commit 51457d8

Please sign in to comment.