evervault 0.1.2 → 1.1.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: 0e666a2b2b7d130e4221e2c25f963fdd6a3e7e986f08127be6f3937ab7f94e52
4
- data.tar.gz: dbd8034681606769295e1a2678b019c02f7600b96918c6bb737a5ad0bb9f0616
3
+ metadata.gz: 2b06c4fd79f260d853fa0fd738ae225c9670c6b7561bc6cb66ffac99850f0576
4
+ data.tar.gz: 6f39d076798f8b52dc557c1ff3f3834f67abe21874ad530ff399f6404ea46842
5
5
  SHA512:
6
- metadata.gz: 4830d141e96f9b47a65a581bd214a0cd4d86d440e6f2fdc6f70cd9abd7371f86ef331982d65d3850a19edbc56833994aec8463bc5d55fb0c4ea73c66b6d23abe
7
- data.tar.gz: 6d22b0c92ea74e1679513b53431ac7ebe78ba79b215eaea9ebf84062cb2ec45ba4a6e99a1075e66c93a39f9d8c7b1305f0e717c2f9408f4270a7a74d34ab5ca3
6
+ metadata.gz: 862d87c3a14651f46d6efef35c9d679d0e42a79fc90adb619ff4501ac8e304fa3c55650875e6c9e24bf7cf95c8d9bfef304f6af5157b0eab726c93f35d4e3c89
7
+ data.tar.gz: f7074851247c1794960daa610c0360423a41631be458752ea24f7e09230c1e20a3b7d862b23e3fc85cfd4a6815f8aceef60e517d5e30229fd0a008e0b10eae6e
@@ -11,7 +11,7 @@ jobs:
11
11
  fail-fast: false
12
12
  matrix:
13
13
  os: [ubuntu, macos]
14
- ruby: [2.5, 2.6, 2.7, jruby, jruby-head, truffleruby]
14
+ ruby: [2.5, 2.6, 2.7, 3.1, truffleruby]
15
15
  runs-on: ${{ matrix.os }}-latest
16
16
  continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
17
17
  steps:
@@ -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
@@ -9,3 +9,4 @@ gem "pry"
9
9
  gem "webmock"
10
10
  gem "gem-release"
11
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).
16
14
 
