├── .rspec ├── lib ├── ejson_wrapper │ ├── version.rb │ ├── decrypt_private_key_with_kms.rb │ ├── generate.rb │ └── decrypt_ejson_file.rb └── ejson_wrapper.rb ├── Rakefile ├── bin ├── setup └── console ├── .gitignore ├── Gemfile ├── spec ├── spec_helper.rb ├── ejson_wrapper │ ├── decrypt_private_key_with_kms_spec.rb │ └── generate_spec.rb └── ejson_wrapper_spec.rb ├── .github └── workflows │ └── test.yml ├── ejson_wrapper.gemspec ├── exe └── ejson_wrapper └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/ejson_wrapper/version.rb: -------------------------------------------------------------------------------- 1 | module EjsonWrapper 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :development do 8 | gem 'pry' 9 | gem 'rake', '~> 13.0' 10 | gem 'rexml' 11 | gem 'rspec', '~> 3.0' 12 | end 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "ejson_wrapper" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "ejson_wrapper" 3 | require 'pry' 4 | 5 | RSpec.configure do |config| 6 | # Enable flags like --only-failures and --next-failure 7 | config.example_status_persistence_file_path = ".rspec_status" 8 | 9 | # Disable RSpec exposing methods globally on `Module` and `main` 10 | config.disable_monkey_patching! 11 | 12 | config.expect_with :rspec do |c| 13 | c.syntax = :expect 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3"] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: ruby/setup-ruby@v1 13 | with: 14 | ruby-version: ${{ matrix.ruby }} 15 | bundler-cache: true 16 | - run: bundle exec rake 17 | -------------------------------------------------------------------------------- /lib/ejson_wrapper.rb: -------------------------------------------------------------------------------- 1 | require "ejson_wrapper/version" 2 | require "ejson_wrapper/decrypt_private_key_with_kms" 3 | require "ejson_wrapper/decrypt_ejson_file" 4 | require "ejson_wrapper/generate" 5 | 6 | module EJSONWrapper 7 | def self.decrypt(file_path, key_dir: nil, private_key: nil, use_kms: false, region: nil) 8 | if use_kms 9 | private_key = private_key_decrypted(file_path, region: region) 10 | end 11 | DecryptEJSONFile.call(file_path, key_dir: key_dir, private_key: private_key) 12 | end 13 | 14 | def self.generate(**args) 15 | Generate.new.call(**args) 16 | end 17 | 18 | def self.private_key_decrypted(file_path, region: nil) 19 | DecryptPrivateKeyWithKMS.call(file_path, region: region) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/ejson_wrapper/decrypt_private_key_with_kms.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-kms' 2 | require 'base64' 3 | 4 | module EJSONWrapper 5 | PrivateKeyNotFound = Class.new(StandardError) 6 | 7 | class DecryptPrivateKeyWithKMS 8 | def self.call(file_path, **args) 9 | new.call(file_path, **args) 10 | end 11 | 12 | KEY = '_private_key_enc' 13 | 14 | def call(ejson_file_path, region:) 15 | ejson_hash = JSON.parse(File.read(ejson_file_path)) 16 | encrypted_private_key = ejson_hash.fetch(KEY) do 17 | raise PrivateKeyNotFound, "Private key was not found in ejson file under key #{KEY}" 18 | end 19 | decrypt(Base64.decode64(encrypted_private_key), region: region) 20 | end 21 | 22 | private 23 | 24 | def decrypt(ciphertext_blob, region:) 25 | Aws::KMS::Client.new(region: region).decrypt(ciphertext_blob: ciphertext_blob).plaintext 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /ejson_wrapper.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "ejson_wrapper/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ejson_wrapper" 8 | spec.version = EjsonWrapper::VERSION 9 | spec.authors = ["Steve Hodgkiss"] 10 | spec.email = ["steve@hodgkiss.me"] 11 | 12 | spec.summary = %q{Invoke EJSON from Ruby} 13 | spec.description = %q{Invoke EJSON from Ruby} 14 | spec.homepage = "https://github.com/envato/ejson_wrapper" 15 | 16 | if spec.respond_to?(:metadata) 17 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 18 | else 19 | raise "RubyGems 2.0 or newer is required to protect against " \ 20 | "public gem pushes." 21 | end 22 | 23 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 24 | f.match(%r{^(test|spec|features)/}) 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "ejson" 31 | spec.add_dependency "aws-sdk-kms" 32 | spec.add_dependency "base64" 33 | end 34 | -------------------------------------------------------------------------------- /spec/ejson_wrapper/decrypt_private_key_with_kms_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe EJSONWrapper::DecryptPrivateKeyWithKMS do 2 | let(:kms_client) { instance_double(Aws::KMS::Client) } 3 | let(:decrypt_response) { double(plaintext: private_key) } 4 | let(:private_key) { 'private-key' } 5 | let(:ejson_file) { '{ "_private_key_enc": "blah", "_public_key": "pubkey" }' } 6 | let(:region) { 'ap-southeast-2' } 7 | 8 | before do 9 | allow(Aws::KMS::Client).to receive(:new).and_return(kms_client) 10 | allow(kms_client).to receive(:decrypt).and_return(decrypt_response) 11 | allow(File).to receive(:read).with('config/secrets/test.ejson').and_return(ejson_file) 12 | end 13 | 14 | it 'decrypts with KMS' do 15 | described_class.call('config/secrets/test.ejson', region: region) 16 | expect(kms_client).to have_received(:decrypt).with(ciphertext_blob: Base64.decode64('blah')) 17 | end 18 | 19 | it 'returns the plaintext' do 20 | response = described_class.call('config/secrets/test.ejson', region: region) 21 | expect(response).to eq('private-key') 22 | end 23 | 24 | it 'uses the provided region' do 25 | response = described_class.call('config/secrets/test.ejson', region: region) 26 | expect(Aws::KMS::Client).to have_received(:new).with(region: region) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ejson_wrapper/generate.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'aws-sdk-kms' 3 | 4 | module EJSONWrapper 5 | KeygenFailed = Class.new(StandardError) 6 | 7 | class Generate 8 | def call(region:, kms_key_id:, file:) 9 | public_key, private_key = *keygen 10 | encrypted_private_key = encrypt_with_kms_key(region, kms_key_id, private_key) 11 | ejson_file = JSON.pretty_generate( 12 | '_public_key' => public_key, 13 | '_private_key_enc' => encrypted_private_key 14 | ) 15 | File.write(file, ejson_file) 16 | puts "Generated EJSON file #{file}" 17 | end 18 | 19 | private 20 | 21 | def keygen 22 | output = invoke_ejson_keygen 23 | extract_keys(output) 24 | end 25 | 26 | def invoke_ejson_keygen 27 | stdout, status = Open3.capture2e('ejson', 'keygen') 28 | raise KeygenFailed, stdout unless status.success? 29 | stdout 30 | end 31 | 32 | def extract_keys(output) 33 | lines = output.split("\n") 34 | [lines[1], lines[3]] 35 | end 36 | 37 | def encrypt_with_kms_key(region, key_id, plaintext) 38 | client = Aws::KMS::Client.new(region: region) 39 | response = client.encrypt( 40 | key_id: key_id, 41 | plaintext: plaintext 42 | ) 43 | Base64.encode64(response.ciphertext_blob).strip 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/ejson_wrapper/decrypt_ejson_file.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module EJSONWrapper 4 | DecryptionFailed = Class.new(StandardError) 5 | 6 | class DecryptEJSONFile 7 | def self.call(file_path, **args) 8 | new.call(file_path, **args) 9 | end 10 | 11 | def call(file_path, key_dir: nil, private_key: nil) 12 | decrypted_json = invoke_decrypt(file_path, key_dir: key_dir, private_key: private_key) 13 | parse_json(decrypted_json) 14 | end 15 | 16 | private 17 | 18 | def invoke_decrypt(file_path, key_dir:, private_key:) 19 | command = ['ejson', 'decrypt'] 20 | options = {} 21 | if private_key 22 | options[:stdin_data] = private_key 23 | command << '--key-from-stdin' 24 | end 25 | command << file_path.to_s 26 | stdout, status = Open3.capture2(ejson_env(key_dir), *command, options) 27 | raise DecryptionFailed, stdout unless status.success? 28 | stdout 29 | end 30 | 31 | def ejson_env(key_dir) 32 | { 33 | 'EJSON_KEYDIR' => key_dir 34 | }.select { |_, v| !v.nil? } 35 | end 36 | 37 | def parse_json(decrypted_json) 38 | JSON.parse(decrypted_json, symbolize_names: true).tap do |secrets| 39 | secrets.delete(:_public_key) 40 | secrets.delete(:_private_key_enc) 41 | end.freeze 42 | rescue JSON::ParserError 43 | raise DecryptionFailed, "Failed to parse JSON output from EJSON" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/ejson_wrapper/generate_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe EJSONWrapper::Generate do 2 | let(:kms_client) { instance_double(Aws::KMS::Client) } 3 | let(:key_id) { 'key-id' } 4 | let(:file) { 'file.ejson' } 5 | let(:private_key_enc) { 'kms-encrypted-private-key' } 6 | let(:encrypt_response) { double(ciphertext_blob: private_key_enc) } 7 | let(:public_key) { '8ea2fbfb3291284dbcb1d5c7de5ff89dc51ffab896e791ca090493fb784f2a58' } 8 | let(:private_key) { 'e41d030042d4bae253a496129ba0f489b1db824d457b633d987022bc852efc04' } 9 | let(:ejson_keygen) { 10 | <<-EOS 11 | Public Key: 12 | #{public_key} 13 | Private Key: 14 | #{private_key} 15 | EOS 16 | } 17 | 18 | before do 19 | allow(File).to receive(:write) 20 | allow(Aws::KMS::Client).to receive(:new).and_return(kms_client) 21 | allow(kms_client).to receive(:encrypt).and_return(encrypt_response) 22 | allow(Open3).to receive(:capture2e).with('ejson', 'keygen').and_return([ejson_keygen, double(:'success?' => true)]) 23 | described_class.new.call(region: 'ap-southeast-2', kms_key_id: key_id, file: file) 24 | end 25 | 26 | it 'encrypts the private key' do 27 | expect(kms_client).to have_received(:encrypt).with(key_id: key_id, plaintext: private_key) 28 | end 29 | 30 | it 'writes an ejson file' do 31 | ejson = { 32 | '_public_key' => public_key, 33 | '_private_key_enc' => Base64.encode64(private_key_enc).strip 34 | } 35 | expect(File).to have_received(:write).with(file, JSON.pretty_generate(ejson)) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /exe/ejson_wrapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "ejson_wrapper" 5 | 6 | require 'optparse' 7 | 8 | options = { 9 | region: nil, 10 | kms_key_id: nil 11 | } 12 | option_parser = OptionParser.new do |opts| 13 | opts.banner = 'Usage: ejson_wrapper {generate,decrypt,reveal_key} [options]' 14 | 15 | opts.on('--region R', String, 'AWS Region') do |v| 16 | options[:region] = v 17 | end 18 | 19 | opts.on('--kms-key-id K', String, 'KMS Key ID') do |v| 20 | options[:kms_key_id] = v 21 | end 22 | 23 | opts.on('--file F', String, 'EJSON file to read or write') do |v| 24 | options[:file] = v 25 | end 26 | 27 | opts.on('--secret S', String, 'Secret to extract') do |v| 28 | options[:secret] = v 29 | end 30 | end 31 | 32 | command = ARGV[0] 33 | 34 | option_parser.parse! 35 | 36 | if options[:region].nil? 37 | STDERR.puts "Missing --region option" 38 | STDERR.puts option_parser 39 | exit 1 40 | end 41 | 42 | if options[:file].nil? 43 | STDERR.puts "Missing --file option" 44 | STDERR.puts option_parser 45 | exit 1 46 | end 47 | 48 | case command 49 | when 'generate' 50 | if options[:kms_key_id].nil? 51 | STDERR.puts "Missing --kms-key-id option" 52 | STDERR.puts option_parser 53 | exit 1 54 | end 55 | 56 | EJSONWrapper.generate(region: options[:region], 57 | kms_key_id: options[:kms_key_id], 58 | file: options[:file]) 59 | when 'decrypt' 60 | decrypted_secrets = EJSONWrapper.decrypt(options[:file], use_kms: true, region: options[:region]) 61 | if options[:secret] 62 | secret = options[:secret].to_sym 63 | unless decrypted_secrets.key?(secret) 64 | STDERR.puts "Secret not found" 65 | exit 1 66 | end 67 | puts decrypted_secrets.fetch(secret) 68 | else 69 | puts JSON.pretty_generate(decrypted_secrets) 70 | end 71 | 72 | when 'reveal_key' 73 | begin 74 | puts EJSONWrapper.private_key_decrypted(options[:file], region: options[:region]) 75 | rescue Errno::ENOENT 76 | STDERR.puts "Secrets file not found" 77 | exit 1 78 | end 79 | 80 | else 81 | STDERR.puts option_parser.banner 82 | exit 1 83 | end 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EJSON Wrapper 2 | 3 | Wraps the [`ejson`](https://github.com/Shopify/ejson) program to safely execute it and parse the resulting JSON. Additionally it offers a feature to encrypt/decrypt secrets with encrypted private key using AWS KMS. 4 | 5 | ## Prerequisites 6 | 7 | * [`ejson`](https://github.com/Shopify/ejson) application 8 | * Path to `ejson` binary is included in `PATH` environment variable 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'ejson_wrapper' 16 | ``` 17 | 18 | And then execute: 19 | 20 | ``` 21 | $ bundle 22 | ``` 23 | 24 | Or install it yourself as: 25 | 26 | ``` 27 | $ gem install ejson_wrapper 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Decrypting EJSON files 33 | 34 | Ensure your application has [AWS IAM Permission to decrypt with KMS](https://docs.aws.amazon.com/kms/latest/developerguide/iam-policies.html#iam-policy-example-encrypt-decrypt-specific-cmks). 35 | 36 | In Ruby code: 37 | 38 | ``` 39 | # Private key is in /opt/ejson/keys 40 | EJSONWrapper.decrypt('myfile.ejson') 41 | => { :my_api_key => 'secret' } 42 | 43 | # Private key is in /alternate/key/dir 44 | EJSONWrapper.decrypt('myfile.ejson', key_dir: 'alternate/key/dir') 45 | => { :my_api_key => 'secret' } 46 | 47 | # Private key is in memory 48 | EJSONWrapper.decrypt('myfile.ejson', private_key: 'be8597abaa68bbfa23193624b1ed5e2cd6b9a8015e722138b23ecd3c90239b2d') 49 | => { :my_api_key => 'secret' } 50 | 51 | # Private key is stored inside the ejson file itself as _private_key_enc (encrypted with KMS & Base64 encoded) 52 | EJSONWrapper.decrypt('myfile.ejson', use_kms: true, region: 'ap-southeast-2') 53 | => { :my_api_key => 'secret' } 54 | ``` 55 | 56 | Command line: 57 | 58 | ``` 59 | # decrypt all 60 | $ ejson_wrapper decrypt --file file.ejson --region us-east-1 61 | { 62 | "my_api_key": "[secret]" 63 | } 64 | 65 | # decrypt & extract a specific secret 66 | $ ejson_wrapper decrypt --file file.ejson --region us-east-1 --secret my_api_key 67 | [secret] 68 | ``` 69 | 70 | ### Generating EJSON files 71 | 72 | Ensure your application has [AWS IAM Permission to encrypt with KMS](https://docs.aws.amazon.com/kms/latest/developerguide/iam-policies.html#iam-policy-example-encrypt-decrypt-specific-cmks). 73 | 74 | Firstly, the EJSON is generated to have public key and Base64 encoded & encrypted private key in `_public_key` and `_private_key_enc` respectively with: 75 | 76 | Using CLI: 77 | 78 | ``` 79 | $ ejson_wrapper generate --region $AWS_REGION --kms-key-id [key_id] --file myfile.ejson 80 | Generated EJSON file myfile.ejson 81 | ``` 82 | 83 | OR Ruby code: 84 | 85 | ``` 86 | # Generate encrypted EJSON file (overwritting the unencrypted EJSON file) 87 | EJSONWrapper.generate(region: ENV['AWS_REGION'], kms_key_id: 'key_id', file: 'myfile.ejson') 88 | => Generated EJSON file myfile.ejson 89 | ``` 90 | 91 | Verify to ensure the new file contain the two required keys: 92 | 93 | ``` 94 | $ cat myfile.ejson 95 | { 96 | "_public_key": "[public_key]", 97 | "_private_key_enc":"[base64_encoded_encrypted_private_key]", 98 | } 99 | ``` 100 | 101 | You now can add secrets into the EJSON file, in following example `my_api_key` in plaintext entry is added: 102 | 103 | ``` 104 | # myfile.ejson 105 | { 106 | "_public_key": "[public_key]", 107 | "_private_key_enc":"[base64_encoded_encrypted_private_key]", 108 | "my_api_key": "plaintext" 109 | } 110 | ``` 111 | 112 | to encrypt the secrets, run following command: 113 | 114 | ``` 115 | $ ejson encrypt myfile.ejson 116 | ``` 117 | 118 | Verify to ensure the secret is encrypted correctly: 119 | 120 | ``` 121 | $ cat myfile.ejson 122 | { 123 | "_public_key": "[public_key]", 124 | "_private_key_enc":"[base64_encoded_encrypted_private_key]", 125 | "my_api_key": "encrypted_secret" 126 | } 127 | ``` 128 | 129 | ## Development 130 | 131 | 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. 132 | 133 | 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 134 | 135 | ## Contributing 136 | 137 | Bug reports and pull requests are welcome on GitHub at https://github.com/envato/ejson_wrapper. 138 | -------------------------------------------------------------------------------- /spec/ejson_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe EJSONWrapper do 2 | let(:private_key) { 'be8597abaa68bbfa23193624b1ed5e2cd6b9a8015e722138b23ecd3c90239b2d' } 3 | let(:public_key) { '4f541845020de031e09a40910d10e2f731fe8d555808158fb36b343ad3619f72' } 4 | let(:encrypted_secret) { 'EJ[1:eenrVkocrCrgrLobF0K/YSqMt0CAg/bHgGtnX7VWh2M=:OxDHz5edtlVj2C8EHRvVU3O0JSsKvfNB:PbECFxlvBHs943P1U8rkXsiEBCVD]' } 5 | let(:ejson_tempfile) { Tempfile.new('ejson') } 6 | let(:ejson_contents) { %'{"_public_key":"#{public_key}","secret":"#{encrypted_secret}"}' } 7 | 8 | context "decrypt" do 9 | def decrypt(file_path, **args) 10 | EJSONWrapper.decrypt(file_path, **args) 11 | end 12 | 13 | context "when the ejson file doesn't exist" do 14 | it 'raises an error' do 15 | expect { decrypt('/tmp/unkown_blah_123909u2309') }.to raise_error(EJSONWrapper::DecryptionFailed) 16 | end 17 | end 18 | 19 | context 'when the ejson file exists' do 20 | before do 21 | ejson_tempfile.write(ejson_contents) 22 | ejson_tempfile.close 23 | end 24 | 25 | it 'decrypts the file given a keydir with the private key' do 26 | Dir.mktmpdir do |key_dir| 27 | File.write(File.join(key_dir, public_key), private_key) 28 | decrypted_secrets = decrypt(ejson_tempfile.path, key_dir: key_dir) 29 | expect(decrypted_secrets[:secret]).to eq 'sssh!' 30 | end 31 | end 32 | 33 | it 'decrypts given a private key argument' do 34 | decrypted_secrets = decrypt(ejson_tempfile.path, private_key: private_key) 35 | expect(decrypted_secrets[:secret]).to eq 'sssh!' 36 | end 37 | 38 | it "doesn't include _public_key or _private_key_enc in the decrypted secrets" do 39 | decrypted_secrets = decrypt(ejson_tempfile.path, private_key: private_key) 40 | expect(decrypted_secrets.key?(:_public_key)).to eq false 41 | expect(decrypted_secrets.key?(:_private_key_enc)).to eq false 42 | end 43 | 44 | it "doesn't supply a key dir env var when it's not supplied as an argument" do 45 | decrypted_secrets = '{"api_key_1": "my-secret-api-key"}' 46 | allow(Open3).to receive(:capture2).and_return([decrypted_secrets, double(success?: true)]) 47 | decrypt(ejson_tempfile.path) 48 | expect(Open3).to have_received(:capture2).with({}, 'ejson', 'decrypt', ejson_tempfile.path, {}) 49 | end 50 | 51 | context 'when the ejson file has a _private_key_enc key and use_kms: true' do 52 | let(:ejson_contents) { %'{"_public_key":"#{public_key}","secret":"#{encrypted_secret}", "_private_key_enc": "#{private_key_enc}"}' } 53 | let(:private_key_enc) { "priv-key-enc" } 54 | 55 | before do 56 | client = instance_double(Aws::KMS::Client) 57 | allow(Aws::KMS::Client).to receive(:new).and_return(client) 58 | response = double(plaintext: private_key) 59 | allow(client).to receive(:decrypt).with(ciphertext_blob: Base64.decode64(private_key_enc)).and_return(response) 60 | end 61 | 62 | it 'decrypts with KMS' do 63 | decrypted_secrets = decrypt(ejson_tempfile.path, use_kms: true) 64 | expect(decrypted_secrets[:secret]).to eq 'sssh!' 65 | end 66 | end 67 | 68 | context 'when the stdout of ejson contains invalid JSON' do 69 | let(:decrypted_secrets) { '{"api_key_1": "my-secret-api-key", "' } 70 | 71 | before do 72 | allow(Open3).to receive(:capture2).and_return([decrypted_secrets, double(success?: true)]) 73 | end 74 | 75 | it "doesn't throw an error with the contents of the decrypted JSON" do 76 | expect { decrypt(ejson_tempfile.path) }.to raise_error { |error| 77 | expect(error.message).to_not include('my-secret-api-key') 78 | } 79 | end 80 | 81 | it 'throws a decryption failed error' do 82 | expect { 83 | decrypt(ejson_tempfile.path) 84 | }.to raise_error(EJSONWrapper::DecryptionFailed, /Failed to parse/) 85 | end 86 | end 87 | end 88 | end 89 | 90 | context 'reveal_key' do 91 | def reveal_key(file_path, **args) 92 | EJSONWrapper.private_key_decrypted(file_path, region: args[:region]) 93 | end 94 | 95 | context "when the ejson file doesn't exist" do 96 | it 'raises an error' do 97 | expect { reveal_key('/tmp/unkown_blah_123909u2309') }.to raise_error(Errno::ENOENT) 98 | end 99 | end 100 | 101 | context 'when the ejson file exists' do 102 | let(:ejson_contents) { %'{"_public_key":"#{public_key}","secret":"#{encrypted_secret}", "_private_key_enc": "#{private_key_enc}"}' } 103 | let(:private_key_enc) { "priv-key-enc" } 104 | let(:kms_client) { instance_double(Aws::KMS::Client) } 105 | let(:kms_response) { instance_double(Aws::KMS::Types::DecryptResponse, plaintext: private_key) } 106 | 107 | before do 108 | ejson_tempfile.write(ejson_contents) 109 | ejson_tempfile.close 110 | 111 | allow(Aws::KMS::Client).to receive(:new).and_return(kms_client) 112 | allow(kms_client).to receive(:decrypt).with(ciphertext_blob: Base64.decode64(private_key_enc)).and_return(kms_response) 113 | end 114 | 115 | it 'prints out the private key' do 116 | Dir.mktmpdir do |key_dir| 117 | File.write(File.join(key_dir, public_key), private_key) 118 | key = reveal_key(ejson_tempfile.path, region: 'us-east-1') 119 | expect(key).to eq private_key 120 | end 121 | end 122 | end 123 | end 124 | end 125 | --------------------------------------------------------------------------------