main
 1module TFA
 2  class SecureStorage
 3    def initialize(original, passphrase_request)
 4      @original = original
 5      @passphrase_request = passphrase_request
 6    end
 7
 8    def encrypt!(algorithm = "AES-256-CBC")
 9      cipher = OpenSSL::Cipher.new(algorithm)
10      cipher.encrypt
11      cipher.key = digest
12      cipher.iv = iv = cipher.random_iv
13      plain_text = IO.read(@original.path)
14      json = JSON.generate(
15        algorithm: algorithm,
16        iv: Base64.encode64(iv),
17        cipher_text: Base64.encode64(cipher.update(plain_text) + cipher.final),
18      )
19      IO.write(@original.path, json)
20    end
21
22    def decrypt!
23      data = JSON.parse(IO.read(@original.path), symbolize_names: true)
24      decipher = OpenSSL::Cipher.new(data[:algorithm])
25      decipher.decrypt
26      decipher.key = digest
27      decipher.iv = Base64.decode64(data[:iv])
28      plain_text = decipher.update(Base64.decode64(data[:cipher_text]))
29      IO.write(@original.path, plain_text + decipher.final)
30    end
31
32    def encrypted?
33      return false unless File.exist?(@original.path)
34      JSON.parse(IO.read(@original.path))
35      true
36    rescue JSON::ParserError
37      false
38    end
39
40    private
41
42    def method_missing(name, *args, &block)
43      super unless @original.respond_to?(name)
44
45      was_encrypted = encrypted?
46      if was_encrypted
47        encrypted_content = IO.read(@original.path)
48        decrypt!
49        original_sha256 = Digest::SHA256.file(@original.path)
50      end
51      result = @original.public_send(name, *args, &block)
52      if was_encrypted
53        new_sha256 = Digest::SHA256.file(@original.path)
54
55        if original_sha256 == new_sha256
56          IO.write(@original.path, encrypted_content)
57        else
58          encrypt!
59        end
60      end
61      result
62    end
63
64    def respond_to_missing?(method, *)
65      @original.respond_to?(method)
66    end
67
68    def digest
69      @digest ||= Digest::SHA256.digest(@passphrase_request.call)
70    end
71  end
72end