eyaml 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +21 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +107 -0
- data/LICENSE.txt +21 -0
- data/README.md +107 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/eyaml +7 -0
- data/bin/setup +10 -0
- data/eyaml.gemspec +29 -0
- data/lib/eyaml.rb +94 -0
- data/lib/eyaml/cli.rb +70 -0
- data/lib/eyaml/encryption_manager.rb +99 -0
- data/lib/eyaml/railtie.rb +44 -0
- data/lib/eyaml/util.rb +19 -0
- data/lib/eyaml/version.rb +5 -0
- metadata +120 -0
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
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.0
|
data/Gemfile
ADDED
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
data/bin/console
ADDED
data/bin/eyaml
ADDED
data/bin/setup
ADDED
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
|
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: []
|