Add credentials using a generic EncryptedConfiguration class (#30067)

* WIP: Add credentials using a generic EncryptedConfiguration class

This is sketch code so far.

* Flesh out EncryptedConfiguration and test it

* Better name

* Add command and generator for credentials

* Use the Pathnames

* Extract EncryptedFile from EncryptedConfiguration and add serializers

* Test EncryptedFile

* Extract serializer validation

* Stress the point about losing comments

* Allow encrypted configuration to be read without parsing for display

* Use credentials by default and base them on the master key

* Derive secret_key_base in test/dev, source it from credentials in other envs

And document the usage.

* Document the new credentials setup

* Stop generating the secrets.yml file now that we have credentials

* Document what we should have instead

Still need to make it happen, tho.

* [ci skip] Keep wording to `key base`; prefer defaults.

Usually we say we change defaults, not "spec" out a release.

Can't use backticks in our sdoc generated documentation either.

* Abstract away OpenSSL; prefer MessageEncryptor.

* Spare needless new when raising.

* Encrypted file test shouldn't depend on subclass.

* [ci skip] Some woordings.

* Ditch serializer future coding.

* I said flip it. Flip it good.

* [ci skip] Move require_master_key to the real production.rb.

* Add require_master_key to abort the boot process.

In case the master key is required in a certain environment
we should inspect that the key is there and abort if it isn't.

* Print missing key message and exit immediately.

Spares us a lengthy backtrace and prevents further execution.

I've verified the behavior in a test app, but couldn't figure the
test out as loading the app just exits immediately with:

```
/Users/kasperhansen/Documents/code/rails/activesupport/lib/active_support/testing/isolation.rb:23:in `load': marshal data too short (ArgumentError)
	from /Users/kasperhansen/Documents/code/rails/activesupport/lib/active_support/testing/isolation.rb:23:in `run'
	from /Users/kasperhansen/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/minitest-5.10.2/lib/minitest.rb:830:in `run_one_method'
	from /Users/kasperhansen/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/minitest-5.10.2/lib/minitest/parallel.rb:32:in `block (2 levels) in start'
```

It's likely we need to capture and prevent the exit somehow.
Kernel.stub(:exit) didn't work. Leaving it for tomorrow.

* Fix require_master_key config test.

Loading the app would trigger the `exit 1` per require_master_key's
semantics, which then aborted the test.

Fork and wait for the child process to finish, then inspect the
exit status.

Also check we aborted because of a missing master key, so something
else didn't just abort the boot.

Much <3 to @tenderlove for the tip.

* Support reading/writing configs via methods.

* Skip needless deep symbolizing.

* Remove save; test config reader elsewhere.

* Move secret_key_base check to when we're reading it.

Otherwise we'll abort too soon since we don't assign the secret_key_base
to secrets anymore.

* Add missing string literal comments; require unneeded yaml require.

* ya ya ya, rubocop.

* Add master_key/credentials after bundle.

Then we can reuse the existing message on `rails new bc4`.

It'll look like:

```
Using web-console 3.5.1 from https://github.com/rails/web-console.git (at master@ce985eb)
Using rails 5.2.0.alpha from source at `/Users/kasperhansen/Documents/code/rails`
Using sass-rails 5.0.6
Bundle complete! 16 Gemfile dependencies, 72 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Adding config/master.key to store the master encryption key: 97070158c44b4675b876373a6bc9d5a0

Save this in a password manager your team can access.

If you lose the key, no one, including you, can access anything encrypted with it.

      create  config/master.key
```

And that'll be executed even if `--skip-bundle` was passed.

* Ensure test app has secret_key_base.

* Assign secret_key_base to app or omit.

* Merge noise

* Split options for dynamic delegation into its own method and use deep symbols to make it work

* Update error to point to credentials instead

* Appease Rubocop

* Validate secret_key_base when reading it.

Instead of relying on the validation in key_generator move that into
secret_key_base itself.

* Fix generator and secrets test.

Manually add config.read_encrypted_secrets since it's not there by default
anymore.

Move mentions of config/secrets.yml to config/credentials.yml.enc.

* Remove files I have no idea how they got here.

* [ci skip] swap secrets for credentials.

* [ci skip] And now, changelogs are coming.
This commit is contained in:
David Heinemeier Hansson 2017-09-11 13:21:20 -05:00 committed by Kasper Timm Hansen
parent 80573a099e
commit 69f976b859
28 changed files with 678 additions and 123 deletions

@ -33,12 +33,12 @@ module Session
#
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
#
# Configure your secret key in <tt>config/secrets.yml</tt>:
# By default, your secret key base is derived from your application name in
# the test and development environments. In all other environments, it is stored
# encrypted in the <tt>config/credentials.yml.enc</tt> file.
#
# development:
# secret_key_base: 'secret key'
#
# To generate a secret key for an existing application, run <tt>rails secret</tt>.
# If your application was not updated to Rails 5.2 defaults, the secret_key_base
# will be found in the old <tt>config/secrets.yml</tt> file.
#
# If you are upgrading an existing Rails 3 app, you should leave your
# existing secret_token in place and simply add the new secret_key_base.

@ -29,9 +29,7 @@ class Engine < Rails::Engine # :nodoc:
initializer "active_storage.verifier" do
config.after_initialize do |app|
if app.secrets.secret_key_base.present?
ActiveStorage.verifier = app.message_verifier("ActiveStorage")
end
ActiveStorage.verifier = app.message_verifier("ActiveStorage")
end
end

@ -1,3 +1,22 @@
* Add `config/credentials.yml.enc` to store production app secrets.
Allows saving any authentication credentials for third party services
directly in repo encrypted with `config/master.key` or `ENV["RAILS_MASTER_KEY"]`.
This will eventually replace `Rails.application.secrets` and the encrypted
secrets introduced in Rails 5.1.
*DHH*, *Kasper Timm Hansen*
* Add `ActiveSupport::EncryptedFile` and `ActiveSupport::EncryptedConfiguration`.
Allows for stashing encrypted files or configuration directly in repo by
encrypting it with a key.
Backs the new credentials setup above, but can also be used independently.
*DHH*, *Kasper Timm Hansen*
* `Module#delegate_missing_to` now raises `DelegationError` if target is nil,
similar to `Module#delegate`.

@ -0,0 +1,42 @@
# frozen_string_literal: true
require "yaml"
require "active_support/encrypted_file"
require "active_support/ordered_options"
require "active_support/core_ext/object/inclusion"
require "active_support/core_ext/module/delegation"
module ActiveSupport
class EncryptedConfiguration < EncryptedFile
delegate :[], :fetch, to: :config
delegate_missing_to :options
def initialize(config_path:, key_path:, env_key:)
super content_path: config_path, key_path: key_path, env_key: env_key
end
# Allow a config to be started without a file present
def read
super
rescue ActiveSupport::EncryptedFile::MissingContentError
""
end
def config
@config ||= deserialize(read).deep_symbolize_keys
end
private
def options
@options ||= ActiveSupport::InheritableOptions.new(config)
end
def serialize(config)
config.present? ? YAML.dump(config) : ""
end
def deserialize(config)
config.present? ? YAML.load(config) : {}
end
end
end

@ -0,0 +1,101 @@
# frozen_string_literal: true
require "pathname"
require "active_support/message_encryptor"
require "active_support/core_ext/string/strip"
require "active_support/core_ext/module/delegation"
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
CIPHER = "aes-128-gcm"
def self.generate_key
SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
end
attr_reader :content_path, :key_path, :env_key
def initialize(content_path:, key_path:, env_key:)
@content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path)
@env_key = env_key
end
def key
read_env_key || read_key_file || handle_missing_key
end
def read
if 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 = "#{content_path.basename}.#{Process.pid}"
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)
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)
end
def read_env_key
ENV[env_key]
end
def read_key_file
key_path.binread.strip if key_path.exist?
end
def handle_missing_key
raise MissingKeyError, key_path: key_path, env_key: env_key
end
end
end

