eyaml 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []