Class: Sequel::Plugins::ColumnEncryption::Cryptor

Inherits:
Object
  • Object
show all
Defined in:
lib/sequel/plugins/column_encryption.rb

Overview

Cryptor handles the encryption and decryption of rows for a key set. It also provides methods that return search prefixes, which datasets use in queries.

The same cryptor can support non-searchable, searchable, and case-insensitive searchable columns.

Constant Summary collapse

NOT_SEARCHABLE =

Flags

0
SEARCHABLE =
1
LOWERCASE_SEARCHABLE =
2
DEFAULT_PADDING =

This is the default padding, but up to 2x the padding can be used for a record.

8

Instance Method Summary collapse

Constructor Details

#initialize(keys) ⇒ Cryptor

Keys should be an array of arrays containing key_id, key string, auth_data, and padding.



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/sequel/plugins/column_encryption.rb', line 357

def initialize(keys)
  if !keys || keys.empty?
    raise Error, "Cannot initialize encryptor without encryption key"
  end

  # First key is used for encryption
  @key_id, @key, @auth_data, @padding = keys[0]

  # All keys are candidates for decryption
  @key_map = {}
  keys.each do |key_id, key, auth_data, padding|
    @key_map[key_id] = [key, auth_data, padding].freeze
  end

  freeze
end

Instance Method Details

#case_insensitive_searchable_encrypt(data) ⇒ Object

Encrypt in case insensitive searchable format with the first configured encryption key.



443
444
445
# File 'lib/sequel/plugins/column_encryption.rb', line 443

def case_insensitive_searchable_encrypt(data)
  _encrypt(data, _search_prefix(data.downcase, LOWERCASE_SEARCHABLE, @key_id, @key))
end

#current_key_prefix(search_type) ⇒ Object

The prefix string of columns for the given search type and the first configured encryption key. Used to find values that do not use this prefix in order to perform reencryption.



449
450
451
# File 'lib/sequel/plugins/column_encryption.rb', line 449

def current_key_prefix(search_type)
  urlsafe_encode64("#{search_type.chr}\0#{@key_id.chr}")
end

#decrypt(data) ⇒ Object

Decrypt using any supported format and any available key.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
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
# File 'lib/sequel/plugins/column_encryption.rb', line 375

def decrypt(data)
  begin
    data = urlsafe_decode64(data)
  rescue ArgumentError
    raise Error, "Unable to decode encrypted column: invalid base64"
  end

  unless data.getbyte(1) == 0 && data.getbyte(3) == 0
    raise Error, "Unable to decode encrypted column: invalid format"
  end

  flags = data.getbyte(0)

  key, auth_data = @key_map[data.getbyte(2)]
  unless key
    raise Error, "Unable to decode encrypted column: invalid key id"
  end

  case flags
  when NOT_SEARCHABLE
    if data.bytesize < 65
      raise Error, "Decoded encrypted column smaller than minimum size"
    end

    data.slice!(0, 4)
  when SEARCHABLE, LOWERCASE_SEARCHABLE
    if data.bytesize < 97
      raise Error, "Decoded encrypted column smaller than minimum size"
    end

    data.slice!(0, 36)
  else
    raise Error, "Unable to decode encrypted column: invalid flags"
  end

  key_part = data.slice!(0, 32)
  cipher_iv = data.slice!(0, 12)
  auth_tag = data.slice!(0, 16)

  cipher = OpenSSL::Cipher.new("aes-256-gcm")
  cipher.decrypt
  cipher.iv = cipher_iv
  cipher.key = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, key_part)
  cipher.auth_data = auth_data
  cipher.auth_tag = auth_tag
  begin
    decrypted_data = cipher.update(data) << cipher.final
  rescue OpenSSL::Cipher::CipherError => e
    raise Error, "Unable to decrypt encrypted column: #{e.class} (probably due to encryption key or auth data mismatch or corrupt data)"
  end

  # Remove padding
  decrypted_data.slice!(0, decrypted_data.getbyte(0) + 1)

  decrypted_data
end

#encrypt(data) ⇒ Object

Encrypt in not searchable format with the first configured encryption key.



433
434
435
# File 'lib/sequel/plugins/column_encryption.rb', line 433

def encrypt(data)
  _encrypt(data, "#{NOT_SEARCHABLE.chr}\0#{@key_id.chr}\0")
end

#lowercase_search_prefixes(data) ⇒ Object

The prefix values to search for the given data (an array of strings), assuming the column uses the case insensitive searchable format.



461
462
463
# File 'lib/sequel/plugins/column_encryption.rb', line 461

def lowercase_search_prefixes(data)
  _search_prefixes(data.downcase, LOWERCASE_SEARCHABLE)
end

#regular_and_lowercase_search_prefixes(data) ⇒ Object

The prefix values to search for the given data (an array of strings), assuming the column uses either the searchable or the case insensitive searchable format. Should be used only when transitioning between formats (used by the :search_both option when encrypting columns).



468
469
470
# File 'lib/sequel/plugins/column_encryption.rb', line 468

def regular_and_lowercase_search_prefixes(data)
  search_prefixes(data) + lowercase_search_prefixes(data)
end

#search_prefixes(data) ⇒ Object

The prefix values to search for the given data (an array of strings), assuming the column uses the searchable format.



455
456
457
# File 'lib/sequel/plugins/column_encryption.rb', line 455

def search_prefixes(data)
  _search_prefixes(data, SEARCHABLE)
end

#searchable_encrypt(data) ⇒ Object

Encrypt in searchable format with the first configured encryption key.



438
439
440
# File 'lib/sequel/plugins/column_encryption.rb', line 438

def searchable_encrypt(data)
  _encrypt(data, _search_prefix(data, SEARCHABLE, @key_id, @key))
end