security_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/README.md +39 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/security_client.rb +562 -0
- data/lib/security_client/version.rb +3 -0
- data/security_client.gemspec +25 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 055b4ebf4046542527af7b8df744c592ff0921a8e5d08af6f470e2e72c96c8c2
|
4
|
+
data.tar.gz: 694fa499f049beaae0480892cb2b2bc0100f1e22fed3eb94ab80c250b026d1a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0cb89ec2fca2a529d4a75c39505efcaefd6fd2b1716ec435cc206e56d7b7d03bd8f67b1437295c936147425d062afd6c0422ae5ba2854aefa4da31f01ff476e9
|
7
|
+
data.tar.gz: 1e6dc6e527eb4d63796d96f000f37245d5b83cf19a7eb9d72c26c955a797099889138d7f1178492a012ba4a5a9915188b58ccfdfda8db09ccb1ea810a94ff7c1
|
data/.DS_Store
ADDED
Binary file
|
data/.gitignore
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at vinay.ymca@gmail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# SecurityClient
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/security_client`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'security_client'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install security_client
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/security_client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
|
+
|
37
|
+
## Code of Conduct
|
38
|
+
|
39
|
+
Everyone interacting in the SecurityClient project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/security_client/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "security_client"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,562 @@
|
|
1
|
+
require "security_client/version"
|
2
|
+
require 'ostruct'
|
3
|
+
require 'httparty'
|
4
|
+
require "active_support/all"
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
module SecurityClient
|
8
|
+
class Voltron
|
9
|
+
|
10
|
+
def initialize access_key_id:, secret_signing_key:, secret_crypto_access_key: , host:
|
11
|
+
@access_key_id = access_key_id
|
12
|
+
@secret_signing_key = secret_signing_key
|
13
|
+
@secret_crypto_access_key = secret_crypto_access_key
|
14
|
+
@host = host
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_attributes
|
18
|
+
return OpenStruct.new(access_key_id: @access_key_id, secret_signing_key: @secret_signing_key, secret_crypto_access_key: @secret_crypto_access_key, host: @host)
|
19
|
+
end
|
20
|
+
|
21
|
+
def encrypt uses:, data:
|
22
|
+
creds = self.get_attributes
|
23
|
+
begin
|
24
|
+
enc = SecurityClient::Encryption.new(creds, 1)
|
25
|
+
res = enc.begin() + enc.update(data) + enc.end()
|
26
|
+
enc.close()
|
27
|
+
rescue
|
28
|
+
enc.close() if enc
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
puts res
|
32
|
+
return res
|
33
|
+
end
|
34
|
+
|
35
|
+
def decrypt data:
|
36
|
+
creds = self.get_attributes
|
37
|
+
begin
|
38
|
+
dec = Decryption.new(creds)
|
39
|
+
res = dec.begin() + dec.update(data) + dec.end()
|
40
|
+
dec.close()
|
41
|
+
rescue
|
42
|
+
dec.close() if dec
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
puts res
|
46
|
+
return res
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class SecurityClient::Encryption
|
53
|
+
def initialize(creds, uses)
|
54
|
+
|
55
|
+
raise RuntimeError, 'Some of your credentials are missing, please check!' if !validate_creds(creds)
|
56
|
+
|
57
|
+
# Set host, either the default or the one given by caller
|
58
|
+
@host = creds.host.blank? ? VOLTRON_HOST : creds.host
|
59
|
+
|
60
|
+
# Set the credentials in instance varibales to be used among methods
|
61
|
+
# The client's public API key (used to identify the client to the server
|
62
|
+
@papi = creds.access_key_id
|
63
|
+
|
64
|
+
# The client's secret API key (used to authenticate HTTP requests)
|
65
|
+
@sapi = creds.secret_signing_key
|
66
|
+
|
67
|
+
# The client's secret RSA encryption key/password (used to decrypt the client's RSA key from the server). This key is not retained by this object.
|
68
|
+
@srsa = creds.secret_crypto_access_key
|
69
|
+
|
70
|
+
# Build the endpoint URL
|
71
|
+
url = endpoint_base + '/encryption/key'
|
72
|
+
|
73
|
+
# Build the Request Body with the number of uses of key
|
74
|
+
query = {uses: uses}
|
75
|
+
|
76
|
+
# Retrieve the necessary headers to make the request using Auth Object
|
77
|
+
headers = SecurityClient::Auth.build_headers(@papi, @sapi, endpoint, query, @host,'post')
|
78
|
+
|
79
|
+
@encryption_started = false
|
80
|
+
@encryption_ready = true
|
81
|
+
|
82
|
+
# Request a new encryption key from the server. if the request
|
83
|
+
# fails, the function raises a HTTPError indicating
|
84
|
+
# the status code returned by the server. this exception is
|
85
|
+
# propagated back to the caller
|
86
|
+
|
87
|
+
begin
|
88
|
+
response = HTTParty.post(
|
89
|
+
url,
|
90
|
+
body: query.to_json,
|
91
|
+
headers: headers
|
92
|
+
)
|
93
|
+
rescue HTTParty::Error
|
94
|
+
raise RuntimeError, 'Cant reach server'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Response status is 201 Created
|
98
|
+
if response.code == WEBrick::HTTPStatus::RC_CREATED
|
99
|
+
# The code below largely assumes that the server returns
|
100
|
+
# a json object that contains the members and is formatted
|
101
|
+
# according to the Voltron REST specification.
|
102
|
+
|
103
|
+
# Build the key object
|
104
|
+
@key = {}
|
105
|
+
@key['id'] = response['key_fingerprint']
|
106
|
+
@key['session'] = response['encryption_session']
|
107
|
+
@key['security_model'] = response['security_model']
|
108
|
+
@key['algorithm'] = response['security_model']['algorithm'].downcase
|
109
|
+
@key['max_uses'] = response['max_uses']
|
110
|
+
@key['uses'] = 0
|
111
|
+
@key['encrypted'] = Base64.strict_decode64(response['encrypted_data_key'])
|
112
|
+
|
113
|
+
# Get encrypted private key from response body
|
114
|
+
encrypted_private_key = response['encrypted_private_key']
|
115
|
+
# Get wrapped data key from response body
|
116
|
+
wrapped_data_key = response['wrapped_data_key']
|
117
|
+
# Decrypt the encryped private key using @srsa supplied
|
118
|
+
private_key = OpenSSL::PKey::RSA.new(encrypted_private_key,@srsa)
|
119
|
+
# Decode WDK from base64 format
|
120
|
+
wdk = Base64.strict_decode64(wrapped_data_key)
|
121
|
+
# Use private key to decrypt the wrapped data key
|
122
|
+
dk = private_key.private_decrypt(wdk,OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
123
|
+
@key['raw'] = dk
|
124
|
+
# Build the algorithm object
|
125
|
+
@algo = SecurityClient::Algo.new.get_algo(@key['algorithm'])
|
126
|
+
else
|
127
|
+
# Raise the error if response is not 201
|
128
|
+
raise RuntimeError, "HTTPError Response: Expected 201, got #{response.code}"
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
def begin
|
134
|
+
# Begin the encryption process
|
135
|
+
|
136
|
+
# When this function is called, the encryption object increments
|
137
|
+
# the number of uses of the key and creates a new internal context
|
138
|
+
# to be used to encrypt the data.
|
139
|
+
# If the encryption object is not yet ready to be used, throw an error
|
140
|
+
raise RuntimeError, 'Encryption not ready' if !@encryption_ready
|
141
|
+
|
142
|
+
# if Encryption cipher context already exists
|
143
|
+
raise RuntimeError, 'Encryption already in progress' if @encryption_started
|
144
|
+
# If max uses > uses
|
145
|
+
raise RuntimeError, 'Maximum key uses exceeded' if @key['uses'] >= @key['max_uses']
|
146
|
+
@key['uses'] += 1
|
147
|
+
# create a new Encryption context and initialization vector
|
148
|
+
@enc , @iv = SecurityClient::Algo.new.encryptor(@algo, @key['raw'])
|
149
|
+
|
150
|
+
# Pack the result into bytes to get a byte string
|
151
|
+
struct = [0, 0, @algo[:id], @iv.length, @key['encrypted'].length].pack('CCCCn')
|
152
|
+
@encryption_started = true
|
153
|
+
return struct + @iv + @key['encrypted']
|
154
|
+
end
|
155
|
+
|
156
|
+
def update(data)
|
157
|
+
raise RuntimeError, 'Encryption is not Started' if !@encryption_started
|
158
|
+
# Encryption of some plain text is perfomed here
|
159
|
+
# Any cipher text produced by the operation is returned
|
160
|
+
@enc.update(data)
|
161
|
+
end
|
162
|
+
|
163
|
+
def end
|
164
|
+
raise RuntimeError, 'Encryption is not Started' if !@encryption_started
|
165
|
+
# This function finalizes the encryption (producing the final
|
166
|
+
# cipher text for the encryption, if necessary) and adds any
|
167
|
+
# authentication information (if required by the algorithm).
|
168
|
+
# Any data produced is returned by the function.
|
169
|
+
|
170
|
+
# Finalize an encryption
|
171
|
+
res = @enc.final
|
172
|
+
if @algo[:tag_length] != 0
|
173
|
+
# Add the tag to the cipher text
|
174
|
+
res+= @enc.auth_tag
|
175
|
+
end
|
176
|
+
@encryption_started = false
|
177
|
+
# Return the encrypted result
|
178
|
+
return res
|
179
|
+
end
|
180
|
+
|
181
|
+
def close
|
182
|
+
raise RuntimeError, 'Encryption currently running' if @encryption_started
|
183
|
+
# If the key was used less times than was requested, send an update to the server
|
184
|
+
if @key['uses'] < @key['max_uses']
|
185
|
+
query_url = "#{endpoint}/#{@key['id']}/#{@key['session']}"
|
186
|
+
url = "#{endpoint_base}/encryption/key/#{@key['id']}/#{@key['session']}"
|
187
|
+
query = {actual: @key['uses'], requested: @key['max_uses']}
|
188
|
+
headers = Auth.build_headers(@papi, @sapi, query_url, query, @host, 'patch')
|
189
|
+
response = HTTParty.patch(
|
190
|
+
url,
|
191
|
+
body: query.to_json,
|
192
|
+
headers: headers
|
193
|
+
)
|
194
|
+
remove_instance_variable(:@key)
|
195
|
+
@encryption_ready = false;
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def endpoint_base
|
200
|
+
@host + '/api/v0'
|
201
|
+
end
|
202
|
+
|
203
|
+
def endpoint
|
204
|
+
'/api/v0/encryption/key'
|
205
|
+
end
|
206
|
+
|
207
|
+
def validate_creds(credentials)
|
208
|
+
# This method checks for the presence of the credentials
|
209
|
+
!credentials.access_key_id.blank? and !credentials.secret_signing_key.blank? and !credentials.secret_crypto_access_key.blank?
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class SecurityClient::Algo
|
214
|
+
def set_algo
|
215
|
+
@algorithm = {
|
216
|
+
"aes-256-gcm"=>{
|
217
|
+
id:0,
|
218
|
+
algorithm: OpenSSL::Cipher::AES256,
|
219
|
+
mode: OpenSSL::Cipher::AES256.new(:GCM),
|
220
|
+
key_length: 32,
|
221
|
+
iv_length: 12,
|
222
|
+
tag_length: 16
|
223
|
+
},
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
def get_algo(name)
|
228
|
+
set_algo[name]
|
229
|
+
end
|
230
|
+
|
231
|
+
def encryptor(obj,key, iv=nil)
|
232
|
+
# key : A byte string containing the key to be used with this encryption
|
233
|
+
# If the caller specifies the initialization vector, it must be
|
234
|
+
# the correct length and, if so, will be used. If it is not
|
235
|
+
# specified, the function will generate a new one
|
236
|
+
|
237
|
+
cipher = obj[:mode]
|
238
|
+
raise RuntimeError, 'Invalid key length' if key.length != obj[:key_length]
|
239
|
+
|
240
|
+
raise RuntimeError, 'Invalid initialization vector length' if (iv!= nil and iv.length != obj[:iv_length])
|
241
|
+
cipher.encrypt
|
242
|
+
cipher.key = key
|
243
|
+
iv = cipher.random_iv
|
244
|
+
return cipher, iv
|
245
|
+
end
|
246
|
+
|
247
|
+
def decryptor(obj, key, iv)
|
248
|
+
cipher = obj[:mode]
|
249
|
+
raise RuntimeError, 'Invalid key length' if key.length != obj[:key_length]
|
250
|
+
|
251
|
+
raise RuntimeError, 'Invalid initialization vector length' if (iv!= nil and iv.length != obj[:iv_length])
|
252
|
+
cipher = obj[:mode]
|
253
|
+
cipher.decrypt
|
254
|
+
cipher.key = key
|
255
|
+
cipher.iv = iv
|
256
|
+
return cipher
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
class SecurityClient::Auth
|
261
|
+
def self.build_headers(papi, sapi, endpoint, query, host, http_method)
|
262
|
+
|
263
|
+
# This function calculates the signature for the message, adding the Signature header
|
264
|
+
# to contain the data. Certain HTTP headers are required for
|
265
|
+
# signature calculation and will be added by this code as
|
266
|
+
# necessary. The constructed headers object is returned
|
267
|
+
|
268
|
+
# the '(request-target)' is part of the signed data.
|
269
|
+
# it's value is 'http_method path?query'
|
270
|
+
reqt = "#{http_method} #{endpoint}"
|
271
|
+
|
272
|
+
# The time at which the signature was created expressed as the unix epoch
|
273
|
+
created = Time.now.to_i
|
274
|
+
|
275
|
+
# the Digest header is always included/overridden by
|
276
|
+
# this code. it is a hash of the body of the http message
|
277
|
+
# and is always present even if the body is empty
|
278
|
+
hash_sha512 = OpenSSL::Digest::SHA512.new
|
279
|
+
hash_sha512 << JSON.dump(query)
|
280
|
+
digest = 'SHA-512='+Base64.strict_encode64(hash_sha512.digest)
|
281
|
+
|
282
|
+
# Initialize the headers object to be returned via this method
|
283
|
+
all_headers = {}
|
284
|
+
# The content type of request
|
285
|
+
all_headers['content-type'] = 'application/json'
|
286
|
+
# The request target calculated above(reqt)
|
287
|
+
all_headers['(request-target)'] = reqt
|
288
|
+
# The date and time in GMT format
|
289
|
+
all_headers['date'] = get_date
|
290
|
+
# The host specified by the caller
|
291
|
+
all_headers['host'] = get_host(host)
|
292
|
+
all_headers['(created)'] = created
|
293
|
+
all_headers['digest'] = digest
|
294
|
+
headers = ['content-type', 'date', 'host', '(created)', '(request-target)', 'digest']
|
295
|
+
|
296
|
+
# include the specified headers in the hmac calculation. each
|
297
|
+
# header is of the form 'header_name: header value\n'
|
298
|
+
# included headers are also added to an ordered list of headers
|
299
|
+
# which is included in the message
|
300
|
+
hmac = OpenSSL::HMAC.new(sapi, OpenSSL::Digest::SHA512.new)
|
301
|
+
headers.each do |header|
|
302
|
+
if all_headers.key?(header)
|
303
|
+
hmac << "#{header}: #{all_headers[header]}\n"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
all_headers.delete('(created)')
|
308
|
+
all_headers.delete('(request-target)')
|
309
|
+
all_headers.delete('host')
|
310
|
+
|
311
|
+
# Build the Signature header itself
|
312
|
+
all_headers['signature'] = 'keyId="' + papi + '"'
|
313
|
+
all_headers['signature'] += ', algorithm="hmac-sha512"'
|
314
|
+
all_headers['signature'] += ', created=' + created.to_s
|
315
|
+
all_headers['signature'] += ', headers="' + headers.join(" ") + '"'
|
316
|
+
all_headers['signature'] += ', signature="'
|
317
|
+
all_headers['signature'] += Base64.strict_encode64(hmac.digest)
|
318
|
+
all_headers['signature'] += '"'
|
319
|
+
|
320
|
+
return all_headers
|
321
|
+
end
|
322
|
+
|
323
|
+
def self.get_host(host)
|
324
|
+
uri = URI(host)
|
325
|
+
return "#{uri.hostname}:#{uri.port}"
|
326
|
+
end
|
327
|
+
|
328
|
+
def self.get_date
|
329
|
+
DateTime.now.in_time_zone('GMT').strftime("%a, %d %b %Y") + " " + DateTime.now.in_time_zone('GMT').strftime("%H:%M:%S") + " GMT"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
class SecurityClient::Decryption
|
334
|
+
def initialize(creds)
|
335
|
+
# Initialize the decryption module object
|
336
|
+
# Set the credentials in instance varibales to be used among methods
|
337
|
+
# the server to which to make the request
|
338
|
+
raise RuntimeError, 'Some of your credentials are missing, please check!' if !validate_creds(creds)
|
339
|
+
@host = creds.host.blank? ? VOLTRON_HOST : creds.host
|
340
|
+
|
341
|
+
# The client's public API key (used to identify the client to the server
|
342
|
+
@papi = creds.access_key_id
|
343
|
+
|
344
|
+
# The client's secret API key (used to authenticate HTTP requests)
|
345
|
+
@sapi = creds.secret_signing_key
|
346
|
+
|
347
|
+
# The client's secret RSA encryption key/password (used to decrypt the client's RSA key from the server). This key is not retained by this object.
|
348
|
+
@srsa = creds.secret_crypto_access_key
|
349
|
+
|
350
|
+
@decryption_ready = true
|
351
|
+
@decryption_started = false
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
def endpoint_base
|
356
|
+
@host + '/api/v0'
|
357
|
+
end
|
358
|
+
|
359
|
+
def endpoint
|
360
|
+
'/api/v0/decryption/key'
|
361
|
+
end
|
362
|
+
|
363
|
+
def begin
|
364
|
+
# Begin the decryption process
|
365
|
+
|
366
|
+
# This interface does not take any cipher text in its arguments
|
367
|
+
# in an attempt to maintain an API that corresponds to the
|
368
|
+
# encryption object. In doing so, the work that can take place
|
369
|
+
# in this function is limited. without any data, there is no
|
370
|
+
# way to determine which key is in use or decrypt any data.
|
371
|
+
#
|
372
|
+
# this function simply throws an error if starting an decryption
|
373
|
+
# while one is already in progress, and initializes the internal
|
374
|
+
# buffer
|
375
|
+
|
376
|
+
raise RuntimeError, 'Decryption is not ready' if !@decryption_ready
|
377
|
+
|
378
|
+
raise RuntimeError, 'Decryption Already Started' if @decryption_started
|
379
|
+
|
380
|
+
raise RuntimeError, 'Decryption already in progress' if @key.present? and @key.key?("dec")
|
381
|
+
@decryption_started = true
|
382
|
+
@data = ''
|
383
|
+
end
|
384
|
+
|
385
|
+
def update(data)
|
386
|
+
# Decryption of cipher text is performed here
|
387
|
+
# Cipher text must be passed to this function in the order in which it was output from the encryption.update function.
|
388
|
+
|
389
|
+
# Each encryption has a header on it that identifies the algorithm
|
390
|
+
# used and an encryption of the data key that was used to encrypt
|
391
|
+
# the original plain text. there is no guarantee how much of that
|
392
|
+
# data will be passed to this function or how many times this
|
393
|
+
# function will be called to process all of the data. to that end,
|
394
|
+
# this function buffers data internally, when it is unable to
|
395
|
+
# process it.
|
396
|
+
#
|
397
|
+
# The function buffers data internally until the entire header is
|
398
|
+
# received. once the header has been received, the encrypted data
|
399
|
+
# key is sent to the server for decryption. after the header has
|
400
|
+
# been successfully handled, this function always decrypts all of
|
401
|
+
# the data in its internal buffer *except* for however many bytes
|
402
|
+
# are specified by the algorithm's tag size. see the end() function
|
403
|
+
# for details.
|
404
|
+
|
405
|
+
raise RuntimeError, 'Decryption is not Started' if !@decryption_started
|
406
|
+
|
407
|
+
# Append the incoming data in the internal data buffer
|
408
|
+
@data = @data + data
|
409
|
+
|
410
|
+
# if there is no key or 'dec' member of key, then the code is still trying to build a complete header
|
411
|
+
if !@key.present? or !@key.key?("dec")
|
412
|
+
struct_length = [1,1,1,1,1].pack('CCCCn').length
|
413
|
+
packed_struct = @data[0...struct_length]
|
414
|
+
|
415
|
+
# Does the buffer contain enough of the header to
|
416
|
+
# determine the lengths of the initialization vector
|
417
|
+
# and the key?
|
418
|
+
if @data.length > struct_length
|
419
|
+
# Unpack the values packed in encryption
|
420
|
+
version, flag_for_later, algorithm_id, iv_length, key_length = packed_struct.unpack('CCCCn')
|
421
|
+
|
422
|
+
# verify flag and version are 0
|
423
|
+
raise RuntimeError, 'invalid encryption header' if version != 0 or flag_for_later != 0
|
424
|
+
|
425
|
+
# Does the buffer contain the entire header?
|
426
|
+
if @data.length > struct_length + iv_length + key_length
|
427
|
+
# Extract the initialization vector
|
428
|
+
iv = @data[struct_length...iv_length + struct_length]
|
429
|
+
# Extract the encryped key
|
430
|
+
encrypted_key = @data[struct_length + iv_length...key_length + struct_length + iv_length]
|
431
|
+
# Remove the header from the buffer
|
432
|
+
@data = @data[struct_length + iv_length + key_length..-1]
|
433
|
+
|
434
|
+
# generate a local identifier for the key
|
435
|
+
hash_sha512 = OpenSSL::Digest::SHA512.new
|
436
|
+
hash_sha512 << encrypted_key
|
437
|
+
client_id = hash_sha512.digest
|
438
|
+
|
439
|
+
if @key.present?
|
440
|
+
if @key['client_id'] != client_id
|
441
|
+
close()
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# IF key object not exists, request a new one from the server
|
446
|
+
if !@key.present?
|
447
|
+
url = endpoint_base + "/decryption/key"
|
448
|
+
query = {encrypted_data_key: Base64.strict_encode64(encrypted_key)}
|
449
|
+
headers = Auth.build_headers(@papi, @sapi, endpoint, query, @host, 'post')
|
450
|
+
|
451
|
+
response = HTTParty.post(
|
452
|
+
url,
|
453
|
+
body: query.to_json,
|
454
|
+
headers: headers
|
455
|
+
)
|
456
|
+
|
457
|
+
# Response status is 200 OK
|
458
|
+
if response.code == WEBrick::HTTPStatus::RC_OK
|
459
|
+
@key = {}
|
460
|
+
@key['finger_print'] = response['key_fingerprint']
|
461
|
+
@key['client_id'] = client_id
|
462
|
+
@key['session'] = response['encryption_session']
|
463
|
+
|
464
|
+
@key['algorithm'] = 'aes-256-gcm'
|
465
|
+
|
466
|
+
encrypted_private_key = response['encrypted_private_key']
|
467
|
+
# Decrypt the encryped private key using SRSA
|
468
|
+
private_key = OpenSSL::PKey::RSA.new(encrypted_private_key,@srsa)
|
469
|
+
|
470
|
+
wrapped_data_key = response['wrapped_data_key']
|
471
|
+
# Decode WDK from base64 format
|
472
|
+
wdk = Base64.strict_decode64(wrapped_data_key)
|
473
|
+
# Use private key to decrypt the wrapped data key
|
474
|
+
dk = private_key.private_decrypt(wdk,OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
475
|
+
|
476
|
+
@key['raw'] = dk
|
477
|
+
@key['uses'] = 0
|
478
|
+
else
|
479
|
+
# Raise the error if response is not 200
|
480
|
+
raise RuntimeError, "HTTPError Response: Expected 201, got #{response.code}"
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# If the key object exists, create a new decryptor
|
485
|
+
# with the initialization vector from the header and
|
486
|
+
# the decrypted key (which is either new from the
|
487
|
+
# server or cached from the previous decryption). in
|
488
|
+
# either case, increment the key usage
|
489
|
+
|
490
|
+
if @key.present?
|
491
|
+
@algo = Algo.new.get_algo(@key['algorithm'])
|
492
|
+
@key['dec'] = Algo.new.decryptor(@algo, @key['raw'], iv)
|
493
|
+
@key['uses'] += 1
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
# if the object has a key and a decryptor, then decrypt whatever
|
500
|
+
# data is in the buffer, less any data that needs to be saved to
|
501
|
+
# serve as the tag.
|
502
|
+
plain_text = ''
|
503
|
+
if @key.present? and @key.key?("dec")
|
504
|
+
size = @data.length - @algo[:tag_length]
|
505
|
+
if size > 0
|
506
|
+
puts @data[0..size-1]
|
507
|
+
|
508
|
+
plain_text = @key['dec'].update(@data[0..size-1])
|
509
|
+
@data = @data[size..-1]
|
510
|
+
end
|
511
|
+
return plain_text
|
512
|
+
end
|
513
|
+
|
514
|
+
end
|
515
|
+
|
516
|
+
def end
|
517
|
+
raise RuntimeError, 'Decryption is not Started' if !@decryption_started
|
518
|
+
# The update function always maintains tag-size bytes in
|
519
|
+
# the buffer because this function provides no data parameter.
|
520
|
+
# by the time the caller calls this function, all data must
|
521
|
+
# have already been input to the decryption object.
|
522
|
+
|
523
|
+
sz = @data.length - @algo[:tag_length]
|
524
|
+
|
525
|
+
raise RuntimeError, 'Invalid Tag!' if sz < 0
|
526
|
+
if sz == 0
|
527
|
+
@key['dec'].auth_tag = @data
|
528
|
+
begin
|
529
|
+
pt = @key['dec'].final
|
530
|
+
# Delete the decryptor context
|
531
|
+
@key.delete('dec')
|
532
|
+
# Return the decrypted plain data
|
533
|
+
@decryption_started = false
|
534
|
+
return pt
|
535
|
+
rescue Exception => e
|
536
|
+
print 'Invalid cipher data or tag!'
|
537
|
+
return ''
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
def close
|
543
|
+
raise RuntimeError, 'Decryption currently running' if @decryption_started
|
544
|
+
# Reset the internal state of the decryption object
|
545
|
+
if @key.present?
|
546
|
+
if @key['uses'] > 0
|
547
|
+
query_url = "#{endpoint}/#{@key['finger_print']}/#{@key['session']}"
|
548
|
+
url = "#{endpoint_base}/decryption/key/#{@key['finger_print']}/#{@key['session']}"
|
549
|
+
query = {uses: @key['uses']}
|
550
|
+
headers = Auth.build_headers(@papi, @sapi, query_url, query, @host, 'patch')
|
551
|
+
response = HTTParty.patch(
|
552
|
+
url,
|
553
|
+
body: query.to_json,
|
554
|
+
headers: headers
|
555
|
+
)
|
556
|
+
remove_instance_variable(:@data)
|
557
|
+
remove_instance_variable(:@key)
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "security_client/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "security_client"
|
7
|
+
spec.version = SecurityClient::VERSION
|
8
|
+
spec.authors = ["vinaymehta"]
|
9
|
+
spec.email = ["vinay.ymca@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{Ubiq Security ruby client}
|
12
|
+
|
13
|
+
# Specify which files should be added to the gem when it is released.
|
14
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency 'httparty', '~> 0.13.7'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: security_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- vinaymehta
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httparty
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.13.7
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.13.7
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- vinay.ymca@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".DS_Store"
|
63
|
+
- ".gitignore"
|
64
|
+
- CODE_OF_CONDUCT.md
|
65
|
+
- Gemfile
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- bin/console
|
69
|
+
- bin/setup
|
70
|
+
- lib/security_client.rb
|
71
|
+
- lib/security_client/version.rb
|
72
|
+
- security_client.gemspec
|
73
|
+
homepage:
|
74
|
+
licenses: []
|
75
|
+
metadata: {}
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubygems_version: 3.0.4
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Ubiq Security ruby client
|
95
|
+
test_files: []
|