e3db 1.0.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.
@@ -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: []