Permalink
Cannot retrieve contributors at this time
134 lines (105 sloc)
3.87 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
rails/activesupport/lib/active_support/encrypted_file.rb
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require "pathname" | |
require "tmpdir" | |
require "active_support/message_encryptor" | |
module ActiveSupport | |
class EncryptedFile | |
class MissingContentError < RuntimeError | |
def initialize(content_path) | |
super "Missing encrypted content file in #{content_path}." | |
end | |
end | |
class MissingKeyError < RuntimeError | |
def initialize(key_path:, env_key:) | |
super \ | |
"Missing encryption key to decrypt file with. " + | |
"Ask your team for your master key and write it to #{key_path} or put it in the ENV['#{env_key}']." | |
end | |
end | |
class InvalidKeyLengthError < RuntimeError | |
def initialize | |
super "Encryption key must be exactly #{EncryptedFile.expected_key_length} characters." | |
end | |
end | |
CIPHER = "aes-128-gcm" | |
def self.generate_key | |
SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER)) | |
end | |
def self.expected_key_length # :nodoc: | |
@expected_key_length ||= generate_key.length | |
end | |
attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key | |
def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:) | |
@content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path } | |
@key_path = Pathname.new(key_path) | |
@env_key, @raise_if_missing_key = env_key, raise_if_missing_key | |
end | |
# Returns the encryption key, first trying the environment variable | |
# specified by +env_key+, then trying the key file specified by +key_path+. | |
# If +raise_if_missing_key+ is true, raises MissingKeyError if the | |
# environment variable is not set and the key file does not exist. | |
def key | |
read_env_key || read_key_file || handle_missing_key | |
end | |
# Returns truthy if #key is truthy. Returns falsy otherwise. Unlike #key, | |
# does not raise MissingKeyError when +raise_if_missing_key+ is true. | |
def key? | |
read_env_key || read_key_file | |
end | |
# Reads the file and returns the decrypted content. | |
# | |
# Raises: | |
# - MissingKeyError if the key is missing and +raise_if_missing_key+ is true. | |
# - MissingContentError if the encrypted file does not exist or otherwise | |
# if the key is missing. | |
# - ActiveSupport::MessageEncryptor::InvalidMessage if the content cannot be | |
# decrypted or verified. | |
def read | |
if !key.nil? && content_path.exist? | |
decrypt content_path.binread | |
else | |
raise MissingContentError, content_path | |
end | |
end | |
def write(contents) | |
IO.binwrite "#{content_path}.tmp", encrypt(contents) | |
FileUtils.mv "#{content_path}.tmp", content_path | |
end | |
def change(&block) | |
writing read, &block | |
end | |
private | |
def writing(contents) | |
tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}" | |
tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file) | |
tmp_path.binwrite contents | |
yield tmp_path | |
updated_contents = tmp_path.binread | |
write(updated_contents) if updated_contents != contents | |
ensure | |
FileUtils.rm(tmp_path) if tmp_path&.exist? | |
end | |
def encrypt(contents) | |
check_key_length | |
encryptor.encrypt_and_sign contents | |
end | |
def decrypt(contents) | |
encryptor.decrypt_and_verify contents | |
end | |
def encryptor | |
@encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER, serializer: Marshal) | |
end | |
def read_env_key | |
ENV[env_key].presence | |
end | |
def read_key_file | |
@key_file_contents ||= (key_path.binread.strip if key_path.exist?) | |
end | |
def handle_missing_key | |
raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key | |
end | |
def check_key_length | |
raise InvalidKeyLengthError if key&.length != self.class.expected_key_length | |
end | |
end | |
end |