Skip to content

Commit 855eb39

Browse files
authored
Multi cipher support (#2)
Allow extra cipher support, such as `aes-256-gcm` Resolves #4
1 parent e157d33 commit 855eb39

14 files changed

+134
-26
lines changed

Diff for: CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## [Unreleased]
10+
11+
### Changed
12+
13+
- Internal: Encryptor can now use other ciphers than the default
14+
15+
916

1017
## [0.3.3] - 2020-07-25
1118

Diff for: README.md

+14-3
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ There are a few ways to use the library, depending on how advanced your use case
3636
The easiest way to get started is to use the CLI.
3737

3838
```shell
39-
diffcrypt decrypt -k $(cat test/fixtures/master.key) test/fixtures/example.yml.enc
40-
diffcrypt encrypt -k $(cat test/fixtures/master.key) test/fixtures/example.yml
39+
diffcrypt decrypt -k $(cat test/fixtures/aes-128-gcm.key) test/fixtures/example.yml.enc
40+
diffcrypt encrypt -k $(cat test/fixtures/aes-128-gcm.key) test/fixtures/example.yml
4141
```
4242

4343

@@ -69,7 +69,7 @@ the built in encrypter. All existing `rails credentials:edit` also work with thi
6969
require 'diffcrypt/rails/encrypted_configuration'
7070
module Rails
7171
class Application
72-
def encrypted(path, key_path: 'config/master.key', env_key: 'RAILS_MASTER_KEY')
72+
def encrypted(path, key_path: 'config/aes-128-gcm.key', env_key: 'RAILS_MASTER_KEY')
7373
Diffcrypt::Rails::EncryptedConfiguration.new(
7474
config_path: Rails.root.join(path),
7575
key_path: Rails.root.join(key_path),
@@ -83,6 +83,17 @@ end
8383

8484

8585

86+
## Converting between ciphers
87+
88+
Sometimes you may want to rotate the cipher used on a file. You cab do this rogramtically using the ruby code above, or you can also chain the CLI commands like so:
89+
90+
```shell
91+
diffcrypt decrypt -k $(cat test/fixtures/aes-128-gcm.key) test/fixtures/example.yml.enc > test/fixtures/example.128.yml \
92+
&& diffcrypt encrypt --cipher aes-256-gcm -k $(cat test/fixtures/aes-256-gcm.key) test/fixtures/example.128.yml > test/fixtures/example.256.yml.enc && rm test/fixtures/example.128.yml
93+
```
94+
95+
96+
8697
## Development
8798

8899
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

Diff for: diffcrypt.gemspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
2222

2323
# Specify which files should be added to the gem when it is released.
2424
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
25+
spec.files = Dir.chdir(::File.expand_path(__dir__)) do
2626
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
2727
end
2828
spec.bindir = 'bin'

Diff for: lib/diffcrypt/cli.rb

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
# frozen_string_literal: true
22

33
require_relative './encryptor'
4+
require_relative './file'
45
require_relative './version'
56

67
module Diffcrypt
78
class CLI < Thor
89
desc 'decrypt <path>', 'Decrypt a file'
910
method_option :key, aliases: %i[k], required: true
1011
def decrypt(path)
11-
ensure_file_exists(path)
12-
contents = File.read(path)
13-
puts encryptor.decrypt(contents)
12+
file = File.new(path)
13+
ensure_file_exists(file)
14+
say file.decrypt(key)
1415
end
1516

1617
desc 'encrypt <path>', 'Encrypt a file'
1718
method_option :key, aliases: %i[k], required: true
19+
method_option :cipher, default: Encryptor::DEFAULT_CIPHER
1820
def encrypt(path)
19-
ensure_file_exists(path)
20-
contents = File.read(path)
21-
puts encryptor.encrypt(contents)
21+
file = File.new(path)
22+
ensure_file_exists(file)
23+
say file.encrypt(key, cipher: options[:cipher])
2224
end
2325

2426
desc 'generate-key', 'Generate a 32 bit key'
25-
method_option :cipher, default: Encryptor::CIPHER
27+
method_option :cipher, default: Encryptor::DEFAULT_CIPHER
2628
def generate_key
2729
say Encryptor.generate_key(options[:cipher])
2830
end
@@ -41,8 +43,9 @@ def encryptor
4143
@encryptor ||= Encryptor.new(key)
4244
end
4345

44-
def ensure_file_exists(path)
45-
abort('[ERROR] File does not exist') unless File.exist?(path)
46+
# @param [Diffcrypt::File] path
47+
def ensure_file_exists(file)
48+
abort('[ERROR] File does not exist') unless file.exists?
4649
end
4750

4851
def self.exit_on_failure?

Diff for: lib/diffcrypt/encryptor.rb

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212

1313
module Diffcrypt
1414
class Encryptor
15-
CIPHER = 'aes-128-gcm'
15+
DEFAULT_CIPHER = 'aes-128-gcm'
1616

17-
def self.generate_key(cipher = CIPHER)
17+
def self.generate_key(cipher = DEFAULT_CIPHER)
1818
SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(cipher))
1919
end
2020

21-
def initialize(key)
21+
def initialize(key, cipher: DEFAULT_CIPHER)
2222
@key = key
23-
@encryptor ||= ActiveSupport::MessageEncryptor.new([key].pack('H*'), cipher: CIPHER)
23+
@cipher = cipher
24+
@encryptor ||= ActiveSupport::MessageEncryptor.new([key].pack('H*'), cipher: cipher)
2425
end
2526

2627
# @param [String] contents The raw YAML string to be encrypted
@@ -50,7 +51,7 @@ def encrypt(contents, original_encrypted_contents = nil)
5051
data = encrypt_data contents, original_encrypted_contents
5152
YAML.dump(
5253
'client' => "diffcrypt-#{Diffcrypt::VERSION}",
53-
'cipher' => CIPHER,
54+
'cipher' => @cipher,
5455
'data' => data,
5556
)
5657
end

Diff for: lib/diffcrypt/file.rb

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require_relative './encryptor'
4+
5+
module Diffcrypt
6+
class File
7+
attr_reader :file
8+
9+
def initialize(path)
10+
@path = ::File.absolute_path path
11+
end
12+
13+
def encrypted?
14+
to_yaml['cipher']
15+
end
16+
17+
def cipher
18+
to_yaml['cipher'] || Encryptor::DEFAULT_CIPHER
19+
end
20+
21+
# @return [Boolean]
22+
def exists?
23+
::File.exist?(@path)
24+
end
25+
26+
# @return [String] Raw contents of the file
27+
def read
28+
@read ||= ::File.read(@path)
29+
end
30+
31+
def encrypt(key, cipher: DEFAULT_CIPHER)
32+
return read if encrypted?
33+
34+
Encryptor.new(key, cipher: cipher).encrypt(read)
35+
end
36+
37+
def decrypt(key)
38+
return read unless encrypted?
39+
40+
Encryptor.new(key).decrypt(read)
41+
end
42+
43+
def to_yaml
44+
@to_yaml ||= YAML.safe_load(read)
45+
end
46+
end
47+
end

Diff for: lib/diffcrypt/rails/encrypted_configuration.rb

+8-3
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@ class EncryptedConfiguration
2121
delegate_missing_to :options
2222

2323
def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
24-
@content_path = Pathname.new(File.absolute_path(config_path)).yield_self do |path|
24+
@content_path = Pathname.new(::File.absolute_path(config_path)).yield_self do |path|
2525
path.symlink? ? path.realpath : path
2626
end
2727
@key_path = Pathname.new(key_path)
2828
@env_key = env_key
2929
@raise_if_missing_key = raise_if_missing_key
30-
@active_support_encryptor = ActiveSupport::MessageEncryptor.new([key].pack('H*'), cipher: Encryptor::CIPHER)
30+
31+
# TODO: Use Diffcrypt::File to ensure correct cipher is used
32+
@active_support_encryptor = ActiveSupport::MessageEncryptor.new(
33+
[key].pack('H*'),
34+
cipher: Encryptor::DEFAULT_CIPHER,
35+
)
3136
end
3237

3338
# Determines if file is using the diffable format, or still
@@ -73,7 +78,7 @@ def change(&block)
7378
# rubocop:disable Metrics/AbcSize
7479
def writing(contents)
7580
tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
76-
tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
81+
tmp_path = Pathname.new ::File.join(Dir.tmpdir, tmp_file)
7782
tmp_path.binwrite contents
7883

7984
yield tmp_path

Diff for: test/diffcrypt/encryptor_test.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class Diffcrypt::EncryptorTest < Minitest::Test
1111
def test_it_includes_client_info_at_root
1212
content = "---\nkey: value"
13-
expected_pattern = /---\nclient: diffcrypt-#{Diffcrypt::VERSION}\ncipher: #{Diffcrypt::Encryptor::CIPHER}\ndata:\n key: #{ENCRYPTED_VALUE_PATTERN}\n/
13+
expected_pattern = /---\nclient: diffcrypt-#{Diffcrypt::VERSION}\ncipher: #{Diffcrypt::Encryptor::DEFAULT_CIPHER}\ndata:\n key: #{ENCRYPTED_VALUE_PATTERN}\n/
1414
assert_match expected_pattern, Diffcrypt::Encryptor.new(TEST_KEY).encrypt(content)
1515
end
1616

Diff for: test/diffcrypt/file_test.rb

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
require 'thor'
5+
6+
require_relative '../../lib/diffcrypt/file'
7+
8+
class Diffcrypt::FileTest < Minitest::Test
9+
def setup
10+
@path = "#{__dir__}/../../tmp/test-content-plain.yml"
11+
@content = 'key: value'
12+
::File.write(@path, @content)
13+
14+
@encrypted_path = "#{__dir__}/../../tmp/test-content-encrypted.yml"
15+
@encrypted_content = "client: diffcrypt-test\ncipher: #{Diffcrypt::Encryptor::DEFAULT_CIPHER}\ndata:\n key: value"
16+
::File.write(@encrypted_path, @encrypted_content)
17+
end
18+
19+
def test_it_reads_content
20+
file = Diffcrypt::File.new(@path)
21+
assert_equal file.read, @content
22+
end
23+
24+
def test_it_idntifies_as_unencrypted
25+
file = Diffcrypt::File.new(@path)
26+
refute file.encrypted?
27+
end
28+
29+
def test_it_idntifies_as_encrypted
30+
file = Diffcrypt::File.new(@encrypted_path)
31+
assert file.encrypted?
32+
end
33+
end

Diff for: test/diffcrypt/rails/encrypted_configuration_test.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class Diffcrypt::Rails::EncryptedConfigurationTest < Minitest::Test
88
def configuration
99
Diffcrypt::Rails::EncryptedConfiguration.new(
1010
config_path: "#{__dir__}/../../fixtures/example.yml.enc",
11-
key_path: "#{__dir__}/../../fixtures/master.key",
11+
key_path: "#{__dir__}/../../fixtures/aes-128-gcm.key",
1212
env_key: 'RAILS_MASTER_KEY',
1313
raise_if_missing_key: false,
1414
)
@@ -17,10 +17,10 @@ def configuration
1717
# This verifies that encrypted and unecrypted data can't be accidently the
1818
# same, which would create false positive tests and a major security issue
1919
def test_that_fixtures_are_different
20-
refute_equal File.read("#{__dir__}/../../fixtures/example.yml.enc"), File.read("#{__dir__}/../../fixtures/example.yml")
20+
refute_equal ::File.read("#{__dir__}/../../fixtures/example.yml.enc"), ::File.read("#{__dir__}/../../fixtures/example.yml")
2121
end
2222

2323
def test_that_fixture_can_be_decrypted
24-
assert_equal configuration.read, File.read("#{__dir__}/../../fixtures/example.yml")
24+
assert_equal configuration.read, ::File.read("#{__dir__}/../../fixtures/example.yml")
2525
end
2626
end
File renamed without changes.

Diff for: test/fixtures/aes-256-gcm.key

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
35bc348e9cbca231e05279ba668658333b45754e2b423c85fac831317d6f7bba

Diff for: test/test_helper.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
#
2222
# @example Generate an expected value for tests
2323
# Diffcrypt::Encryptor.new('99e1f86b9e61f24c56ff4108dd415091').encrypt_string('some value here')
24-
TEST_KEY = File.read("#{__dir__}/fixtures/master.key").strip
24+
TEST_KEY = ::File.read("#{__dir__}/fixtures/aes-128-gcm.key").strip
2525

2626
require 'minitest/autorun'

Diff for: tmp/.keep

Whitespace-only changes.

0 commit comments

Comments
 (0)