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

- old
+ new

@@ -1,14 +1,93 @@ -class Lockbox +module Lockbox class Encryptor + def initialize(**options) + options = Lockbox.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 + + 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) + end + + # legacy for attr_encrypted def self.encrypt(options) box(options).encrypt(options[:value]) end + # legacy for attr_encrypted def self.decrypt(options) box(options).decrypt(options[:value]) end + # legacy for attr_encrypted def self.box(options) options = options.slice(:key, :encryption_key, :decryption_key, :algorithm, :previous_versions) options[:algorithm] = "aes-gcm" if options[:algorithm] == "aes-256-gcm" Lockbox.new(options) end