Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [Unreleased]

### Changed

- Internal: Encryptor can now use other ciphers than the default



## [0.3.3] - 2020-07-25

Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```


Expand Down Expand Up @@ -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),
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion diffcrypt.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
21 changes: 12 additions & 9 deletions lib/diffcrypt/cli.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
# frozen_string_literal: true

require_relative './encryptor'
require_relative './file'
require_relative './version'

module Diffcrypt
class CLI < Thor
desc 'decrypt <path>', '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 <path>', 'Encrypt a file'
method_option :key, aliases: %i[k], required: true
method_option :cipher, default: Encryptor::DEFAULT_CIPHER
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, cipher: options[:cipher])
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
Expand All @@ -41,8 +43,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?
Expand Down
11 changes: 6 additions & 5 deletions lib/diffcrypt/encryptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
'data' => data,
)
end
Expand Down
47 changes: 47 additions & 0 deletions lib/diffcrypt/file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

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, cipher: DEFAULT_CIPHER)
return read if encrypted?

Encryptor.new(key, cipher: cipher).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
11 changes: 8 additions & 3 deletions lib/diffcrypt/rails/encrypted_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,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
Expand Down Expand Up @@ -73,7 +78,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
Expand Down
2 changes: 1 addition & 1 deletion test/diffcrypt/encryptor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}\ndata:\n key: #{ENCRYPTED_VALUE_PATTERN}\n/
expected_pattern = /---\nclient: diffcrypt-#{Diffcrypt::VERSION}\ncipher: #{Diffcrypt::Encryptor::DEFAULT_CIPHER}\ndata:\n key: #{ENCRYPTED_VALUE_PATTERN}\n/
assert_match expected_pattern, Diffcrypt::Encryptor.new(TEST_KEY).encrypt(content)
end

Expand Down
33 changes: 33 additions & 0 deletions test/diffcrypt/file_test.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions test/diffcrypt/rails/encrypted_configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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
File renamed without changes.
1 change: 1 addition & 0 deletions test/fixtures/aes-256-gcm.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
35bc348e9cbca231e05279ba668658333b45754e2b423c85fac831317d6f7bba
2 changes: 1 addition & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Empty file added tmp/.keep
Empty file.