security_client 0.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 +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: []
|