File: encryptor.rb

package info (click to toggle)
ruby-lockbox 2.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 224 kB
  • sloc: ruby: 1,447; makefile: 4
file content (87 lines) | stat: -rw-r--r-- 2,939 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
module Lockbox
  class Encryptor
    def initialize(**options)
      options = Lockbox.default_options.merge(options)
      @encode = options.delete(:encode)
      # option may be renamed to binary: true
      # warn "[lockbox] Lockbox 1.0 will default to encode: true. Pass encode: false to keep the current behavior." if @encode.nil?
      previous_versions = options.delete(:previous_versions)

      @boxes =
        [Box.new(**options)] +
        Array(previous_versions).reject { |v| v.key?(:master_key) }.map { |v| Box.new(key: options[:key], **v) }
    end

    def encrypt(message, **options)
      message = check_string(message)
      ciphertext = @boxes.first.encrypt(message, **options)
      ciphertext = [ciphertext].pack("m0") if @encode
      ciphertext
    end

    def decrypt(ciphertext, **options)
      ciphertext = ciphertext.unpack1("m") if @encode
      ciphertext = check_string(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)
      str = str.read if str.respond_to?(:read)
      # Ruby uses "no implicit conversion of Object into String"
      raise TypeError, "can't convert #{str.class.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)
      target.set_encoding(source.external_encoding) if source.respond_to?(:external_encoding)
    end
  end
end