Class: Transcryptor::Instance
- Inherits:
-
Object
- Object
- Transcryptor::Instance
- Defined in:
- lib/transcryptor.rb
Defined Under Namespace
Classes: NoKeyException
Instance Attribute Summary collapse
-
#migration_instance ⇒ Object
Returns the value of attribute migration_instance.
Instance Method Summary collapse
-
#column_exists?(_table_name, _column_name) ⇒ Boolean
XXX: MySQL2 specific! TODO: adapt to different backends Return
true
iff column_column_name
exists in table_table_name
. -
#dec(opts) ⇒ Object
When given a block, the encryptor params can be modified before passing over to Encryptor for the encryption process.
-
#enc(opts) ⇒ Object
iv
can betrue
. - #execute(*args) ⇒ Object
-
#get_column_names_from(table_name, table_spec) ⇒ Object
Meant to be used by both #up and #down.
-
#initialize(migration_instance) ⇒ Instance
constructor
A new instance of Instance.
-
#re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_') ⇒ Object
table_name
is the SQL table name for the record at id =record_id
. - #sanitize(sql_fragment) ⇒ Object
- #set_clauses_for_re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_') ⇒ Object
- #updown_migrate(table_column_spec, old_spec, new_spec, decrypt_opts_fn, encrypt_opts_fn) ⇒ Object
Constructor Details
#initialize(migration_instance) ⇒ Instance
Returns a new instance of Instance.
109 110 111 |
# File 'lib/transcryptor.rb', line 109 def initialize(migration_instance) self.migration_instance = migration_instance end |
Instance Attribute Details
#migration_instance ⇒ Object
Returns the value of attribute migration_instance.
107 108 109 |
# File 'lib/transcryptor.rb', line 107 def migration_instance @migration_instance end |
Instance Method Details
#column_exists?(_table_name, _column_name) ⇒ Boolean
XXX: MySQL2 specific! TODO: adapt to different backends Return true
iff column _column_name
exists in table _table_name
. Cached for performance.
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/transcryptor.rb', line 346 def column_exists?(_table_name, _column_name) table_name = _table_name.to_sym column_name = _column_name.to_sym @column_exists ||= {} @column_exists[table_name] ||= {} exists = @column_exists[table_name][column_name] !exists.nil? ? exists : @column_exists[table_name][column_name] = begin raw_result = execute <<-EOF SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE column_name = #{sanitize column_name} AND table_name = #{sanitize table_name} AND TABLE_SCHEMA = DATABASE() EOF result = raw_result.to_a.flatten[0] == 1 result end end |
#dec(opts) ⇒ Object
When given a block, the encryptor params can be modified before passing over to Encryptor for the encryption process.
insecure_mode
is automatically set to true
if no iv
is provided. It can also be specified by user but will not be able to override the true
if no iv
is given. This should match what is expected to work in Encryptor.
decode64_iv
- if +true+, base64-decodes the given +iv+ before passing to Encryptor.
decode64_salt
- if +true+, base64-decodes the given +salt+ before passing to
Encryptor.
decode64_value
- if +true+, base64-decodes the given +value+ before passing to
Encryptor.
encode64_iv
- if +true+, base64-encodes the given +iv+ before passing to Encryptor.
encode64_salt
- if +true+, base64-encodes the given +salt+ before passing to
Encryptor.
encode64_value
- if +true+, base64-encodes the given +value+ before passing to
Encryptor.
NOTE: The operations decode64-* and encode64-* decribed above may cancel each other out.
This is a design uncertainty and may change in a later version.
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 |
# File 'lib/transcryptor.rb', line 487 def dec opts value = opts[:value] key = opts[:key] algo = opts[:algorithm] || 'aes-256-gcm' iv = opts[:iv] salt = opts[:salt] has_iv = iv && iv != '' has_salt = salt && salt != '' iv = Base64.decode64(iv) if has_iv && opts.delete(:decode64_iv) salt = Base64.decode64(salt) if has_salt && opts.delete(:decode64_salt) value = Base64.decode64(value) if opts.delete(:decode64_value) iv = Base64.encode64(iv) if has_iv && opts.delete(:encode64_iv) salt = Base64.encode64(salt) if has_salt && opts.delete(:encode64_salt) value = Base64.encode64(value) if opts.delete(:encode64_value) cryptor_opts = { value: value, key: key, iv: iv, salt: salt, algorithm: algo, # e.g. key length may be too short insecure_mode: ! has_iv || !! opts[:insecure_mode], } # puts "key was: #{key}" if block_given? # puts "wow yay block given." cryptor_opts = yield cryptor_opts # puts "new cryptor_opts is:" # pp cryptor_opts end key = cryptor_opts[:key] key = Base64.encode64(key) if opts.delete(:encode64_key) key = Base64.decode64(key) if opts.delete(:decode64_key) cryptor_opts[:key] = key # puts "transcryptor#dec,opts=#{cryptor_opts.pretty_inspect}" raise NoKeyException.new("encryption :key is nil") if key.nil? # puts 'cryptor opts' # pp cryptor_opts { value: ::Encryptor.decrypt(cryptor_opts) } end |
#enc(opts) ⇒ Object
iv
can be true
. If so, we generate IV for you. If iv
is truthy, we use iv
directly. Likewise for salt
. Default algorithm is ‘aes-256-gcm’ as per default of attr_encrypted v3. You may opt to use ‘aes-256-cbc’, like in attr_encrypted v1.
When given a block, the encryptor params can be modified before passing over to Encryptor for the encryption process.
decode64_iv
- if +true+, base64-decodes the given +iv+ before passing to Encryptor.
decode64_salt
- if +true+, base64-decodes the given +salt+ before passing to
Encryptor.
decode64_value
- if +true+, base64-decodes the given +value+ before passing to
Encryptor.
encode64_iv
- if +true+, base64-encodes the +iv+ output by Encryptor.
encode64_salt
- if +true+, base64-encodes the +salt+ output by Encryptor.
encode64_value
- if +true+, base64-encodes the +value+ output by Encryptor.
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'lib/transcryptor.rb', line 394 def enc opts value = opts[:value] ek = opts[:key] algo = opts[:algorithm] || 'aes-256-gcm' iv = opts[:iv] iv = OpenSSL::Cipher.new(algo).random_iv if iv === true salt = opts[:salt] salt = SecureRandom.random_bytes if salt === true has_iv = !iv.nil? && iv != '' has_salt = !salt.nil? && salt != '' cryptor_opts = { value: value, key: ek, algorithm: algo, value_present: false, # so as to force regenerating of random_iv @ encryptor insecure_mode: !! opts[:insecure_mode] || ! has_iv, } puts "in enc: opts = #{opts.pretty_inspect}" iv = Base64.decode64(iv) if has_iv && opts.delete(:decode64_iv) salt = Base64.decode64(salt) if has_salt && opts.delete(:decode64_salt) value = Base64.decode64(value) if opts.delete(:decode64_value) cryptor_opts = cryptor_opts.merge(iv: iv) if has_iv cryptor_opts = cryptor_opts.merge(salt: salt) if has_salt cryptor_opts = cryptor_opts.merge(value: value) if block_given? cryptor_opts = yield cryptor_opts ek = cryptor_opts[:key] end raise NoKeyException.new("encryption :key is nil") if ek.nil? puts "cryptor opts:" pp cryptor_opts result_stuff = { value: ::Encryptor.encrypt(cryptor_opts), key: ek, } iv = Base64.encode64(iv) if has_iv && opts.delete(:encode64_iv) salt = Base64.encode64(salt) if has_salt && opts.delete(:encode64_salt) value = Base64.encode64(result_stuff[:value]) if opts.delete(:encode64_value) result_stuff[:value] = value # puts "has iv? #{has_iv} = #{iv.pretty_inspect}" # puts "has salt? #{has_salt} = #{salt.pretty_inspect}" result_stuff = result_stuff.merge(iv: iv) if has_iv result_stuff = result_stuff.merge(salt: salt) if has_salt result_stuff end |
#execute(*args) ⇒ Object
113 114 115 116 117 118 |
# File 'lib/transcryptor.rb', line 113 def execute *args puts "\e[38;5;141m" puts puts args puts "\e[0m" migration_instance.execute *args end |
#get_column_names_from(table_name, table_spec) ⇒ Object
Meant to be used by both #up and #down.
table_column_spec: {
table1: {
id_column: :id,
columns: {
column1: {
prefix: 'encoded_',
key: :encryption_key_1,
},
column2: {
prefix: 'xXx_en_ing_',
key: :encryption_key_2,
suffix: '_crypted_xXx',
},
}
},
table2: {
id_column: :id,
columns: {
column3: {
prefix: 'encoded_',
key: :encryption_key_3,
},
column4: {
prefix: 'xXx_en_ing_',
key: :encryption_key_4,
suffix: '_crypted_xXx',
},
}
},
}
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/transcryptor.rb', line 157 def get_column_names_from(table_name, table_spec) id_name = table_spec[:id_column] column_specs = table_spec[:columns] puts "table name is #{table_name}" puts "table psec is #{table_spec}" res = [ id_name ] + column_specs.map do |column_name, column_spec| column_prefix = column_spec[:prefix] column_key_field = column_spec[:key] column_suffix = column_spec[:suffix] full_column_name = :"#{column_prefix}#{column_name}#{column_suffix}" [ full_column_name, column_key_field ] + %i[iv salt].reduce([]) do |acc, suffix| extra_column_name = :"#{full_column_name}_#{suffix}" acc << extra_column_name if column_exists?(table_name, extra_column_name) acc end end.flatten.compact.uniq pp res res end |
#re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_') ⇒ Object
table_name
is the SQL table name for the record at id = record_id
. attrs_specs
is an Array like so: [ {
old: {
key: String,
value: String,
attr_name: String,
algorithm: String,
iv: String | Nil,
salt: String | Nil,
},
new: {
algorithm: String,
iv: String | Bool,
salt: String | Bool,
},
} ]
Assumptions: Encrypted attribute SQL column names are all prefixed with “encrypted_”, and also suffixed with “_iv” & “_salt” for the corresponding iv and salt.
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/transcryptor.rb', line 279 def re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_') set_statement = set_clauses_for_re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_'). join(', ') update_statement = <<-EOF UPDATE `#{table_name}` SET #{set_statement} WHERE id = #{ActiveRecord::Base.sanitize(record_id)} EOF puts puts "\e[38;5;42m" puts update_statement puts "\e[0m" execute(update_statement) end |
#sanitize(sql_fragment) ⇒ Object
120 121 122 |
# File 'lib/transcryptor.rb', line 120 def sanitize(sql_fragment) ActiveRecord::Base.sanitize(sql_fragment) end |
#set_clauses_for_re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_') ⇒ Object
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/transcryptor.rb', line 296 def set_clauses_for_re_encrypt(table_name, record_id, attrs_specs, decrypt_opts_fn, encrypt_opts_fn, column_prefix = 'encrypted_') # puts "attrs_specs:" # pp attrs_specs attrs_specs.map do |attr_spec| old_spec = attr_spec[:old] new_spec = attr_spec[:new] plain_stuff = dec(old_spec) do |opts| decrypt_opts_fn.call(opts) end result_stuff = enc(new_spec.merge(value: plain_stuff[:value])) do |opts| encrypt_opts_fn.call(opts) end new_ciphertext = result_stuff[:value] attr_name = old_spec[:attr_name] extra_columns = %i[iv salt].reduce({}) do |acc, suffix| extra_column_name = "#{column_prefix}#{attr_name}_#{suffix}" acc[suffix] = extra_column_name if column_exists?(table_name, extra_column_name) # TODO: perhaps these checks could be done at the beginning, in # a 'validate_params' method. raise Exception.new( "Error: Column #{extra_column_name} doesn't exist " \ "but is needed for #{suffix}. Aborting." ) if result_stuff[suffix] && !acc[suffix] acc end ( [ "`#{column_prefix}#{attr_name}` = #{sanitize(new_ciphertext)}" ] + extra_columns.reduce([]) do |acc, (suffix, extra_column_name)| acc << "`#{extra_column_name}` = #{ sanitize(result_stuff[suffix]) }" acc end.flatten ).map{|s| s.force_encoding('utf-8')} end end |
#updown_migrate(table_column_spec, old_spec, new_spec, decrypt_opts_fn, encrypt_opts_fn) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/transcryptor.rb', line 179 def updown_migrate(table_column_spec, old_spec, new_spec, decrypt_opts_fn, encrypt_opts_fn) # puts "table column spec is:" # pp table_column_spec table_column_spec.each do |table_name, table_spec| column_specs = table_spec[:columns] relevant_column_names = get_column_names_from(table_name, table_spec) puts "relevant column names are:" pp relevant_column_names execute( "SELECT #{relevant_column_names.join(', ')} FROM `#{table_name}`" ).each do |_db_values| id, _dontcare = _db_values puts 'db values' pp _db_values # A map: { :db_field_name => "value" } db_values = Hash[relevant_column_names.map(&:to_sym).zip(_db_values)] # Build up reencryption params to pass to reencrypt(). encrypted_attrs = column_specs.keys.map do |attr_name| column_spec = column_specs[attr_name] column_prefix = column_spec[:prefix] column_key_field = column_spec[:key] column_suffix = column_spec[:suffix] full_column_name = :"#{column_prefix}#{attr_name}#{column_suffix}" encrypted_value = db_values[:"#{full_column_name}"] key = db_values[:"#{column_key_field}"] # +key+ could be nil, but it's OK, since it may be provided via # other means, e.g. encrypt_opts_fn and decrypt_opts_fn. unless encrypted_value.nil? || encrypted_value == "" res = { attr_name: attr_name, key: key, value: encrypted_value, } # Merge in iv and/or salt as appropriate. %i[iv salt].reduce(res) do |acc, suffix| extra_column_name = :"#{full_column_name}_#{suffix}" if relevant_column_names.include?(extra_column_name) acc[suffix] = db_values[extra_column_name] end acc end end end.compact next if encrypted_attrs.empty? re_encrypt( table_name, id, encrypted_attrs.map do |attr| { # These would be in +attr+ as approprate. # salt: old_salt, # iv: old_iv, # key: old_key, # attr_name: attr_name, # value: encrypted_value, old: attr.merge(old_spec), new: { key: attr[:key], }.merge(new_spec), } end, decrypt_opts_fn, encrypt_opts_fn, ) end end end |