eyaml 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e589d4929a268cf0c2d783b52e67da11a5ddea2a471f826f5d440de02b39f56e
4
+ data.tar.gz: 1f94b57be40ec52893b62aea8f16ee0a81a997b69672852c3430553e2aa24f5f
5
+ SHA512:
6
+ metadata.gz: 6a1ba5c9640edbe938325c02290405c102ba10f7a68f01c7701740944a208cf531abdc95b94f3ed730e9db90ebf89db848eedf6ad45eec7ddf6b39c362b4480f
7
+ data.tar.gz: 4e3490d63c5319fa6855a4257fe848663709ecc4e1d54105332f9b2a30352edf0a9221999f58a1a5ba61165721ef0d3627a5c166204a533ac55c6a6c4b6b29c1
@@ -0,0 +1,21 @@
1
+ name: ruby
2
+ on: push
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+
8
+ steps:
9
+ - name: Checkout code
10
+ uses: actions/checkout@v2
11
+
12
+ - name: Run with fresh bundle
13
+ run: rm Gemfile.lock
14
+
15
+ - name: Setup Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ bundler-cache: true
19
+
20
+ - name: Run tests
21
+ run: bundle exec rake spec
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.0
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "ffi", github: "cheddar-me/ffi", branch: "apple-m1", submodules: true
8
+ gem "rbnacl"
9
+
10
+ gem "railties"
11
+
12
+ gem "pry"
13
+ gem "fakefs"
data/Gemfile.lock ADDED
@@ -0,0 +1,107 @@
1
+ GIT
2
+ remote: https://github.com/cheddar-me/ffi.git
3
+ revision: 6d091e74c04c4bae9680c47595d58e879469790c
4
+ branch: apple-m1
5
+ submodules: true
6
+ specs:
7
+ ffi (1.15.0)
8
+
9
+ PATH
10
+ remote: .
11
+ specs:
12
+ eyaml (0.1.0)
13
+ rbnacl (~> 7.1)
14
+ thor (~> 1.1)
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ actionpack (6.1.3.1)
20
+ actionview (= 6.1.3.1)
21
+ activesupport (= 6.1.3.1)
22
+ rack (~> 2.0, >= 2.0.9)
23
+ rack-test (>= 0.6.3)
24
+ rails-dom-testing (~> 2.0)
25
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
26
+ actionview (6.1.3.1)
27
+ activesupport (= 6.1.3.1)
28
+ builder (~> 3.1)
29
+ erubi (~> 1.4)
30
+ rails-dom-testing (~> 2.0)
31
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
32
+ activesupport (6.1.3.1)
33
+ concurrent-ruby (~> 1.0, >= 1.0.2)
34
+ i18n (>= 1.6, < 2)
35
+ minitest (>= 5.1)
36
+ tzinfo (~> 2.0)
37
+ zeitwerk (~> 2.3)
38
+ builder (3.2.4)
39
+ coderay (1.1.3)
40
+ concurrent-ruby (1.1.8)
41
+ crass (1.0.6)
42
+ diff-lcs (1.4.4)
43
+ erubi (1.10.0)
44
+ fakefs (1.3.2)
45
+ i18n (1.8.10)
46
+ concurrent-ruby (~> 1.0)
47
+ loofah (2.9.0)
48
+ crass (~> 1.0.2)
49
+ nokogiri (>= 1.5.9)
50
+ method_source (1.0.0)
51
+ minitest (5.14.4)
52
+ nokogiri (1.11.2-arm64-darwin)
53
+ racc (~> 1.4)
54
+ pry (0.14.0)
55
+ coderay (~> 1.1)
56
+ method_source (~> 1.0)
57
+ racc (1.5.2)
58
+ rack (2.2.3)
59
+ rack-test (1.1.0)
60
+ rack (>= 1.0, < 3)
61
+ rails-dom-testing (2.0.3)
62
+ activesupport (>= 4.2.0)
63
+ nokogiri (>= 1.6)
64
+ rails-html-sanitizer (1.3.0)
65
+ loofah (~> 2.3)
66
+ railties (6.1.3.1)
67
+ actionpack (= 6.1.3.1)
68
+ activesupport (= 6.1.3.1)
69
+ method_source
70
+ rake (>= 0.8.7)
71
+ thor (~> 1.0)
72
+ rake (13.0.3)
73
+ rbnacl (7.1.1)
74
+ ffi
75
+ rspec (3.10.0)
76
+ rspec-core (~> 3.10.0)
77
+ rspec-expectations (~> 3.10.0)
78
+ rspec-mocks (~> 3.10.0)
79
+ rspec-core (3.10.1)
80
+ rspec-support (~> 3.10.0)
81
+ rspec-expectations (3.10.1)
82
+ diff-lcs (>= 1.2.0, < 2.0)
83
+ rspec-support (~> 3.10.0)
84
+ rspec-mocks (3.10.2)
85
+ diff-lcs (>= 1.2.0, < 2.0)
86
+ rspec-support (~> 3.10.0)
87
+ rspec-support (3.10.2)
88
+ thor (1.1.0)
89
+ tzinfo (2.0.4)
90
+ concurrent-ruby (~> 1.0)
91
+ zeitwerk (2.4.2)
92
+
93
+ PLATFORMS
94
+ arm64-darwin-20
95
+
96
+ DEPENDENCIES
97
+ eyaml!
98
+ fakefs
99
+ ffi!
100
+ pry
101
+ railties
102
+ rake (~> 13.0)
103
+ rbnacl
104
+ rspec (~> 3.0)
105
+
106
+ BUNDLED WITH
107
+ 2.2.15
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Emil Stolarsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # eyaml
2
+
3
+ `eyaml` is a tool for asymmetric encryption of YAML and JSON files. It's largely based on [`ejson`](https://github.com/Shopify/ejson) and backwards compatible with any `*.ejson` file.
4
+
5
+ Assymetric encryption is handled by [RubyCrypto/rbnacl](https://github.com/RubyCrypto/rbnacl/wiki) using a [sealed box](https://github.com/RubyCrypto/rbnacl/wiki/Public-Key-Encryption).
6
+
7
+ ## Installation
8
+
9
+ To install `eyaml`, run:
10
+
11
+ ```shell
12
+ gem install eyaml
13
+ ```
14
+
15
+ Or alternatively, you can add it to your Gemfile:
16
+ ```ruby
17
+ gem 'eyaml'
18
+ ```
19
+
20
+ ### Dependencies
21
+
22
+ `eyaml` depends on [libsodium](https://github.com/jedisct1/libsodium). At least `1.0.0` is required.
23
+
24
+ For MacOS users, libsodium is available via homebrew and can be installed with:
25
+ ```shell
26
+ brew install libsodium
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ `eyaml` requires that a file has a `_public_key` attribute that corresponds to the value generated by running `eyaml keygen`. Adding a plaintext value into the file and running `eyaml encrypt secrets.eyaml` (for a file called `secrets.eyaml`) will encrypt the value using the public key in the same file. To decrypt, ensure a private key is accessible and run `eyaml decrypt secrets.eyaml`
32
+
33
+ `eyaml` supports both JSON and YAML with the extensions `eyaml`, `eyml`, and `ejson`. It will using the extension to determine the format of its output.
34
+
35
+ ### CLI
36
+
37
+ `eyaml` is primarily interacted through its CLI.
38
+
39
+ ```
40
+ -> % eyaml help
41
+ Commands:
42
+ eyaml decrypt # Decrypt an EYAML file
43
+ eyaml encrypt # (Re-)encrypt one or more EYAML files
44
+ eyaml help [COMMAND] # Describe available commands or one specific command
45
+ eyaml keygen # Generate a new EYAML keypair
46
+
47
+ Options:
48
+ -k, [--keydir=KEYDIR] # Directory containing EYAML keys
49
+ ```
50
+
51
+ #### `eyaml encrypt`
52
+
53
+ (Re-)encrypt one or more EYAML files. This is used whenever you add a new value to the config file.
54
+
55
+ ```shell
56
+ -> % eyaml encrypt config/secrets.production.eyaml
57
+ Wrote 517 bytes to config/secrets.production.eyaml.
58
+ ```
59
+
60
+
61
+ #### `eyaml decrypt`
62
+
63
+ Decrypts the provided EYAML file.
64
+
65
+ ```shell
66
+ -> % eyaml decrypt config/secrets.production.eyaml
67
+ _public_key: d1c7ba73c520445c5ba14984da8119f2f7b8df7bcdb3f37f5afe9613b118936a
68
+ secret: password
69
+ ```
70
+
71
+ #### `eyaml keygen`
72
+
73
+ Generates the keypair for the encryption flow to work. The public key must be placed into the file at `_public_key` and the private key must be saved in the default key directory (`/opt/ejson/keys`) with the filename being the public key and the contents, the private key, a key directory you'll provide later, or just pass the `--write` flag for `eyaml` to handle it for you.
74
+
75
+ ```shell
76
+ -> % eyaml keygen
77
+ Public Key: a3dbdef9efd1e52a34588de56a6cf9b03bbc2aaf0edda145cfbd9a6370a0a849
78
+ Private Key: b01592942ba10f152bcf7c6b6734f6392554c578ff24cebcc62f9e3da6fcf302
79
+
80
+ # Or by using the --write flag
81
+
82
+ -> % eyaml keygen --write
83
+ Public Key: a3dbdef9efd1e52a34588de56a6cf9b03bbc2aaf0edda145cfbd9a6370a0a849
84
+
85
+ -> % cat /opt/ejson/keys/a3dbdef9efd1e52a34588de56a6cf9b03bbc2aaf0edda145cfbd9a6370a0a849
86
+ b01592942ba10f152bcf7c6b6734f6392554c578ff24cebcc62f9e3da6fcf302
87
+ ```
88
+
89
+ ### Rails
90
+
91
+ `eyaml` comes with baked in Rails support. It will search for a secrets file in `config/`, decrypt, and load the first valid one it finds.
92
+ `secrets.{eyaml|eyml|ejson}` (e.g. `config/secrets.eyaml`) then `secrets.$env.{eyaml|eyml|ejson}` (e.g. `secrets.production.eyml`).
93
+
94
+ Instead of needing a private key locally, you can provide it to EYAML by setting `EJSON_PRIVATE_KEY` and it'll be automatically used for decrypting the secrets file.
95
+
96
+ ### Apple M1 Support
97
+
98
+ If you're using the new Apple M1, you need to ensure that you're using a `ffi` that is working. We've temporarily been including a fork with a fix in any `Gemfile` where we've included `eyaml`:
99
+
100
+ ```ruby
101
+ gem "ffi", github: "cheddar-me/ffi", branch: "apple-m1", submodules: true
102
+ ```
103
+
104
+ ## Development
105
+
106
+ To get started, make sure you have a working version of Ruby locally. Then clone the repo, and run `bin/setup` (this will install `libsodium` if you're on a Mac and setup bundler). Running `bundle exec rake` or `bundle exec rake spec` will run the test suite.
107
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "eyaml"
6
+
7
+ require "pry"
8
+ Pry.start
data/bin/eyaml ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "eyaml"
6
+
7
+ EYAML::CLI.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ if [ -e "/opt/homebrew/bin/brew" ]; then
7
+ /opt/homebrew/bin/brew install libsodium
8
+ fi
9
+
10
+ bundle install
data/eyaml.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/eyaml/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "eyaml"
7
+ spec.version = EYAML::VERSION
8
+ spec.authors = ["Emil Stolarsky"]
9
+ spec.email = ["emil@cheddar.me"]
10
+
11
+ spec.summary = "Asymmetric keywise encryption for YAML"
12
+ spec.description = "Secret management by encrypting values in a YAML file with a public/private keypair"
13
+ spec.homepage = "https://github.com/cheddar-me/eyaml"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.1")
16
+
17
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
19
+ end
20
+ spec.bindir = "bin"
21
+ spec.executables = "eyaml"
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "thor", "~> 1.1"
25
+ spec.add_dependency "rbnacl", "~> 7.1"
26
+
27
+ spec.add_development_dependency("rake", "~> 13.0")
28
+ spec.add_development_dependency("rspec", "~> 3.0")
29
+ end
data/lib/eyaml.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "rbnacl"
5
+ require "base64"
6
+ require "yaml"
7
+ require "json"
8
+ require "pathname"
9
+
10
+ module EYAML
11
+ class MissingPublicKey < StandardError; end
12
+
13
+ DEFAULT_KEYDIR = "/opt/ejson/keys"
14
+ INTERNAL_PUB_KEY = "_public_key"
15
+ SUPPORTED_EXTENSIONS = %w[eyaml eyml ejson]
16
+
17
+ class << self
18
+ def generate_keypair(save: false, keydir: nil)
19
+ public_key, private_key = EncryptionManager.new_keypair
20
+
21
+ if save
22
+ keypair_file_path = File.expand_path(public_key, ensure_keydir(keydir))
23
+ File.write(keypair_file_path, private_key)
24
+ end
25
+
26
+ [public_key, private_key]
27
+ end
28
+
29
+ def encrypt(plaindata, keydir: nil)
30
+ public_key = load_public_key(plaindata)
31
+ private_key = load_private_key_from(public_key: public_key, keydir: keydir)
32
+
33
+ encryption_manager = EncryptionManager.new(plaindata, public_key, private_key)
34
+ encryption_manager.encrypt
35
+ end
36
+
37
+ def encrypt_file_in_place(file_path, keydir: nil)
38
+ plaindata = YAML.load_file(file_path)
39
+ cipherdata = encrypt(plaindata, keydir: keydir)
40
+
41
+ eyaml = format_for_file(cipherdata, file_path)
42
+
43
+ File.write(file_path, eyaml)
44
+ eyaml.bytesize
45
+ end
46
+
47
+ def decrypt(cipherdata, **key_options)
48
+ public_key = load_public_key(cipherdata)
49
+ private_key = load_private_key_from(public_key: public_key, **key_options)
50
+
51
+ encryption_manager = EncryptionManager.new(cipherdata, public_key, private_key)
52
+ encryption_manager.decrypt
53
+ end
54
+
55
+ def decrypt_file(file_path, **key_options)
56
+ cipherdata = YAML.load_file(file_path)
57
+ plaindata = decrypt(cipherdata, **key_options)
58
+ format_for_file(plaindata, file_path)
59
+ end
60
+
61
+ private
62
+
63
+ def load_public_key(data)
64
+ raise EYAML::MissingPublicKey unless data.has_key?(INTERNAL_PUB_KEY)
65
+ data.fetch(INTERNAL_PUB_KEY)
66
+ end
67
+
68
+ def load_private_key_from(public_key:, keydir: nil, private_key: nil)
69
+ return private_key unless private_key.nil?
70
+ File.read(File.expand_path(public_key, ensure_keydir(keydir)))
71
+ end
72
+
73
+ def ensure_keydir(keydir)
74
+ keydir || ENV["EJSON_KEYDIR"] || DEFAULT_KEYDIR
75
+ end
76
+
77
+ def format_for_file(data, file_path)
78
+ case File.extname(file_path)
79
+ when ".eyaml", ".eyml"
80
+ EYAML::Util.pretty_yaml(data)
81
+ when ".ejson"
82
+ JSON.pretty_generate(data)
83
+ else
84
+ raise EYAML::InvalidFormatError, "Unsupported file type"
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ require_relative "eyaml/version"
91
+ require_relative "eyaml/util"
92
+ require_relative "eyaml/cli"
93
+ require_relative "eyaml/encryption_manager"
94
+ require_relative "eyaml/railtie" if defined?(Rails)
data/lib/eyaml/cli.rb ADDED
@@ -0,0 +1,70 @@
1
+ module EYAML
2
+ class InvalidFormatError < StandardError; end
3
+
4
+ class CLI < Thor
5
+ class_option :keydir, aliases: "-k", type: :string, desc: "Directory containing EYAML keys"
6
+
7
+ desc "encrypt", "(Re-)encrypt one or more EYAML files"
8
+ def encrypt(*files)
9
+ files.each do |file|
10
+ file_path = Pathname.new(file)
11
+ next unless file_path.exist?
12
+
13
+ bytes_written = EYAML.encrypt_file_in_place(
14
+ file_path,
15
+ keydir: options.fetch(:keydir, nil)
16
+ )
17
+
18
+ puts "Wrote #{bytes_written} bytes to #{file_path}."
19
+ end
20
+ end
21
+
22
+ method_option :output, type: :string, desc: "print output to the provided file, rather than stdout", aliases: "-o"
23
+ method_option :"key-from-stdin", type: :boolean, desc: "read the private key from STDIN", default: false
24
+ desc "decrypt", "Decrypt an EYAML file"
25
+ def decrypt(file)
26
+ file_path = Pathname.new(file)
27
+ unless file_path.exist?
28
+ puts "#{file} doesn't exist"
29
+ return
30
+ end
31
+
32
+ key_options = if options.fetch(:"key-from-stdin")
33
+ # Read key from STDIN
34
+ {private_key: $stdin.gets}
35
+ else
36
+ {keydir: options.fetch(:keydir, nil)}
37
+ end
38
+
39
+ eyaml = EYAML.decrypt_file(file, **key_options)
40
+
41
+ if options.has_key?("output")
42
+ output_file = Pathname.new(options.fetch(:output))
43
+ File.write(output_file, eyaml)
44
+ return
45
+ end
46
+
47
+ puts eyaml
48
+ end
49
+
50
+ method_option :write, type: :boolean, aliases: "-w", desc: "rather than printing both keys, print the public and write the private into the keydir", default: false
51
+ desc "keygen", "Generate a new EYAML keypair"
52
+ def keygen
53
+ public_key, private_key = EYAML.generate_keypair(
54
+ save: options.fetch(:write),
55
+ keydir: options.fetch(:keydir, nil)
56
+ )
57
+
58
+ puts "Public Key: #{public_key}"
59
+ puts "Private Key: #{private_key}" unless options.fetch(:write)
60
+ end
61
+
62
+ map e: :encrypt
63
+ map d: :decrypt
64
+ map g: :keygen
65
+
66
+ def self.exit_on_failure?
67
+ true
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,99 @@
1
+ module EYAML
2
+ class UnsupportedVersionError < StandardError; end
3
+
4
+ class EncryptionManager
5
+ FORMAT_REGEX = /\AEJ\[(?<version>[^:]+):(?<session_public_key>[^:]+):(?<nonce>[^:]+):(?<text>[^\]]+)\]\z/
6
+ FORMAT_VERSION = "1"
7
+
8
+ class << self
9
+ def new_keypair
10
+ private_key = RbNaCl::PrivateKey.generate
11
+
12
+ [
13
+ RbNaCl::Util.bin2hex(private_key.public_key),
14
+ RbNaCl::Util.bin2hex(private_key)
15
+ ]
16
+ end
17
+ end
18
+
19
+ def initialize(yaml, public_key, private_key)
20
+ @tree = yaml
21
+ @public_key = EYAML::Util.ensure_binary_encoding(public_key)
22
+ @private_key = EYAML::Util.ensure_binary_encoding(private_key)
23
+ end
24
+
25
+ def decrypt
26
+ traverse(@tree) do |text|
27
+ encrypted?(text) ? decrypt_text(text) : text
28
+ end
29
+ end
30
+
31
+ def encrypt
32
+ traverse(@tree) do |text|
33
+ encrypted?(text) ? text : encrypt_text(text)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def encrypt_text(plaintext)
40
+ nonce = RbNaCl::Random.random_bytes(encryption_box.nonce_bytes)
41
+ ciphertext = encryption_box.encrypt(nonce, plaintext)
42
+
43
+ [
44
+ "EJ[#{FORMAT_VERSION}",
45
+ Base64.strict_encode64(session_public_key),
46
+ Base64.strict_encode64(nonce),
47
+ "#{Base64.strict_encode64(ciphertext)}]"
48
+ ].join(":")
49
+ end
50
+
51
+ def decrypt_text(ciphertext)
52
+ captures = ciphertext.match(FORMAT_REGEX).named_captures
53
+ wire_version = captures.fetch("version")
54
+ old_session_public_key = Base64.decode64(captures.fetch("session_public_key"))
55
+ nonce = Base64.decode64(captures.fetch("nonce"))
56
+ text = Base64.decode64(captures.fetch("text"))
57
+
58
+ raise UnsupportedVersionError, "EYAML only supports version 1" unless wire_version == FORMAT_VERSION
59
+
60
+ box = decryption_box(old_session_public_key)
61
+ box.decrypt(nonce, text)
62
+ end
63
+
64
+ def encryption_box
65
+ @encryption_box ||= RbNaCl::Box.new(@public_key, session_private_key)
66
+ end
67
+
68
+ def decryption_box(public_key_encrypted_with)
69
+ @decryption_box ||= {}
70
+ @decryption_box[public_key_encrypted_with] ||= RbNaCl::Box.new(public_key_encrypted_with, @private_key)
71
+ end
72
+
73
+ def session_private_key
74
+ @session_private_key ||= RbNaCl::PrivateKey.generate
75
+ end
76
+
77
+ def session_public_key
78
+ @session_public_key ||= session_private_key.public_key
79
+ end
80
+
81
+ def encrypted?(text)
82
+ FORMAT_REGEX.match?(text)
83
+ end
84
+
85
+ def traverse(tree, &block)
86
+ tree.map do |key, value|
87
+ if value.is_a?(Hash)
88
+ next [key, traverse(value, &block)]
89
+ end
90
+ # TODO(es): Add tests for keys with an underscore prefix not doing a nested skip
91
+ if key.start_with?("_")
92
+ next [key, value]
93
+ end
94
+
95
+ [key, block.call(value)]
96
+ end.to_h
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EYAML
4
+ module Rails
5
+ Rails = ::Rails
6
+ private_constant :Rails
7
+
8
+ class Railtie < Rails::Railtie
9
+ PRIVATE_KEY_ENV_VAR = "EJSON_PRIVATE_KEY"
10
+
11
+ config.before_configuration do
12
+ secrets_files.each do |file|
13
+ next unless valid?(file)
14
+
15
+ # If private_key is nil (i.e. when $EJSON_PRIVATE_KEY is not set), EYAML will search
16
+ # for a public/private key in the key directory (either $EJSON_KEYDIR, if set, or /opt/ejson/keys)
17
+ cipherdata = YAML.load_file(file)
18
+ secrets = EYAML.decrypt(cipherdata, private_key: ENV[PRIVATE_KEY_ENV_VAR])
19
+ .deep_symbolize_keys
20
+ .except(:_public_key)
21
+
22
+ break Rails.application.secrets.deep_merge!(secrets)
23
+ end
24
+ end
25
+
26
+ class << self
27
+ private
28
+
29
+ def valid?(pathname)
30
+ pathname.exist?
31
+ end
32
+
33
+ def secrets_files
34
+ EYAML::SUPPORTED_EXTENSIONS.map do |ext|
35
+ [
36
+ Rails.root.join("config", "secrets.#{ext}"),
37
+ Rails.root.join("config", "secrets.#{Rails.env}.#{ext}")
38
+ ]
39
+ end.flatten
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/eyaml/util.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EYAML
4
+ class Util
5
+ class << self
6
+ def pretty_yaml(some_hash)
7
+ some_hash.to_yaml.delete_prefix("---\n")
8
+ end
9
+
10
+ def ensure_binary_encoding(str)
11
+ if str.encoding == Encoding::BINARY
12
+ return str
13
+ end
14
+
15
+ RbNaCl::Util.hex2bin(str)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EYAML
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eyaml
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Emil Stolarsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rbnacl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Secret management by encrypting values in a YAML file with a public/private
70
+ keypair
71
+ email:
72
+ - emil@cheddar.me
73
+ executables:
74
+ - eyaml
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".github/workflows/test.yml"
79
+ - ".gitignore"
80
+ - ".rspec"
81
+ - ".ruby-version"
82
+ - Gemfile
83
+ - Gemfile.lock
84
+ - LICENSE.txt
85
+ - README.md
86
+ - Rakefile
87
+ - bin/console
88
+ - bin/eyaml
89
+ - bin/setup
90
+ - eyaml.gemspec
91
+ - lib/eyaml.rb
92
+ - lib/eyaml/cli.rb
93
+ - lib/eyaml/encryption_manager.rb
94
+ - lib/eyaml/railtie.rb
95
+ - lib/eyaml/util.rb
96
+ - lib/eyaml/version.rb
97
+ homepage: https://github.com/cheddar-me/eyaml
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.5.1
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.2.3
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: Asymmetric keywise encryption for YAML
120
+ test_files: []