evervault 0.1.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Evervault](https://evervault.com/evervault.svg)](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
|
+
[![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
|
-
|
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
|