17
- We are currently in invite-only early access. You can apply for early access [here](https://evervault.com).
15
+ ## Documentation
16
+
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
+ | cage_name | 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,9 +117,22 @@ 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
+ | cage_name | String | Name of the Cage to be run |
100
121
  | data | dict | Data to be encrypted |
101
122
 
123
+ ### Evervault.create_run_token
124
+
125
+ `Evervault.create_run_token` creates a single use, time bound token for invoking a cage.
126
+
127
+ ```ruby
128
+ Evervault.create_run_token(cage_name = String, data = Hash)
129
+ ```
130
+
131
+ | Parameter | Type | Description |
132
+ | --------- | ------ | ---------------------------------------------------- |
133
+ | cage_name | String | Name of the Cage the run token should be created for |
134
+ | data | Hash | Payload that the token can be used with |
135
+
102
136
  ### Evervault.cages
103
137
 
104
138
  Return a hash of your team's Cage objects in hash format, with cage-name as keys
@@ -129,7 +163,7 @@ Evervault.cages
129
163
 
130
164
  ### Evervault.cage_list
131
165
 
132
- Return a `CageList` object, containing a list of your team's cages
166
+ Return a `CageList` object, containing a list of your team's Cages
133
167
 
134
168
  ```ruby
135
169
  Evervault.cage_list
@@ -167,7 +201,7 @@ Evervault.cage_list
167
201
 
168
202
  #### CageList.to_hash
169
203
 
170
- Converts a list of cages to a hash with keys of CageName => Cage Model
204
+ Converts a list of Cages to a hash with keys of CageName => Cage Model
171
205
 
172
206
  ```ruby
173
207
  Evervault.cage_list.to_hash
@@ -210,9 +244,9 @@ Evervault.cage_list.to_hash
210
244
 
211
245
  ### Evervault::Models::Cage.run
212
246
 
213
- Each Cage model exposes a `run` method, which allows you to run that particular cage.
247
+ Each Cage model exposes a `run` method, which allows you to run that particular Cage.
214
248
 
215
- *Note*: this does not encrypt data before running the cage
249
+ *Note*: this does not encrypt data before running the Cage.
216
250
  ```ruby
217
251
  cage = Evervault.cage_list.cages[0]
218
252
  cage.run({'name': 'testing'})
@@ -221,7 +255,8 @@ cage.run({'name': 'testing'})
221
255
 
222
256
  | Parameter | Type | Description |
223
257
  | --------- | ---- | ----------- |
224
- | data | Hash | Payload for the cage |
258
+ | data | Hash | Payload for the Cage |
259
+ | options | Hash | [Options for the Cage run](#Cage-Run-Options) |
225
260
 
226
261
  ## Development
227
262
 
@@ -233,6 +268,9 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
233
268
 
234
269
  Bug reports and pull requests are welcome on GitHub at https://github.com/evervault/evervault-ruby.
235
270
 
271
+ ## Feedback
272
+
273
+ Questions or feedback? [Let us know](mailto:support@evervault.com).
236
274
 
237
275
  ## License
238
276
 
@@ -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,16 @@ 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
57
+
58
+ def create_run_token(cage_name, data)
59
+ @request_handler.post("v2/functions/#{cage_name}/run-token", data)
60
+ end
49
61
  end
50
62
  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
@@ -6,51 +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)
14
+ def execute(method, url, params, optional_headers = {}, is_ca = false)
38
15
  resp = Faraday.send(method, url) do |req|
39
16
  req.body = params.nil? || params.empty? ? nil : params.to_json
40
- req.headers = build_headers
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
23
+ end
24
+ return JSON.parse(resp.body)
41
25
  end
42
- return JSON.parse(resp.body) if resp.status >= 200 && resp.status <= 300
43
- Evervault::Errors::ErrorMap.raise_errors_on_failure(resp.status, resp.body)
26
+ Evervault::Errors::ErrorMap.raise_errors_on_failure(resp.status, resp.body, resp.headers)
44
27
  end
45
28
 
46
- private def build_headers
47
- {
29
+ private def build_headers(optional_headers)
30
+ optional_headers.merge({
48
31
  "AcceptEncoding": "gzip, deflate",
49
32
  "Accept": "application/json",
50
33
  "Content-Type": "application/json",
51
34
  "User-Agent": "evervault-ruby/#{VERSION}",
52
- "Api-Key": @api_key
53
- }
35
+ "Api-Key": @api_key,
36
+ })
54
37
  end
55
38
  end
56
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.2"
2
+ VERSION = "1.1.0"
3
+ EV_VERSION = {"prime256v1" => "NOC", "secp256k1" => "DUB"}
3
4
  end
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.2
4
+ version: 1.1.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-11-04 00:00:00.000000000 Z
11
+ date: 2022-09-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -24,7 +24,6 @@ files:
24
24
  - ".gitignore"
25
25
  - ".rspec"
26
26
  - Gemfile
27
- - Gemfile.lock
28
27
  - LICENSE.txt
29
28
  - README.md
30
29
  - Rakefile
@@ -34,10 +33,13 @@ files:
34
33
  - lib/evervault.rb
35
34
  - lib/evervault/client.rb
36
35
  - lib/evervault/crypto/client.rb
37
- - lib/evervault/crypto/key.rb
36
+ - lib/evervault/crypto/curves/base.rb
37
+ - lib/evervault/crypto/curves/p256.rb
38
38
  - lib/evervault/errors/error_map.rb
39
39
  - lib/evervault/errors/errors.rb
40
40
  - lib/evervault/http/request.rb
41
+ - lib/evervault/http/request_handler.rb
42
+ - lib/evervault/http/request_intercept.rb
41
43
  - lib/evervault/models/cage.rb
42
44
  - lib/evervault/models/cage_list.rb
43
45
  - lib/evervault/version.rb
@@ -65,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
67
  - !ruby/object:Gem::Version
66
68
  version: '0'
67
69
  requirements: []
68
- rubygems_version: 3.1.2
70
+ rubygems_version: 3.1.6
69
71
  signing_key:
70
72
  specification_version: 4
71
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
- faraday (1.1.0)
15
- multipart-post (>= 1.2, < 3)
16
- ruby2_keywords
17
- gem-release (2.2.0)
18
- hashdiff (1.0.1)
19
- method_source (1.0.0)
20
- multipart-post (2.1.1)
21
- pry (0.13.1)
22
- coderay (~> 1.1)
23
- method_source (~> 1.0)
24
- public_suffix (4.0.6)
25
- rake (12.3.3)
26
- rspec (3.9.0)
27
- rspec-core (~> 3.9.0)
28
- rspec-expectations (~> 3.9.0)
29
- rspec-mocks (~> 3.9.0)
30
- rspec-core (3.9.3)
31
- rspec-support (~> 3.9.3)
32
- rspec-expectations (3.9.2)
33
- diff-lcs (>= 1.2.0, < 2.0)
34
- rspec-support (~> 3.9.0)
35
- rspec-mocks (3.9.1)
36
- diff-lcs (>= 1.2.0, < 2.0)
37
- rspec-support (~> 3.9.0)
38
- rspec-support (3.9.3)
39
- ruby2_keywords (0.0.2)
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
- faraday
51
- gem-release
52
- pry
53
- rake (~> 12.0)
54
- rspec (~> 3.0)
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