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.


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

def initialize(keys)
  if 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.


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

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.


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

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

#decrypt(data) ⇒ Object

Decrypt using any supported format and any available key.


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
431
# File 'lib/sequel/plugins/column_encryption.rb', line 376

def decrypt(data)
  begin
    data = Base64.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.


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

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.


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

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).


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

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.


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

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

#searchable_encrypt(data) ⇒ Object

Encrypt in searchable format with the first configured encryption key.


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

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