evervault 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e94804544d442e809f5a381a6e650091afd686aea080f54edf50c10ed0c3751b
4
- data.tar.gz: 2aa82fb3c03cdd1424b13392165c135d3607401c9f8b2532c4fa1d12044f4046
3
+ metadata.gz: 7382a19b6a9fa82b2d37caa8f3e440f7bfce74bc18a237f13bdbd598dbb0d855
4
+ data.tar.gz: 78d8c04663caeca5ec0ff9336f403ec496b69fa61b25f134c9fae66eff91ab78
5
5
  SHA512:
6
- metadata.gz: b84446fc0f2e3a2e0f9b5505934060d4483bf1e0452b0e0b14cb1befc871a43ba1835d4fe985e7144bda8be5162914e334e2fc425e7b1d77a80a3b7c98e7b725
7
- data.tar.gz: 4891c7eb1bd66ff1677fa56a403b60763bfce49c7e4e10e5bae56a5828132d831ac51972c0e4a43877bbca4c483c674e4ac0456ee2449b04429218666acde9da
6
+ metadata.gz: f0154d3c18eaa8685e6f1d26069c286609eaeb0bf8cc56049f53870ea3d374d0c7f45aeb855e47094dfc30b8a55d54d1122fac2ffc008e8a252ca843e711081e
7
+ data.tar.gz: 3915f0582de2f85ca71e9769f1cb4bf2bdd3013f50903beb8ae1946a39d9dd87733e340159aaf5c5192872ab1685ab39d6decd43d45487bd56dc8a4ea1914b4c
@@ -0,0 +1,6 @@
1
+ # This file is used to automatically request reviews on PRs.
2
+
3
+ # These owners will be the default owners for everything in
4
+ # the repo, unless a match on a later line takes precedence.
5
+ # "@evervault/product-engineering" below requests review from all members of Product Engineering.
6
+ * @evervault/product-engineering
@@ -0,0 +1,6 @@
1
+ # Why
2
+ Add a short description about this PR.
3
+ Add links to issues, tech plans etc.
4
+
5
+ # How
6
+ Describe how you've approached the problem
@@ -1,19 +1,38 @@
1
1
  name: Publish Gem
2
2
 
3
- on: [push]
4
-
3
+ on:
4
+ push:
5
+ tags:
6
+ - v*
5
7
 
6
8
  jobs:
7
-
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu, macos]
14
+ ruby: [2.5, 2.6, 2.7, 3.1, truffleruby]
15
+ runs-on: ${{ matrix.os }}-latest
16
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby }}
22
+ - run: bundle install
23
+ - run: bundle exec rake
24
+
8
25
  build:
9
26
  runs-on: ubuntu-latest
10
-
27
+ needs: test
28
+
11
29
  steps:
12
30
  - uses: actions/checkout@v2
13
31
  - uses: ruby/setup-ruby@v1
14
32
  with:
15
33
  ruby-version: 2.7
16
34
  - name: Release Gem
35
+ if: contains(github.ref, 'refs/tags/v')
17
36
  run: ./scripts/release.sh
18
37
  env:
19
38
  GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -7,7 +7,7 @@ jobs:
7
7
  fail-fast: false
8
8
  matrix:
9
9
  os: [ubuntu, macos]
10
- ruby: [2.5, 2.6, 2.7, jruby, jruby-head, truffleruby]
10
+ ruby: [2.5, 2.6, 2.7, 3.1, truffleruby]
11
11
  runs-on: ${{ matrix.os }}-latest
12
12
  continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
13
13
  steps:
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ gemspec
6
6
  gem "rake", "~> 12.0"
7
7
  gem "rspec", "~> 3.0"
8
8
  gem "pry"
9
- gem "typhoeus"
10
9
  gem "webmock"
11
10
  gem "gem-release"
