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