aboutsummaryrefslogtreecommitdiffstats
path: root/railties/lib
diff options
context:
space:
mode:
authorWojciech Wnętrzak <w.wnetrzak@gmail.com>2017-11-14 11:44:23 +0100
committerKasper Timm Hansen <kaspth@gmail.com>2017-11-15 21:29:15 +0100
commit7a8728a03986489e1c843ed850afc2c16fb6eb06 (patch)
tree697428ddbb785a4ce32a77a43a5487914d28d3d1 /railties/lib
parented100166874fb4a542c5aaba933a4cca5ed72269 (diff)
downloadrails-7a8728a03986489e1c843ed850afc2c16fb6eb06.tar.gz
rails-7a8728a03986489e1c843ed850afc2c16fb6eb06.tar.bz2
rails-7a8728a03986489e1c843ed850afc2c16fb6eb06.zip
Add CLI to manage encrypted files/configs.
To edit/show encrypted file: ``` bin/rails encrypted:edit config/staging_tokens.yml.enc bin/rails encrypted:edit config/staging_tokens.yml.enc --key config/staging.key bin/rails encrypted:show config/staging_tokens.yml.enc ``` Also provides a backing Rails.application.encrypted API for Ruby access: ```ruby Rails.application.encrypted("config/staging_tokens.yml.enc").read Rails.application.encrypted("config/staging_tokens.yml.enc").config Rails.application.encrypted("config/staging_tokens.yml.enc", key: "config/staging.key") ```
Diffstat (limited to 'railties/lib')
-rw-r--r--railties/lib/rails/application.rb39
-rw-r--r--railties/lib/rails/command/helpers/editor.rb33
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb29
-rw-r--r--railties/lib/rails/commands/encrypted/encrypted_command.rb77
-rw-r--r--railties/lib/rails/generators/rails/credentials/credentials_generator.rb19
-rw-r--r--railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb38
-rw-r--r--railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb53
-rw-r--r--railties/lib/rails/generators/rails/master_key/master_key_generator.rb24
8 files changed, 263 insertions, 49 deletions
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index ade8cb6a48..31bc542308 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -433,10 +433,41 @@ module Rails
# 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"
+ @credentials ||= encrypted("config/credentials.yml.enc")
+ end
+
+ # Shorthand to decrypt any encrypted configurations or files.
+ #
+ # For any file added with `bin/rails encrypted:edit` call `read` to decrypt
+ # the file with the master key.
+ # The master key is either stored in `config/master.key` or `ENV["RAILS_MASTER_KEY"]`.
+ #
+ # Rails.application.encrypted("config/mystery_man.key").read
+ # # => "We've met before, haven't we?"
+ #
+ # It's also possible to interpret encrypted YAML files with `config`.
+ #
+ # Rails.application.encrypted("config/credentials.yml.enc").config
+ # # => { next_guys_line: "I don't think so. Where was it you think we met?" }
+ #
+ # Any top-level configs are also accessible directly on the return value:
+ #
+ # Rails.application.encrypted("config/credentials.yml.enc").next_guys_line
+ # # => "I don't think so. Where was it you think we met?"
+ #
+ # The files or configs can also be encrypted with a custom key. To decrypt with
+ # a key in the `ENV`, use:
+ #
+ # Rails.application.encrypted("config/special_tokens.yml.enc", env_key: "SPECIAL_TOKENS")
+ #
+ # Or to decrypt with a file, that should be version control ignored, relative to `Rails.root`:
+ #
+ # Rails.application.encrypted("config/special_tokens.yml.enc", key_path: "config/special_tokens.key")
+ def encrypted(path, key_path: "config/master.key", env_key: "RAILS_MASTER_KEY")
+ ActiveSupport::EncryptedConfiguration.new \
+ config_path: Rails.root.join(path),
+ key_path: Rails.root.join(key_path),
+ env_key: env_key
end
def to_app #:nodoc:
diff --git a/railties/lib/rails/command/helpers/editor.rb b/railties/lib/rails/command/helpers/editor.rb
new file mode 100644
index 0000000000..5e9ecc05e7
--- /dev/null
+++ b/railties/lib/rails/command/helpers/editor.rb
@@ -0,0 +1,33 @@
+require "active_support/encrypted_file"
+
+module Rails
+ module Command
+ module Helpers
+ module Editor
+ private
+ def ensure_editor_available(command:)
+ if ENV["EDITOR"].to_s.empty?
+ say "No $EDITOR to open file in. Assign one like this:"
+ say ""
+ say %(EDITOR="mate --wait" #{command})
+ 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 catch_editing_exceptions
+ yield
+ rescue Interrupt
+ say "Aborted changing file: nothing saved."
+ rescue ActiveSupport::EncryptedFile::MissingKeyError => error
+ say error.message
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index e5d3d01431..8085f07c2b 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -1,10 +1,13 @@
# frozen_string_literal: true
require "active_support"
+require "rails/command/helpers/editor"
module Rails
module Command
class CredentialsCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+
no_commands do
def help
say "Usage:\n #{self.class.banner}"
@@ -16,41 +19,25 @@ module Rails
def edit
require_application_and_environment!
- ensure_editor_available || (return)
+ ensure_editor_available(command: "bin/rails credentials:edit") || (return)
ensure_master_key_has_been_added
ensure_credentials_have_been_added
- change_credentials_in_system_editor
+ catch_editing_exceptions do
+ change_credentials_in_system_editor
+ end
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
diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb
new file mode 100644
index 0000000000..898094f1a4
--- /dev/null
+++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "active_support"
+require "rails/command/helpers/editor"
+
+module Rails
+ module Command
+ class EncryptedCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+
+ class_option :key, aliases: "-k", type: :string,
+ default: "config/master.key", desc: "The Rails.root relative path to the encryption key"
+
+ no_commands do
+ def help
+ say "Usage:\n #{self.class.banner}"
+ say ""
+ end
+ end
+
+ def edit(file_path)
+ require_application_and_environment!
+
+ ensure_editor_available(command: "bin/rails encrypted:edit") || (return)
+ ensure_encryption_key_has_been_added(options[:key])
+ ensure_encrypted_file_has_been_added(file_path, options[:key])
+
+ catch_editing_exceptions do
+ change_encrypted_file_in_system_editor(file_path, options[:key])
+ end
+
+ say "File encrypted and saved."
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?"
+ end
+
+ def show(file_path)
+ require_application_and_environment!
+
+ say Rails.application.encrypted(file_path, key_path: options[:key]).read.presence ||
+ "File '#{file_path}' does not exist. Use bin/rails encrypted:edit #{file_path} to change that."
+ end
+
+ private
+ def ensure_encryption_key_has_been_added(key_path)
+ encryption_key_file_generator.add_key_file(key_path)
+ encryption_key_file_generator.ignore_key_file(key_path)
+ end
+
+ def ensure_encrypted_file_has_been_added(file_path, key_path)
+ encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
+ end
+
+ def change_encrypted_file_in_system_editor(file_path, key_path)
+ Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path|
+ system("#{ENV["EDITOR"]} #{tmp_path}")
+ end
+ end
+
+
+ def encryption_key_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
+
+ Rails::Generators::EncryptionKeyFileGenerator.new
+ end
+
+ def encrypted_file_generator
+ require "rails/generators"
+ require "rails/generators/rails/encrypted_file/encrypted_file_generator"
+
+ Rails::Generators::EncryptedFileGenerator.new
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
index 52cb4bd8bf..ab15da5423 100644
--- a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
+++ b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
@@ -7,14 +7,11 @@ 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)
+ unless credentials.exist?
template = credentials_template
- say "Adding #{CONFIG_PATH} to store encrypted credentials."
+ say "Adding #{credentials.content_path} to store encrypted credentials."
say ""
say "The following content has been encrypted with the Rails master key:"
say ""
@@ -29,13 +26,17 @@ module Rails
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
+ credentials.write(credentials_template)
end
private
+ def credentials
+ ActiveSupport::EncryptedConfiguration.new \
+ config_path: "config/credentials.yml.enc",
+ key_path: "config/master.key",
+ env_key: "RAILS_MASTER_KEY"
+ end
+
def credentials_template
"# aws:\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" +
diff --git a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
new file mode 100644
index 0000000000..ddce5f6fe2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "rails/generators/base"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class EncryptedFileGenerator < Base
+ def add_encrypted_file(file_path, key_path)
+ unless File.exist?(file_path)
+ say "Adding #{file_path} to store encrypted content."
+ say ""
+ say "The following content has been encrypted with the encryption key:"
+ say ""
+ say template, :on_green
+ say ""
+
+ add_encrypted_file_silently(file_path, key_path)
+
+ say "You can edit encrypted file with `bin/rails encrypted:edit #{file_path}`."
+ say ""
+ end
+ end
+
+ def add_encrypted_file_silently(file_path, key_path, template = encrypted_file_template)
+ unless File.exist?(file_path)
+ setup = { content_path: file_path, key_path: key_path, env_key: "RAILS_MASTER_KEY" }
+ ActiveSupport::EncryptedFile.new(setup).write(template)
+ end
+ end
+
+ private
+ def encrypted_file_template
+ "# aws:\n# access_key_id: 123\n# secret_access_key: 345\n\n"
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb
new file mode 100644
index 0000000000..dd0d0c6c66
--- /dev/null
+++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "rails/generators/base"
+require "active_support/encrypted_file"
+
+module Rails
+ module Generators
+ class EncryptionKeyFileGenerator < Base
+ def add_key_file(key_path)
+ key_path = Pathname.new(key_path)
+
+ unless key_path.exist?
+ key = ActiveSupport::EncryptedFile.generate_key
+
+ log "Adding #{key_path} to store the encryption key: #{key}"
+ log ""
+ log "Save this in a password manager your team can access."
+ log ""
+ log "If you lose the key, no one, including you, can access anything encrypted with it."
+
+ log ""
+ add_key_file_silently(key_path, key)
+ log ""
+ end
+ end
+
+ def add_key_file_silently(key_path, key = nil)
+ create_file key_path, key || ActiveSupport::EncryptedFile.generate_key
+ end
+
+ def ignore_key_file(key_path, ignore: key_ignore(key_path))
+ if File.exist?(".gitignore")
+ unless File.read(".gitignore").include?(ignore)
+ log "Ignoring #{key_path} so it won't end up in Git history:"
+ log ""
+ append_to_file ".gitignore", ignore
+ log ""
+ end
+ else
+ log "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:"
+ log ignore, :on_green
+ log ""
+ end
+ end
+
+ private
+ def key_ignore(key_path)
+ [ "", "/#{key_path}", "" ].join("\n")
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
index 29d83f5d81..e57f07c1ae 100644
--- a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
+++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-require "rails/generators/base"
require "pathname"
+require "rails/generators/base"
+require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
require "active_support/encrypted_file"
module Rails
@@ -20,31 +21,24 @@ module Rails
log "If you lose the key, no one, including you, can access anything encrypted with it."
log ""
- add_master_key_file_silently key
+ add_master_key_file_silently(key)
log ""
end
end
def add_master_key_file_silently(key = nil)
- create_file MASTER_KEY_PATH, key || ActiveSupport::EncryptedFile.generate_key
+ key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key)
end
def ignore_master_key_file
- if File.exist?(".gitignore")
- unless File.read(".gitignore").include?(key_ignore)
- log "Ignoring #{MASTER_KEY_PATH} so it won't end up in Git history:"
- log ""
- append_to_file ".gitignore", key_ignore
- log ""
- end
- else
- log "IMPORTANT: Don't commit #{MASTER_KEY_PATH}. Add this to your ignore file:"
- log key_ignore, :on_green
- log ""
- end
+ key_file_generator.ignore_key_file(MASTER_KEY_PATH, ignore: key_ignore)
end
private
+ def key_file_generator
+ EncryptionKeyFileGenerator.new
+ end
+
def key_ignore
[ "", "# Ignore master key for decrypting credentials and more.", "/#{MASTER_KEY_PATH}", "" ].join("\n")
end