@ -49,6 +49,17 @@ class Railtie < Rails::Railtie # :nodoc:
Date.beginning_of_week_default = beginning_of_week_default
end
initializer "active_support.require_master_key" do |app|
if app.config.respond_to?(:require_master_key) && app.config.require_master_key
begin
app.credentials.key
rescue ActiveSupport::EncryptedFile::MissingKeyError => error
$stderr.puts error.message
exit 1
end
end
end
initializer "active_support.set_configs" do |app|
app.config.active_support.each do |k, v|
k = "#{k}="

@ -0,0 +1,65 @@
# frozen_string_literal: true
require "abstract_unit"
require "active_support/encrypted_configuration"
class EncryptedConfigurationTest < ActiveSupport::TestCase
setup do
@credentials_config_path = File.join(Dir.tmpdir, "credentials.yml.enc")
@credentials_key_path = File.join(Dir.tmpdir, "master.key")
File.write(@credentials_key_path, ActiveSupport::EncryptedConfiguration.generate_key)
@credentials = ActiveSupport::EncryptedConfiguration.new \
config_path: @credentials_config_path, key_path: @credentials_key_path, env_key: "RAILS_MASTER_KEY"
end
teardown do
FileUtils.rm_rf @credentials_config_path
FileUtils.rm_rf @credentials_key_path
end
test "reading configuration by env key" do
FileUtils.rm_rf @credentials_key_path
begin
ENV["RAILS_MASTER_KEY"] = ActiveSupport::EncryptedConfiguration.generate_key
@credentials.write({ something: { good: true, bad: false } }.to_yaml)
assert @credentials[:something][:good]
assert_not @credentials.dig(:something, :bad)
assert_nil @credentials.fetch(:nothing, nil)
ensure
ENV["RAILS_MASTER_KEY"] = nil
end
end
test "reading configuration by key file" do
@credentials.write({ something: { good: true } }.to_yaml)
assert @credentials.something[:good]
end
test "change configuration by key file" do
@credentials.write({ something: { good: true } }.to_yaml)
@credentials.change do |config_file|
config = YAML.load(config_file.read)
config_file.write config.merge(new: "things").to_yaml
end
assert @credentials.something[:good]
assert_equal "things", @credentials[:new]
end
test "raises key error when accessing config via bang method" do
assert_raise(KeyError) { @credentials.something! }
end
private
def new_credentials_configuration
ActiveSupport::EncryptedConfiguration.new \
config_path: @credentials_config_path,
key_path: @credentials_key_path,
env_key: "RAILS_MASTER_KEY"
end
end

