class Transcryptor::Instance

Attributes

migration_instance[RW]

Public Class Methods

new(migration_instance) click to toggle source
# File lib/transcryptor.rb, line 109
def initialize(migration_instance)
  self.migration_instance = migration_instance
end

Public Instance Methods

column_exists?(_table_name, _column_name) click to toggle source

XXX: MySQL2 specific! TODO: adapt to different backends Return true iff column _column_name exists in table _table_name. Cached for performance.

# 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) { |cryptor_opts| ... } click to toggle source

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.

# 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) { |cryptor_opts| ... } click to toggle source

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.
# 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) click to toggle source
# 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) click to toggle source

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',
    },
  }
},

}

# 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_') click to toggle source

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.

# 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) click to toggle source
# 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_') click to toggle source
# 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) click to toggle source
# 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