11
+ gem "faraday"
12
+ gem "rexml"
data/README.md CHANGED
@@ -1,94 +1,115 @@
1
- # Evervault
2
- <p align="center">
3
- <img src="res/logo.svg">
4
- </p>
1
+ [![Evervault](https://evervault.com/evervault.svg)](https://evervault.com/)
5
2
 
6
- <p align="center">
7
- <a href="https://github.com/evervault/evervault-ruby/actions?query=workflow%3Aevervault-unit-tests"><img alt="Evervault unit tests status" src="https://github.com/evervault/evervault-ruby/workflows/evervault-unit-tests/badge.svg"></a>
8
- </p>
3
+ [![Unit Tests Status](https://github.com/evervault/evervault-ruby/workflows/evervault-unit-tests/badge.svg)](https://github.com/evervault/evervault-ruby/actions?query=workflow%3Aevervault-unit-tests)
9
4
 
5
+ # Evervault Ruby SDK
6
+
7
+ The [Evervault](https://evervault.com) Ruby SDK is a toolkit for encrypting data as it enters your server, working with Cages, and proxying your outbound API requests to specific domains through [Outbound Relay](https://docs.evervault.com/concepts/relay/outbound-interception) to allow them to be decrypted before reaching their target.
10
8
 
11
9
  ## Getting Started
12
- Ruby SDK for [Evervault](https://evervault.com)
13
- ### Prerequisites
14
10
 
15
- To get started with the Evervault Ruby SDK, you will need to have created a team on the evervault dashboard.
11
+ Before starting with the Evervault Ruby SDK, you will need to [create an account](https://app.evervault.com/register) and a team.
12
+
13
+ For full installation support, [book time here](https://calendly.com/evervault/cages-onboarding).
14
+
15
+ ## Documentation
16
16
 
17
- We are currently in invite-only early access. You can apply for early access [here](https://evervault.com).
17
+ See the Evervault [Ruby SDK documentation](https://docs.evervault.com/ruby).
18
18
 
19
19
  ## Installation
20
20
 
21
- Add this line to your application's Gemfile:
21
+ There are two ways to install the Ruby SDK.
22
+
23
+ #### 1. With Gemfile
24
+
25
+ Add this line to your application's `Gemfile`:
22
26
 
23
27
  ```ruby
24
28
  gem 'evervault'
25
29
  ```
26
30
 
27
- And then execute:
31
+ Then, run:
32
+
28
33
  ```sh
29
- bundle install
34
+ bundle install
30
35
  ```
31
- Or install it yourself as:
36
+ #### 2. By yourself
37
+
38
+ Just run:
39
+
32
40
  ```sh
33
- gem install evervault
41
+ gem install evervault
34
42
  ```
35
43
 
36
44
  ## Setup
37
45
 
38
- Evervault can be initialized as a singleton throughout the lifecycle of your application.
46
+ To make Evervault available for use in your app:
47
+
39
48
  ```ruby
40
49
  require "evervault"
41
50
 
42
- # Initialize the client with your team's api key
51
+ # Initialize the client with your team's API key
43
52
  Evervault.api_key = <YOUR-API-KEY>
44
53
 
45
54
  # Encrypt your data and run a cage
46
- result = Evervault.encrypt_and_run(<CAGE-NAME>, { hello: 'World!' })
55
+ encrypted_data = Evervault.encrypt({ hello: 'World!' })
56
+
57
+ # Process the encrypted data in a Cage
58
+ result = Evervault.run(<CAGE-NAME>, encrypted_data)
47
59
  ```
48
60
 
49
- It's recommended to re-use your Evervault client, to prevent additional overhead of loading keys at runtime, so the singleton pattern should be the go-to pattern for most use-cases.
61
+ ## Reference
50
62
 
51
- However, if you'd prefer to initialize different clients at different times, for example, if you have multiple teams and need to switch context, you can simply create a client:
52
- ```ruby
53
- require "evervault"
63
+ The Evervault Ruby SDK exposes eight methods.
54
64
 
55
- # Initialize the client with your team's api key
56
- evervault = Evervault::Client.new(api_key: <YOUR-API-KEY>)
65
+ ### Evervault.encrypt
57
66
 
58
- # Encrypt your data and run a cage
59
- result = evervault.encrypt_and_run(<CAGE-NAME>, { hello: 'World!' })
67
+ `Evervault.encrypt` encrypts data for use in your [Evervault Cages](https://docs.evervault.com/tutorial). To encrypt data on your server, simply pass a supported value into the `Evervault.encrypt` method and then you can store the encrypted data in your database as normal.
68
+
69
+ ```ruby
70
+ Evervault.encrypt(data = String | Number | Boolean | Hash | Array)
60
71
  ```
61
72
 
62
- ## API Reference
73
+ | Parameter | Type | Description |
74
+ | --------- | ---- | ----------- |
75
+ | data | `String`, `Number`, `Boolean`, `Hash`, `Array` | Data to be encrypted |
63
76
 
64
- ### Evervault.encrypt
77
+ ### Evervault.relay
65
78
 
66
- Encrypt lets you encrypt data for use in any of your evervault cages. You can use it to store encrypted data to be used in a cage at another time.
79
+ `Evervault.relay` specifies which domains should be proxied through outbound relay. See [Outbound Relay](https://docs.evervault.com/concepts/relay/outbound-interception) to learn more.
67
80
 
68
81
  ```ruby
69
- Evervault.encrypt(data = Hash | String)
82
+ Evervault.relay(decryption_domains = Array)
70
83
  ```
71
84
 
72
85
  | Parameter | Type | Description |
73
86
  | --------- | ---- | ----------- |
74
- | data | Hash or String | Data to be encrypted |
87
+ | decryption_domains | `Array` | Requests sent to any of the domains listed will be proxied through outbound relay |
75
88
 
76
89
  ### Evervault.run
77
90
 
78
- Run lets you invoke your evervault cages with a given payload.
91
+ `Evervault.run` invokes a Cage with a given payload.
79
92
 
80
93
  ```ruby
81
- Evervault.run(cage_name = String, data = Hash)
94
+ Evervault.run(cage_name = String, data = Hash[, options = Hash])
82
95
  ```
83
96
 
84
97
  | Parameter | Type | Description |
85
98
  | --------- | ---- | ----------- |
86
- | cageName | String | Name of the cage to be run |
87
- | data | Hash | Payload for the cage |
99
+ | cageName | String | Name of the Cage to be run |
100
+ | data | Hash | Payload for the Cage |
101
+ | options | Hash | [Options for the Cage run](#Cage-Run-Options) |
102
+
103
+ #### Cage Run Options
104
+
105
+ | Option | Type | Default | Description |
106
+ | ------ | ---- | ------- | ----------- |
107
+ | `async` | `Boolean` | `false` | Run your Cage in async mode. Async Cage runs will be queued for processing. |
108
+ | `version` | `Integer` | `nil` | Specify the version of your Cage to run. By default, the latest version will be run. |
88
109
 
89
110
  ### Evervault.encrypt_and_run
90
111
 
91
- Encrypt your data and use it as the payload to invoke the cage.
112
+ Encrypt your data and use it as the payload to invoke the Cage.
92
113
 
93
114
  ```ruby
94
115
  Evervault.encrypt_and_run(cage_name = String, data = Hash)
@@ -96,7 +117,7 @@ Evervault.encrypt_and_run(cage_name = String, data = Hash)
96
117
 
97
118
  | Parameter | Type | Description |
98
119
  | --------- | ---- | ----------- |
99
- | cageName | String | Name of the cage to be run |
120
+ | cageName | String | Name of the Cage to be run |
100
121
  | data | dict | Data to be encrypted |
101
122
 
102
123
  ### Evervault.cages
@@ -129,7 +150,7 @@ Evervault.cages
129
150
 
130
151
  ### Evervault.cage_list
131
152
 
132
- Return a `CageList` object, containing a list of your team's cages
153
+ Return a `CageList` object, containing a list of your team's Cages
133
154
 
134
155
  ```ruby
135
156
  Evervault.cage_list
@@ -167,7 +188,7 @@ Evervault.cage_list
167
188
 
168
189
  #### CageList.to_hash
169
190
 
170
- Converts a list of cages to a hash with keys of CageName => Cage Model
191
+ Converts a list of Cages to a hash with keys of CageName => Cage Model
171
192
 
172
193
  ```ruby
173
194
  Evervault.cage_list.to_hash
@@ -210,9 +231,9 @@ Evervault.cage_list.to_hash
210
231
 
211
232
  ### Evervault::Models::Cage.run
212
233
 
213
- Each Cage model exposes a `run` method, which allows you to run that particular cage.
234
+ Each Cage model exposes a `run` method, which allows you to run that particular Cage.
214
235
 
215
- *Note*: this does not encrypt data before running the cage
236
+ *Note*: this does not encrypt data before running the Cage.
216
237
  ```ruby
217
238
  cage = Evervault.cage_list.cages[0]
218
239
  cage.run({'name': 'testing'})
@@ -221,7 +242,8 @@ cage.run({'name': 'testing'})
221
242
 
222
243
  | Parameter | Type | Description |
223
244
  | --------- | ---- | ----------- |
224
- | data | Hash | Payload for the cage |
245
+ | data | Hash | Payload for the Cage |
246
+ | options | Hash | [Options for the Cage run](#Cage-Run-Options) |
225
247
 
226
248
  ## Development
227
249
 
@@ -233,6 +255,9 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
233
255
 
234
256
  Bug reports and pull requests are welcome on GitHub at https://github.com/evervault/evervault-ruby.
235
257
 
258
+ ## Feedback
259
+
260
+ Questions or feedback? [Let us know](mailto:support@evervault.com).
236
261
 
237
262
  ## License
238
263
 
@@ -1,4 +1,6 @@
1
1
  require_relative "http/request"
2
+ require_relative "http/request_handler"
3
+ require_relative "http/request_intercept"
2
4
  require_relative "crypto/client"
3
5
  require_relative "models/cage_list"
4
6
 
@@ -9,33 +11,35 @@ module Evervault
9
11
  def initialize(
10
12
  api_key:,
11
13
  base_url: "https://api.evervault.com/",
12
- cage_run_url: "https://cage.run/",
13
- request_timeout: 30
14
+ cage_run_url: "https://run.evervault.com/",
15
+ relay_url: "https://relay.evervault.com:8443",
16
+ ca_host: "https://ca.evervault.com",
17
+ request_timeout: 30,
18
+ curve: 'prime256v1'
14
19
  )
15
- @api_key = api_key
16
- @base_url = base_url
17
- @cage_run_url = cage_run_url
18
- @request =
19
- Evervault::Http::Request.new(
20
- api_key: api_key,
21
- timeout: request_timeout,
22
- base_url: base_url,
23
- cage_run_url: cage_run_url
20
+ @request = Evervault::Http::Request.new(timeout: request_timeout, api_key: api_key)
21
+ @intercept = Evervault::Http::RequestIntercept.new(
22
+ request: @request, ca_host: ca_host, api_key: api_key, relay_url: relay_url
23
+ )
24
+ @request_handler =
25
+ Evervault::Http::RequestHandler.new(
26
+ request: @request, base_url: base_url, cage_run_url: cage_run_url, cert: @intercept
24
27
  )
25
- @crypto_client = Evervault::Crypto::Client.new(request: @request)
28
+ @crypto_client = Evervault::Crypto::Client.new(request_handler: @request_handler, curve: curve)
29
+ @intercept.setup()
26
30
  end
27
31
 
28
32
  def encrypt(data)
29
33
  @crypto_client.encrypt(data)
30
34
  end
31
35
 
32
- def run(cage_name, encrypted_data)
33
- @request.post(cage_name, encrypted_data, cage_run: true)
36
+ def run(cage_name, encrypted_data, options = {})
37
+ @request_handler.post(cage_name, encrypted_data, options: options, cage_run: true)
34
38
  end
35
39
 
36
- def encrypt_and_run(cage_name, data)
40
+ def encrypt_and_run(cage_name, data, options = {})
37
41
  encrypted_data = encrypt(data)
38
- run(cage_name, encrypted_data)
42
+ run(cage_name, encrypted_data, options)
39
43
  end
40
44
 
41
45
  def cages
@@ -43,8 +47,12 @@ module Evervault
43
47
  end
44
48
 
45
49
  def cage_list
46
- cages = @request.get("cages")
50
+ cages = @request_handler.get("cages")
47
51
  @cage_list ||= Evervault::Models::CageList.new(cages: cages["cages"], request: @request)
48
52
  end
53
+
54
+ def relay(decryption_domains=[])
55
+ @intercept.setup_domains(decryption_domains)
56
+ end
49
57
  end
50
58
  end
@@ -1,5 +1,6 @@
1
1
  require_relative "../errors/errors"
2
- require_relative "key"
2
+ require_relative "curves/p256"
3
+ require_relative "../version"
3
4
  require "openssl"
4
5
  require "base64"
5
6
  require "json"
@@ -8,80 +9,106 @@ require "securerandom"
8
9
  module Evervault
9
10
  module Crypto
10
11
  class Client
11
- attr_reader :request
12
- def initialize(request:)
13
- @request = request
12
+ attr_reader :request_handler
13
+ def initialize(request_handler:, curve:)
14
+ @curve = curve
15
+ @p256 = Evervault::Crypto::Curves::P256.new()
16
+ @ev_version = base_64_remove_padding(
17
+ Base64.strict_encode64(EV_VERSION[curve])
18
+ )
19
+ response = request_handler.get("cages/key")
20
+ key = @curve == 'secp256k1' ? 'ecdhKey' : 'ecdhP256Key'
21
+ @team_key = response[key]
14
22
  end
15
23
 
16
24
  def encrypt(data)
17
25
  raise Evervault::Errors::UndefinedDataError.new(
18
26
  "Data is required for encryption"
19
- ) if data.nil? || data.empty?
27
+ ) if data.nil? || (data.instance_of?(String) && data.empty?)
28
+
29
+ raise Evervault::Errors::UnsupportedEncryptType.new(
30
+ "Encryption is not supported for #{data.class}"
31
+ ) if !(encryptable_data?(data) || data.instance_of?(Hash) || data.instance_of?(Array))
20
32
 
21
- if data.instance_of? Hash
22
- encrypt_hash(data)
23
- elsif encryptable_data?(data)
24
- encrypt_string(data)
25
- end
33
+ traverse_and_encrypt(data)
26
34
  end
27
35
 
28
- private def encrypt_string(data)
29
- cipher = OpenSSL::Cipher::AES256.new(:GCM).encrypt
36
+ private def encrypt_string(data_to_encrypt)
37
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
38
+
39
+ shared_key = generate_shared_key()
40
+ cipher.key = shared_key
41
+
30
42
  iv = cipher.random_iv
31
- root_key = cipher.random_key
32
- cipher.key = root_key
33
43
  cipher.iv = iv
34
- encrypted_data = cipher.update(data) + cipher.final
35
- encrypted_buffer = encrypted_data + cipher.auth_tag
36
- encrypted_key =
37
- team_key.public_key.public_encrypt(
38
- root_key,
39
- OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
40
- )
41
- data = [encrypted_key, encrypted_buffer, iv].map { |val| Base64.strict_encode64(val) }
42
- format(header_type(data), *data)
44
+
45
+ if (@curve == 'prime256v1')
46
+ cipher.auth_data = Base64.strict_decode64(@team_key)
47
+ end
48
+
49
+ encrypted_data = cipher.update(data_to_encrypt.to_s) + cipher.final + cipher.auth_tag
50
+
51
+ ephemeral_key_compressed_string = @ephemeral_public_key.to_octet_string(:compressed)
52
+
53
+ format(header_type(data_to_encrypt), Base64.strict_encode64(iv), Base64.strict_encode64(ephemeral_key_compressed_string), Base64.strict_encode64(encrypted_data))
43
54
  end
44
55
 
45
- private def encrypt_hash(data)
56
+ private def traverse_and_encrypt(data)
46
57
  if encryptable_data?(data)
47
58
  return encrypt_string(data)
48
59
  elsif data.instance_of?(Hash)
49
60
  encrypted_data = {}
50
- data.each { |key, value| encrypted_data[key] = encrypt_hash(value) }
61
+ data.each { |key, value| encrypted_data[key] = traverse_and_encrypt(value) }
62
+ return encrypted_data
63
+ elsif data.instance_of?(Array)
64
+ encrypted_data = data.map { |value| traverse_and_encrypt(value) }
51
65
  return encrypted_data
66
+ else
67
+ raise Evervault::Errors::UnsupportedEncryptType.new(
68
+ "Encryption is not supported for #{data.class}"
69
+ )
52
70
  end
53
71
  data
54
72
  end
55
73
 
56
74
  private def encryptable_data?(data)
57
- data.instance_of?(String) || data.instance_of?(Array) ||
58
- [true, false].include?(data) || data.instance_of?(Integer) ||
59
- data.instance_of?(Float)
75
+ data.instance_of?(String) || [true, false].include?(data) ||
76
+ data.instance_of?(Integer) || data.instance_of?(Float)
60
77
  end
61
78
 
62
- private def team_key
63
- @team_key ||= Key.new(public_key: @request.get("cages/key")["key"])
79
+ private def generate_shared_key()
80
+ ec = OpenSSL::PKey::EC.new(@curve)
81
+ ec.generate_key
82
+ @ephemeral_public_key = ec.public_key
83
+
84
+ decoded_team_key = OpenSSL::BN.new(Base64.strict_decode64(@team_key), 2)
85
+ group_for_team_key = OpenSSL::PKey::EC::Group.new(@curve)
86
+ team_key_point = OpenSSL::PKey::EC::Point.new(group_for_team_key, decoded_team_key)
87
+
88
+ shared_key = ec.dh_compute_key(team_key_point)
89
+
90
+ if @curve == 'prime256v1'
91
+ # Perform KDF
92
+ encoded_ephemeral_key = @p256.encode(decompressed_key: @ephemeral_public_key.to_bn(:uncompressed).to_s(16)).to_der
93
+ hash_input = shared_key + [00, 00, 00, 01].pack('C*') + encoded_ephemeral_key
94
+ hash = OpenSSL::Digest::SHA256.new()
95
+ digest = hash.digest(hash_input)
96
+ return digest
97
+ end
98
+
99
+ shared_key
64
100
  end
65
101
 
66
- private def format(header, encrypted_key, encrypted_data, iv)
67
- header =
68
- utf8_to_base_64_url(
69
- { iss: "evervault", version: 1, datatype: header }.to_json
70
- )
71
- payload =
72
- utf8_to_base_64_url(
73
- {
74
- cageData: encrypted_key,
75
- keyIv: iv,
76
- sharedEncryptedData: encrypted_data
77
- }.to_json
78
- )
79
- "#{header}.#{payload}.#{SecureRandom.uuid}"
102
+ private def format(datatype, iv, team_key, encrypted_data)
103
+ "ev:#{@ev_version}#{
104
+ datatype != 'string' ? ':' + datatype : ''
105
+ }:#{base_64_remove_padding(iv)}:#{base_64_remove_padding(
106
+ team_key
107
+ )}:#{base_64_remove_padding(encrypted_data)}:$"
80
108
  end
81
109
 
82
- private def utf8_to_base_64_url(data)
83
- b64_string = Base64.strict_encode64(data)
84
- b64_string.gsub("+", "-").gsub("/", "_")
110
+ private def base_64_remove_padding(str)
111
+ str.gsub(/={1,2}$/, '');
85
112
  end
86
113
 
87
114
  private def header_type(data)
@@ -0,0 +1,38 @@
1
+ require "openssl"
2
+
3
+ module Evervault
4
+ module Crypto
5
+ class CurveBase
6
+
7
+ def initialize(curve_name:, curve_values:)
8
+ @asn1Encoder = buildEncoder(curve_values: curve_values)
9
+ end
10
+
11
+ def encode(decompressed_key:)
12
+ @asn1Encoder.call(decompressed_key)
13
+ end
14
+
15
+ private def buildEncoder(curve_values:)
16
+ a = OpenSSL::ASN1::OctetString.new([curve_values::A].pack('H*'))
17
+ b = OpenSSL::ASN1::OctetString.new([curve_values::B].pack('H*'))
18
+ seed = OpenSSL::ASN1::BitString.new([curve_values::SEED].pack('H*'))
19
+ curve = OpenSSL::ASN1::Sequence.new([a, b, seed])
20
+
21
+ field_type = OpenSSL::ASN1::ObjectId.new("1.2.840.10045.1.1")
22
+ parameters = OpenSSL::ASN1::Integer.new(curve_values::P.to_i(16))
23
+ field_id = OpenSSL::ASN1::Sequence.new([field_type, parameters])
24
+
25
+ version = OpenSSL::ASN1::Integer.new(1)
26
+ base = OpenSSL::ASN1::OctetString.new([curve_values::GENERATOR].pack('H*'))
27
+ order = OpenSSL::ASN1::Integer.new(curve_values::N.to_i(16))
28
+ cofactor = OpenSSL::ASN1::Integer.new(curve_values::H.to_i(16))
29
+ ec_parameters = OpenSSL::ASN1::Sequence.new([version, field_id, curve, base, order, cofactor])
30
+
31
+ algorithm = OpenSSL::ASN1::ObjectId.new("1.2.840.10045.2.1")
32
+ algorithm_identifier = OpenSSL::ASN1::Sequence.new([algorithm, ec_parameters])
33
+
34
+ return lambda {|public_key| OpenSSL::ASN1::Sequence.new([algorithm_identifier, OpenSSL::ASN1::BitString.new([public_key].pack('H*'))])}
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "base"
2
+
3
+ # https://neuromancer.sk/std/x962/prime256v1
4
+
5
+ module P256_CONSTANTS
6
+ A = "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC"
7
+ B = "5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B"
8
+ SEED = "C49D360886E704936A6678E1139D26B7819F7E90"
9
+ P = "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF"
10
+ GENERATOR = "046B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C2964FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"
11
+ N = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"
12
+ H = "01"
13
+ end
14
+
15
+ module Evervault
16
+ module Crypto
17
+ module Curves
18
+ class P256 < CurveBase
19
+ def initialize()
20
+ super(curve_name: 'prime256v1', curve_values: P256_CONSTANTS)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,7 +3,7 @@ require_relative "errors"
3
3
  module Evervault
4
4
  module Errors
5
5
  class ErrorMap
6
- def self.raise_errors_on_failure(status_code, body)
6
+ def self.raise_errors_on_failure(status_code, body, headers)
7
7
  return if status_code < 400
8
8
  case status_code
9
9
  when 404
@@ -13,7 +13,11 @@ module Evervault
13
13
  when 401
14
14
  raise AuthenticationError.new("Unauthorized")
15
15
  when 403
16
- raise AuthenticationError.new("Forbidden")
16
+ if (headers.include? "x-evervault-error-code") && (headers["x-evervault-error-code"] == "forbidden-ip-error")
17
+ raise ForbiddenIPError.new("IP is not present in Cage whitelist")
18
+ else
19
+ raise AuthenticationError.new("Forbidden")
20
+ end
17
21
  when 500
18
22
  raise ServerError.new("Server Error")
19
23
  when 502
@@ -23,5 +23,11 @@ module Evervault
23
23
  class InvalidPublicKeyError < EvervaultError; end
24
24
 
25
25
  class UnexpectedError < EvervaultError; end
26
+
27
+ class CertDownloadError < EvervaultError; end
28
+
29
+ class UnsupportedEncryptType < EvervaultError; end
30
+
31
+ class ForbiddenIPError < EvervaultError; end
26
32
  end
27
33
  end
@@ -1,4 +1,4 @@
1
- require "typhoeus"
1
+ require "faraday"
2
2
  require "json"
3
3
  require_relative "../version"
4
4
  require_relative "../errors/error_map"
@@ -6,63 +6,34 @@ require_relative "../errors/error_map"
6
6
  module Evervault
7
7
  module Http
8
8
  class Request
9
- def initialize(api_key:, base_url:, cage_run_url:, timeout:)
10
- @api_key = api_key
9
+ def initialize(timeout:, api_key:)
11
10
  @timeout = timeout
12
- @base_url = base_url
13
- @cage_run_url = cage_run_url
14
- end
15
-
16
- def get(path, params = nil)
17
- execute(:get, build_url(path), params)
18
- end
19
-
20
- def put(path, params)
21
- execute(:put, build_url(path), params)
22
- end
23
-
24
- def delete(path, params)
25
- execute(:delete, build_url(path), params)
26
- end
27
-
28
- def post(path, params, cage_run: false)
29
- execute(:post, build_url(path, cage_run), params)
30
- end
31
-
32
- private def build_url(path, cage_run = false)
33
- return "#{@base_url}#{path}" unless cage_run
34
- "#{@cage_run_url}#{path}"
11
+ @api_key = api_key
35
12
  end
36
13
 
37
- def execute(method, url, params)
38
- Typhoeus::Config.user_agent = "evervault-ruby/#{VERSION}"
39
- req =
40
- Typhoeus::Request.new(
41
- url,
42
- method: method,
43
- params: params,
44
- headers: build_headers
45
- )
46
- req.on_complete do |response|
47
- if response.success?
48
- return JSON.parse(response.body)
49
- else
50
- Evervault::Errors::ErrorMap.raise_errors_on_failure(
51
- response.code,
52
- response.body
53
- )
14
+ def execute(method, url, params, optional_headers = {}, is_ca = false)
15
+ resp = Faraday.send(method, url) do |req|
16
+ req.body = params.nil? || params.empty? ? nil : params.to_json
17
+ req.headers = build_headers(optional_headers)
18
+ req.options.timeout = @timeout
19
+ end
20
+ if resp.status >= 200 && resp.status <= 300
21
+ if is_ca
22
+ return resp.body
54
23
  end
24
+ return JSON.parse(resp.body)
55
25
  end
56
- resp = req.run
26
+ Evervault::Errors::ErrorMap.raise_errors_on_failure(resp.status, resp.body, resp.headers)
57
27
  end
58
28
 
59
- private def build_headers
60
- {
29
+ private def build_headers(optional_headers)
30
+ optional_headers.merge({
61
31
  "AcceptEncoding": "gzip, deflate",
62
32
  "Accept": "application/json",
63
33
  "Content-Type": "application/json",
64
- "Api-Key": @api_key
65
- }
34
+ "User-Agent": "evervault-ruby/#{VERSION}",
35
+ "Api-Key": @api_key,
36
+ })
66
37
  end
67
38
  end
68
39
  end
@@ -0,0 +1,68 @@
1
+ require "faraday"
2
+ require "json"
3
+ require_relative "../version"
4
+ require_relative "../errors/error_map"
5
+
6
+ module Evervault
7
+ module Http
8
+ class RequestHandler
9
+ def initialize(request:, base_url:, cage_run_url:, cert:)
10
+ @request = request
11
+ @base_url = base_url
12
+ @cage_run_url = cage_run_url
13
+ @cert = cert
14
+ end
15
+
16
+ def get(path, params = nil)
17
+ if @cert.is_certificate_expired()
18
+ @cert.setup()
19
+ end
20
+ @request.execute(:get, build_url(path), params)
21
+ end
22
+
23
+ def put(path, params)
24
+ if @cert.is_certificate_expired()
25
+ @cert.setup()
26
+ end
27
+ @request.execute(:put, build_url(path), params)
28
+ end
29
+
30
+ def delete(path, params)
31
+ if @cert.is_certificate_expired()
32
+ @cert.setup()
33
+ end
34
+ @request.execute(:delete, build_url(path), params)
35
+ end
36
+
37
+ def post(path, params, options: {}, cage_run: false)
38
+ if @cert.is_certificate_expired()
39
+ @cert.setup()
40
+ end
41
+ @request.execute(:post, build_url(path, cage_run), params, build_cage_run_headers(options, cage_run))
42
+ end
43
+
44
+ private def build_url(path, cage_run = false)
45
+ return "#{@base_url}#{path}" unless cage_run
46
+ "#{@cage_run_url}#{path}"
47
+ end
48
+
49
+ private def build_cage_run_headers(options, cage_run = false)
50
+ optional_headers = {}
51
+ return optional_headers unless cage_run
52
+ if options.key?(:async)
53
+ if options[:async]
54
+ optional_headers["x-async"] = "true"
55
+ end
56
+ options.delete(:async)
57
+ end
58
+ if options.key?(:version)
59
+ if options[:version].is_a? Integer
60
+ optional_headers["x-version-id"] = options[:version].to_s
61
+ end
62
+ options.delete(:version)
63
+ end
64
+ optional_headers.merge(options)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,135 @@
1
+ require "faraday"
2
+ require "json"
3
+ require "tempfile"
4
+ require "openssl"
5
+ require 'net/http'
6
+ require_relative "../version"
7
+ require_relative "../errors/errors"
8
+
9
+ module NetHTTPOverride
10
+ @@api_key = nil
11
+ @@relay_url = nil
12
+ @@relay_port = nil
13
+ @@cert = nil
14
+ @@decrypt_if_exact = []
15
+ @@decrypt_if_ends_with = []
16
+
17
+ def self.set_api_key(value)
18
+ @@api_key = value
19
+ end
20
+
21
+ def self.set_relay_url(value)
22
+ relay_address_and_port = value.gsub(/(^\w+:|^)\/\//, '').split(":")
23
+ @@relay_url = relay_address_and_port[0]
24
+ @@relay_port = relay_address_and_port[1]
25
+ end
26
+
27
+ def self.set_cert(value)
28
+ @@cert = value
29
+ end
30
+
31
+ def self.add_to_decrypt_if_exact(value)
32
+ @@decrypt_if_exact.append(value)
33
+ end
34
+
35
+ def self.add_to_decrypt_if_ends_with(value)
36
+ @@decrypt_if_ends_with.append(value)
37
+ end
38
+
39
+ def should_decrypt(domain)
40
+ return (@@decrypt_if_exact.include? domain) || (@@decrypt_if_ends_with.any? { |suffix| domain.end_with? suffix })
41
+ end
42
+
43
+ def connect
44
+ if should_decrypt(conn_address)
45
+ @cert_store = OpenSSL::X509::Store.new
46
+ @cert_store.add_cert(@@cert)
47
+ @proxy_from_env = false
48
+ @proxy_address = @@relay_url
49
+ @proxy_port = @@relay_port
50
+ end
51
+ super
52
+ end
53
+
54
+ def request(req, body = nil, &block)
55
+ should_decrypt = should_decrypt(@address)
56
+ if should_decrypt
57
+ req["Proxy-Authorization"] = @@api_key
58
+ end
59
+ super
60
+ end
61
+ end
62
+
63
+ Net::HTTP.send :prepend, NetHTTPOverride
64
+
65
+ module Evervault
66
+ module Http
67
+ class RequestIntercept
68
+ def initialize(request:, ca_host:, api_key:, relay_url:)
69
+ NetHTTPOverride.set_api_key(api_key)
70
+ NetHTTPOverride.set_relay_url(relay_url)
71
+
72
+ @request = request
73
+ @ca_host = ca_host
74
+ @expire_date = nil
75
+ @initial_date = nil
76
+ end
77
+
78
+ def is_certificate_expired()
79
+ if @expire_date
80
+ now = Time.now
81
+ if now > @expire_date || now < @initial_date
82
+ return true
83
+ end
84
+ end
85
+ return false
86
+ end
87
+
88
+ def setup_domains(decrypt_domains=[])
89
+ for domain in decrypt_domains
90
+ if domain.start_with?("www.")
91
+ domain = domain[4..-1]
92
+ end
93
+ NetHTTPOverride.add_to_decrypt_if_exact(domain)
94
+ NetHTTPOverride.add_to_decrypt_if_ends_with("." + domain)
95
+ NetHTTPOverride.add_to_decrypt_if_ends_with("@" + domain)
96
+ end
97
+ end
98
+
99
+ def setup
100
+ get_cert()
101
+ end
102
+
103
+ def get_cert()
104
+ ca_content = nil
105
+ i = 0
106
+
107
+ while !ca_content && i < 1
108
+ i += 1
109
+ begin
110
+ ca_content = @request.execute("get", @ca_host, nil, {}, is_ca: true)
111
+ rescue;
112
+ end
113
+ end
114
+
115
+ if !ca_content || ca_content == ""
116
+ raise Evervault::Errors::CertDownloadError.new("Unable to install the Evervault root certificate from #{@ca_host}")
117
+ end
118
+
119
+ cert = OpenSSL::X509::Certificate.new ca_content
120
+ set_cert_expire_date(cert)
121
+ NetHTTPOverride.set_cert(cert)
122
+ end
123
+
124
+ def set_cert_expire_date(cert)
125
+ begin
126
+ @expire_date = cert.not_after
127
+ @initial_date = cert.not_before
128
+ rescue => exception
129
+ @expire_date = nil
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+
@@ -9,8 +9,8 @@ module Evervault
9
9
  @request = request
10
10
  end
11
11
 
12
- def run(params)
13
- @request.post(self.name, params, cage_run: true)
12
+ def run(params, options = {})
13
+ @request.post(self.name, params, options: options, cage_run: true)
14
14
  end
15
15
 
16
16
  end
@@ -1,3 +1,4 @@
1
1
  module Evervault
2
- VERSION = "0.1.1"
2
+ VERSION = "1.0.0"
3
+ EV_VERSION = {"prime256v1" => "NOC", "secp256k1" => "DUB"}
3
4
  end
data/lib/evervault.rb CHANGED
@@ -6,24 +6,8 @@ module Evervault
6
6
  class << self
7
7
  attr_accessor :api_key
8
8
 
9
- def run(cage_name, encrypted_data)
10
- client.run(cage_name, encrypted_data)
11
- end
12
-
13
- def encrypt_and_run(cage_name, data)
14
- client.encrypt_and_run(cage_name, data)
15
- end
16
-
17
- def encrypt(data)
18
- client.encrypt(data)
19
- end
20
-
21
- def cages
22
- client.cages
23
- end
24
-
25
- def cage_list
26
- client.cage_list
9
+ def method_missing(method, *args, &block)
10
+ client.send(method, *args, &block)
27
11
  end
28
12
 
29
13
  private def client
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evervault
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonny O'Mahony
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-23 00:00:00.000000000 Z
11
+ date: 2022-07-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -17,12 +17,13 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - ".github/CODEOWNERS"
21
+ - ".github/pull_request_template.md"
20
22
  - ".github/workflows/release.yml"
21
23
  - ".github/workflows/run-tests.yml"
22
24
  - ".gitignore"
23
25
  - ".rspec"
24
26
  - Gemfile
25
- - Gemfile.lock
26
27
  - LICENSE.txt
27
28
  - README.md
28
29
  - Rakefile
@@ -32,10 +33,13 @@ files:
32
33
  - lib/evervault.rb
33
34
  - lib/evervault/client.rb
34
35
  - lib/evervault/crypto/client.rb
35
- - lib/evervault/crypto/key.rb
36
+ - lib/evervault/crypto/curves/base.rb
37
+ - lib/evervault/crypto/curves/p256.rb
36
38
  - lib/evervault/errors/error_map.rb
37
39
  - lib/evervault/errors/errors.rb
38
40
  - lib/evervault/http/request.rb
41
+ - lib/evervault/http/request_handler.rb
42
+ - lib/evervault/http/request_intercept.rb
39
43
  - lib/evervault/models/cage.rb
40
44
  - lib/evervault/models/cage_list.rb
41
45
  - lib/evervault/version.rb
@@ -63,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
67
  - !ruby/object:Gem::Version
64
68
  version: '0'
65
69
  requirements: []
66
- rubygems_version: 3.1.4
70
+ rubygems_version: 3.1.6
67
71
  signing_key:
68
72
  specification_version: 4
69
73
  summary: Ruby SDK to run Evervault Cages
data/Gemfile.lock DELETED
@@ -1,58 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- evervault (0.1.1)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- addressable (2.7.0)
10
- public_suffix (>= 2.0.2, < 5.0)
11
- coderay (1.1.3)
12
- crack (0.4.4)
13
- diff-lcs (1.4.4)
14
- ethon (0.12.0)
15
- ffi (>= 1.3.0)
16
- ffi (1.13.1)
17
- gem-release (2.2.0)
18
- hashdiff (1.0.1)
19
- method_source (1.0.0)
20
- pry (0.13.1)
21
- coderay (~> 1.1)
22
- method_source (~> 1.0)
23
- public_suffix (4.0.6)
24
- rake (12.3.3)
25
- rspec (3.9.0)
26
- rspec-core (~> 3.9.0)
27
- rspec-expectations (~> 3.9.0)
28
- rspec-mocks (~> 3.9.0)
29
- rspec-core (3.9.3)
30
- rspec-support (~> 3.9.3)
31
- rspec-expectations (3.9.2)
32
- diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.9.0)
34
- rspec-mocks (3.9.1)
35
- diff-lcs (>= 1.2.0, < 2.0)
36
- rspec-support (~> 3.9.0)
37
- rspec-support (3.9.3)
38
- typhoeus (1.4.0)
39
- ethon (>= 0.9.0)
40
- webmock (3.9.3)
41
- addressable (>= 2.3.6)
42
- crack (>= 0.3.2)
43
- hashdiff (>= 0.4.0, < 2.0.0)
44
-
45
- PLATFORMS
46
- ruby
47
-
48
- DEPENDENCIES
49
- evervault!
50
- gem-release
51
- pry
52
- rake (~> 12.0)
53
- rspec (~> 3.0)
54
- typhoeus
55
- webmock
56
-
57
- BUNDLED WITH
58
- 2.1.4
@@ -1,19 +0,0 @@
1
- require "openssl"
2
-
3
- module Evervault
4
- module Crypto
5
- class Key
6
- attr_reader :public_key
7
- def initialize(public_key:)
8
- @public_key = OpenSSL::PKey::RSA.new(format_key(public_key))
9
- end
10
-
11
- private def format_key(key)
12
- key_header = "-----BEGIN PUBLIC KEY-----\n"
13
- key_footer = "-----END PUBLIC KEY-----"
14
- return key if key.include?(key_header) && key.include?(key_footer)
15
- "#{key_header}#{key.scan(/.{0,64}/).join("\n")}#{key_footer}"
16
- end
17
- end
18
- end
19
- end