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 +4 -4
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/run-tests.yml +1 -1
- data/Gemfile +1 -0
- data/README.md +81 -43
- data/lib/evervault/client.rb +29 -17
- data/lib/evervault/crypto/client.rb +74 -47
- data/lib/evervault/crypto/curves/base.rb +38 -0
- data/lib/evervault/crypto/curves/p256.rb +25 -0
- data/lib/evervault/errors/error_map.rb +6 -2
- data/lib/evervault/errors/errors.rb +6 -0
- data/lib/evervault/http/request.rb +16 -33
- data/lib/evervault/http/request_handler.rb +68 -0
- data/lib/evervault/http/request_intercept.rb +135 -0
- data/lib/evervault/models/cage.rb +2 -2
- data/lib/evervault/version.rb +2 -1
- metadata +7 -5
- data/Gemfile.lock +0 -58
- data/lib/evervault/crypto/key.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b06c4fd79f260d853fa0fd738ae225c9670c6b7561bc6cb66ffac99850f0576
|
4
|
+
data.tar.gz: 6f39d076798f8b52dc557c1ff3f3834f67abe21874ad530ff399f6404ea46842
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
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,
|
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
data/README.md
CHANGED
@@ -1,94 +1,115 @@
|
|
1
|
-
|
2
|
-
<p align="center">
|
3
|
-
<img src="res/logo.svg">
|
4
|
-
</p>
|
1
|
+
[](https://evervault.com/)
|
5
2
|
|
6
|
-
|
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
|
+
[](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
|
-
|
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
|
-
|
15
|
+
## Documentation
|
16
|
+
|
17
|
+
See the Evervault [Ruby SDK documentation](https://docs.evervault.com/ruby).
|
18
18
|
|
19
19
|
## Installation
|
20
20
|
|
21
|
-
|
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
|
-
|
31
|
+
Then, run:
|
32
|
+
|
28
33
|
```sh
|
29
|
-
|
34
|
+
bundle install
|
30
35
|
```
|
31
|
-
|
36
|
+
#### 2. By yourself
|
37
|
+
|
38
|
+
Just run:
|
39
|
+
|
32
40
|
```sh
|
33
|
-
|
41
|
+
gem install evervault
|
34
42
|
```
|
35
43
|
|
36
44
|
## Setup
|
37
45
|
|
38
|
-
|
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
|
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
|
-
|
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
|
-
|
61
|
+
## Reference
|
50
62
|
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
require "evervault"
|
63
|
+
The Evervault Ruby SDK exposes eight methods.
|
54
64
|
|
55
|
-
|
56
|
-
evervault = Evervault::Client.new(api_key: <YOUR-API-KEY>)
|
65
|
+
### Evervault.encrypt
|
57
66
|
|
58
|
-
|
59
|
-
|
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
|
-
|
73
|
+
| Parameter | Type | Description |
|
74
|
+
| --------- | ---- | ----------- |
|
75
|
+
| data | `String`, `Number`, `Boolean`, `Hash`, `Array` | Data to be encrypted |
|
63
76
|
|
64
|
-
### Evervault.
|
77
|
+
### Evervault.relay
|
65
78
|
|
66
|
-
|
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.
|
82
|
+
Evervault.relay(decryption_domains = Array)
|
70
83
|
```
|
71
84
|
|
72
85
|
| Parameter | Type | Description |
|
73
86
|
| --------- | ---- | ----------- |
|
74
|
-
|
|
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
|
-
|
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
|
-
|
|
87
|
-
| data | Hash | Payload for the
|
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
|
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
|
-
|
|
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
|
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
|
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
|
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
|
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
|
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
|
|
data/lib/evervault/client.rb
CHANGED
@@ -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://
|
13
|
-
|
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
|
-
@
|
16
|
-
@
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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(
|
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
|
-
@
|
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 = @
|
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 "
|
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 :
|
12
|
-
def initialize(
|
13
|
-
@
|
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
|
-
|
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(
|
29
|
-
cipher = OpenSSL::Cipher
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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] =
|
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) ||
|
58
|
-
|
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
|
63
|
-
|
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(
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
83
|
-
|
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
|
-
|
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(
|
10
|
-
@api_key = api_key
|
9
|
+
def initialize(timeout:, api_key:)
|
11
10
|
@timeout = timeout
|
12
|
-
@
|
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
|
-
|
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
|
+
|
data/lib/evervault/version.rb
CHANGED
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:
|
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:
|
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/
|
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.
|
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
|
data/lib/evervault/crypto/key.rb
DELETED
@@ -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
|