diff --git a/.github/dependabot-auto-merge.yml b/.github/dependabot-auto-merge.yml new file mode 100644 index 0000000..addf11f --- /dev/null +++ b/.github/dependabot-auto-merge.yml @@ -0,0 +1,17 @@ +name: Dependabot auto-merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..26746e2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: 'bundler' + directory: '/' + schedule: + interval: 'daily' + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..93fb178 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ +name: Build + +on: push + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run specs + run: bundle exec rake diff --git a/.gitignore b/.gitignore index e3200e0..b04a8c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,56 +1,11 @@ -*.gem -*.rbc -/.config +/.bundle/ +/.yardoc +/_yardoc/ /coverage/ -/InstalledFiles +/doc/ /pkg/ /spec/reports/ -/spec/examples.txt -/test/tmp/ -/test/version_tmp/ /tmp/ -# Used by dotenv library to load environment variables. -# .env - -# Ignore Byebug command history file. -.byebug_history - -## Specific to RubyMotion: -.dat* -.repl_history -build/ -*.bridgesupport -build-iPhoneOS/ -build-iPhoneSimulator/ - -## Specific to RubyMotion (use of CocoaPods): -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# vendor/Pods/ - -## Documentation cache and generated files: -/.yardoc/ -/_yardoc/ -/doc/ -/rdoc/ - -## Environment normalization: -/.bundle/ -/vendor/bundle -/lib/bundler/man/ - -# for a library or gem, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# Gemfile.lock -# .ruby-version -# .ruby-gemset - -# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: -.rvmrc - -# Used by RuboCop. Remote config files pulled in from inherit_from directive. -# .rubocop-https?--* +# rspec failure tracking +.rspec_status diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..94f4dbe --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,26 @@ +require: + - rubocop-rspec + - rubocop-performance + +# Omakase Ruby styling for Rails +inherit_gem: + rubocop-rails-omakase: rubocop.yml + +AllCops: + NewCops: enable + TargetRubyVersion: 3.3 + +Lint: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: false + +RSpec/DescribedClass: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..1dd1998 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.3.5 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6b5fca2 --- /dev/null +++ b/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in tts_voices.gemspec +gemspec + +gem "rake" +gem "rspec" +gem "rubocop-rails-omakase", require: false +gem "rubocop-performance", require: false +gem "rubocop-rspec", require: false diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..3a65817 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,129 @@ +PATH + remote: . + specs: + encrypted_credentials (0.1.0) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.1.3.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + ast (2.4.2) + aws-eventstream (1.3.0) + aws-partitions (1.975.0) + aws-sdk-core (3.205.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-ssm (1.178.0) + aws-sdk-core (~> 3, >= 3.205.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.2.0) + bigdecimal (3.1.8) + concurrent-ruby (1.3.1) + connection_pool (2.4.1) + diff-lcs (1.5.1) + drb (2.2.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.7.2) + language_server-protocol (3.17.0.3) + minitest (5.23.0) + mutex_m (0.2.0) + parallel (1.24.0) + parser (3.3.1.0) + ast (~> 2.4.1) + racc + racc (1.7.3) + rack (3.0.11) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.9.1) + rexml (3.2.8) + strscan (>= 3.0.9) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + rubocop (1.63.5) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) + rubocop-capybara (2.20.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.25.1) + rubocop (~> 1.41) + rubocop-minitest (0.35.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-performance (1.21.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.24.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + rubocop-rspec (2.29.2) + rubocop (~> 1.40) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + rubocop-rspec_rails (~> 2.28) + rubocop-rspec_rails (2.28.3) + rubocop (~> 1.40) + ruby-progressbar (1.13.0) + strscan (3.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + +PLATFORMS + arm64-darwin-22 + ruby + +DEPENDENCIES + aws-sdk-ssm + encrypted_credentials! + rake + rspec + rubocop-performance + rubocop-rails-omakase + rubocop-rspec + +BUNDLED WITH + 2.5.18 diff --git a/README.md b/README.md index 988b27d..6c9488f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,99 @@ -# encrypted_credentials -Encrypted credentials without Rails +# EncryptedCredentials + +[![Build](https://github.com/somleng/encrypted_credentials/actions/workflows/main.yml/badge.svg)](https://github.com/somleng/encrypted_credentials/actions/workflows/main.yml) + +Rails like [Encrypted credentials](https://guides.rubyonrails.org/security.html#environmental-security) for plain ruby with no dependencies. + +## Installation + +Add to the application's Gemfile: + +```rb +# Gemfile + +gem "encrypted_credentials", github: "somleng/encrypted_credentials" +``` + +## Usage + +Given the following files: + +```yml +# config/app_settings.yml + +default: &default + foo: "bar" + password: "secret" + +production: &production + <<: *default + password: "<%= AppSettings.credentials.fetch('password') %>" + +staging: + <<: *production + +development: &development + <<: *default + +test: + <<: *development +``` + +```yml +# config/credentials.yml.enc +# edit this file by running: +# bundle exec edit_credentials -f config/credentials.yml.enc -k config/master.key + +production: &production + password: "super-secret" + +staging: + <<: *production +``` + +```rb +# config/app_settings.rb +require "encrypted_credentials/app_settings" +require "encrypted_credentials/encrypted_file" + +AppSettings = Class.new(EncryptedCredentials::AppSettings) do + def initialize(**) + super( + file: Pathname(File.expand_path("app_settings.yml", __dir__)), + encrypted_file: EncryptedCredentials::EncryptedFile.new( + file: Pathname(File.expand_path("credentials.yml.enc", __dir__)) + ) + ** + ) + end +end.new +``` + +```bash +APP_ENV=production ./bin/console +``` + +```rb +AppSettings.env +# "production" + +AppSettings.fetch(:password) +# "super-secret" + +AppSettings.fetch(:foo) +# "bar" +``` + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/somleng/encrypted_credentials. + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b6ae734 --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..e4b4900 --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "encrypted_credentials" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/encrypted_credentials.gemspec b/encrypted_credentials.gemspec new file mode 100644 index 0000000..97a845a --- /dev/null +++ b/encrypted_credentials.gemspec @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require_relative "lib/encrypted_credentials/version" + +Gem::Specification.new do |spec| + spec.name = "encrypted_credentials" + spec.version = EncryptedCredentials::VERSION + spec.authors = [ "David Wilkie" ] + spec.email = [ "dwilkie@gmail.com" ] + + spec.summary = "Encrypted Credentials" + spec.description = "Encrypted credentials without Rails" + spec.homepage = "https://github.com/somleng/encrypted_credentials" + spec.license = "MIT" + spec.required_ruby_version = ">= 2.6.0" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/somleng/encrypted_credentials" + + # 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(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + (File.expand_path(f) == __FILE__) || + f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]) + end + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = [ "lib" ] + + # Uncomment to register a new dependency of your gem + spec.add_development_dependency "aws-sdk-ssm" + + # For more information and examples about making a new gem, check out our + # guide at: https://bundler.io/guides/creating_gem.html + spec.metadata["rubygems_mfa_required"] = "true" +end diff --git a/exe/edit_credentials b/exe/edit_credentials new file mode 100755 index 0000000..7f6edfa --- /dev/null +++ b/exe/edit_credentials @@ -0,0 +1,41 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "optparse" +require "tempfile" +require "fileutils" +require "encrypted_credentials/encrypted_file" +require "encrypted_credentials/coder" + +Options = Struct.new(:filepath, :key) +options = Options.new + +parser = OptionParser.new do |opts| + opts.banner = "Usage: EDITOR=vim edit_credentials [options]" + + opts.on("-f", "--file FILENAME", "Encrypted credentials file") { |o| options.filepath = Pathname(o) } + opts.on("-k", "--keyfile KEY_FILE", "Key file. Default: $APP_MASTER_KEY") { |o| options.key = File.read(o) } +end + +parser.parse! + +raise(ArgumentError, "Missing credentials file") if options.filepath.nil? +options.key ||= ENV.fetch("APP_MASTER_KEY") +options.key.chomp! + +coder = EncryptedCredentials::Coder.new(key: options.key) +encrypted_file = EncryptedCredentials::EncryptedFile.new(file: options.filepath, coder:) + +Tempfile.create(options.filepath.basename.to_s) do |tmpfile| + if options.filepath.exist? + tmpfile.write(encrypted_file.read) + tmpfile.flush + tmpfile.rewind + end + + system("#{ENV.fetch("EDITOR", "vi")} #{tmpfile.path}") + + updated_content = File.read(tmpfile) + encrypted_file.write(updated_content) +end diff --git a/lib/encrypted_credentials.rb b/lib/encrypted_credentials.rb new file mode 100644 index 0000000..cd99c08 --- /dev/null +++ b/lib/encrypted_credentials.rb @@ -0,0 +1,4 @@ +module EncryptedCredentials +end + +require_relative "encrypted_credentials/version" diff --git a/lib/encrypted_credentials/app_settings.rb b/lib/encrypted_credentials/app_settings.rb new file mode 100644 index 0000000..8f82329 --- /dev/null +++ b/lib/encrypted_credentials/app_settings.rb @@ -0,0 +1,40 @@ +require "yaml" +require "erb" +require "pathname" + +module EncryptedCredentials + class AppSettings + attr_reader :file, :encrypted_file, :environment + + def initialize(file:, **options) + @file = file + @encrypted_file = options[:encrypted_file] + @environment = (options.fetch(:environment) { ENV.fetch("APP_ENV", "development") }).to_s + end + + def fetch(key) + settings.fetch(key.to_s) + end + + def env + environment + end + + def [](key) + settings[key.to_s] + end + + def credentials + @credentials ||= encrypted_file.credentials.fetch(env, {}) + end + + private + + def settings + @settings ||= begin + data = YAML.load(file.read, aliases: true).fetch(env, {}) + YAML.load(ERB.new(data.to_yaml).result) + end + end + end +end diff --git a/lib/encrypted_credentials/coder.rb b/lib/encrypted_credentials/coder.rb new file mode 100644 index 0000000..71e0e20 --- /dev/null +++ b/lib/encrypted_credentials/coder.rb @@ -0,0 +1,34 @@ +require "openssl" + +module EncryptedCredentials + class Coder + DEFAULT_ALGORITHM = "aes-256-cbc" + + attr_reader :algorithm + + def initialize(**options) + @key = options.fetch(:key) { -> { ENV.fetch("APP_MASTER_KEY") } } + @algorithm = options.fetch(:algorithm, DEFAULT_ALGORITHM) + end + + def encrypt(content) + cipher = OpenSSL::Cipher.new(algorithm) + cipher.encrypt + cipher.key = [ key ].pack("H*") + cipher.update(content) + cipher.final + end + + def decrypt(content) + cipher = OpenSSL::Cipher.new(algorithm) + cipher.decrypt + cipher.key = [ key ].pack("H*") + cipher.update(content) + cipher.final + end + + private + + def key + @key.respond_to?(:call) ? @key.call : @key + end + end +end diff --git a/lib/encrypted_credentials/encrypted_environment_variables.rb b/lib/encrypted_credentials/encrypted_environment_variables.rb new file mode 100644 index 0000000..4898001 --- /dev/null +++ b/lib/encrypted_credentials/encrypted_environment_variables.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "aws-sdk-ssm" + +module EncryptedCredentials + class EncryptedEnvironmentVariables + attr_reader :ssm_client, :environment + + SSM_PARAMETER_NAME_PATTERN = "_SSM_PARAMETER_NAME" + + def initialize(ssm_client: Aws::SSM::Client.new, environment: ENV) + @ssm_client = ssm_client + @environment = environment + end + + def decrypt + return if ssm_parameter_names.empty? + + decryption_result = decrypt_parameters(ssm_parameter_names.values) + set_env_from_parameters(decryption_result.parameters) + end + + private + + def ssm_parameter_names + @ssm_parameter_names ||= environment.select { |key, _| key.end_with?(SSM_PARAMETER_NAME_PATTERN) } + end + + def decrypt_parameters(names) + ssm_client.get_parameters(names:, with_decryption: true) + end + + def set_env_from_parameters(parameters) + ssm_parameter_names.each do |name, value| + env_name = name.delete_suffix(SSM_PARAMETER_NAME_PATTERN) + environment[env_name] = parameters.find { |parameter| parameter.name == value }.value + end + end + end +end diff --git a/lib/encrypted_credentials/encrypted_file.rb b/lib/encrypted_credentials/encrypted_file.rb new file mode 100644 index 0000000..4370ee0 --- /dev/null +++ b/lib/encrypted_credentials/encrypted_file.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "yaml" +require "base64" +require_relative "coder" + +module EncryptedCredentials + class EncryptedFile + attr_reader :file, :coder + + def initialize(file:, **options) + @file = file + @coder = options.fetch(:coder) { Coder.new } + end + + def credentials + content = read + YAML.load(content, aliases: true) + end + + def read + encrypted_content = Base64.strict_decode64(file.binread) + coder.decrypt(encrypted_content) + end + + def write(content) + encrypted_content = coder.encrypt(content) + file.binwrite(Base64.strict_encode64(encrypted_content)) + end + end +end diff --git a/lib/encrypted_credentials/version.rb b/lib/encrypted_credentials/version.rb new file mode 100644 index 0000000..fd4154a --- /dev/null +++ b/lib/encrypted_credentials/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module EncryptedCredentials + VERSION = "0.1.0" +end diff --git a/spec/encrypted_credentials/app_settings_spec.rb b/spec/encrypted_credentials/app_settings_spec.rb new file mode 100644 index 0000000..8a968d5 --- /dev/null +++ b/spec/encrypted_credentials/app_settings_spec.rb @@ -0,0 +1,13 @@ +require "spec_helper" + +module EncryptedCredentials + RSpec.describe AppSettings do + it "handles app settings" do + app_settings = TestAppSettings.new + + expect(app_settings.env).to eq("production") + expect(app_settings.fetch(:foo)).to eq("bar") + expect(app_settings[:secret]).to eq("secret") + end + end +end diff --git a/spec/encrypted_credentials/coder_spec.rb b/spec/encrypted_credentials/coder_spec.rb new file mode 100644 index 0000000..89fa607 --- /dev/null +++ b/spec/encrypted_credentials/coder_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" +require "encrypted_credentials/coder" +require "securerandom" + +module EncryptedCredentials + RSpec.describe Coder do + it "encrypts and decrypts" do + coder = Coder.new(key: SecureRandom.alphanumeric(64)) + content = "bar" + + encrypted_content = coder.encrypt(content) + + expect(coder.decrypt(encrypted_content)).to eq(content) + end + end +end diff --git a/spec/encrypted_credentials/encrypted_environment_variables_spec.rb b/spec/encrypted_credentials/encrypted_environment_variables_spec.rb new file mode 100644 index 0000000..d7f6068 --- /dev/null +++ b/spec/encrypted_credentials/encrypted_environment_variables_spec.rb @@ -0,0 +1,37 @@ +require "spec_helper" +require "encrypted_credentials/encrypted_environment_variables" + +module EncryptedCredentials + RSpec.describe EncryptedEnvironmentVariables do + it "Decrypts environment variables from the AWS parameter store" do + environment = { + "FOOBAR_SSM_PARAMETER_NAME" => "foobar", + "BAZ_SSM_PARAMETER_NAME" => "baz" + } + + ssm_client = Aws::SSM::Client.new( + stub_responses: { + get_parameters: { + parameters: [ + Aws::SSM::Types::Parameter.new( + name: "baz", + value: "baz-secret" + ), + Aws::SSM::Types::Parameter.new( + name: "foobar", + value: "foobar-secret" + ) + ] + } + } + ) + + EncryptedEnvironmentVariables.new(ssm_client:, environment:).decrypt + + expect(environment).to include( + "FOOBAR" => "foobar-secret", + "BAZ" => "baz-secret" + ) + end + end +end diff --git a/spec/encrypted_credentials/encrypted_file_spec.rb b/spec/encrypted_credentials/encrypted_file_spec.rb new file mode 100644 index 0000000..47375b3 --- /dev/null +++ b/spec/encrypted_credentials/encrypted_file_spec.rb @@ -0,0 +1,35 @@ +require "spec_helper" +require "encrypted_credentials/encrypted_file" +require "encrypted_credentials/coder" + +module EncryptedCredentials + RSpec.describe EncryptedFile do + it "reads an encrypted file as yaml" do + encrypted_credentials = file_fixture("credentials.yml.enc") + + encrypted_file = EncryptedFile.new(file: encrypted_credentials, coder: build_coder) + credentials = encrypted_file.credentials + + expect(credentials.fetch("production")).to eq({ "key" => "secret" }) + expect(credentials.fetch("staging")).to eq(credentials.fetch("production")) + end + + it "writes an encrypted file" do + encrypted_credentials = file_fixture("credentials.yml.enc") + + FileUtils.mkdir_p("tmp") + FileUtils.cp(encrypted_credentials, "tmp/credentials.yml.enc") + + encrypted_file = EncryptedFile.new(file: Pathname("tmp/credentials.yml.enc"), coder: build_coder) + + encrypted_file.write({ "foo" => "bar" }.to_yaml) + credentials = encrypted_file.credentials + + expect(credentials).to eq({ "foo" => "bar" }) + end + + def build_coder + Coder.new(key: file_fixture("master.key").read.chomp) + end + end +end diff --git a/spec/encrypted_credentials_spec.rb b/spec/encrypted_credentials_spec.rb new file mode 100644 index 0000000..bb2e705 --- /dev/null +++ b/spec/encrypted_credentials_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe EncryptedCredentials do + it "has a version number" do + expect(EncryptedCredentials::VERSION).not_to be nil + end +end diff --git a/spec/fixtures/files/app_settings.yml b/spec/fixtures/files/app_settings.yml new file mode 100644 index 0000000..fa4e981 --- /dev/null +++ b/spec/fixtures/files/app_settings.yml @@ -0,0 +1,6 @@ +default: &default + foo: bar + +production: &production + <<: *default + secret: "<%= TestAppSettings.new.credentials.fetch('key') %>" diff --git a/spec/fixtures/files/credentials.yml.enc b/spec/fixtures/files/credentials.yml.enc new file mode 100644 index 0000000..9ec6eab --- /dev/null +++ b/spec/fixtures/files/credentials.yml.enc @@ -0,0 +1 @@ +vn/mSbDBFv/VoeZuQCZ3ILvKmbFUz6H4j+IylQuGQfy4CucwGyCp1L0bUwhSKX5tYrr+9GDmSnfOPlFFzXRGm3plyRUjGFxYP6vR/U1F9qk= \ No newline at end of file diff --git a/spec/fixtures/files/master.key b/spec/fixtures/files/master.key new file mode 100644 index 0000000..7070a45 --- /dev/null +++ b/spec/fixtures/files/master.key @@ -0,0 +1 @@ +XsDgaIYB6cqbxnqjgb9y9XlXuer4BEACer3Pf5YDHqoAkkCrlmwrj2VC0gEFDi05 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..740f706 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "encrypted_credentials" + +Dir[File.dirname(__FILE__) + "/support/**/*.rb"].sort.each { |f| require f } + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/spec/support/file_fixture_helpers.rb b/spec/support/file_fixture_helpers.rb new file mode 100644 index 0000000..a2cb35b --- /dev/null +++ b/spec/support/file_fixture_helpers.rb @@ -0,0 +1,11 @@ +require "pathname" + +module FileFixtureHelpers + def file_fixture(filename) + Pathname(File.expand_path("../fixtures/files/#{filename}", __dir__)) + end +end + +RSpec.configure do |config| + config.include(FileFixtureHelpers) +end diff --git a/spec/support/test_app_settings.rb b/spec/support/test_app_settings.rb new file mode 100644 index 0000000..30b0505 --- /dev/null +++ b/spec/support/test_app_settings.rb @@ -0,0 +1,23 @@ +require "encrypted_credentials/app_settings" +require "encrypted_credentials/encrypted_file" +require "encrypted_credentials/coder" + +class TestAppSettings < EncryptedCredentials::AppSettings + def initialize(**options) + super(file: Pathname(file_fixture("app_settings.yml")), encrypted_file:, environment: :production, **options) + end + + private + + def encrypted_file + @encrypted_file ||= EncryptedCredentials::EncryptedFile.new(file: Pathname(file_fixture("credentials.yml.enc")), coder:) + end + + def coder + @coder = EncryptedCredentials::Coder.new(key: File.read(file_fixture("master.key")).chomp) + end + + def file_fixture(filename) + File.expand_path("../fixtures/files/#{filename}", __dir__) + end +end