lib/lockbox.rb in lockbox-0.2.5 vs lib/lockbox.rb in lockbox-0.3.0

- old
+ new

@@ -5,11 +5,13 @@ # modules require "lockbox/box" require "lockbox/encryptor" require "lockbox/key_generator" require "lockbox/io" +require "lockbox/migrator" require "lockbox/model" +require "lockbox/padding" require "lockbox/utils" require "lockbox/version" # integrations require "lockbox/carrier_wave_extensions" if defined?(CarrierWave) @@ -23,15 +25,17 @@ ActiveSupport.on_load(:mongoid) do Mongoid::Document::ClassMethods.include(Lockbox::Model) end end -class Lockbox +module Lockbox class Error < StandardError; end class DecryptionError < Error; end class PaddingError < Error; end + extend Padding + class << self attr_accessor :default_options attr_writer :master_key end self.default_options = {} @@ -39,115 +43,13 @@ def self.master_key @master_key ||= ENV["LOCKBOX_MASTER_KEY"] end def self.migrate(model, restart: false) - # get fields - fields = model.lockbox_attributes.select { |k, v| v[:migrating] } - - # get blind indexes - blind_indexes = model.respond_to?(:blind_indexes) ? model.blind_indexes.select { |k, v| v[:migrating] } : {} - - # build relation - relation = model.unscoped - - unless restart - attributes = fields.map { |_, v| v[:encrypted_attribute] } - attributes += blind_indexes.map { |_, v| v[:bidx_attribute] } - - if defined?(ActiveRecord::Base) && model.is_a?(ActiveRecord::Base) - attributes.each_with_index do |attribute, i| - relation = - if i == 0 - relation.where(attribute => nil) - else - relation.or(model.unscoped.where(attribute => nil)) - end - end - end - end - - if relation.respond_to?(:find_each) - relation.find_each do |record| - migrate_record(record, fields: fields, blind_indexes: blind_indexes, restart: restart) - end - else - relation.all.each do |record| - migrate_record(record, fields: fields, blind_indexes: blind_indexes, restart: restart) - end - end + Migrator.new(model).migrate(restart: restart) end - # private - def self.migrate_record(record, fields:, blind_indexes:, restart:) - fields.each do |k, v| - record.send("#{v[:attribute]}=", record.send(k)) if restart || !record.send(v[:encrypted_attribute]) - end - blind_indexes.each do |k, v| - record.send("compute_#{k}_bidx") if restart || !record.send(v[:bidx_attribute]) - end - record.save(validate: false) if record.changed? - end - - def initialize(**options) - options = self.class.default_options.merge(options) - previous_versions = options.delete(:previous_versions) - - @boxes = - [Box.new(options)] + - Array(previous_versions).map { |v| Box.new({key: options[:key]}.merge(v)) } - end - - def encrypt(message, **options) - message = check_string(message, "message") - @boxes.first.encrypt(message, **options) - end - - def decrypt(ciphertext, **options) - ciphertext = check_string(ciphertext, "ciphertext") - - # ensure binary - if ciphertext.encoding != Encoding::BINARY - # dup to prevent mutation - ciphertext = ciphertext.dup.force_encoding(Encoding::BINARY) - end - - @boxes.each_with_index do |box, i| - begin - return box.decrypt(ciphertext, **options) - rescue => e - # returning DecryptionError instead of PaddingError - # is for end-user convenience, not for security - error_classes = [DecryptionError, PaddingError] - error_classes << RbNaCl::LengthError if defined?(RbNaCl::LengthError) - error_classes << RbNaCl::CryptoError if defined?(RbNaCl::CryptoError) - if error_classes.any? { |ec| e.is_a?(ec) } - raise DecryptionError, "Decryption failed" if i == @boxes.size - 1 - else - raise e - end - end - end - end - - def encrypt_io(io, **options) - new_io = Lockbox::IO.new(encrypt(io.read, **options)) - copy_metadata(io, new_io) - new_io - end - - def decrypt_io(io, **options) - new_io = Lockbox::IO.new(decrypt(io.read, **options)) - copy_metadata(io, new_io) - new_io - end - - def decrypt_str(ciphertext, **options) - message = decrypt(ciphertext, **options) - message.force_encoding(Encoding::UTF_8) - end - def self.generate_key SecureRandom.hex(32) end def self.generate_key_pair @@ -175,72 +77,10 @@ def self.to_hex(str) str.unpack("H*").first end - PAD_FIRST_BYTE = "\x80".b - PAD_ZERO_BYTE = "\x00".b - - # ISO/IEC 7816-4 - # same as Libsodium - # https://libsodium.gitbook.io/doc/padding - # apply prior to encryption - # note: current implementation does not - # try to minimize side channels - def self.pad(str, size: 16) - raise ArgumentError, "Invalid size" if size < 1 - - str = str.dup.force_encoding(Encoding::BINARY) - - pad_length = size - 1 - pad_length -= str.bytesize % size - - str << PAD_FIRST_BYTE - pad_length.times do - str << PAD_ZERO_BYTE - end - - str - end - - # note: current implementation does not - # try to minimize side channels - def self.unpad(str, size: 16) - raise ArgumentError, "Invalid size" if size < 1 - - if str.encoding != Encoding::BINARY - str = str.dup.force_encoding(Encoding::BINARY) - end - - i = 1 - while i <= size - case str[-i] - when PAD_ZERO_BYTE - i += 1 - when PAD_FIRST_BYTE - return str[0..-(i + 1)] - else - break - end - end - - raise Lockbox::PaddingError, "Invalid padding" - end - - private - - def check_string(str, name) - str = str.read if str.respond_to?(:read) - raise TypeError, "can't convert #{name} to string" unless str.respond_to?(:to_str) - str.to_str - end - - def copy_metadata(source, target) - target.original_filename = - if source.respond_to?(:original_filename) - source.original_filename - elsif source.respond_to?(:path) - File.basename(source.path) - end - target.content_type = source.content_type if source.respond_to?(:content_type) + # legacy + def self.new(**options) + Encryptor.new(**options) end end