@ -0,0 +1,50 @@
# frozen_string_literal: true
require "abstract_unit"
require "active_support/encrypted_file"
class EncryptedFileTest < ActiveSupport::TestCase
setup do
@content = "One little fox jumped over the hedge"
@content_path = File.join(Dir.tmpdir, "content.txt.enc")
@key_path = File.join(Dir.tmpdir, "content.txt.key")
File.write(@key_path, ActiveSupport::EncryptedFile.generate_key)
@encrypted_file = ActiveSupport::EncryptedFile.new \
content_path: @content_path, key_path: @key_path, env_key: "CONTENT_KEY"
end
teardown do
FileUtils.rm_rf @content_path
FileUtils.rm_rf @key_path
end
test "reading content by env key" do
FileUtils.rm_rf @key_path
begin
ENV["CONTENT_KEY"] = ActiveSupport::EncryptedFile.generate_key
@encrypted_file.write @content
assert_equal @content, @encrypted_file.read
ensure
ENV["CONTENT_KEY"] = nil
end
end
test "reading content by key file" do
@encrypted_file.write(@content)
assert_equal @content, @encrypted_file.read
end
test "change content by key file" do
@encrypted_file.write(@content)
@encrypted_file.change do |file|
file.write(file.read + " and went by the lake")
end
assert_equal "#{@content} and went by the lake", @encrypted_file.read
end
end

