transcryptor 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6813eb2a6c0d37ead4decf3e6e1ee7df68a988b8
4
+ data.tar.gz: 5154be7dd43bc80e31bba691f7e597914697a49a
5
+ SHA512:
6
+ metadata.gz: 1501fe5d238025d10dc60985956e39905fc1151b1a0d35b0d4e98b04d2ed4e2eabdf6b4543e42d88056e95d451fd1acc22ac1b62b2798fd5aa5537d2f14e3cef
7
+ data.tar.gz: 497c6bd3468048f641378626d0019f76ac8184f7fa75749a290a4e15814cf1a28ee8e91b91aaea9ef918422ff766df07039f59d154c3136380a85fe4749f07de
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
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.3.2
5
+ before_install: gem install bundler -v 1.13.7
@@ -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 jeffrey.lau@ribose.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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in transcryptor.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Ribose Inc.
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,196 @@
1
+ = Transcryptor
2
+ :source-highlighter: pygments
3
+
4
+ image:https://img.shields.io/travis/riboseinc/transcryptor/master.svg["Build Status", link="https://travis-ci.org/riboseinc/transcryptor"]
5
+ image:https://img.shields.io/coverity/scan/12786.svg["Coverity Scan Build Status", link="https://scan.coverity.com/projects/riboseinc-transcryptor"]
6
+
7
+ Transcryptor provides utility functions to help migrate records encrypted with
8
+ https://github.com/attr-encrypted/attr_encrypted[`attr_encrypted`] from one
9
+ encryption configuration to another.
10
+
11
+ == Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ [source,ruby]
16
+ ----
17
+ gem 'transcryptor', github: 'riboseinc/transcryptor'
18
+ ----
19
+
20
+ And then execute:
21
+
22
+ ----
23
+ bundle
24
+ ----
25
+
26
+ Or install it yourself as:
27
+
28
+ ----
29
+ gem install transcryptor
30
+ ----
31
+
32
+ == Usage
33
+
34
+ Given:
35
+
36
+ . you have already set up tables to be encrypted with `attr_encrypted`,
37
+ . you'd like to migrate your columns from:
38
+ +
39
+ (`algorithm~1~`, `iv~1~`, `salt~1~`, `key~1~`)
40
+ +
41
+ to
42
+ +
43
+ (`algorithm~2~`, `iv~2~`, `salt~2~`, `key~2~`)
44
+ +
45
+ where:
46
+ +
47
+ .. `algorithm` can be `aes-256-cbc`, `aes-256-gcm` or any others that
48
+ `attr_encrypted` supports,
49
+ .. `salt` can be optional,
50
+ .. `iv` can be optional (!!).
51
+
52
+ Then:
53
+
54
+ . Create a migration like so:
55
+ +
56
+ [source,ruby]
57
+ ----
58
+ class ReencryptUsersAndDocumentsWithNewKeys < ActiveRecord::Migration
59
+
60
+ def transcryptor
61
+ Transcryptor.init(self)
62
+ end
63
+
64
+ # +keyifier+ mirrors the functionality provided by the :key Proc in
65
+ # attr_encrypted.
66
+ # NOTE: Has to return the entire Hash.
67
+ #
68
+ def old_keyifier
69
+ -> opts {
70
+ opts[:key] = ENV['old_master_encryption_key'] + opts[:key]
71
+ opts
72
+ }
73
+ end
74
+
75
+ def new_keyifier
76
+ -> opts {
77
+ opts[:key] = ENV['new_master_encryption_key'] + opts[:key]
78
+ opts
79
+ }
80
+ end
81
+
82
+ # Define the current DB schema for Transcryptor.
83
+ # Format:
84
+ # {
85
+ # <table_i>: {
86
+ # id_column: <the column name for record id>,
87
+ # columns: {
88
+ # <column_i>: {
89
+ # prefix: <attr_encrypted prefix string (optional)>,
90
+ # suffix: <attr_encrypted suffix string (optional)>,
91
+ # key: <the column storing the key to encrypt column_i>,
92
+ # },
93
+ # }
94
+ # },
95
+ # }
96
+ #
97
+ def table_column_spec
98
+ {
99
+ users: {
100
+ id_column: :id,
101
+ columns: {
102
+ email: {
103
+ prefix: 'encrypted_',
104
+ key: :ekey,
105
+ },
106
+ birthday: {
107
+ prefix: 'encrypted_',
108
+ key: :ekey,
109
+ },
110
+ }
111
+ },
112
+ documents: {
113
+ id_column: :id,
114
+ columns: {
115
+ passphrase: {
116
+ prefix: 'secret_',
117
+ key: :enc_key,
118
+ },
119
+ }
120
+ },
121
+ }
122
+ end
123
+
124
+ #
125
+ # Run transcryptor.updown_migrate() for both #up and #down.
126
+ # Give it:
127
+ # - the table-column specification,
128
+ # - the old encryption configuration (at least any one of: algorithm, iv,
129
+ # salt, key)
130
+ # - the new encryption configuration (at least any one of: algorithm, iv,
131
+ # salt, key)
132
+ # - optional params-modifying Proc before passing to Encryptor.decrypt (used
133
+ # by attr_encrypted)
134
+ # - optional params-modifying Proc before passing to Encryptor.encrypt (used
135
+ # by attr_encrypted)
136
+ #
137
+ def up
138
+ transcryptor.updown_migrate(
139
+ table_column_spec,
140
+ {
141
+ algorithm: 'aes-256-cbc',
142
+ decode64_value: true,
143
+ }, {
144
+ algorithm: 'aes-256-gcm',
145
+ encode64_iv: true,
146
+ encode64_value: true,
147
+ iv: true,
148
+ },
149
+ old_keyifier,
150
+ new_keyifier,
151
+ )
152
+ end
153
+
154
+ def down
155
+ transcryptor.updown_migrate(
156
+ table_column_spec,
157
+ {
158
+ algorithm: 'aes-256-gcm',
159
+ decode64_iv: true,
160
+ decode64_value: true,
161
+ }, {
162
+ algorithm: 'aes-256-cbc',
163
+ iv: false,
164
+ salt: false,
165
+ encode64_value: true,
166
+ insecure_mode: true,
167
+ },
168
+ new_keyifier,
169
+ old_keyifier,
170
+ )
171
+ end
172
+
173
+ ----
174
+ . Run `bundle exec db:migrate`
175
+ . Done!
176
+
177
+ == Development
178
+
179
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
180
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
181
+ prompt that will allow you to experiment.
182
+
183
+ == Contributing
184
+
185
+ Bug reports and pull requests are welcome on GitHub at
186
+ https://github.com/riboseinc/transcryptor. This project is intended to be a
187
+ safe, welcoming space for collaboration, and contributors are expected to
188
+ adhere to the http://contributor-covenant.org[Contributor Covenant] code of
189
+ conduct.
190
+
191
+
192
+ == License
193
+
194
+ The gem is available as open source under the terms of the
195
+ http://opensource.org/licenses/MIT[MIT License].
196
+
@@ -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 "transcryptor"
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,545 @@
1
+ # encoding: utf-8
2
+ require "transcryptor/version"
3
+ require "active_support"
4
+ require "active_record"
5
+ require 'encryptor'
6
+
7
+ # To use Transcryptor, here is a sample migration that showcases this:
8
+ #
9
+ # class ReencryptUsersAndDocumentsWithNewKeys < ActiveRecord::Migration
10
+ #
11
+ # def transcryptor
12
+ # Transcryptor.init(self)
13
+ # end
14
+ #
15
+ # # +keyifier+ mirrors the functionality provided by the :key Proc in
16
+ # # attr_encrypted.
17
+ # # NOTE: Has to return the entire Hash.
18
+ # #
19
+ # def old_keyifier
20
+ # -> opts {
21
+ # opts[:key] = ENV['old_master_encryption_key'] + opts[:key]
22
+ # opts
23
+ # }
24
+ # end
25
+ #
26
+ # def new_keyifier
27
+ # -> opts {
28
+ # opts[:key] = ENV['new_master_encryption_key'] + opts[:key]
29
+ # opts
30
+ # }
31
+ # end
32
+ #
33
+ # def table_column_spec
34
+ # {
35
+ # users: {
36
+ # id_column: :id,
37
+ # columns: {
38
+ # email: {
39
+ # prefix: 'encrypted_',
40
+ # key: :ekey,
41
+ # },
42
+ # birthday: {
43
+ # prefix: 'encrypted_',
44
+ # key: :ekey,
45
+ # },
46
+ # }
47
+ # },
48
+ # documents: {
49
+ # id_column: :id,
50
+ # columns: {
51
+ # passphrase: {
52
+ # prefix: 'encrypted_',
53
+ # key: :ekey,
54
+ # },
55
+ # }
56
+ # },
57
+ # }
58
+ # end
59
+ #
60
+ # def up
61
+ # transcryptor.updown_migrate(
62
+ # table_column_spec,
63
+ # {
64
+ # algorithm: 'aes-256-cbc',
65
+ # decode64_value: true,
66
+ # }, {
67
+ # algorithm: 'aes-256-gcm',
68
+ # encode64_iv: true,
69
+ # encode64_value: true,
70
+ # iv: true,
71
+ # },
72
+ # old_keyifier,
73
+ # new_keyifier,
74
+ # )
75
+ # end
76
+ #
77
+ # def down
78
+ # transcryptor.updown_migrate(
79
+ # table_column_spec,
80
+ # {
81
+ # algorithm: 'aes-256-gcm',
82
+ # decode64_iv: true,
83
+ # decode64_value: true,
84
+ # }, {
85
+ # algorithm: 'aes-256-cbc',
86
+ # iv: false,
87
+ # salt: false,
88
+ # encode64_value: true,
89
+ # insecure_mode: true,
90
+ # },
91
+ # new_keyifier,
92
+ # old_keyifier,
93
+ # )
94
+ # end
95
+ #
96
+ module Transcryptor
97
+
98
+ # Initialize Transcryptor instance with the migration instance.
99
+ # This step allows typical migration methods like #execute to be invoked
100
+ # from this gem.
101
+ def self.init(migration_instance = Kernel.caller)
102
+ Instance.new(migration_instance)
103
+ end
104
+
105
+ class Instance
106
+
107
+ attr_accessor :migration_instance
108
+
109
+ def initialize(migration_instance)
110
+ self.migration_instance = migration_instance
111
+ end
112
+
113
+ def execute *args
114
+ puts "\e[38;5;141m"
115
+ puts puts args
116
+ puts "\e[0m"
117
+ migration_instance.execute *args
118
+ end
119
+
120
+ def sanitize(sql_fragment)
121
+ ActiveRecord::Base.sanitize(sql_fragment)
122
+ end
123
+
124
+ # Meant to be used by both #up and #down.
125
+ #
126
+ # table_column_spec:
127
+ # {
128
+ # table1: {
129
+ # id_column: :id,
130
+ # columns: {
131
+ # column1: {
132
+ # prefix: 'encoded_',
133
+ # key: :encryption_key_1,
134
+ # },
135
+ # column2: {
136
+ # prefix: 'xXx_en_ing_',
137
+ # key: :encryption_key_2,
138
+ # suffix: '_crypted_xXx',
139
+ # },
140
+ # }
141
+ # },
142
+ # table2: {
143
+ # id_column: :id,
144
+ # columns: {
145
+ # column3: {
146
+ # prefix: 'encoded_',
147
+ # key: :encryption_key_3,
148
+ # },
149
+ # column4: {
150
+ # prefix: 'xXx_en_ing_',
151
+ # key: :encryption_key_4,
152
+ # suffix: '_crypted_xXx',
153
+ # },
154
+ # }
155
+ # },
156
+ # }
157
+ def get_column_names_from(table_name, table_spec)
158
+ id_name = table_spec[:id_column]
159
+ column_specs = table_spec[:columns]
160
+
161
+ puts "table name is #{table_name}"
162
+ puts "table psec is #{table_spec}"
163
+ res = [ id_name ] + column_specs.map do |column_name, column_spec|
164
+ column_prefix = column_spec[:prefix]
165
+ column_key_field = column_spec[:key]
166
+ column_suffix = column_spec[:suffix]
167
+ full_column_name = :"#{column_prefix}#{column_name}#{column_suffix}"
168
+
169
+ [ full_column_name, column_key_field ] + %i[iv salt].reduce([]) do |acc, suffix|
170
+ extra_column_name = :"#{full_column_name}_#{suffix}"
171
+ acc << extra_column_name if column_exists?(table_name, extra_column_name)
172
+ acc
173
+ end
174
+ end.flatten.compact.uniq
175
+ pp res
176
+ res
177
+ end
178
+
179
+ def updown_migrate(table_column_spec, old_spec, new_spec, decrypt_opts_fn, encrypt_opts_fn)
180
+
181
+ # puts "table column spec is:"
182
+ # pp table_column_spec
183
+
184
+ table_column_spec.each do |table_name, table_spec|
185
+ column_specs = table_spec[:columns]
186
+ relevant_column_names = get_column_names_from(table_name, table_spec)
187
+ puts "relevant column names are:"
188
+ pp relevant_column_names
189
+
190
+ execute(
191
+ "SELECT #{relevant_column_names.join(', ')} FROM `#{table_name}`"
192
+ ).each do |_db_values|
193
+ id, _dontcare = _db_values
194
+
195
+ puts 'db values'
196
+ pp _db_values
197
+ # A map: { :db_field_name => "value" }
198
+ db_values =
199
+ Hash[relevant_column_names.map(&:to_sym).zip(_db_values)]
200
+
201
+ # Build up reencryption params to pass to reencrypt().
202
+ encrypted_attrs = column_specs.keys.map do |attr_name|
203
+
204
+ column_spec = column_specs[attr_name]
205
+ column_prefix = column_spec[:prefix]
206
+ column_key_field = column_spec[:key]
207
+ column_suffix = column_spec[:suffix]
208
+ full_column_name = :"#{column_prefix}#{attr_name}#{column_suffix}"
209
+
210
+ encrypted_value = db_values[:"#{full_column_name}"]
211
+ key = db_values[:"#{column_key_field}"]
212
+ # +key+ could be nil, but it's OK, since it may be provided via
213
+ # other means, e.g. encrypt_opts_fn and decrypt_opts_fn.
214
+
215
+ unless encrypted_value.nil? || encrypted_value == ""
216
+ res = {
217
+ attr_name: attr_name,
218
+ key: key,
219
+ value: encrypted_value,
220
+ }
221
+
222
+ # Merge in iv and/or salt as appropriate.
223
+ %i[iv salt].reduce(res) do |acc, suffix|
224
+ extra_column_name = :"#{full_column_name}_#{suffix}"
225
+ if relevant_column_names.include?(extra_column_name)
226
+ acc[suffix] = db_values[extra_column_name]
227
+ end
228
+ acc
229
+ end
230
+ end
231
+ end.compact
232
+
233
+ next if encrypted_attrs.empty?
234
+
235
+ re_encrypt(
236
+ table_name,
237
+ id,
238
+ encrypted_attrs.map do |attr|
239
+ {
240
+ # These would be in +attr+ as approprate.
241
+ # salt: old_salt,
242
+ # iv: old_iv,
243
+ # key: old_key,
244
+ # attr_name: attr_name,
245
+ # value: encrypted_value,
246
+ old: attr.merge(old_spec),
247
+ new: { key: attr[:key], }.merge(new_spec),
248
+ }
249
+ end,
250
+ decrypt_opts_fn,
251
+ encrypt_opts_fn,
252
+ )
253
+ end
254
+
255
+ end
256
+ end
257
+
258
+ # +table_name+ is the SQL table name for the record at id = +record_id+.
259
+ # +attrs_specs+ is an Array like so: [ {
260
+ # old: {
261
+ # key: String,
262
+ # value: String,
263
+ # attr_name: String,
264
+ # algorithm: String,
265
+ # iv: String | Nil,
266
+ # salt: String | Nil,
267
+ # },
268
+ # new: {
269
+ # algorithm: String,
270
+ # iv: String | Bool,
271
+ # salt: String | Bool,
272
+ # },
273
+ # } ]
274
+ #
275
+ # Assumptions: Encrypted attribute SQL column names are all prefixed with
276
+ # "encrypted_", and also suffixed with "_iv" & "_salt" for the corresponding
277
+ # iv and salt.
278
+ #
279
+ def re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_')
280
+ set_statement =
281
+ set_clauses_for_re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_').
282
+ join(', ')
283
+
284
+ update_statement = <<-EOF
285
+ UPDATE `#{table_name}`
286
+ SET #{set_statement}
287
+ WHERE id = #{ActiveRecord::Base.sanitize(record_id)}
288
+ EOF
289
+
290
+ puts puts "\e[38;5;42m"
291
+ puts update_statement
292
+ puts "\e[0m"
293
+ execute(update_statement)
294
+ end
295
+
296
+ def set_clauses_for_re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_')
297
+ # puts "attrs_specs:"
298
+ # pp attrs_specs
299
+
300
+ attrs_specs.map do |attr_spec|
301
+
302
+ old_spec = attr_spec[:old]
303
+ new_spec = attr_spec[:new]
304
+
305
+ plain_stuff = dec(old_spec) do |opts|
306
+ decrypt_opts_fn.call(opts)
307
+ end
308
+ result_stuff = enc(new_spec.merge(value: plain_stuff[:value])) do |opts|
309
+ encrypt_opts_fn.call(opts)
310
+ end
311
+
312
+ new_ciphertext = result_stuff[:value]
313
+ attr_name = old_spec[:attr_name]
314
+
315
+ extra_columns = %i[iv salt].reduce({}) do |acc, suffix|
316
+ extra_column_name = "#{column_prefix}#{attr_name}_#{suffix}"
317
+ acc[suffix] = extra_column_name if column_exists?(table_name, extra_column_name)
318
+
319
+ # TODO: perhaps these checks could be done at the beginning, in
320
+ # a 'validate_params' method.
321
+ raise Exception.new(
322
+ "Error: Column #{extra_column_name} doesn't exist " \
323
+ "but is needed for #{suffix}. Aborting."
324
+ ) if result_stuff[suffix] && !acc[suffix]
325
+ acc
326
+ end
327
+
328
+ (
329
+ [
330
+ "`#{column_prefix}#{attr_name}` = #{sanitize(new_ciphertext)}"
331
+ ] +
332
+ extra_columns.reduce([]) do |acc, (suffix, extra_column_name)|
333
+ acc << "`#{extra_column_name}` = #{
334
+ sanitize(result_stuff[suffix])
335
+ }"
336
+ acc
337
+ end.flatten
338
+ ).map{|s| s.force_encoding('utf-8')}
339
+
340
+ end
341
+ end
342
+
343
+ # XXX: MySQL2 specific! TODO: adapt to different backends
344
+ # Return +true+ iff column +_column_name+ exists in table +_table_name+.
345
+ # Cached for performance.
346
+ def column_exists?(_table_name, _column_name)
347
+ table_name = _table_name.to_sym
348
+ column_name = _column_name.to_sym
349
+ @column_exists ||= {}
350
+ @column_exists[table_name] ||= {}
351
+ exists = @column_exists[table_name][column_name]
352
+ !exists.nil? ? exists : @column_exists[table_name][column_name] =
353
+ begin
354
+ raw_result = execute <<-EOF
355
+ SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
356
+ WHERE
357
+ column_name = #{sanitize column_name} AND
358
+ table_name = #{sanitize table_name} AND
359
+ TABLE_SCHEMA = DATABASE()
360
+ EOF
361
+
362
+ result = raw_result.to_a.flatten[0] == 1
363
+ result
364
+ end
365
+ end
366
+
367
+ class NoKeyException < StandardError ; end
368
+
369
+ # +iv+ can be +true+. If so, we generate IV for you.
370
+ # If +iv+ is truthy, we use +iv+ directly.
371
+ # Likewise for +salt+.
372
+ # Default algorithm is 'aes-256-gcm' as per default of attr_encrypted v3.
373
+ # You may opt to use 'aes-256-cbc', like in attr_encrypted v1.
374
+ #
375
+ # When given a block, the encryptor params can be modified before passing
376
+ # over to Encryptor for the encryption process.
377
+ #
378
+ # +decode64_iv+
379
+ # - if +true+, base64-decodes the given +iv+ before passing to Encryptor.
380
+ # +decode64_salt+
381
+ # - if +true+, base64-decodes the given +salt+ before passing to
382
+ # Encryptor.
383
+ # +decode64_value+
384
+ # - if +true+, base64-decodes the given +value+ before passing to
385
+ # Encryptor.
386
+ #
387
+ # +encode64_iv+
388
+ # - if +true+, base64-encodes the +iv+ output by Encryptor.
389
+ # +encode64_salt+
390
+ # - if +true+, base64-encodes the +salt+ output by Encryptor.
391
+ # +encode64_value+
392
+ # - if +true+, base64-encodes the +value+ output by Encryptor.
393
+ #
394
+ def enc opts
395
+ value = opts[:value]
396
+ ek = opts[:key]
397
+ algo = opts[:algorithm] || 'aes-256-gcm'
398
+
399
+ iv = opts[:iv]
400
+ iv = OpenSSL::Cipher.new(algo).random_iv if iv === true
401
+
402
+ salt = opts[:salt]
403
+ salt = SecureRandom.random_bytes if salt === true
404
+
405
+ has_iv = !iv.nil? && iv != ''
406
+ has_salt = !salt.nil? && salt != ''
407
+
408
+ cryptor_opts = {
409
+ value: value,
410
+ key: ek,
411
+ algorithm: algo,
412
+ value_present: false, # so as to force regenerating of random_iv @ encryptor
413
+ insecure_mode: !! opts[:insecure_mode] || ! has_iv,
414
+ }
415
+
416
+ puts "in enc: opts = #{opts.pretty_inspect}"
417
+
418
+ iv = Base64.decode64(iv) if has_iv && opts.delete(:decode64_iv)
419
+ salt = Base64.decode64(salt) if has_salt && opts.delete(:decode64_salt)
420
+ value = Base64.decode64(value) if opts.delete(:decode64_value)
421
+
422
+ cryptor_opts = cryptor_opts.merge(iv: iv) if has_iv
423
+ cryptor_opts = cryptor_opts.merge(salt: salt) if has_salt
424
+ cryptor_opts = cryptor_opts.merge(value: value)
425
+
426
+ if block_given?
427
+ cryptor_opts = yield cryptor_opts
428
+ ek = cryptor_opts[:key]
429
+ end
430
+
431
+ raise NoKeyException.new("encryption :key is nil") if ek.nil?
432
+
433
+ puts "cryptor opts:"
434
+ pp cryptor_opts
435
+
436
+ result_stuff = {
437
+ value: ::Encryptor.encrypt(cryptor_opts),
438
+ key: ek,
439
+ }
440
+
441
+ iv = Base64.encode64(iv) if has_iv && opts.delete(:encode64_iv)
442
+ salt = Base64.encode64(salt) if has_salt && opts.delete(:encode64_salt)
443
+ value = Base64.encode64(result_stuff[:value]) if opts.delete(:encode64_value)
444
+
445
+ result_stuff[:value] = value
446
+
447
+ # puts "has iv? #{has_iv} = #{iv.pretty_inspect}"
448
+ # puts "has salt? #{has_salt} = #{salt.pretty_inspect}"
449
+
450
+ result_stuff = result_stuff.merge(iv: iv) if has_iv
451
+ result_stuff = result_stuff.merge(salt: salt) if has_salt
452
+ result_stuff
453
+ end
454
+
455
+ #
456
+ # When given a block, the encryptor params can be modified before passing
457
+ # over to Encryptor for the encryption process.
458
+ #
459
+ # +insecure_mode+ is automatically set to +true+ if no +iv+ is provided.
460
+ # It can also be specified by user but will not be able to override the
461
+ # +true+ if no +iv+ is given. This should match what is expected to work
462
+ # in Encryptor.
463
+ #
464
+ # +decode64_iv+
465
+ # - if +true+, base64-decodes the given +iv+ before passing to Encryptor.
466
+ # +decode64_salt+
467
+ # - if +true+, base64-decodes the given +salt+ before passing to
468
+ # Encryptor.
469
+ # +decode64_value+
470
+ # - if +true+, base64-decodes the given +value+ before passing to
471
+ # Encryptor.
472
+ #
473
+ # +encode64_iv+
474
+ # - if +true+, base64-encodes the given +iv+ before passing to Encryptor.
475
+ # +encode64_salt+
476
+ # - if +true+, base64-encodes the given +salt+ before passing to
477
+ # Encryptor.
478
+ # +encode64_value+
479
+ # - if +true+, base64-encodes the given +value+ before passing to
480
+ # Encryptor.
481
+ #
482
+ # NOTE: The operations decode64-* and encode64-* decribed above may cancel
483
+ # each other out.
484
+ #
485
+ # This is a design uncertainty and may change in a later version.
486
+ #
487
+ def dec opts
488
+ value = opts[:value]
489
+ key = opts[:key]
490
+ algo = opts[:algorithm] || 'aes-256-gcm'
491
+ iv = opts[:iv]
492
+ salt = opts[:salt]
493
+
494
+ has_iv = iv && iv != ''
495
+ has_salt = salt && salt != ''
496
+
497
+ iv = Base64.decode64(iv) if has_iv && opts.delete(:decode64_iv)
498
+ salt = Base64.decode64(salt) if has_salt && opts.delete(:decode64_salt)
499
+ value = Base64.decode64(value) if opts.delete(:decode64_value)
500
+
501
+ iv = Base64.encode64(iv) if has_iv && opts.delete(:encode64_iv)
502
+ salt = Base64.encode64(salt) if has_salt && opts.delete(:encode64_salt)
503
+ value = Base64.encode64(value) if opts.delete(:encode64_value)
504
+
505
+ cryptor_opts = {
506
+ value: value,
507
+ key: key,
508
+ iv: iv,
509
+ salt: salt,
510
+ algorithm: algo,
511
+
512
+ # e.g. key length may be too short
513
+ insecure_mode: ! has_iv || !! opts[:insecure_mode],
514
+ }
515
+
516
+ # puts "key was: #{key}"
517
+
518
+ if block_given?
519
+ # puts "wow yay block given."
520
+ cryptor_opts = yield cryptor_opts
521
+ # puts "new cryptor_opts is:"
522
+ # pp cryptor_opts
523
+ end
524
+
525
+ key = cryptor_opts[:key]
526
+
527
+ key = Base64.encode64(key) if opts.delete(:encode64_key)
528
+ key = Base64.decode64(key) if opts.delete(:decode64_key)
529
+
530
+ cryptor_opts[:key] = key
531
+
532
+ # puts "transcryptor#dec,opts=#{cryptor_opts.pretty_inspect}"
533
+
534
+ raise NoKeyException.new("encryption :key is nil") if key.nil?
535
+
536
+ # puts 'cryptor opts'
537
+ # pp cryptor_opts
538
+
539
+ {
540
+ value: ::Encryptor.decrypt(cryptor_opts)
541
+ }
542
+ end
543
+
544
+ end
545
+ end
@@ -0,0 +1,3 @@
1
+ module Transcryptor
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'transcryptor/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "transcryptor"
8
+ spec.version = Transcryptor::VERSION
9
+ spec.authors = ["Ribose Inc."]
10
+ spec.email = ["open.source@ribose.com"]
11
+
12
+ spec.summary = %q{Assists your everyday re-encryption needs, in Rails.}
13
+ spec.homepage = "https://github.com/riboseinc/transcryptor"
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ # if spec.respond_to?(:metadata)
19
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
20
+ # else
21
+ # raise "RubyGems 2.0 or newer is required to protect against " \
22
+ # "public gem pushes."
23
+ # end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{^(test|spec|features)/})
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "attr_encrypted", "~> 3.0"
33
+ spec.add_dependency "activerecord", "~> 4.0"
34
+
35
+ spec.add_development_dependency "bundler", "~> 1.13"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: transcryptor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-07-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: attr_encrypted
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description:
84
+ email:
85
+ - open.source@ribose.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - CODE_OF_CONDUCT.md
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.adoc
97
+ - Rakefile
98
+ - bin/console
99
+ - bin/setup
100
+ - lib/transcryptor.rb
101
+ - lib/transcryptor/version.rb
102
+ - transcryptor.gemspec
103
+ homepage: https://github.com/riboseinc/transcryptor
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.5.2
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Assists your everyday re-encryption needs, in Rails.
127
+ test_files: []