Skip to content

Commit cf3e9a8

Browse files
authored
Deep rails integration (#47)
Improves the way diffcrypt is embedded in applications, with less configuration. It can also handle native rails credentials a bit better.
1 parent 570e409 commit cf3e9a8

File tree

11 files changed

+219
-16
lines changed

11 files changed

+219
-16
lines changed

.rubocop.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ Style/Documentation:
1010
Metrics/MethodLength:
1111
Exclude:
1212
- test/**/*_test.rb
13-
TrailingCommaInArrayLiteral:
13+
Style/TrailingCommaInArrayLiteral:
1414
EnforcedStyleForMultiline: consistent_comma
1515
Style/TrailingCommaInArguments:
1616
EnforcedStyleForMultiline: consistent_comma
17+
Style/TrailingCommaInHashLiteral:
18+
EnforcedStyleForMultiline: consistent_comma
1719
Style/AccessorGrouping:
1820
EnforcedStyle: separated
1921

README.md

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,10 @@ Currently there is not native support for rails, but ActiveSupport can be monkey
6666
the built in encrypter. All existing `rails credentials:edit` also work with this method.
6767

6868
```ruby
69-
require 'diffcrypt/rails/encrypted_configuration'
69+
# config/application.rb
7070
module Rails
7171
class Application
72-
def encrypted(path, key_path: 'config/aes-128-gcm.key', env_key: 'RAILS_MASTER_KEY')
73-
Diffcrypt::Rails::EncryptedConfiguration.new(
74-
config_path: Rails.root.join(path),
75-
key_path: Rails.root.join(key_path),
76-
env_key: env_key,
77-
raise_if_missing_key: config.require_master_key,
78-
)
79-
end
72+
include Diffcrypt::Rails::ApplicationHelper
8073
end
8174
end
8275
```

Rakefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ Rake::TestTask.new(:test) do |t|
88
t.libs << 'lib'
99
t.test_files = FileList['test/**/*_test.rb']
1010
end
11-
1211
task default: :test
12+
13+
path = File.expand_path(__dir__)
14+
Dir.glob("#{path}/lib/diffcrypt/tasks/**/*.rake").sort.each { |f| load f }

lib/diffcrypt.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'diffcrypt/encryptor'
44
require 'diffcrypt/version'
5+
require 'diffcrypt/railtie' if defined?(Rails)
56

67
module Diffcrypt
78
class Error < StandardError; end

lib/diffcrypt/encryptor.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ def decrypt_hash(data)
4747
# @param [String] contents The raw YAML string to be encrypted
4848
# @param [String, nil] original_encrypted_contents The original (encrypted) content to determine which keys have changed
4949
# @return [String]
50-
def encrypt(contents, original_encrypted_contents = nil)
50+
def encrypt(contents, original_encrypted_contents = nil, cipher: nil)
5151
data = encrypt_data contents, original_encrypted_contents
5252
YAML.dump(
5353
'client' => "diffcrypt-#{Diffcrypt::VERSION}",
54-
'cipher' => @cipher,
54+
'cipher' => cipher || @cipher,
5555
'data' => data,
5656
)
5757
end

lib/diffcrypt/file.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ def encrypted?
1414
to_yaml['cipher']
1515
end
1616

17+
# Determines the cipher to use for encryption/decryption
1718
def cipher
19+
return 'aes-128-gcm' if format == 'activesupport'
20+
1821
to_yaml['cipher'] || Encryptor::DEFAULT_CIPHER
1922
end
2023

@@ -23,11 +26,33 @@ def exists?
2326
::File.exist?(@path)
2427
end
2528

29+
# Determines the format to be used for encryption
30+
# @return [String] diffcrypt|activesupport
31+
def format
32+
return 'diffcrypt' if read == ''
33+
return 'diffcrypt' if read.index('---')&.zero?
34+
35+
'activesupport'
36+
end
37+
2638
# @return [String] Raw contents of the file
2739
def read
40+
return '' unless ::File.exist?(@path)
41+
2842
@read ||= ::File.read(@path)
43+
@read
44+
end
45+
46+
# Save the encrypted contents back to disk
47+
# @return [Boolean] True is file save was successful
48+
def write(key, data, cipher: nil)
49+
cipher ||= self.cipher
50+
yaml = ::YAML.dump(data)
51+
contents = Encryptor.new(key, cipher: cipher).encrypt(yaml)
52+
::File.write(@path, contents)
2953
end
3054

55+
# TODO: This seems useless, figure out what's up
3156
def encrypt(key, cipher: DEFAULT_CIPHER)
3257
return read if encrypted?
3358

@@ -42,7 +67,7 @@ def decrypt(key)
4267
end
4368

4469
def to_yaml
45-
@to_yaml ||= YAML.safe_load(read)
70+
@to_yaml ||= YAML.safe_load(read) || {}
4671
end
4772
end
4873
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require_relative './encrypted_configuration'
4+
5+
module Diffcrypt
6+
module Rails
7+
module ApplicationHelper
8+
def encrypted(path, key_path: 'config/master.key', env_key: 'RAILS_MASTER_KEY')
9+
config_path, key_path = resolve_encrypted_paths(path, key_path)
10+
11+
Diffcrypt::Rails::EncryptedConfiguration.new(
12+
config_path: config_path,
13+
key_path: key_path,
14+
env_key: env_key,
15+
raise_if_missing_key: config.require_master_key,
16+
)
17+
end
18+
19+
protected
20+
21+
def resolve_encrypted_paths(config_path, key_path)
22+
config_path_abs = ::Rails.root.join(config_path)
23+
key_path_abs = ::Rails.root.join(key_path)
24+
25+
# We always want to use `config/credentials/[environment]` for consistency
26+
# If the master credentials do not exist, and a user has not specificed an environment, default to development
27+
if config_path == 'config/credentials.yml.enc' && ::File.exist?(config_path_abs.to_s) == false
28+
config_path_abs = ::Rails.root.join('config/credentials/development.yml.enc')
29+
key_path_abs = ::Rails.root.join('config/credentials/development.key')
30+
end
31+
32+
[
33+
config_path_abs,
34+
key_path_abs,
35+
]
36+
end
37+
end
38+
end
39+
end

lib/diffcrypt/rails/encrypted_configuration.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ def writing(contents)
8888
end
8989
# rubocop:enable Metrics/AbcSize
9090

91+
# Standard rails credentials encrypt the entire file. We need to detect this to use the correct
92+
# data interface
93+
# @return [Boolean]
94+
def rails_native_credentials?(contents)
95+
contents.index('---').nil?
96+
end
97+
9198
# @param [String] contents The new content to be encrypted
9299
# @param [String] diff_against The original (encrypted) content to determine which keys have changed
93100
# @return [String] Encrypted content to commit
@@ -98,7 +105,7 @@ def encrypt(contents, original_encrypted_contents = nil)
98105
# @param [String] contents
99106
# @return [String]
100107
def decrypt(contents)
101-
if contents.index('---').nil?
108+
if rails_native_credentials?(contents)
102109
active_support_encryptor.decrypt_and_verify contents
103110
else
104111
encryptor.decrypt contents
@@ -110,7 +117,7 @@ def decrypt(contents)
110117
def active_support_encryptor
111118
@active_support_encryptor ||= ActiveSupport::MessageEncryptor.new(
112119
[key].pack('H*'),
113-
cipher: @diffcrypt_file.cipher,
120+
cipher: 'aes-128-gcm',
114121
)
115122
end
116123

lib/diffcrypt/railtie.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
require 'diffcrypt/rails/application_helper'
4+
5+
module Diffcrypt
6+
class Railtie < ::Rails::Railtie
7+
railtie_name :diffcrypt
8+
9+
rake_tasks do
10+
path = ::File.expand_path(__dir__)
11+
::Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
12+
end
13+
end
14+
end

lib/diffcrypt/tasks/rails.rake

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
namespace :diffcrypt do
4+
desc 'Initialize credentials for all environments'
5+
task :init, %i[environments] do |_t, args|
6+
args.with_defaults(
7+
environments: 'development,test,staging,production',
8+
)
9+
environments = args.environments.split(',')
10+
11+
environments.each do |environment|
12+
key_path = Rails.root.join('config', 'credentials', "#{environment}.key")
13+
file_path = Rails.root.join('config', 'credentials', "#{environment}.yml.enc")
14+
gitignore_path = Rails.root.join('.gitignore')
15+
next if File.exist?(file_path) || File.exist?(key_path)
16+
17+
# Generate a new key
18+
key = Diffcrypt::Encryptor.generate_key
19+
key_dir = File.dirname(key_path)
20+
Dir.mkdir(key_dir) unless Dir.exist?(key_dir)
21+
::File.write(key_path, key)
22+
23+
# Encrypt default contents
24+
file = Diffcrypt::File.new(file_path)
25+
data = {
26+
'secret_key_base' => SecureRandom.hex(32),
27+
}
28+
file.write(key, data)
29+
30+
# Ensure .key files are always ignored
31+
::File.open(gitignore_path, 'a') do |f|
32+
f.write("\nconfig/credentials/*.key")
33+
end
34+
end
35+
end
36+
end

0 commit comments

Comments
 (0)