mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
1677c64ee0
In encrypted secrets, the tmp file is used as a fixed file (`secrets.yml.enc` under the tmp directory). And that tmp file will be removed after process. Therefore, if edit secrets at the same time with multiple applications, the tmp file was conflicting. In order to avoid the above issue, added pid to tmp file.
121 lines
3 KiB
Ruby
121 lines
3 KiB
Ruby
require "yaml"
|
|
require "active_support/message_encryptor"
|
|
require "active_support/core_ext/string/strip"
|
|
|
|
module Rails
|
|
# Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘
|
|
class Secrets # :nodoc:
|
|
class MissingKeyError < RuntimeError
|
|
def initialize
|
|
super(<<-end_of_message.squish)
|
|
Missing encryption key to decrypt secrets with.
|
|
Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"]
|
|
end_of_message
|
|
end
|
|
end
|
|
|
|
@cipher = "aes-128-gcm"
|
|
@root = File # Wonky, but ensures `join` uses the current directory.
|
|
|
|
class << self
|
|
attr_writer :root
|
|
|
|
def parse(paths, env:)
|
|
paths.each_with_object(Hash.new) do |path, all_secrets|
|
|
require "erb"
|
|
|
|
secrets = YAML.load(ERB.new(preprocess(path)).result) || {}
|
|
all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"]
|
|
all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env]
|
|
end
|
|
end
|
|
|
|
def generate_key
|
|
SecureRandom.hex(OpenSSL::Cipher.new(@cipher).key_len)
|
|
end
|
|
|
|
def key
|
|
ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key
|
|
end
|
|
|
|
def template
|
|
<<-end_of_template.strip_heredoc
|
|
# See `secrets.yml` for tips on generating suitable keys.
|
|
# production:
|
|
# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289
|
|
|
|
end_of_template
|
|
end
|
|
|
|
def encrypt(data)
|
|
encryptor.encrypt_and_sign(data)
|
|
end
|
|
|
|
def decrypt(data)
|
|
encryptor.decrypt_and_verify(data)
|
|
end
|
|
|
|
def read
|
|
decrypt(IO.binread(path))
|
|
end
|
|
|
|
def write(contents)
|
|
IO.binwrite("#{path}.tmp", encrypt(contents))
|
|
FileUtils.mv("#{path}.tmp", path)
|
|
end
|
|
|
|
def read_for_editing(&block)
|
|
writing(read, &block)
|
|
end
|
|
|
|
def read_template_for_editing(&block)
|
|
writing(template, &block)
|
|
end
|
|
|
|
private
|
|
def handle_missing_key
|
|
raise MissingKeyError
|
|
end
|
|
|
|
def read_key_file
|
|
if File.exist?(key_path)
|
|
IO.binread(key_path).strip
|
|
end
|
|
end
|
|
|
|
def key_path
|
|
@root.join("config", "secrets.yml.key")
|
|
end
|
|
|
|
def path
|
|
@root.join("config", "secrets.yml.enc").to_s
|
|
end
|
|
|
|
def preprocess(path)
|
|
if path.end_with?(".enc")
|
|
decrypt(IO.binread(path))
|
|
else
|
|
IO.read(path)
|
|
end
|
|
end
|
|
|
|
def writing(contents)
|
|
tmp_file = "#{File.basename(path)}.#{Process.pid}"
|
|
tmp_path = File.join(Dir.tmpdir, tmp_file)
|
|
IO.binwrite(tmp_path, contents)
|
|
|
|
yield tmp_path
|
|
|
|
updated_contents = IO.binread(tmp_path)
|
|
|
|
write(updated_contents) if updated_contents != contents
|
|
ensure
|
|
FileUtils.rm(tmp_path) if File.exist?(tmp_path)
|
|
end
|
|
|
|
def encryptor
|
|
@encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher)
|
|
end
|
|
end
|
|
end
|
|
end
|