dry-credentials 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +167 -0
- data/lib/dry/credentials/encryptor.rb +129 -0
- data/lib/dry/credentials/errors.rb +25 -0
- data/lib/dry/credentials/extension.rb +58 -0
- data/lib/dry/credentials/helpers.rb +97 -0
- data/lib/dry/credentials/settings.rb +40 -0
- data/lib/dry/credentials/version.rb +7 -0
- data/lib/dry/credentials/yaml.rb +57 -0
- data/lib/dry/credentials.rb +34 -0
- data/lib/dry-credentials.rb +3 -0
- data.tar.gz.sig +3 -0
- metadata +210 -0
- metadata.gz.sig +3 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 24deb6ca2d926a11c530c6e81821af802fe1ac36e4fafb8c2273619afdd4ff8e
|
4
|
+
data.tar.gz: 5be212ecb261ab3134b80f14d93a8ca9b7492be642b7a129b16239b0b727410f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a4e0f03fa13561dbc97b110b2d3f660889f0fe76d98615b981af07bf5b44481669f9b176692f77508d71d3629a8cf05c98262601cef5db4a5ee04b0d04de158a
|
7
|
+
data.tar.gz: a0258c2537012e92dfa82ec88d1881a740d04eb9fae617fbafc79a3dabb9e0ff526e51856218a0316c5d99a1c357edb681d404cd2f831f7f79e183659de2dde3
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
## Main
|
2
|
+
|
3
|
+
Nothing so far
|
4
|
+
|
5
|
+
## 0.1.0
|
6
|
+
|
7
|
+
#### Initial implementation
|
8
|
+
|
9
|
+
* Require Ruby 3.0 or newer
|
10
|
+
* Class mixin featuring the `credentials` macro:
|
11
|
+
* Block to change (default) settings such as the cipher
|
12
|
+
* Bang method to edit or reload credentials
|
13
|
+
* Arbitrary method chain to query credentials
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sven Schwyn
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
[![Version](https://img.shields.io/gem/v/dry-credentials.svg?style=flat)](https://rubygems.org/gems/dry-credentials)
|
2
|
+
[![Tests](https://img.shields.io/github/actions/workflow/status/svoop/dry-credentials/test.yml?style=flat&label=tests)](https://github.com/svoop/dry-credentials/actions?workflow=Test)
|
3
|
+
[![Code Climate](https://img.shields.io/codeclimate/maintainability/svoop/dry-credentials.svg?style=flat)](https://codeclimate.com/github/svoop/dry-credentials/)
|
4
|
+
[![Donorbox](https://img.shields.io/badge/donate-on_donorbox-yellow.svg)](https://donorbox.org/bitcetera)
|
5
|
+
|
6
|
+
# Dry::Credentials
|
7
|
+
|
8
|
+
Manage and deploy secrets (access keys, API tokens etc) in encrypted files which can safely be committed to the code repository. To decrypt and and use them, only one environment variable containing the corresponding key is required.
|
9
|
+
|
10
|
+
While similar in purpose to ActiveSupport::EncryptedConfiguration, this lightweight implementation doesn't introduce any dependencies.
|
11
|
+
|
12
|
+
* [Homepage](https://github.com/svoop/dry-credentials)
|
13
|
+
* [API](https://www.rubydoc.info/gems/dry-credentials)
|
14
|
+
* Author: [Sven Schwyn - Bitcetera](https://bitcetera.com)
|
15
|
+
|
16
|
+
## Install
|
17
|
+
|
18
|
+
### Security
|
19
|
+
|
20
|
+
This gem is [cryptographically signed](https://guides.rubygems.org/security/#using-gems) in order to assure it hasn't been tampered with. Unless already done, please add the author's public key as a trusted certificate now:
|
21
|
+
|
22
|
+
```
|
23
|
+
gem cert --add <(curl -Ls https://raw.github.com/svoop/dry-credentials/main/certs/svoop.pem)
|
24
|
+
```
|
25
|
+
|
26
|
+
### Bundler
|
27
|
+
|
28
|
+
Add the following to the <tt>Gemfile</tt> or <tt>gems.rb</tt> of your [Bundler](https://bundler.io) powered Ruby project:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
gem 'dry-credentials'
|
32
|
+
```
|
33
|
+
|
34
|
+
And then install the bundle:
|
35
|
+
|
36
|
+
```
|
37
|
+
bundle install --trust-policy MediumSecurity
|
38
|
+
```
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
Extend any class with `Dry::Credentials` to use the [default settings](#defaults):
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class App
|
46
|
+
extend Dry::Credentials
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
The `credentials` macro allows you to tweak the settings:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class App
|
54
|
+
extend Dry::Credentials
|
55
|
+
|
56
|
+
credentials do
|
57
|
+
env "sandbox"
|
58
|
+
dir "/path/to/credentials"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
⚠️ The `dir` must exist and have the proper permissions set.
|
64
|
+
|
65
|
+
Now initialize the credentials for this `env`:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
App.credentials.edit!
|
69
|
+
```
|
70
|
+
|
71
|
+
It creates `/path/to/credentials/sandbox.yml.enc` (where the encrypted credentials are stored) and opens this file using your favourite editor as per the `EDITOR` environment variable.
|
72
|
+
|
73
|
+
For the sake of this example, let's assume you paste the following credentials:
|
74
|
+
|
75
|
+
```yml
|
76
|
+
otp:
|
77
|
+
secret_key: ZcikLNiUQoqOo594oH2eqw04HPclhjkpgvpBik/40oU=
|
78
|
+
salt: 583506a49c71724a9f085bf2e70362df9d973f08d6575191cab6a177dfb872c6
|
79
|
+
meta:
|
80
|
+
realm: main
|
81
|
+
```
|
82
|
+
|
83
|
+
When you close the editor, the credentials are encrypted and stored. This first time only, the key to encrypt and decrypt is printed to STDOUT:
|
84
|
+
|
85
|
+
```
|
86
|
+
SANDBOX_CREDENTIALS_KEY=68656973716a4e706e336733377245732b6e77584c6c772b5432446532456f674767664271374a623876383d
|
87
|
+
```
|
88
|
+
|
89
|
+
⚠️ In case you've entered invalid YAML, a warning will be printed and the editor reopens immediately.
|
90
|
+
|
91
|
+
To decrypt the credentials and use them in your app, you have to set just this one environment variable containing the key, in this case:
|
92
|
+
|
93
|
+
```sh
|
94
|
+
export SANDBOX_CREDENTIALS_KEY=68656973716a4e706e336733377245732b6e77584c6c772b5432446532456f674767664271374a623876383d
|
95
|
+
```
|
96
|
+
|
97
|
+
With this in place, you can use the decrypted credentials anywhere in your app:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
App.credentials.otp.secret_key
|
101
|
+
# => "ZcikLNiUQoqOo594oH2eqw04HPclhjkpgvpBik/40oU="
|
102
|
+
|
103
|
+
App.credentials.otp.meta.realm
|
104
|
+
# => "main"
|
105
|
+
```
|
106
|
+
|
107
|
+
## Environments
|
108
|
+
|
109
|
+
Credentials are isolated into environments which most likely will, but don't necessarily have to align with the environments of the app framework you're using.
|
110
|
+
|
111
|
+
By default, the current environment is read from `RACK_ENV`.
|
112
|
+
|
113
|
+
⚠️ For safety reasons, don't share the same key across multiple environments!
|
114
|
+
|
115
|
+
## Reload Credentials
|
116
|
+
|
117
|
+
The credentials are lazy loaded when queried for the first time. After that, changes in the encrypted credentials files are not taken into account at runtime for efficiency reasons.
|
118
|
+
|
119
|
+
However, you can schedule a reload:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
App.credentials.reload!
|
123
|
+
```
|
124
|
+
|
125
|
+
The reload is not done immediately but the next time credentials are queried.
|
126
|
+
|
127
|
+
## Edit Credentials
|
128
|
+
|
129
|
+
This gem does not provide any CLI tools to edit the credentials. You should integrate it into your app instead e.g. with a Rake task or an extension to the CLI tool of the app framework you're using.
|
130
|
+
|
131
|
+
You can explicitly pass the environment to edit:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
App.credentials.edit! "production"
|
135
|
+
```
|
136
|
+
|
137
|
+
Editing credentials implicitly schedules a `reload!`.
|
138
|
+
|
139
|
+
## Settings
|
140
|
+
|
141
|
+
If you have to, you can access the settings programmatically:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
App.credentials[:env] # => "production"
|
145
|
+
```
|
146
|
+
|
147
|
+
### Defaults
|
148
|
+
|
149
|
+
Setting | Default | Description
|
150
|
+
--------|---------|------------
|
151
|
+
`env` | `-> { ENV["RACK_ENV"] }` | environment such as `development`
|
152
|
+
`dir` | `"config/credentials"` | directory where encrypted credentials are stored
|
153
|
+
`cipher` | `"aes-256-gcm"` | any of `OpenSSL::Cipher.ciphers`
|
154
|
+
`digest` | `"sha256"` | sign digest used if the cipher doesn't support AEAD
|
155
|
+
`serializer` | `Marshal` | serializer responding to `dump` and `load`
|
156
|
+
|
157
|
+
## Development
|
158
|
+
|
159
|
+
To install the development dependencies and then run the test suite:
|
160
|
+
|
161
|
+
```
|
162
|
+
bundle install
|
163
|
+
bundle exec rake # run tests once
|
164
|
+
bundle exec guard # run tests whenever files are modified
|
165
|
+
```
|
166
|
+
|
167
|
+
You're welcome to [submit issues](https://github.com/svoop/dry-credentials/issues) and contribute code by [forking the project and submitting pull requests](https://docs.github.com/en/get-started/quickstart/fork-a-repo).
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Inspired by +ActiveSupport::EncryptedConfiguration+, the most recent compare
|
4
|
+
# to pinpoint newly discovered safety issues was done against version 7.0.4.2.
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Credentials
|
8
|
+
class Encryptor
|
9
|
+
|
10
|
+
DEFAULT_CIPHER = 'aes-256-gcm'
|
11
|
+
DEFAULT_DIGEST = 'sha256'
|
12
|
+
DEFAULT_SERIALIZER = Marshal
|
13
|
+
SEPARATOR = '--'
|
14
|
+
|
15
|
+
attr_reader :cipher
|
16
|
+
|
17
|
+
# @param cipher [String] any of +OpenSSL::Cipher.ciphers+
|
18
|
+
# @param digest [String] any of +openssl list+
|
19
|
+
# @param serializer [Class] must respond to +dump+ and +load+
|
20
|
+
def initialize(cipher: DEFAULT_CIPHER, digest: DEFAULT_DIGEST, serializer: DEFAULT_SERIALIZER)
|
21
|
+
@cipher = OpenSSL::Cipher.new(cipher)
|
22
|
+
@digest, @serializer = digest, serializer
|
23
|
+
end
|
24
|
+
|
25
|
+
# Generate a random key with the length requird by the current cipher,
|
26
|
+
# then Base64 encodes and unpacks all bytes to hex.
|
27
|
+
#
|
28
|
+
# @return [String] key
|
29
|
+
def generate_key
|
30
|
+
unpack(encode(SecureRandom.bytes(cipher.key_len)))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Encrypts the object
|
34
|
+
#
|
35
|
+
# Relies on encrypted authenticated encryption mode if available for the
|
36
|
+
# selected cipher. Otherwise, the encrypted string is HMAC signed.
|
37
|
+
#
|
38
|
+
# @param object [Object] object to be encrypted
|
39
|
+
# @param key [String] key (Base64 encoded and unpacked to hex)
|
40
|
+
# @return [String] encrypted and authenticated/signed string
|
41
|
+
def encrypt(object, key:)
|
42
|
+
cipher.encrypt
|
43
|
+
cipher.key = decoded_key = decode(pack(key.strip))
|
44
|
+
iv = cipher.random_iv
|
45
|
+
cipher.auth_data = '' if aead?
|
46
|
+
cipher.update(@serializer.dump(object)).then do |data|
|
47
|
+
data << cipher.final
|
48
|
+
data = encode(data) + SEPARATOR + encode(iv)
|
49
|
+
data << SEPARATOR + if aead?
|
50
|
+
encode(cipher.auth_tag)
|
51
|
+
else
|
52
|
+
hmac(decoded_key, data)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Decrypts the encrypted object
|
58
|
+
#
|
59
|
+
# @param encrypted_object [String] encrypted object to be decrypted
|
60
|
+
# @param key [String] key (Base64 encoded and unpacked to hex)
|
61
|
+
# @return [Object] verified and decrypted object
|
62
|
+
def decrypt(encrypted_object, key:)
|
63
|
+
cipher.decrypt
|
64
|
+
cipher.key = decoded_key = decode(pack(key.strip))
|
65
|
+
payload, iv, auth_tag = encrypted_object.strip.split(SEPARATOR)
|
66
|
+
if auth_tag.nil? ||
|
67
|
+
(aead? && decode(auth_tag).bytes.length != auth_tag_length) ||
|
68
|
+
(!aead? && hmac(decoded_key, payload + SEPARATOR + iv) != auth_tag)
|
69
|
+
then
|
70
|
+
fail Dry::Credentials::InvalidEncryptedObjectError
|
71
|
+
end
|
72
|
+
cipher.iv = decode(iv)
|
73
|
+
if aead?
|
74
|
+
cipher.auth_tag = decode(auth_tag)
|
75
|
+
cipher.auth_data = ''
|
76
|
+
end
|
77
|
+
cipher.update(decode(payload)).then do |data|
|
78
|
+
data << cipher.final
|
79
|
+
@serializer.load(data)
|
80
|
+
end
|
81
|
+
rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
|
82
|
+
raise Dry::Credentials::InvalidEncryptedObjectError
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# @example
|
88
|
+
# encode("abc") # => "YWJj"
|
89
|
+
def encode(string)
|
90
|
+
Base64.strict_encode64(string)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @example
|
94
|
+
# decode("YWJj") # => "abc"
|
95
|
+
def decode(string)
|
96
|
+
Base64.strict_decode64(string)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @example
|
100
|
+
# unpack("YWJj") # => "59574a6a"
|
101
|
+
def unpack(string)
|
102
|
+
string.unpack1('H*')
|
103
|
+
end
|
104
|
+
|
105
|
+
# @example
|
106
|
+
# pack("59574a6a") # => "YWJj"
|
107
|
+
def pack(string)
|
108
|
+
[string].pack('H*')
|
109
|
+
end
|
110
|
+
|
111
|
+
# Whether the cipher supports AEAD (Authenticated Encryption with
|
112
|
+
# Associated Data) or not - in which case a HMAC signature using the
|
113
|
+
# +digest+ is used instead
|
114
|
+
def aead?
|
115
|
+
@auth ||= cipher.authenticated?
|
116
|
+
end
|
117
|
+
|
118
|
+
def hmac(key, string)
|
119
|
+
OpenSSL::HMAC.hexdigest(@digest, key, string)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @see https://github.com/ruby/openssl/issues/63
|
123
|
+
def auth_tag_length
|
124
|
+
16
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Credentials
|
5
|
+
class UnrecognizedSettingError < StandardError
|
6
|
+
def initialize(msg='setting not recognized') = super
|
7
|
+
end
|
8
|
+
|
9
|
+
class EnvNotSetError < StandardError
|
10
|
+
def initialize(msg='env must be set') = super
|
11
|
+
end
|
12
|
+
|
13
|
+
class KeyNotSetError < StandardError
|
14
|
+
def initialize(msg='key must be set') = super
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidEncryptedObjectError < StandardError
|
18
|
+
def initialize(msg='corrupt encrypted object or wrong key') = super
|
19
|
+
end
|
20
|
+
|
21
|
+
class YAMLFormatError < StandardError
|
22
|
+
def initialize(msg='top level must be a dictionary') = super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Credentials
|
5
|
+
class Extension
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@settings = Dry::Credentials::Settings.new
|
9
|
+
@injected = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Load the credentials once
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
# @return [self]
|
16
|
+
def load!
|
17
|
+
helpers = Dry::Credentials::Helpers.new(self)
|
18
|
+
if @injected.none? && !helpers.create?
|
19
|
+
@injected = Dry::Credentials::YAML.new(helpers.read_yaml).inject_into(self)
|
20
|
+
end
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reload the credentials
|
25
|
+
#
|
26
|
+
# @return [self]
|
27
|
+
def reload!
|
28
|
+
singleton_class.undef_method(@injected.pop) until @injected.empty?
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Edit credentials file
|
33
|
+
#
|
34
|
+
# @param env [String] name of the env to edit the credentials for
|
35
|
+
# @return [self]
|
36
|
+
def edit!(env=nil)
|
37
|
+
helpers = Dry::Credentials::Helpers.new(self, env)
|
38
|
+
create = helpers.create?
|
39
|
+
yaml = helpers.read_yaml
|
40
|
+
begin
|
41
|
+
yaml = helpers.edit_yaml yaml
|
42
|
+
end until helpers.yaml_valid? yaml
|
43
|
+
helpers.write_yaml yaml
|
44
|
+
puts [helpers.key_ev, ENV[helpers.key_ev]].join('=') if create
|
45
|
+
reload!
|
46
|
+
end
|
47
|
+
|
48
|
+
# Query settings
|
49
|
+
#
|
50
|
+
# @param setting [String] name of the setting
|
51
|
+
# @return [String] setting value
|
52
|
+
def [](setting)
|
53
|
+
@settings.send(setting)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Credentials
|
5
|
+
class Helpers
|
6
|
+
|
7
|
+
# Helpers wrapped in a separate class in order not to pollute the
|
8
|
+
# extension with methods that could collide with credentials
|
9
|
+
#
|
10
|
+
# @param extension [Dry::Credentials::Extension] extension using the helpers
|
11
|
+
# @param env [String, nil] overrides env setting
|
12
|
+
def initialize(extension, env=nil)
|
13
|
+
@extension = extension
|
14
|
+
@env = env || extension[:env]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Read the encrypted file and return the decrypted YAML content
|
18
|
+
#
|
19
|
+
# @return [String] YAML content
|
20
|
+
def read_yaml
|
21
|
+
return '' if create?
|
22
|
+
encryptor.decrypt(encrypted_file.read, key: key)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Write the decrypted YAML content to the encrypted file
|
26
|
+
#
|
27
|
+
# @param yaml [String] YAML content
|
28
|
+
def write_yaml(yaml)
|
29
|
+
encrypted_file.write(encryptor.encrypt(yaml, key: key))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Open the given YAML in the preferred editor
|
33
|
+
#
|
34
|
+
# @param yaml [String] YAML content to edit
|
35
|
+
# @return [String] edited YAML content
|
36
|
+
def edit_yaml(yaml)
|
37
|
+
Tempfile.create('dryc') do |tempfile|
|
38
|
+
tempfile.write yaml
|
39
|
+
tempfile.close
|
40
|
+
system %Q(#{ENV['EDITOR']} "#{tempfile.path}")
|
41
|
+
File.read(tempfile.path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Whether the YAML content can be read
|
46
|
+
#
|
47
|
+
# @param yaml [String] YAML content
|
48
|
+
# @return [Boolean]
|
49
|
+
def yaml_valid?(yaml)
|
50
|
+
Dry::Credentials::YAML.new(yaml)
|
51
|
+
true
|
52
|
+
rescue Dry::Credentials::YAMLFormatError, Psych::SyntaxError => error
|
53
|
+
warn "WARNING: #{error.message}"
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Whether a new encrypted file will be created
|
58
|
+
#
|
59
|
+
# @return [Boolean]
|
60
|
+
def create?
|
61
|
+
!encrypted_file.exist?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Name of the environment variable holding the key
|
65
|
+
#
|
66
|
+
# @return [String]
|
67
|
+
def key_ev
|
68
|
+
fail Dry::Credentials::EnvNotSetError unless @env
|
69
|
+
"#{@env.upcase}_CREDENTIALS_KEY"
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def key
|
75
|
+
if create?
|
76
|
+
ENV[key_ev] = encryptor.generate_key
|
77
|
+
else
|
78
|
+
ENV[key_ev] or fail Dry::Credentials::KeyNotSetError
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def encrypted_file
|
83
|
+
fail Dry::Credentials::EnvNotSetError unless @env
|
84
|
+
Pathname(@extension[:dir]).realpath.join("#{@env}.yml.enc")
|
85
|
+
end
|
86
|
+
|
87
|
+
def encryptor
|
88
|
+
Dry::Credentials::Encryptor.new(
|
89
|
+
cipher: @extension[:cipher],
|
90
|
+
digest: @extension[:digest],
|
91
|
+
serializer: @extension[:serializer]
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Credentials
|
5
|
+
class Settings
|
6
|
+
|
7
|
+
DEFAULT_SETTINGS = {
|
8
|
+
env: -> { ENV['RACK_ENV'] },
|
9
|
+
dir: 'config/credentials',
|
10
|
+
cipher: 'aes-256-gcm',
|
11
|
+
digest: 'sha256',
|
12
|
+
serializer: Marshal
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@settings = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(key, value=nil)
|
20
|
+
fail Dry::Credentials::UnrecognizedSettingError, key unless DEFAULT_SETTINGS.has_key? key
|
21
|
+
if value
|
22
|
+
@settings[key] = value
|
23
|
+
else
|
24
|
+
resolve(@settings[key] || DEFAULT_SETTINGS[key])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def resolve(value)
|
31
|
+
if value.respond_to? :call
|
32
|
+
value.call
|
33
|
+
else
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Credentials
|
5
|
+
class YAML
|
6
|
+
|
7
|
+
# @param string [String] YAML document content
|
8
|
+
def initialize(yaml)
|
9
|
+
@yaml = yaml
|
10
|
+
@hash = ::YAML.safe_load yaml
|
11
|
+
fail Dry::Credentials::YAMLFormatError unless @hash.instance_of? Hash
|
12
|
+
rescue Psych::DisallowedClass
|
13
|
+
raise Dry::Credentials::YAMLFormatError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Define readers for the first level of the credentials on the
|
17
|
+
# given +object+.
|
18
|
+
#
|
19
|
+
# @param [Object] object to inject the methods into
|
20
|
+
# @return [Array] injected methods
|
21
|
+
def inject_into(object)
|
22
|
+
Query.new(@hash).send(:inject_into, object)
|
23
|
+
end
|
24
|
+
|
25
|
+
class Query
|
26
|
+
# @param hash [Hash] hash of hashes containing the credentials
|
27
|
+
def initialize(hash)
|
28
|
+
@hash = hash
|
29
|
+
inject_into self
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get all credentials below the current node as a hash.
|
33
|
+
#
|
34
|
+
# @return [Hash] credentials
|
35
|
+
def to_h
|
36
|
+
@hash
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def inject_into(object)
|
42
|
+
@hash.each do |key, value|
|
43
|
+
object.define_singleton_method key do
|
44
|
+
if value.instance_of? Hash
|
45
|
+
Query.new value
|
46
|
+
else
|
47
|
+
value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@hash.keys
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
require "yaml"
|
5
|
+
require "securerandom"
|
6
|
+
require "openssl"
|
7
|
+
require "base64"
|
8
|
+
|
9
|
+
require_relative "credentials/version"
|
10
|
+
require_relative "credentials/errors"
|
11
|
+
require_relative "credentials/encryptor"
|
12
|
+
require_relative "credentials/yaml"
|
13
|
+
require_relative "credentials/settings"
|
14
|
+
require_relative "credentials/helpers"
|
15
|
+
require_relative "credentials/extension"
|
16
|
+
|
17
|
+
module Dry
|
18
|
+
module Credentials
|
19
|
+
def credentials(&block)
|
20
|
+
if block
|
21
|
+
__credentials_extension__
|
22
|
+
.instance_variable_get('@settings')
|
23
|
+
.instance_eval(&block)
|
24
|
+
self
|
25
|
+
else
|
26
|
+
__credentials_extension__.load!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def __credentials_extension__
|
31
|
+
@__credentials_extension__ ||= Extension.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data.tar.gz.sig
ADDED
metadata
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dry-credentials
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sven Schwyn
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDODCCAiCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhydWJ5
|
14
|
+
L0RDPWJpdGNldGVyYS9EQz1jb20wHhcNMjIxMTA2MTIzNjUwWhcNMjMxMTA2MTIz
|
15
|
+
NjUwWjAjMSEwHwYDVQQDDBhydWJ5L0RDPWJpdGNldGVyYS9EQz1jb20wggEiMA0G
|
16
|
+
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcLg+IHjXYaUlTSU7R235lQKD8ZhEe
|
17
|
+
KMhoGlSUonZ/zo1OT3KXcqTCP1iMX743xYs6upEGALCWWwq+nxvlDdnWRjF3AAv7
|
18
|
+
ikC+Z2BEowjyeCCT/0gvn4ohKcR0JOzzRaIlFUVInlGSAHx2QHZ2N8ntf54lu7nd
|
19
|
+
L8CiDK8rClsY4JBNGOgH9UC81f+m61UUQuTLxyM2CXfAYkj/sGNTvFRJcNX+nfdC
|
20
|
+
hM9r2kH1+7wsa8yG7wJ2IkrzNACD8v84oE6qVusN8OLEMUI/NaEPVPbw2LUM149H
|
21
|
+
PVa0i729A4IhroNnFNmw4wOC93ARNbM1+LW36PLMmKjKudf5Exg8VmDVAgMBAAGj
|
22
|
+
dzB1MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSfK8MtR62mQ6oN
|
23
|
+
yoX/VKJzFjLSVDAdBgNVHREEFjAUgRJydWJ5QGJpdGNldGVyYS5jb20wHQYDVR0S
|
24
|
+
BBYwFIEScnVieUBiaXRjZXRlcmEuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAYG2na
|
25
|
+
ye8OE2DANQIFM/xDos/E4DaPWCJjX5xvFKNKHMCeQYPeZvLICCwyw2paE7Otwk6p
|
26
|
+
uvbg2Ks5ykXsbk5i6vxDoeeOLvmxCqI6m+tHb8v7VZtmwRJm8so0eSX0WvTaKnIf
|
27
|
+
CAn1bVUggczVdNoBXw9WAILKyw9bvh3Ft740XZrR74sd+m2pGwjCaM8hzLvrVbGP
|
28
|
+
DyYhlBeRWyQKQ0WDIsiTSRhzK8HwSTUWjvPwx7SEdIU/HZgyrk0ETObKPakVu6bH
|
29
|
+
kAyiRqgxF4dJviwtqI7mZIomWL63+kXLgjOjMe1SHxfIPo/0ji6+r1p4KYa7o41v
|
30
|
+
fwIwU1MKlFBdsjkd
|
31
|
+
-----END CERTIFICATE-----
|
32
|
+
date: 2023-02-22 00:00:00.000000000 Z
|
33
|
+
dependencies:
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: debug
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
type: :development
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rake
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
type: :development
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: minitest-sound
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: minitest-focus
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: guard
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
name: guard-minitest
|
120
|
+
requirement: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
type: :development
|
126
|
+
prerelease: false
|
127
|
+
version_requirements: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
- !ruby/object:Gem::Dependency
|
133
|
+
name: yard
|
134
|
+
requirement: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
type: :development
|
140
|
+
prerelease: false
|
141
|
+
version_requirements: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
description: |
|
147
|
+
Manage and deploy secrets (access keys, API tokens etc) in encrypted
|
148
|
+
files which can safely be committed to the code repository. To decrypt and
|
149
|
+
and use them, only one environment variable containing the corresponding key
|
150
|
+
is required.
|
151
|
+
|
152
|
+
While similar to ActiveSupport::EncryptedConfiguration, this lightweight
|
153
|
+
implementation introduces as few dependencies as necessary.
|
154
|
+
email:
|
155
|
+
- ruby@bitcetera.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files:
|
159
|
+
- README.md
|
160
|
+
- CHANGELOG.md
|
161
|
+
- LICENSE.txt
|
162
|
+
files:
|
163
|
+
- CHANGELOG.md
|
164
|
+
- LICENSE.txt
|
165
|
+
- README.md
|
166
|
+
- lib/dry-credentials.rb
|
167
|
+
- lib/dry/credentials.rb
|
168
|
+
- lib/dry/credentials/encryptor.rb
|
169
|
+
- lib/dry/credentials/errors.rb
|
170
|
+
- lib/dry/credentials/extension.rb
|
171
|
+
- lib/dry/credentials/helpers.rb
|
172
|
+
- lib/dry/credentials/settings.rb
|
173
|
+
- lib/dry/credentials/version.rb
|
174
|
+
- lib/dry/credentials/yaml.rb
|
175
|
+
homepage: https://github.com/svoop/dry-credentials
|
176
|
+
licenses:
|
177
|
+
- MIT
|
178
|
+
metadata:
|
179
|
+
homepage_uri: https://github.com/svoop/dry-credentials
|
180
|
+
changelog_uri: https://github.com/svoop/dry-credentials/blob/main/CHANGELOG.md
|
181
|
+
source_code_uri: https://github.com/svoop/dry-credentials
|
182
|
+
documentation_uri: https://www.rubydoc.info/gems/dry-credentials
|
183
|
+
bug_tracker_uri: https://github.com/svoop/dry-credentials/issues
|
184
|
+
post_install_message:
|
185
|
+
rdoc_options:
|
186
|
+
- "--title"
|
187
|
+
- Dry::Credentials
|
188
|
+
- "--main"
|
189
|
+
- README.md
|
190
|
+
- "--line-numbers"
|
191
|
+
- "--inline-source"
|
192
|
+
- "--quiet"
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 3.0.0
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubygems_version: 3.4.7
|
207
|
+
signing_key:
|
208
|
+
specification_version: 4
|
209
|
+
summary: A mixin to use encrypted credentials in your classes
|
210
|
+
test_files: []
|
metadata.gz.sig
ADDED