From 6f9d3ee072fe981e109e286640ff6d68e76073b8 Mon Sep 17 00:00:00 2001 From: Marc Qualie Date: Tue, 30 Jun 2020 21:18:57 +0100 Subject: [PATCH 1/5] Add File instance improve internal DSL --- CHANGELOG.md | 1 + diffcrypt.gemspec | 2 +- lib/diffcrypt/cli.rb | 20 +++++---- lib/diffcrypt/encryptor.rb | 11 ++--- lib/diffcrypt/file.rb | 45 +++++++++++++++++++ .../rails/encrypted_configuration.rb | 11 +++-- test/diffcrypt/encryptor_test.rb | 2 +- test/diffcrypt/file_test.rb | 33 ++++++++++++++ .../rails/encrypted_configuration_test.rb | 4 +- test/test_helper.rb | 2 +- tmp/.keep | 0 11 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 lib/diffcrypt/file.rb create mode 100644 test/diffcrypt/file_test.rb create mode 100644 tmp/.keep diff --git a/CHANGELOG.md b/CHANGELOG.md index d729945..f25b585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - CLI: `diffcrypt generate-key` command to generate a new key for a cipher +- Internal: Encryptor can now use other ciphers than the default diff --git a/diffcrypt.gemspec b/diffcrypt.gemspec index c19d5b2..31a2bb1 100644 --- a/diffcrypt.gemspec +++ b/diffcrypt.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |spec| # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path(__dir__)) do + spec.files = Dir.chdir(::File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end spec.bindir = 'bin' diff --git a/lib/diffcrypt/cli.rb b/lib/diffcrypt/cli.rb index a7ef412..d080cba 100644 --- a/lib/diffcrypt/cli.rb +++ b/lib/diffcrypt/cli.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative './encryptor' +require_relative './file' require_relative './version' module Diffcrypt @@ -8,21 +9,21 @@ class CLI < Thor desc 'decrypt ', 'Decrypt a file' method_option :key, aliases: %i[k], required: true def decrypt(path) - ensure_file_exists(path) - contents = File.read(path) - puts encryptor.decrypt(contents) + file = File.new(path) + ensure_file_exists(file) + say file.decrypt(key) end desc 'encrypt ', 'Encrypt a file' method_option :key, aliases: %i[k], required: true def encrypt(path) - ensure_file_exists(path) - contents = File.read(path) - puts encryptor.encrypt(contents) + file = File.new(path) + ensure_file_exists(file) + say file.encrypt(key) end desc 'generate-key', 'Generate a 32 bit key' - method_option :cipher, default: Encryptor::CIPHER + method_option :cipher, default: Encryptor::DEFAULT_CIPHER def generate_key say Encryptor.generate_key(options[:cipher]) end @@ -41,8 +42,9 @@ def encryptor @encryptor ||= Encryptor.new(key) end - def ensure_file_exists(path) - abort('[ERROR] File does not exist') unless File.exist?(path) + # @param [Diffcrypt::File] path + def ensure_file_exists(file) + abort('[ERROR] File does not exist') unless file.exists? end def self.exit_on_failure? diff --git a/lib/diffcrypt/encryptor.rb b/lib/diffcrypt/encryptor.rb index ea6b708..33bc622 100644 --- a/lib/diffcrypt/encryptor.rb +++ b/lib/diffcrypt/encryptor.rb @@ -12,15 +12,16 @@ module Diffcrypt class Encryptor - CIPHER = 'aes-128-gcm' + DEFAULT_CIPHER = 'aes-128-gcm' - def self.generate_key(cipher = CIPHER) + def self.generate_key(cipher = DEFAULT_CIPHER) SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(cipher)) end - def initialize(key) + def initialize(key, cipher: DEFAULT_CIPHER) @key = key - @encryptor ||= ActiveSupport::MessageEncryptor.new([key].pack('H*'), cipher: CIPHER) + @cipher = cipher + @encryptor ||= ActiveSupport::MessageEncryptor.new([key].pack('H*'), cipher: cipher) end # @param [String] contents The raw YAML string to be encrypted @@ -50,7 +51,7 @@ def encrypt(contents, original_encrypted_contents = nil) data = encrypt_data contents, original_encrypted_contents YAML.dump( 'client' => "diffcrypt-#{Diffcrypt::VERSION}", - 'cipher' => CIPHER, + 'cipher' => @cipher, 'checksum' => Digest::MD5.hexdigest(Marshal.dump(data)), 'data' => data, ) diff --git a/lib/diffcrypt/file.rb b/lib/diffcrypt/file.rb new file mode 100644 index 0000000..7bf915b --- /dev/null +++ b/lib/diffcrypt/file.rb @@ -0,0 +1,45 @@ +require_relative './encryptor' + +module Diffcrypt + class File + attr_reader :file + + def initialize(path) + @path = ::File.absolute_path path + end + + def encrypted? + to_yaml['cipher'] + end + + def cipher + to_yaml['cipher'] || Encryptor::DEFAULT_CIPHER + end + + # @return [Boolean] + def exists? + ::File.exist?(@path) + end + + # @return [String] Raw contents of the file + def read + @read ||= ::File.read(@path) + end + + def encrypt(key) + return read if encrypted? + + Encryptor.new(key).encrypt(read) + end + + def decrypt(key) + return read unless encrypted? + + Encryptor.new(key).decrypt(read) + end + + def to_yaml + @to_yaml ||= YAML.safe_load(read) + end + end +end diff --git a/lib/diffcrypt/rails/encrypted_configuration.rb b/lib/diffcrypt/rails/encrypted_configuration.rb index fb5fce1..9608c96 100644 --- a/lib/diffcrypt/rails/encrypted_configuration.rb +++ b/lib/diffcrypt/rails/encrypted_configuration.rb @@ -20,13 +20,18 @@ class EncryptedConfiguration delegate_missing_to :options def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:) - @content_path = Pathname.new(File.absolute_path(config_path)).yield_self do |path| + @content_path = Pathname.new(::File.absolute_path(config_path)).yield_self do |path| path.symlink? ? path.realpath : path end @key_path = Pathname.new(key_path) @env_key = env_key @raise_if_missing_key = raise_if_missing_key - @active_support_encryptor = ActiveSupport::MessageEncryptor.new([key].pack('H*'), cipher: Encryptor::CIPHER) + + # TODO: Use Diffcrypt::File to ensure correct cipher is used + @active_support_encryptor = ActiveSupport::MessageEncryptor.new( + [key].pack('H*'), + cipher: Encryptor::DEFAULT_CIPHER, + ) end # Determines if file is using the diffable format, or still @@ -72,7 +77,7 @@ def change(&block) # rubocop:disable Metrics/AbcSize def writing(contents) tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}" - tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file) + tmp_path = Pathname.new ::File.join(Dir.tmpdir, tmp_file) tmp_path.binwrite contents yield tmp_path diff --git a/test/diffcrypt/encryptor_test.rb b/test/diffcrypt/encryptor_test.rb index ba0b27e..02b857c 100644 --- a/test/diffcrypt/encryptor_test.rb +++ b/test/diffcrypt/encryptor_test.rb @@ -10,7 +10,7 @@ class Diffcrypt::EncryptorTest < Minitest::Test def test_it_includes_client_info_at_root content = "---\nkey: value" - expected_pattern = /---\nclient: diffcrypt-#{Diffcrypt::VERSION}\ncipher: #{Diffcrypt::Encryptor::CIPHER}\nchecksum: [a-z0-9]{32}\ndata:\n key: #{ENCRYPTED_VALUE_PATTERN}\n/ + expected_pattern = /---\nclient: diffcrypt-#{Diffcrypt::VERSION}\ncipher: #{Diffcrypt::Encryptor::DEFAULT_CIPHER}\nchecksum: [a-z0-9]{32}\ndata:\n key: #{ENCRYPTED_VALUE_PATTERN}\n/ assert_match expected_pattern, Diffcrypt::Encryptor.new(TEST_KEY).encrypt(content) end diff --git a/test/diffcrypt/file_test.rb b/test/diffcrypt/file_test.rb new file mode 100644 index 0000000..47f394a --- /dev/null +++ b/test/diffcrypt/file_test.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'test_helper' +require 'thor' + +require_relative '../../lib/diffcrypt/file' + +class Diffcrypt::FileTest < Minitest::Test + def setup + @path = "#{__dir__}/../../tmp/test-content-plain.yml" + @content = 'key: value' + ::File.write(@path, @content) + + @encrypted_path = "#{__dir__}/../../tmp/test-content-encrypted.yml" + @encrypted_content = "client: diffcrypt-test\ncipher: #{Diffcrypt::Encryptor::DEFAULT_CIPHER}\ndata:\n key: value" + ::File.write(@encrypted_path, @encrypted_content) + end + + def test_it_reads_content + file = Diffcrypt::File.new(@path) + assert_equal file.read, @content + end + + def test_it_idntifies_as_unencrypted + file = Diffcrypt::File.new(@path) + refute file.encrypted? + end + + def test_it_idntifies_as_encrypted + file = Diffcrypt::File.new(@encrypted_path) + assert file.encrypted? + end +end diff --git a/test/diffcrypt/rails/encrypted_configuration_test.rb b/test/diffcrypt/rails/encrypted_configuration_test.rb index 8e4799f..ae196b5 100644 --- a/test/diffcrypt/rails/encrypted_configuration_test.rb +++ b/test/diffcrypt/rails/encrypted_configuration_test.rb @@ -17,10 +17,10 @@ def configuration # This verifies that encrypted and unecrypted data can't be accidently the # same, which would create false positive tests and a major security issue def test_that_fixtures_are_different - refute_equal File.read("#{__dir__}/../../fixtures/example.yml.enc"), File.read("#{__dir__}/../../fixtures/example.yml") + refute_equal ::File.read("#{__dir__}/../../fixtures/example.yml.enc"), ::File.read("#{__dir__}/../../fixtures/example.yml") end def test_that_fixture_can_be_decrypted - assert_equal configuration.read, File.read("#{__dir__}/../../fixtures/example.yml") + assert_equal configuration.read, ::File.read("#{__dir__}/../../fixtures/example.yml") end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 03b3223..d7dbb32 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,6 +21,6 @@ # # @example Generate an expected value for tests # Diffcrypt::Encryptor.new('99e1f86b9e61f24c56ff4108dd415091').encrypt_string('some value here') -TEST_KEY = File.read("#{__dir__}/fixtures/master.key").strip +TEST_KEY = ::File.read("#{__dir__}/fixtures/master.key").strip require 'minitest/autorun' diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 From d0f4ddb66c8b0fa6cda8b9c51dacc90f2641c210 Mon Sep 17 00:00:00 2001 From: Marc Qualie Date: Tue, 30 Jun 2020 23:02:01 +0100 Subject: [PATCH 2/5] Setup multiple keys for different ciphers --- README.md | 6 +++--- test/diffcrypt/rails/encrypted_configuration_test.rb | 2 +- test/fixtures/{master.key => aes-128-gcm.key} | 0 test/fixtures/aes-256-gcm.key | 1 + test/test_helper.rb | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) rename test/fixtures/{master.key => aes-128-gcm.key} (100%) create mode 100644 test/fixtures/aes-256-gcm.key diff --git a/README.md b/README.md index d88e021..e3126d0 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ There are a few ways to use the library, depending on how advanced your use case The easiest way to get started is to use the CLI. ```shell -diffcrypt decrypt -k $(cat test/fixtures/master.key) test/fixtures/example.yml.enc -diffcrypt encrypt -k $(cat test/fixtures/master.key) test/fixtures/example.yml +diffcrypt decrypt -k $(cat test/fixtures/aes-128-gcm.key) test/fixtures/example.yml.enc +diffcrypt encrypt -k $(cat test/fixtures/aes-128-gcm.key) test/fixtures/example.yml ``` @@ -69,7 +69,7 @@ the built in encrypter. All existing `rails credentials:edit` also work with thi require 'diffcrypt/rails/encrypted_configuration' module Rails class Application - def encrypted(path, key_path: 'config/master.key', env_key: 'RAILS_MASTER_KEY') + def encrypted(path, key_path: 'config/aes-128-gcm.key', env_key: 'RAILS_MASTER_KEY') Diffcrypt::Rails::EncryptedConfiguration.new( config_path: Rails.root.join(path), key_path: Rails.root.join(key_path), diff --git a/test/diffcrypt/rails/encrypted_configuration_test.rb b/test/diffcrypt/rails/encrypted_configuration_test.rb index ae196b5..4dd5cf3 100644 --- a/test/diffcrypt/rails/encrypted_configuration_test.rb +++ b/test/diffcrypt/rails/encrypted_configuration_test.rb @@ -8,7 +8,7 @@ class Diffcrypt::Rails::EncryptedConfigurationTest < Minitest::Test def configuration Diffcrypt::Rails::EncryptedConfiguration.new( config_path: "#{__dir__}/../../fixtures/example.yml.enc", - key_path: "#{__dir__}/../../fixtures/master.key", + key_path: "#{__dir__}/../../fixtures/aes-128-gcm.key", env_key: 'RAILS_MASTER_KEY', raise_if_missing_key: false, ) diff --git a/test/fixtures/master.key b/test/fixtures/aes-128-gcm.key similarity index 100% rename from test/fixtures/master.key rename to test/fixtures/aes-128-gcm.key diff --git a/test/fixtures/aes-256-gcm.key b/test/fixtures/aes-256-gcm.key new file mode 100644 index 0000000..fb295a8 --- /dev/null +++ b/test/fixtures/aes-256-gcm.key @@ -0,0 +1 @@ +35bc348e9cbca231e05279ba668658333b45754e2b423c85fac831317d6f7bba diff --git a/test/test_helper.rb b/test/test_helper.rb index d7dbb32..fa8fd7d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,6 +21,6 @@ # # @example Generate an expected value for tests # Diffcrypt::Encryptor.new('99e1f86b9e61f24c56ff4108dd415091').encrypt_string('some value here') -TEST_KEY = ::File.read("#{__dir__}/fixtures/master.key").strip +TEST_KEY = ::File.read("#{__dir__}/fixtures/aes-128-gcm.key").strip require 'minitest/autorun' From aa687f9b117a17f45ce7e6e8148fda10ba3d8ad2 Mon Sep 17 00:00:00 2001 From: Marc Qualie Date: Tue, 30 Jun 2020 23:02:45 +0100 Subject: [PATCH 3/5] Allow a file to be encrypted with a different filter --- lib/diffcrypt/cli.rb | 3 ++- lib/diffcrypt/file.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/diffcrypt/cli.rb b/lib/diffcrypt/cli.rb index d080cba..930057f 100644 --- a/lib/diffcrypt/cli.rb +++ b/lib/diffcrypt/cli.rb @@ -16,10 +16,11 @@ def decrypt(path) desc 'encrypt ', 'Encrypt a file' method_option :key, aliases: %i[k], required: true + method_option :cipher, default: Encryptor::DEFAULT_CIPHER def encrypt(path) file = File.new(path) ensure_file_exists(file) - say file.encrypt(key) + say file.encrypt(key, cipher: options[:cipher]) end desc 'generate-key', 'Generate a 32 bit key' diff --git a/lib/diffcrypt/file.rb b/lib/diffcrypt/file.rb index 7bf915b..6fa3add 100644 --- a/lib/diffcrypt/file.rb +++ b/lib/diffcrypt/file.rb @@ -26,10 +26,10 @@ def read @read ||= ::File.read(@path) end - def encrypt(key) + def encrypt(key, cipher: DEFAULT_CIPHER) return read if encrypted? - Encryptor.new(key).encrypt(read) + Encryptor.new(key, cipher: cipher).encrypt(read) end def decrypt(key) From 1128c196217135bf6ec245817bb862c75eafc53d Mon Sep 17 00:00:00 2001 From: Marc Qualie Date: Tue, 30 Jun 2020 23:13:23 +0100 Subject: [PATCH 4/5] rubocop --- lib/diffcrypt/file.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/diffcrypt/file.rb b/lib/diffcrypt/file.rb index 6fa3add..9ad6cd6 100644 --- a/lib/diffcrypt/file.rb +++ b/lib/diffcrypt/file.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative './encryptor' module Diffcrypt From 019f2c35cacbcb1ebac429404636796dcda8b308 Mon Sep 17 00:00:00 2001 From: Marc Qualie Date: Mon, 20 Jul 2020 10:01:52 +0100 Subject: [PATCH 5/5] Add instructions for upgrading cipher --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e3126d0..7f5a0ff 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,17 @@ end +## Converting between ciphers + +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: + +```shell +diffcrypt decrypt -k $(cat test/fixtures/aes-128-gcm.key) test/fixtures/example.yml.enc > test/fixtures/example.128.yml \ +&& 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 +``` + + + ## Development 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.