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 +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: []
|