@ -1,3 +1,9 @@
* Derive `secret_key_base` from the app name in development and test environments.
Spares away needless secret configs.
*DHH*, *Kasper Timm Hansen*
* Support multiple versions arguments for `gem` method of Generators.
*Yoshiyuki Hirano*

@ -5,6 +5,7 @@
require "active_support/core_ext/object/blank"
require "active_support/key_generator"
require "active_support/message_verifier"
require "active_support/encrypted_configuration"
require_relative "engine"
require_relative "secrets"
@ -171,12 +172,9 @@ def key_generator
# number of iterations selected based on consultation with the google security
# team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220
@caching_key_generator ||=
if secrets.secret_key_base
unless secrets.secret_key_base.kind_of?(String)
raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`"
end
key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)
ActiveSupport::CachingKeyGenerator.new(key_generator)
if secret_key_base
ActiveSupport::CachingKeyGenerator.new \
ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
else
ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)
end
@ -246,13 +244,11 @@ def config_for(name, env: Rails.env)
# will be used by middlewares and engines to configure themselves.
def env_config
@app_env_config ||= begin
validate_secret_key_config!
super.merge(
"action_dispatch.parameter_filter" => config.filter_parameters,
"action_dispatch.redirect_filter" => config.filter_redirect,
"action_dispatch.secret_token" => secrets.secret_token,
"action_dispatch.secret_key_base" => secrets.secret_key_base,
"action_dispatch.secret_key_base" => secret_key_base,
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
"action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
"action_dispatch.logger" => Rails.logger,
@ -406,6 +402,33 @@ def secrets=(secrets) #:nodoc:
@secrets = secrets
end
# The secret_key_base is used as the input secret to the application's key generator, which in turn
# is used to create all the MessageVerfiers, including the one that signs and encrypts cookies.
#
# In test and development, this is simply derived as a MD5 hash of the application's name.
#
# In all other environments, we look for it first in ENV["SECRET_KEY_BASE"],
# then credentials[:secret_key_base], and finally secrets.secret_key_base. For most applications,
# the correct place to store it is in the encrypted credentials file.
def secret_key_base
if Rails.env.test? || Rails.env.development?
Digest::MD5.hexdigest self.class.name
else
validate_secret_key_base \
ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base
end
end
# Decrypts the credentials hash as kept in `config/credentials.yml.enc`. This file is encrypted with
# the Rails master key, which is either taken from ENV["RAILS_MASTER_KEY"] or from loading
# `config/master.key`.
def credentials
@credentials ||= ActiveSupport::EncryptedConfiguration.new \
config_path: Rails.root.join("config/credentials.yml.enc"),
key_path: Rails.root.join("config/master.key"),
env_key: "RAILS_MASTER_KEY"
end
def to_app #:nodoc:
self
end
@ -504,14 +527,13 @@ def default_middleware_stack #:nodoc:
default_stack.build_stack
end
def validate_secret_key_config! #:nodoc:
if secrets.secret_key_base.blank?
ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " \
"Read the upgrade documentation to learn more about this new config option."
if secrets.secret_token.blank?
raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`"
end
def validate_secret_key_base(secret_key_base)
if secret_key_base.is_a?(String) && secret_key_base.present?
secret_key_base
elsif secret_key_base
raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String`"
elsif secrets.secret_token.blank?
raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `rails credentials:edit`"
end
end

@ -0,0 +1,40 @@
=== Storing Encrypted Credentials in Source Control
The Rails `credentials` commands provide access to encrypted credentials,
so you can safely store access tokens, database passwords, and the like
safely inside the app without relying on a mess of ENVs.
This also allows for atomic deploys: no need to coordinate key changes
to get everything working as the keys are shipped with the code.
=== Setup
Applications after Rails 5.2 automatically have a basic credentials file generated
that just contains the secret_key_base used by the MessageVerifiers, like the one
signing and encrypting cookies.
For applications created prior to Rails 5.2, we'll automatically generate a new
credentials file in `config/credentials.yml.enc` the first time you run `bin/rails credentials:edit`.
If you didn't have a master key saved in `config/master.key`, that'll be created too.
Don't lose this master key! Put it in a password manager your team can access.
Should you lose it no one, including you, will be able to access any encrypted
credentials.
Don't commit the key! Add `config/master.key` to your source control's
ignore file. If you use Git, Rails handles this for you.
Rails also looks for the master key in `ENV["RAILS_MASTER_KEY"]`, if that's easier to manage.
You could prepend that to your server's start command like this:
RAILS_MASTER_KEY="very-secret-and-secure" server.start
=== Editing Credentials
This will open a temporary file in `$EDITOR` with the decrypted contents to edit
the encrypted credentials.
When the temporary file is next saved the contents are encrypted and written to
`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials
from leaking.

