e3db 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8130f8d97af89a3b0dadd56a137f215950df2ff6
4
+ data.tar.gz: 1d8c7ea1c68e443417fe8426ae742e3546f3e378
5
+ SHA512:
6
+ metadata.gz: 40e554e311c5c25c8ea9eeffb2a33c0a27073b9d852ae23895b2577f0d279e909c6401eccc60b0725c0e6a395f558b6059a80801b3a4c65566f4f14795c6ede4
7
+ data.tar.gz: ae4c58bacbeba5924d47a181b2d9e9f179bbdda47bc5987edc3a4baaa8e5b47dc19be87b206042d8832747bf813a9e2468c6414c3063715937a71629e2f591b6
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .integration-test.json
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.0.0
5
+ before_install: gem install bundler -v 1.12.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in e3db.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2017, Tozny, LLC.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,142 @@
1
+
2
+ # Introduction
3
+
4
+ The Tozny End-to-End Encrypted Database (E3DB) is a storage platform
5
+ with powerful sharing and consent management features.
6
+ [Read more on our blog.](https://tozny.com/blog/announcing-project-e3db-the-end-to-end-encrypted-database/)
7
+
8
+ E3DB provides a familiar JSON-based NoSQL-style API for reading, writing,
9
+ and querying data stored securely in the cloud.
10
+
11
+ # Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'e3db'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install e3db
26
+
27
+ At runtime, you will need the `libsodium` cryptography library
28
+ required by the native RbNaCl Ruby library. On most platforms
29
+ a package is available by default:
30
+
31
+ ```shell
32
+ $ brew install libsodium (Mac OS X)
33
+ $ apt-get install libsodium-dev (Ubuntu)
34
+ ```
35
+
36
+ For more information including libsodium installation instructions
37
+ for Windows, see the [libsodium web site](https://download.libsodium.org/doc/installation/).
38
+
39
+ _Windows Users:_ Make sure to download a recent "MSVC" build. Once downloaded, find the most recent `libsodium.dll` inside the ZIP file and copy it to somewhere in your `PATH`.
40
+
41
+ ## Registering a client
42
+
43
+ 1. Download and install the E3DB Command-Line interface (CLI) from our
44
+ [GitHub releases page](https://github.com/tozny/e3db-go/releases).
45
+
46
+ 2. Register an account using the CLI:
47
+
48
+ ```shell
49
+ $ e3db register me@mycompany.com
50
+ ```
51
+
52
+ This will create a new default configuration with a randomly
53
+ generated key pair and API credentials, saving it in `$HOME/.tozny/e3db.json`.
54
+
55
+ ## Loading configuration and creating a client
56
+
57
+ Use the `E3DB::Config.default` class method to load the default
58
+ client configuration, and pass it to the `E3DB::Client` constructor:
59
+
60
+ ```ruby
61
+ require 'e3db'
62
+ config = E3DB::Config.default
63
+ client = E3DB::Client.new(config)
64
+ ```
65
+
66
+ ### Using profiles to manage multiple configurations
67
+
68
+ The E3DB Command-Line Interface allows you to register and manage
69
+ multiple keys and credentials using _profiles_. To register a new
70
+ client under a different profile:
71
+
72
+ ```shell
73
+ $ e3db register --profile=development developers@mycompany.com
74
+ ```
75
+
76
+ You can then use `E3DB::Config.load_profile` to load a specific profile
77
+ inside your Ruby application:
78
+
79
+ ```ruby
80
+ config = E3DB::Config.load_profile('development')
81
+ client = E3DB::Client.new(config)
82
+ ```
83
+
84
+ ## Writing a record
85
+
86
+ To write new records to the database, first create a blank record
87
+ of the correct type using `E3DB::Client#new_record`. Then fill in
88
+ the fields of the record's `data` hash. Finally, write the record
89
+ to the database with `E3DB::Client#write`, which returns the
90
+ unique ID of the newly created record.
91
+
92
+ ```ruby
93
+ record = client.new_record('contact')
94
+ record.data[:first_name] = 'Jon'
95
+ record.data[:last_name] = 'Snow'
96
+ record.data[:phone] = '555-555-1212'
97
+ record_id = client.write(record)
98
+ printf("Wrote record %s\n", record_id)
99
+ ```
100
+
101
+ ## Querying Records
102
+
103
+ E3DB supports many options for querying records based on the fields
104
+ stored in record metadata. Refer to the API documentation for the
105
+ complete set of options that can be passed to `E3DB::Client#query`.
106
+
107
+ For example, to list all records of type `contact` and print a
108
+ simple report containing names and phone numbers:
109
+
110
+ ```ruby
111
+ client.query(type: 'contact') do |record|
112
+ fullname = record.data[:first_name] + ' ' + record.data[:last_name]
113
+ printf("%-40s %s\n", fullname, record.data[:phone])
114
+ end
115
+ ```
116
+
117
+ ## Development
118
+
119
+ Before running tests, register an `integration-test` profile using
120
+ the E3DB command-line tool:
121
+
122
+ ```shell
123
+ $ e3db -p integration-test register me+test@mycompany.com
124
+ ```
125
+
126
+ After checking out the repo, run `bin/setup` to install dependencies. Then,
127
+ run `rake spec` to run the tests. You can also run `bin/console` for an
128
+ interactive prompt that will allow you to experiment.
129
+
130
+ To install this gem onto your local machine, run `bundle exec rake install`.
131
+ To release a new version, update the version number in `version.rb`, and
132
+ then run `bundle exec rake release`, which will create a git tag for the
133
+ version, push git commits and tags, and push the `.gem` file to
134
+ [rubygems.org](https://rubygems.org).
135
+
136
+ ## Contributing
137
+
138
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tozny/e3db-ruby.
139
+
140
+ ## License
141
+
142
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "e3db"
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'e3db/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "e3db"
8
+ spec.version = E3DB::VERSION
9
+ spec.authors = ["Tozny, LLC"]
10
+ spec.email = ["info@tozny.com"]
11
+
12
+ spec.summary = %q{e3db client SDK}
13
+ spec.homepage = "https://tozny.com/e3db"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.12"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+ spec.add_development_dependency 'simplecov', '~> 0.14.1'
25
+
26
+ spec.add_dependency 'dry-struct', '~> 0.2.1'
27
+ spec.add_dependency 'lru_redux', '~> 1.1'
28
+ spec.add_dependency 'rbnacl', '~> 4.0', '>= 4.0.2'
29
+ spec.add_dependency 'net-http-persistent', '~> 2.9.4'
30
+ spec.add_dependency 'faraday_middleware', '~> 0.11.0.1'
31
+ spec.add_dependency 'oauth2', '~> 1.3', '>= 1.3.1'
32
+ end
@@ -0,0 +1,20 @@
1
+ #
2
+ # e3db.rb
3
+ #
4
+ # Copyright (C) 2017, Tozny, LLC.
5
+ # All Rights Reserved.
6
+ #
7
+
8
+
9
+ require 'e3db/version'
10
+ require 'e3db/types'
11
+ require 'e3db/config'
12
+ require 'e3db/client'
13
+ require 'e3db/crypto'
14
+
15
+ # Ruby client library for the Tozny End-to-End Encrypted Database (E3DB) service.
16
+ #
17
+ # @author Tozny, LLC.
18
+ # @version 1.0.0
19
+ module E3DB
20
+ end
@@ -0,0 +1,316 @@
1
+ #
2
+ # client.rb --- E3DB API client.
3
+ #
4
+ # Copyright (C) 2017, Tozny, LLC.
5
+ # All Rights Reserved.
6
+ #
7
+
8
+
9
+ require 'faraday'
10
+ require 'faraday_middleware'
11
+ require 'oauth2'
12
+ require 'rbnacl'
13
+ require 'base64'
14
+ require 'lru_redux'
15
+
16
+ module E3DB
17
+ # Faraday middleware to automatically refresh authentication tokens and
18
+ # pass them to API requests.
19
+ class TokenHelper < Faraday::Middleware
20
+ def initialize(app, client)
21
+ super(app)
22
+ @client = client
23
+ @token = nil
24
+ end
25
+
26
+ def call(env)
27
+ if @token.nil? or @token.expired?
28
+ @token = @client.client_credentials.get_token
29
+ end
30
+
31
+ env[:request_headers]['Authorization'] ||= %(Bearer #{@token.token})
32
+ @app.call env
33
+ end
34
+ end
35
+
36
+ private_constant :TokenHelper
37
+
38
+ # A client's public key information.
39
+ #
40
+ # @!attribute curve25519
41
+ # @return [String] a Base64URL Encoded Curve25519 public key
42
+ class PublicKey < Dry::Struct
43
+ attribute :curve25519, Types::Strict::String
44
+ end
45
+
46
+ # Information sent by the E3DB service about a client.
47
+ #
48
+ # @!attribute client_id
49
+ # @return [String] the client's unique ID string
50
+ # @!attribute public_key
51
+ # @return [PublicKey] the client's public key information
52
+ class ClientInfo < Dry::Struct
53
+ attribute :client_id, Types::Strict::String
54
+ attribute :public_key, PublicKey
55
+ attribute :validated, Types::Strict::Bool
56
+ end
57
+
58
+ # Meta-information about an E3DB record, such as who wrote it,
59
+ # when it was written, and the type of data stored.
60
+ #
61
+ # @!attribute record_id
62
+ # @return [String,nil] the unique ID of this record, or nil if not yet written
63
+ # @!attribute writer_id
64
+ # @return [String] the client ID that wrote this record
65
+ # @!attribute user_id
66
+ # @return [String] the subject client ID (currently == writer_id)
67
+ # @!attribute type
68
+ # @return [String] a free-form description of record content type
69
+ # @!attribute plain
70
+ # @return [Hash<String, String>] this record's plaintext record metadata
71
+ # @!attribute created
72
+ # @return [Time, nil] when this record was created, or nil if unavailable
73
+ # @!attribute last_modified
74
+ # @return [Time, nil] when this record was last modified, or nil if unavailable
75
+ class Meta < Dry::Struct
76
+ attribute :record_id, Types::Strict::String.optional
77
+ attribute :writer_id, Types::Strict::String
78
+ attribute :user_id, Types::Strict::String
79
+ attribute :type, Types::Strict::String
80
+ attribute :plain, Types::Strict::Hash.default { Hash.new }
81
+ attribute :created, Types::Json::DateTime.optional
82
+ attribute :last_modified, Types::Json::DateTime.optional
83
+ end
84
+
85
+ # A E3DB record containing data and metadata. Records are
86
+ # a key/value mapping containing data serialized
87
+ # into strings. All records are encrypted prior to sending them
88
+ # to the server for storage, and decrypted in the client after
89
+ # they are read.
90
+ #
91
+ # The {Client#new_record} method should be used to create a
92
+ # new record that can be written to the database with
93
+ # {Client#write}.
94
+ #
95
+ # To read a record by their unique ID, use {Client#read}, or to
96
+ # query a set of records based on their attributes, use {Client#query}.
97
+ #
98
+ # @!attribute meta
99
+ # @return [Meta] meta-information about this record
100
+ # @!attribute data
101
+ # @return [Hash<String, String>] this record's application-specific data
102
+ class Record < Dry::Struct
103
+ attribute :meta, Meta
104
+ attribute :data, Types::Strict::Hash.default { Hash.new }
105
+ end
106
+
107
+ # A connection to the E3DB service used to perform database operations.
108
+ #
109
+ # @!attribute [r] config
110
+ # @return [Config] the client configuration object
111
+ class Client
112
+ attr_reader :config
113
+
114
+ # Create a connection to the E3DB service given a configuration.
115
+ #
116
+ # @param config [Config] configuration and credentials to use
117
+ # @return [Client] a connection to the E3DB service
118
+ def initialize(config)
119
+ @config = config
120
+ @public_key = RbNaCl::PublicKey.new(Crypto.base64decode(@config.public_key))
121
+ @private_key = RbNaCl::PrivateKey.new(Crypto.base64decode(@config.private_key))
122
+
123
+ @ak_cache = LruRedux::ThreadSafeCache.new(1024)
124
+ @oauth_client = OAuth2::Client.new(
125
+ config.api_key_id,
126
+ config.api_secret,
127
+ :site => config.api_url,
128
+ :token_url => '/v1/auth/token',
129
+ :auth_scheme => :basic_auth,
130
+ :raise_errors => false)
131
+
132
+ if config.logging
133
+ @oauth_client.connection.response :logger, ::Logger.new($stdout)
134
+ end
135
+
136
+ @conn = Faraday.new(DEFAULT_API_URL) do |faraday|
137
+ faraday.use TokenHelper, @oauth_client
138
+ faraday.request :json
139
+ faraday.response :raise_error
140
+ if config.logging
141
+ faraday.response :logger, nil, :bodies => true
142
+ end
143
+ faraday.adapter :net_http_persistent
144
+ end
145
+ end
146
+
147
+ # Query the server for information about an E3DB client.
148
+ #
149
+ # @param client_id [String] client ID to look up
150
+ # @return [ClientInfo] information about this client
151
+ def client_info(client_id)
152
+ resp = @conn.get(get_url('v1', 'storage', 'clients', client_id))
153
+ ClientInfo.new(JSON.parse(resp.body, symbolize_names: true))
154
+ end
155
+
156
+ # Query the server for a client's public key.
157
+ #
158
+ # @param client_id [String] client ID to look up
159
+ # @return [RbNaCl::PublicKey] decoded Curve25519 public key
160
+ def client_key(client_id)
161
+ if client_id == @config.client_id
162
+ @public_key
163
+ else
164
+ Crypto.decode_public_key(client_info(client_id).public_key.curve25519)
165
+ end
166
+ end
167
+
168
+ # Read a single record by ID from E3DB and return it without
169
+ # decrypting the data fields.
170
+ #
171
+ # @param record_id [String] record ID to look up
172
+ # @return [Record] encrypted record object
173
+ def read_raw(record_id)
174
+ resp = @conn.get(get_url('v1', 'storage', 'records', record_id))
175
+ json = JSON.parse(resp.body, symbolize_names: true)
176
+ Record.new(json)
177
+ end
178
+
179
+ # Read a single record by ID from E3DB and return it.
180
+ #
181
+ # @param record_id [String] record ID to look up
182
+ # @return [Record] decrypted record object
183
+ def read(record_id)
184
+ decrypt_record(read_raw(record_id))
185
+ end
186
+
187
+ # Create a new, empty record that can be written to E3DB
188
+ # by calling {Client#write}.
189
+ #
190
+ # @param type [String] free-form content type of this record
191
+ # @return [Record] an empty record of `type`
192
+ def new_record(type)
193
+ id = @config.client_id
194
+ meta = Meta.new(record_id: nil, writer_id: id, user_id: id,
195
+ type: type, plain: Hash.new, created: nil,
196
+ last_modified: nil)
197
+ Record.new(meta: meta, data: Hash.new)
198
+ end
199
+
200
+ # Write a new record to the E3DB storage service.
201
+ #
202
+ # Create new records with {Client#new_record}.
203
+ #
204
+ # @param record [Record] record to write
205
+ # @return [String] the unique ID of the written record
206
+ def write(record)
207
+ url = get_url('v1', 'storage', 'records')
208
+ resp = @conn.post(url, encrypt_record(record).to_hash)
209
+ json = JSON.parse(resp.body, symbolize_names: true)
210
+ json[:meta][:record_id]
211
+ end
212
+
213
+ # Delete a record from the E3DB storage service.
214
+ #
215
+ # @param record_id [String] unique ID of record to delete
216
+ def delete(record_id)
217
+ resp = @conn.delete(get_url('v1', 'storage', 'records', record_id))
218
+ end
219
+
220
+ class Query < Dry::Struct
221
+ attribute :count, Types::Int
222
+ attribute :include_data, Types::Bool.optional
223
+ attribute :writer_ids, Types::Coercible::Array.member(Types::String).optional
224
+ attribute :user_ids, Types::Coercible::Array.member(Types::String).optional
225
+ attribute :record_ids, Types::Coercible::Array.member(Types::String).optional
226
+ attribute :content_types, Types::Coercible::Array.member(Types::String).optional
227
+ attribute :plain, Types::Hash.optional
228
+ attribute :after_index, Types::Int.optional
229
+
230
+ def after_index=(index)
231
+ @after_index = index
232
+ end
233
+
234
+ def as_json
235
+ JSON.generate(to_hash.reject { |k, v| v.nil? })
236
+ end
237
+ end
238
+
239
+ private_constant :Query
240
+
241
+ DEFAULT_QUERY_COUNT = 100
242
+ private_constant :DEFAULT_QUERY_COUNT
243
+
244
+ # Query E3DB records according to a set of selection criteria.
245
+ #
246
+ # Each record (optionally including data) is yielded to the block
247
+ # argument.
248
+ #
249
+ # @param writer [String,Array<String>] select records written by these client IDs
250
+ # @param record [String,Array<String>] select records with these record IDs
251
+ # @param type [String,Array<string>] select records with these types
252
+ # @param plain [Hash] plaintext query expression to select
253
+ # @param data [Boolean] include data in records
254
+ # @param raw [Boolean] when true don't decrypt record data
255
+ def query(data: true, raw: false, writer: nil, record: nil, type: nil, plain: nil)
256
+ q = Query.new(after_index: 0, include_data: data, writer_ids: writer,
257
+ record_ids: record, content_types: type, plain: plain,
258
+ user_ids: nil, count: DEFAULT_QUERY_COUNT)
259
+ url = get_url('v1', 'storage', 'search')
260
+ loop do
261
+ resp = @conn.post(url, q.as_json)
262
+ json = JSON.parse(resp.body, symbolize_names: true)
263
+ results = json[:results]
264
+ results.each do |r|
265
+ record = Record.new(meta: r[:meta], data: r[:record_data] || Hash.new)
266
+ if data && !raw
267
+ record = decrypt_record(record)
268
+ end
269
+ yield record
270
+ end
271
+
272
+ if results.length < q.count
273
+ break
274
+ end
275
+
276
+ q.after_index = json[:last_index]
277
+ end
278
+ end
279
+
280
+ # Grant another E3DB client access to records of a particular type.
281
+ #
282
+ # @param type [String] type of records to share
283
+ # @param reader_id [String] client ID of reader to grant access to
284
+ def share(type, reader_id)
285
+ if reader_id == @config.client_id
286
+ return
287
+ end
288
+
289
+ id = @config.client_id
290
+ ak = get_access_key(id, id, id, type)
291
+ put_access_key(id, id, reader_id, type, ak)
292
+
293
+ url = get_url('v1', 'storage', 'policy', id, id, reader_id, type)
294
+ @conn.put(url, JSON.generate({:allow => [{:read => {}}]}))
295
+ end
296
+
297
+ # Revoke another E3DB client's access to records of a particular type.
298
+ #
299
+ # @param type [String] type of records to revoke access to
300
+ # @param reader_id [String] client ID of reader to revoke access from
301
+ def revoke(type, reader_id)
302
+ if reader_id == @config.client_id
303
+ return
304
+ end
305
+
306
+ id = @config.client_id
307
+ url = get_url('v1', 'storage', 'policy', id, id, reader_id, type)
308
+ @conn.put(url, JSON.generate({:deny => [{:read => {}}]}))
309
+ end
310
+
311
+ private
312
+ def get_url(*paths)
313
+ sprintf('%s/%s', @config.api_url.chomp('/'), paths.map { |x| URI.escape x }.join('/'))
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,87 @@
1
+ #
2
+ # config.rb --- E3DB configuration files.
3
+ #
4
+ # Copyright (C) 2017, Tozny, LLC.
5
+ # All Rights Reserved.
6
+ #
7
+
8
+
9
+ module E3DB
10
+ DEFAULT_API_URL = 'https://dev.e3db.com/'
11
+
12
+ # Configuration and credentials for E3DB.
13
+ #
14
+ # Typically a configuration is loaded from a JSON file generated
15
+ # during registration via the E3DB administration console
16
+ # or command-line tool. To load a configuration from a JSON file,
17
+ # use {Config.load}.
18
+ #
19
+ # @!attribute version
20
+ # @return [Int] the version number of the configuration format (currently 1)
21
+ # @!attribute client_id
22
+ # @return [String] the client's unique client identifier
23
+ # @!attribute api_key_id
24
+ # @return [String] the client's non-secret API key component
25
+ # @!attribute api_secret
26
+ # @return [String] the client's confidential API key component
27
+ # @!attribute public_key
28
+ # @return [String] the client's Base64URL encoded Curve25519 public key
29
+ # @!attribute private_key
30
+ # @return [String] the client's Base64URL encoded Curve25519 private key
31
+ # @!attribute api_base_url
32
+ # @return [String] the base URL for the E3DB API service
33
+ # @!attribute auth_base_url
34
+ # @return [String] the base URL for the E3DB authentication service
35
+ # @!attribute logging
36
+ # _Warning:_ Log output will contain confidential authentication
37
+ # tokens---do not enable in production if log output isn't confidential!
38
+ # @return [Boolean] a flag to enable HTTP logging when true
39
+ class Config < Dry::Struct
40
+ attribute :version, Types::Int
41
+ attribute :client_id, Types::String
42
+ attribute :api_key_id, Types::String
43
+ attribute :api_secret, Types::String
44
+ attribute :public_key, Types::String
45
+ attribute :private_key, Types::String
46
+ attribute :api_url, Types::String.default(DEFAULT_API_URL)
47
+ attribute :logging, Types::Bool
48
+
49
+ # Load configuration from a JSON file created during registration
50
+ # or with {Config.save}.
51
+ #
52
+ # The configuration file should contain a single JSON object
53
+ # with the following structure:
54
+ #
55
+ # {
56
+ # "version": 1,
57
+ # "client_id": "UUID",
58
+ # "api_key_id": "API_KEY",
59
+ # "api_secret": "API_SECRET",
60
+ # "public_key": "PUBLIC_KEY",
61
+ # "private_key": "PRIVATE_KEY",
62
+ # "api_url": "URL",
63
+ # }
64
+ #
65
+ # @param filename [String] pathname of JSON configuration to load
66
+ # @return [Config] the configuration object loaded from the file
67
+ def self.load(filename)
68
+ json = JSON.parse(File.read(filename), symbolize_names: true)
69
+ if json[:version] != 1
70
+ raise StandardError, "Unsupported config version: #{json[:version]}"
71
+ end
72
+ Config.new(json.merge(:logging => false))
73
+ end
74
+
75
+ def self.default
76
+ return self.load(File.join(Dir.home, '.tozny', 'e3db.json'))
77
+ end
78
+
79
+ def self.load_profile(profile)
80
+ return self.load(File.join(Dir.home, '.tozny', profile, 'e3db.json'))
81
+ end
82
+
83
+ def logging=(value)
84
+ @logging = value
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,139 @@
1
+ #
2
+ # crypto.rb --- E3DB cryptographic operations.
3
+ #
4
+ # Copyright (C) 2017, Tozny, LLC.
5
+ # All Rights Reserved.
6
+ #
7
+
8
+
9
+ module E3DB
10
+ class Client
11
+ private
12
+ def get_access_key(writer_id, user_id, reader_id, type)
13
+ ak_cache_key = [writer_id, user_id, type]
14
+ if @ak_cache.key? ak_cache_key
15
+ return @ak_cache[ak_cache_key]
16
+ end
17
+
18
+ url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
19
+ resp = @conn.get(url)
20
+ json = JSON.parse(resp.body, symbolize_names: true)
21
+
22
+ k = json[:authorizer_public_key][:curve25519]
23
+ authorizer_pubkey = Crypto.decode_public_key(k)
24
+
25
+ fields = json[:eak].split('.', 2)
26
+ ciphertext = Crypto.base64decode(fields[0])
27
+ nonce = Crypto.base64decode(fields[1])
28
+ box = RbNaCl::Box.new(authorizer_pubkey, @private_key)
29
+
30
+ ak = box.decrypt(nonce, ciphertext)
31
+ @ak_cache[ak_cache_key] = ak
32
+ ak
33
+ end
34
+
35
+ def put_access_key(writer_id, user_id, reader_id, type, ak)
36
+ ak_cache_key = [writer_id, user_id, type]
37
+ @ak_cache[ak_cache_key] = ak
38
+
39
+ reader_key = client_key(reader_id)
40
+ nonce = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
41
+ eak = RbNaCl::Box.new(reader_key, @private_key).encrypt(nonce, ak)
42
+
43
+ encoded_eak = sprintf('%s.%s', Crypto.base64encode(eak), Crypto.base64encode(nonce))
44
+
45
+ url = get_url('v1', 'storage', 'access_keys', writer_id, user_id, reader_id, type)
46
+ @conn.put(url, { :eak => encoded_eak })
47
+ end
48
+
49
+ def decrypt_record(encrypted_record)
50
+ record = Record.new(meta: encrypted_record.meta.clone, data: Hash.new)
51
+
52
+ writer_id = record.meta.writer_id
53
+ user_id = record.meta.user_id
54
+ type = record.meta.type
55
+ ak = get_access_key(writer_id, user_id, @config.client_id, type)
56
+
57
+ encrypted_record.data.each do |k, v|
58
+ fields = v.split('.', 4)
59
+
60
+ edk = Crypto.base64decode(fields[0])
61
+ edkN = Crypto.base64decode(fields[1])
62
+ ef = Crypto.base64decode(fields[2])
63
+ efN = Crypto.base64decode(fields[3])
64
+
65
+ dk = RbNaCl::SecretBox.new(ak).decrypt(edkN, edk)
66
+ pv = RbNaCl::SecretBox.new(dk).decrypt(efN, ef)
67
+
68
+ record.data[k] = pv
69
+ end
70
+
71
+ record
72
+ end
73
+
74
+ def encrypt_record(plaintext_record)
75
+ record = Record.new(meta: plaintext_record.meta.clone, data: Hash.new)
76
+
77
+ writer_id = record.meta.writer_id
78
+ user_id = record.meta.user_id
79
+ type = record.meta.type
80
+
81
+ begin
82
+ ak = get_access_key(writer_id, user_id, @config.client_id, type)
83
+ rescue Faraday::ResourceNotFound
84
+ ak = RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes)
85
+ put_access_key(writer_id, user_id, @config.client_id, type, ak)
86
+ end
87
+
88
+ plaintext_record.data.each do |k, v|
89
+ dk = Crypto.secret_box_random_key
90
+ efN = Crypto.secret_box_random_nonce
91
+ ef = RbNaCl::SecretBox.new(dk).encrypt(efN, v)
92
+ edkN = Crypto.secret_box_random_nonce
93
+ edk = RbNaCl::SecretBox.new(ak).encrypt(edkN, dk)
94
+
95
+ record.data[k] = sprintf('%s.%s.%s.%s',
96
+ Crypto.base64encode(edk), Crypto.base64encode(edkN),
97
+ Crypto.base64encode(ef), Crypto.base64encode(efN))
98
+ end
99
+
100
+ record
101
+ end
102
+ end
103
+
104
+ class Crypto
105
+ def self.decode_public_key(s)
106
+ RbNaCl::PublicKey.new(base64decode(s))
107
+ end
108
+
109
+ def self.encode_public_key(k)
110
+ base64encode(k.to_bytes)
111
+ end
112
+
113
+ def self.decode_private_key(s)
114
+ RbNaCl::PrivateKey.new(base64decode(s))
115
+ end
116
+
117
+ def self.encode_private_key(k)
118
+ base64encode(k.to_bytes)
119
+ end
120
+
121
+ def self.secret_box_random_key
122
+ RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes)
123
+ end
124
+
125
+ def self.secret_box_random_nonce
126
+ RbNaCl::Random.random_bytes(RbNaCl::SecretBox.nonce_bytes)
127
+ end
128
+
129
+ def self.base64encode(x)
130
+ Base64.urlsafe_encode64(x, padding: false)
131
+ end
132
+
133
+ def self.base64decode(x)
134
+ Base64.urlsafe_decode64(x)
135
+ end
136
+ end
137
+
138
+ private_constant :Crypto
139
+ end
@@ -0,0 +1,10 @@
1
+
2
+ require 'dry-struct'
3
+
4
+ module E3DB
5
+ module Types
6
+ include Dry::Types.module
7
+ end
8
+
9
+ private_constant :Types
10
+ end
@@ -0,0 +1,3 @@
1
+ module E3DB
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: e3db
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tozny, LLC
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-05-04 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: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
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: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.14.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 0.14.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-struct
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 0.2.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.2.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: lru_redux
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rbnacl
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '4.0'
104
+ - - '>='
105
+ - !ruby/object:Gem::Version
106
+ version: 4.0.2
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ~>
112
+ - !ruby/object:Gem::Version
113
+ version: '4.0'
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: 4.0.2
117
+ - !ruby/object:Gem::Dependency
118
+ name: net-http-persistent
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: 2.9.4
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ~>
129
+ - !ruby/object:Gem::Version
130
+ version: 2.9.4
131
+ - !ruby/object:Gem::Dependency
132
+ name: faraday_middleware
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ~>
136
+ - !ruby/object:Gem::Version
137
+ version: 0.11.0.1
138
+ type: :runtime
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ~>
143
+ - !ruby/object:Gem::Version
144
+ version: 0.11.0.1
145
+ - !ruby/object:Gem::Dependency
146
+ name: oauth2
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ~>
150
+ - !ruby/object:Gem::Version
151
+ version: '1.3'
152
+ - - '>='
153
+ - !ruby/object:Gem::Version
154
+ version: 1.3.1
155
+ type: :runtime
156
+ prerelease: false
157
+ version_requirements: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ~>
160
+ - !ruby/object:Gem::Version
161
+ version: '1.3'
162
+ - - '>='
163
+ - !ruby/object:Gem::Version
164
+ version: 1.3.1
165
+ description:
166
+ email:
167
+ - info@tozny.com
168
+ executables: []
169
+ extensions: []
170
+ extra_rdoc_files: []
171
+ files:
172
+ - .gitignore
173
+ - .rspec
174
+ - .travis.yml
175
+ - Gemfile
176
+ - LICENSE.txt
177
+ - README.md
178
+ - Rakefile
179
+ - bin/console
180
+ - bin/setup
181
+ - e3db.gemspec
182
+ - lib/e3db.rb
183
+ - lib/e3db/client.rb
184
+ - lib/e3db/config.rb
185
+ - lib/e3db/crypto.rb
186
+ - lib/e3db/types.rb
187
+ - lib/e3db/version.rb
188
+ homepage: https://tozny.com/e3db
189
+ licenses:
190
+ - MIT
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - '>='
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 2.0.14.1
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: e3db client SDK
212
+ test_files: []