2017-08-14 13:08:09 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-03-02 13:28:54 -05:00
|
|
|
require "yaml"
|
2017-02-23 17:41:40 -05:00
|
|
|
require "active_support/message_encryptor"
|
2017-02-23 12:15:28 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2017-03-02 13:38:42 -05:00
|
|
|
@cipher = "aes-128-gcm"
|
2017-02-23 12:15:28 -05:00
|
|
|
@root = File # Wonky, but ensures `join` uses the current directory.
|
|
|
|
|
|
|
|
class << self
|
2017-04-01 00:22:25 -04:00
|
|
|
attr_writer :root
|
2017-02-23 12:15:28 -05:00
|
|
|
|
|
|
|
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 key
|
2017-03-02 13:38:01 -05:00
|
|
|
ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key
|
2017-02-23 12:15:28 -05:00
|
|
|
end
|
|
|
|
|
2017-02-23 17:41:40 -05:00
|
|
|
def encrypt(data)
|
|
|
|
encryptor.encrypt_and_sign(data)
|
2017-02-23 12:15:28 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def decrypt(data)
|
2017-02-23 17:41:40 -05:00
|
|
|
encryptor.decrypt_and_verify(data)
|
2017-02-23 12:15:28 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def read
|
|
|
|
decrypt(IO.binread(path))
|
|
|
|
end
|
|
|
|
|
|
|
|
def write(contents)
|
|
|
|
IO.binwrite("#{path}.tmp", encrypt(contents))
|
|
|
|
FileUtils.mv("#{path}.tmp", path)
|
|
|
|
end
|
|
|
|
|
2017-05-23 15:54:01 -04:00
|
|
|
def read_for_editing(&block)
|
|
|
|
writing(read, &block)
|
|
|
|
end
|
2017-02-23 12:15:28 -05:00
|
|
|
|
|
|
|
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")
|
2017-04-01 00:22:25 -04:00
|
|
|
decrypt(IO.binread(path))
|
2017-02-23 12:15:28 -05:00
|
|
|
else
|
|
|
|
IO.read(path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-05-23 15:54:01 -04:00
|
|
|
def writing(contents)
|
2017-07-12 19:06:32 -04:00
|
|
|
tmp_file = "#{File.basename(path)}.#{Process.pid}"
|
|
|
|
tmp_path = File.join(Dir.tmpdir, tmp_file)
|
2017-07-08 05:53:32 -04:00
|
|
|
IO.binwrite(tmp_path, contents)
|
2017-05-23 15:54:01 -04:00
|
|
|
|
|
|
|
yield tmp_path
|
|
|
|
|
2017-07-08 05:53:32 -04:00
|
|
|
updated_contents = IO.binread(tmp_path)
|
2017-07-06 19:17:01 -04:00
|
|
|
|
|
|
|
write(updated_contents) if updated_contents != contents
|
2017-05-23 15:54:01 -04:00
|
|
|
ensure
|
|
|
|
FileUtils.rm(tmp_path) if File.exist?(tmp_path)
|
|
|
|
end
|
|
|
|
|
2017-02-23 17:41:40 -05:00
|
|
|
def encryptor
|
2017-03-02 13:38:42 -05:00
|
|
|
@encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher)
|
2017-02-23 12:15:28 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|