@ -0,0 +1,85 @@
# frozen_string_literal: true
require "active_support"
module Rails
module Command
class CredentialsCommand < Rails::Command::Base # :nodoc:
no_commands do
def help
say "Usage:\n #{self.class.banner}"
say ""
say self.class.desc
end
end
def edit
require_application_and_environment!
ensure_editor_available || (return)
ensure_master_key_has_been_added
ensure_credentials_have_been_added
change_credentials_in_system_editor
say "New credentials encrypted and saved."
rescue Interrupt
say "Aborted changing credentials: nothing saved."
rescue ActiveSupport::EncryptedFile::MissingKeyError => error
say error.message
end
def show
require_application_and_environment!
say Rails.application.credentials.read.presence ||
"No credentials have been added yet. Use bin/rails credentials:edit to change that."
end
private
def ensure_editor_available
if ENV["EDITOR"].to_s.empty?
say "No $EDITOR to open credentials in. Assign one like this:"
say ""
say %(EDITOR="mate --wait" bin/rails credentials:edit)
say ""
say "For editors that fork and exit immediately, it's important to pass a wait flag,"
say "otherwise the credentials will be saved immediately with no chance to edit."
false
else
true
end
end
def ensure_master_key_has_been_added
master_key_generator.add_master_key_file
master_key_generator.ignore_master_key_file
end
def ensure_credentials_have_been_added
credentials_generator.add_credentials_file_silently
end
def change_credentials_in_system_editor
Rails.application.credentials.change do |tmp_path|
system("#{ENV["EDITOR"]} #{tmp_path}")
end
end
def master_key_generator
require_relative "../../generators"
require_relative "../../generators/rails/master_key/master_key_generator"
Rails::Generators::MasterKeyGenerator.new
end
def credentials_generator
require_relative "../../generators"
require_relative "../../generators/rails/credentials/credentials_generator"
Rails::Generators::CredentialsGenerator.new
end
end
end
end

@ -111,7 +111,6 @@ def config
template "routes.rb"
template "application.rb"
template "environment.rb"
template "secrets.yml"
template "cable.yml" unless options[:skip_action_cable]
template "puma.rb" unless options[:skip_puma]
template "spring.rb" if spring_install?
@ -159,6 +158,22 @@ def config_when_updating
end
end
def master_key
require_relative "../master_key/master_key_generator"
after_bundle do
Rails::Generators::MasterKeyGenerator.new.add_master_key_file
end
end
def credentials
require_relative "../credentials/credentials_generator"
after_bundle do
Rails::Generators::CredentialsGenerator.new.add_credentials_file_silently
end
end
def database_yml
template "config/databases/#{options[:database]}.yml", "config/database.yml"
end
@ -289,6 +304,14 @@ def update_config_files
end
remove_task :update_config_files
def create_master_key
build(:master_key)
end
def create_credentials
build(:credentials)
end
def display_upgrade_guide_info
say "\nAfter this, check Rails upgrade guide at http://guides.rubyonrails.org/upgrading_ruby_on_rails.html for more details about upgrading your app."
end

@ -14,10 +14,9 @@ Rails.application.configure do
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Attempt to read encrypted secrets from `config/secrets.yml.enc`.
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.

@ -7,6 +7,9 @@
# Ignore bundler config.
/.bundle
# Ignore master key for decrypting credentials and more.
/config/master.key
<% if sqlite3? -%>
# Ignore the default SQLite database.
/db/*.sqlite3

@ -0,0 +1,46 @@
# frozen_string_literal: true
require_relative "../../base"
require_relative "../master_key/master_key_generator"
require "active_support/encrypted_configuration"
module Rails
module Generators
class CredentialsGenerator < Base
CONFIG_PATH = "config/credentials.yml.enc"
KEY_PATH = "config/master.key"
def add_credentials_file
unless File.exist?(CONFIG_PATH)
template = credentials_template
say "Adding #{CONFIG_PATH} to store encrypted credentials."
say ""
say "The following content has been encrypted with the Rails master key:"
say ""
say template, :on_green
say ""
add_credentials_file_silently(template)
say "You can edit encrypted credentials with `bin/rails credentials:edit`."
say ""
end
end
def add_credentials_file_silently(template = nil)
unless File.exist?(CONFIG_PATH)
setup = { config_path: CONFIG_PATH, key_path: KEY_PATH, env_key: "RAILS_MASTER_KEY" }
ActiveSupport::EncryptedConfiguration.new(setup).write(credentials_template)
end
end
private
def credentials_template
"# amazon:\n# access_key_id: 123\n# secret_access_key: 345\n\n" +
"# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.\n" +
"secret_key_base: #{SecureRandom.hex(64)}"
end
end
end
end

@ -0,0 +1,53 @@
# frozen_string_literal: true
require_relative "../../base"
require "pathname"
require "active_support/encrypted_file"
module Rails
module Generators
class MasterKeyGenerator < Base
MASTER_KEY_PATH = Pathname.new("config/master.key")
def add_master_key_file
unless MASTER_KEY_PATH.exist?
key = ActiveSupport::EncryptedFile.generate_key
say "Adding #{MASTER_KEY_PATH} to store the master encryption key: #{key}"
say ""
say "Save this in a password manager your team can access."
say ""
say "If you lose the key, no one, including you, can access anything encrypted with it."
say ""
add_master_key_file_silently key
say ""
end
end
def add_master_key_file_silently(key = nil)
create_file MASTER_KEY_PATH, key || ActiveSupport::EncryptedFile.generate_key
end
def ignore_master_key_file
if File.exist?(".gitignore")
unless File.read(".gitignore").include?(key_ignore)
say "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:"
say ""
append_to_file ".gitignore", key_ignore
say ""
end
else
say "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:"
say key_ignore, :on_green
say ""
end
end
private
def key_ignore
[ "", "# Ignore master key for decrypting credentials and more.", MASTER_KEY_PATH, "" ].join("\n")
end
end
end
end

@ -15,7 +15,6 @@
module TestApp
class Application < Rails::Application
config.root = __dir__
secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
end
end

@ -476,45 +476,35 @@ def index
test "application message verifier can be used when the key_generator is ActiveSupport::LegacyKeyGenerator" do
app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.credentials.secret_key_base = nil
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
RUBY
app_file "config/secrets.yml", <<-YAML
development:
secret_key_base:
YAML
app "development"
app "production"
assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator
assert_equal app.env_config["action_dispatch.key_generator"].class, ActiveSupport::LegacyKeyGenerator
assert_kind_of ActiveSupport::LegacyKeyGenerator, Rails.application.key_generator
message = app.message_verifier(:sensitive_value).generate("some_value")
assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message)
end
test "warns when secrets.secret_key_base is blank and config.secret_token is set" do
test "raises when secret_key_base is blank" do
app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
Rails.application.credentials.secret_key_base = nil
RUBY
app_file "config/secrets.yml", <<-YAML
development:
secret_key_base:
YAML
app "development"
assert_deprecated(/You didn't set `secret_key_base`./) do
app.env_config
error = assert_raise(ArgumentError) do
app "production"
end
assert_match(/Missing `secret_key_base`./, error.message)
end
test "raise when secrets.secret_key_base is not a type of string" do
app_file "config/secrets.yml", <<-YAML
development:
secret_key_base: 123
YAML
test "raise when secret_key_base is not a type of string" do
add_to_config <<-RUBY
Rails.application.credentials.secret_key_base = 123
RUBY
assert_raise(ArgumentError) do
app "development"
app "production"
end
end
@ -534,7 +524,7 @@ def index
test "application verifier can build different verifiers" do
make_basic_app do |application|
application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
application.credentials.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
application.config.session_store :disabled
end
@ -652,37 +642,15 @@ def index
test "uses ActiveSupport::LegacyKeyGenerator as app.key_generator when secrets.secret_key_base is blank" do
app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.credentials.secret_key_base = nil
Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
RUBY
app_file "config/secrets.yml", <<-YAML
development:
secret_key_base:
YAML
app "development"
app "production"
assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token
assert_nil app.secrets.secret_key_base
assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator
end
test "uses ActiveSupport::LegacyKeyGenerator with config.secret_token as app.key_generator when secrets.secret_key_base is blank" do
app_file "config/initializers/secret_token.rb", <<-RUBY
Rails.application.config.secret_token = ""
RUBY
app_file "config/secrets.yml", <<-YAML
development:
secret_key_base:
YAML
app "development"
assert_equal "", app.config.secret_token
assert_nil app.secrets.secret_key_base
e = assert_raise ArgumentError do
app.key_generator
end
assert_match(/\AA secret is required/, e.message)
assert_nil app.credentials.secret_key_base
assert_kind_of ActiveSupport::LegacyKeyGenerator, app.key_generator
end
test "that nested keys are symbolized the same as parents for hashes more than one level deep" do
@ -699,6 +667,20 @@ def index
assert_equal "697361616320736c6f616e2028656c6f7265737429", app.secrets.smtp_settings[:password]
end
test "require_master_key aborts app boot when missing key" do
skip "can't run without fork" unless Process.respond_to?(:fork)
remove_file "config/master.key"
add_to_config "config.require_master_key = true"
error = capture(:stderr) do
Process.wait(Process.fork { app "development" })
end
assert_equal 1, $?.exitstatus
assert_match(/Missing.*RAILS_MASTER_KEY/, error)
end
test "protect from forgery is the default in a new app" do
make_basic_app

@ -15,10 +15,6 @@ def teardown
teardown_app
end
def app
@app ||= Rails.application
end
define_method :simple_controller do
class ::OmgController < ActionController::Base
def index

@ -337,31 +337,37 @@ def read_raw_cookie
add_to_config <<-RUBY
# Use a static key
secrets.secret_key_base = "known key base"
Rails.application.credentials.secret_key_base = "known key base"
# Enable AEAD cookies
config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
require "#{app_path}/config/environment"
begin
old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production"
get "/foo/write_raw_session"
get "/foo/read_session"
assert_equal "1", last_response.body
require "#{app_path}/config/environment"
get "/foo/write_session"
get "/foo/read_session"
assert_equal "2", last_response.body
get "/foo/write_raw_session"
get "/foo/read_session"
assert_equal "1", last_response.body
get "/foo/read_encrypted_cookie"
assert_equal "2", last_response.body
get "/foo/write_session"
get "/foo/read_session"
assert_equal "2", last_response.body
cipher = "aes-256-gcm"
secret = app.key_generator.generate_key("authenticated encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
get "/foo/read_encrypted_cookie"
assert_equal "2", last_response.body
get "/foo/read_raw_cookie"
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]
cipher = "aes-256-gcm"
secret = app.key_generator.generate_key("authenticated encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
get "/foo/read_raw_cookie"
assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"]
ensure
ENV["RAILS_ENV"] = old_rails_env
end
end
test "session upgrading legacy signed cookies to new signed cookies" do
@ -400,26 +406,32 @@ def read_raw_cookie
add_to_config <<-RUBY
secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
secrets.secret_key_base = nil
Rails.application.credentials.secret_key_base = nil
RUBY
require "#{app_path}/config/environment"
begin
old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production"
get "/foo/write_raw_session"
get "/foo/read_session"
assert_equal "1", last_response.body
require "#{app_path}/config/environment"
get "/foo/write_session"
get "/foo/read_session"
assert_equal "2", last_response.body
get "/foo/write_raw_session"
get "/foo/read_session"
assert_equal "1", last_response.body
get "/foo/read_signed_cookie"
assert_equal "2", last_response.body
get "/foo/write_session"
get "/foo/read_session"
assert_equal "2", last_response.body
verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token)
get "/foo/read_signed_cookie"
assert_equal "2", last_response.body
get "/foo/read_raw_cookie"
assert_equal 2, verifier.verify(last_response.body)["foo"]
verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token)
get "/foo/read_raw_cookie"
assert_equal 2, verifier.verify(last_response.body)["foo"]
ensure
ENV["RAILS_ENV"] = old_rails_env
end
end
test "calling reset_session on request does not trigger an error for API apps" do

@ -16,7 +16,6 @@ def app
require "action_view/railtie"
class MyApp < Rails::Application
secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
config.eager_load = false

@ -125,7 +125,7 @@ def default_files
config/locales/en.yml
config/puma.rb
config/routes.rb
config/secrets.yml
config/credentials.yml.enc
config/spring.rb
config/storage.yml
db

@ -64,7 +64,7 @@
config/locales/en.yml
config/puma.rb
config/routes.rb
config/secrets.yml
config/credentials.yml.enc
config/spring.rb
config/storage.yml
db
@ -287,8 +287,6 @@ def test_app_update_does_not_generate_action_cable_contents_when_skip_action_cab
run_generator [app_root, "--skip-action-cable"]
FileUtils.cd(app_root) do
# For avoid conflict file
FileUtils.rm("#{app_root}/config/secrets.yml")
quietly { system("bin/rails app:update") }
end

@ -149,7 +149,7 @@ def test_generator_without_skips
end
assert_file "#{application_path}/config/environments/production.rb" do |content|
assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content)
assert_match(/^ config\.read_encrypted_secrets = true/, content)
assert_match(/^ # config\.require_master_key = true/, content)
end
end

@ -105,7 +105,6 @@ module Generation
def build_app(options = {})
@prev_rails_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
ENV["SECRET_KEY_BASE"] ||= SecureRandom.hex(16)
FileUtils.rm_rf(app_path)
FileUtils.cp_r(app_template_path, app_path)
@ -163,9 +162,10 @@ def make_basic_app
require "action_controller/railtie"
require "action_view/railtie"
@app = Class.new(Rails::Application)
@app = Class.new(Rails::Application) do
def self.name; "RailtiesTestApp"; end
end
@app.config.eager_load = false
@app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
@app.config.session_store :cookie_store, key: "_myapp_session"
@app.config.active_support.deprecation = :log
@app.config.active_support.test_order = :random

@ -58,12 +58,14 @@ def test_original_script_name
Rails.logger = Logger.new nil
app = Class.new(Rails::Application) {
def self.name; "ScriptNameTestApp"; end
attr_accessor :controller
def initialize
super
app = self
@routes = TestSet.new ->(c) { app.controller = c }
secrets.secret_key_base = "foo"
secrets.secret_token = "foo"
end
def app; routes; end

@ -176,6 +176,10 @@ def run_secrets_generator
Rails::Generators::EncryptedSecretsGenerator.start
end
add_to_config <<-RUBY
config.read_encrypted_secrets = true
RUBY
yield
end
end