secret_keys 1.0.1 → 1.0.3
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 +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +5 -3
- data/VERSION +1 -1
- data/lib/secret_keys/cli.rb +16 -11
- data/lib/secret_keys/encryptor.rb +9 -10
- data/lib/secret_keys.rb +91 -47
- data/secret_keys.gemspec +9 -5
- metadata +11 -15
- data/CHANGE_LOG.md +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd54d07d04fbd4695a6896fec897ee82171f27e10778aa59247dc2c216511149
|
|
4
|
+
data.tar.gz: 6faeb6462cce486f5f4e27619fe3e88e7f1587a1a51949821166ee3359419a0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e509e3f5b8fb49e5c7859f18cd73a620ca89d95c0e82631cdb40bf77366d5dba131107d09d2057236d8e44982a6a232497c799e5723727968b37f6c88f253d13
|
|
7
|
+
data.tar.gz: 772f82950d3efe42d597bc6918f10f8ed2664d6b5738a9af0bb8767fb59b8212a0420a88a5169ad35972600936aace607323921b9bdf80dedf0b66669a7c6e22
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 1.0.3
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Files are now written atomically (write to a temporary file and rename) so a crash or concurrent reader can never see a partially written secrets file. The mode of an existing file is preserved.
|
|
12
|
+
- New secrets files are now created readable and writable only by the owner (mode 0600) instead of relying on the process umask. The mode of an existing file is still preserved.
|
|
13
|
+
- `secret_keys init` now uses an exclusive file create so it can no longer overwrite a file created between the existence check and the write.
|
|
14
|
+
- `SecretKeys#save` no longer raises `ArgumentError` when passed a `Pathname`.
|
|
15
|
+
- An unsupported `.version` value that is a string now raises `SecretKeys::VersionError` instead of a comparison `ArgumentError`; numeric strings like `"1"` are accepted.
|
|
16
|
+
- Input that does not parse to a Hash now raises a clear `ArgumentError` instead of failing with confusing errors; input that parses to nothing (e.g. only comments) loads as an empty document.
|
|
17
|
+
- A non-Hash value in the `.encrypted` key now raises a clear `ArgumentError`.
|
|
18
|
+
- `SecretKeys#encryption_key=` now raises `SecretKeys::EncryptionKeyError` when given a nil or empty key instead of silently keeping the old key.
|
|
19
|
+
- The `--secret-key-file` CLI option now reports a missing file as a clean error instead of an unhandled `Errno::ENOENT`.
|
|
20
|
+
|
|
21
|
+
## 1.0.2
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Follow [RFC 4648](https://www.ietf.org/rfc/rfc4648.txt) base 64 encoding, removing line-feeds from the encoded data.
|
|
26
|
+
|
|
27
|
+
## 1.0.1 (June 01, 2020)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Fix missing documentation links
|
|
32
|
+
|
|
33
|
+
## v1.0.0 (May 31, 2020)
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Initial release
|
data/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Secret Keys
|
|
2
2
|
|
|
3
|
-
[](https://github.com/bdurand/secret_keys/actions/workflows/continuous_integration.yml)
|
|
4
|
+
[](https://github.com/bdurand/secret_keys/actions/workflows/regression_test.yml)
|
|
5
|
+
[](https://github.com/testdouble/standard)
|
|
5
6
|
[](https://badge.fury.io/rb/secret_keys)
|
|
7
|
+
[](https://badge.fury.io/rb/secret_keys)
|
|
6
8
|
|
|
7
9
|
This ruby gem handles encrypting values in a JSON or YAML file. It is yet another solution for storing secrets in a ruby project.
|
|
8
10
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.3
|
data/lib/secret_keys/cli.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require "io/console"
|
|
5
5
|
|
|
6
|
-
require_relative "../secret_keys
|
|
6
|
+
require_relative "../secret_keys"
|
|
7
7
|
|
|
8
8
|
module SecretKeys::CLI
|
|
9
9
|
class Base
|
|
@@ -47,7 +47,7 @@ module SecretKeys::CLI
|
|
|
47
47
|
|
|
48
48
|
def encrypted_file_contents
|
|
49
49
|
encrypted = secrets.encrypted_hash
|
|
50
|
-
string = (format == :yaml ? YAML.dump(encrypted) : JSON.pretty_generate(encrypted))
|
|
50
|
+
string = ((format == :yaml) ? YAML.dump(encrypted) : JSON.pretty_generate(encrypted))
|
|
51
51
|
string << $/ unless string.end_with?($/) # ensure file ends with system dependent new line
|
|
52
52
|
string
|
|
53
53
|
end
|
|
@@ -78,6 +78,7 @@ module SecretKeys::CLI
|
|
|
78
78
|
HELP
|
|
79
79
|
opts.on("--secret-key-file=PATH", String, *secret_file_docs) do |value|
|
|
80
80
|
raise ArgumentError, "You have already passed in the secret key" unless @secret_key.nil?
|
|
81
|
+
raise ArgumentError, "Secret key file '#{value}' does not exist" unless File.exist?(value)
|
|
81
82
|
@secret_key = File.read(value).chomp
|
|
82
83
|
end
|
|
83
84
|
|
|
@@ -165,13 +166,19 @@ module SecretKeys::CLI
|
|
|
165
166
|
def run!
|
|
166
167
|
@secrets = SecretKeys.new({}, secret_key)
|
|
167
168
|
if input.is_a?(String)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
contents = encrypted_file_contents
|
|
170
|
+
begin
|
|
171
|
+
# Open with exclusive create so a file created by another process
|
|
172
|
+
# between now and the write can never be clobbered. The file is
|
|
173
|
+
# created readable and writable only by the owner.
|
|
174
|
+
File.open(input, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
175
|
+
file.write(contents)
|
|
176
|
+
end
|
|
177
|
+
rescue Errno::EEXIST
|
|
178
|
+
warn "Error: Cannot init preexisting file '#{input}'"
|
|
179
|
+
warn "You may want to try calling `secret_keys encrypt/edit` instead"
|
|
171
180
|
exit 1
|
|
172
181
|
end
|
|
173
|
-
|
|
174
|
-
File.write(input, encrypted_file_contents)
|
|
175
182
|
else
|
|
176
183
|
$stdout.write(encrypted_file_contents)
|
|
177
184
|
end
|
|
@@ -220,9 +227,7 @@ module SecretKeys::CLI
|
|
|
220
227
|
raise ArgumentError, "Cannot perform in place editing on streams" unless @input.is_a?(String)
|
|
221
228
|
# make sure we read the file **before** writing to it.
|
|
222
229
|
contents = encrypted_file_contents
|
|
223
|
-
|
|
224
|
-
file.write(contents)
|
|
225
|
-
end
|
|
230
|
+
SecretKeys.atomic_write(@input, contents)
|
|
226
231
|
else
|
|
227
232
|
$stdout.write(encrypted_file_contents)
|
|
228
233
|
$stdout.flush
|
|
@@ -237,7 +242,7 @@ module SecretKeys::CLI
|
|
|
237
242
|
|
|
238
243
|
def run!
|
|
239
244
|
decrypted = secrets.to_h
|
|
240
|
-
string = (format == :yaml ? YAML.dump(decrypted) : JSON.pretty_generate(decrypted))
|
|
245
|
+
string = ((format == :yaml) ? YAML.dump(decrypted) : JSON.pretty_generate(decrypted))
|
|
241
246
|
string << $/ unless string.end_with?($/) # ensure file ends with system dependent new line
|
|
242
247
|
$stdout.write(string)
|
|
243
248
|
$stdout.flush
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "openssl"
|
|
5
|
-
require "base64"
|
|
6
5
|
|
|
7
6
|
# Encyption helper for encrypting and decrypting values using AES-256-GCM and returning
|
|
8
7
|
# as Base64 encoded strings. The encrypted values also include a prefix that can be used
|
|
@@ -22,8 +21,8 @@ class SecretKeys::Encryptor
|
|
|
22
21
|
class << self
|
|
23
22
|
# Create an Encryptor from a password and salt. This is a shortcut for generating an Encryptor
|
|
24
23
|
# with a 32 byte encryption key. The key will be derived from the password and salt.
|
|
25
|
-
# @param [String]
|
|
26
|
-
# @param [String]
|
|
24
|
+
# @param password [String] secret used to encrypt the data
|
|
25
|
+
# @param salt [String] random hex-encoded byte array for key derivation
|
|
27
26
|
# @return [SecretKeys::Encryptor] a new encryptor with key derived from password and salt
|
|
28
27
|
def from_password(password, salt)
|
|
29
28
|
raise ArgumentError, "Password must be present" if password.nil? || password.empty?
|
|
@@ -56,7 +55,7 @@ class SecretKeys::Encryptor
|
|
|
56
55
|
end
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
# @param [String]
|
|
58
|
+
# @param raw_key [String] the key directly passed into the encrypt/decrypt functions. This must be exactly {KEY_LENGTH} bytes long.
|
|
60
59
|
def initialize(raw_key)
|
|
61
60
|
raise ArgumentError, "key must be #{KEY_LENGTH} bytes long" unless raw_key.bytesize == KEY_LENGTH
|
|
62
61
|
@derived_key = raw_key
|
|
@@ -66,7 +65,7 @@ class SecretKeys::Encryptor
|
|
|
66
65
|
# calling this function multiple times will result in different values. Only strings
|
|
67
66
|
# can be encrypted. Any other object type will be return the value passed in.
|
|
68
67
|
#
|
|
69
|
-
# @param [String]
|
|
68
|
+
# @param str [String] string to encrypt (assumes UTF-8)
|
|
70
69
|
# @return [String] Base64 encoded encrypted string with all aes parameters
|
|
71
70
|
def encrypt(str)
|
|
72
71
|
return str unless str.is_a?(String)
|
|
@@ -97,13 +96,13 @@ class SecretKeys::Encryptor
|
|
|
97
96
|
# Decrypt a string with the encryption key. If the value is not a string or it was
|
|
98
97
|
# not encrypted with the encryption key, the value itself will be returned.
|
|
99
98
|
#
|
|
100
|
-
# @param [String]
|
|
99
|
+
# @param encrypted_str [String] Base64 encoded encrypted string with aes params (from {#encrypt})
|
|
101
100
|
# @return [String] decrypted string value
|
|
102
101
|
# @raise [OpenSSL::Cipher::CipherError] there is something wrong with the encoded data (usually incorrect key)
|
|
103
102
|
def decrypt(encrypted_str)
|
|
104
103
|
return encrypted_str unless self.class.encrypted?(encrypted_str)
|
|
105
104
|
|
|
106
|
-
decrypt_str = encrypted_str[ENCRYPTED_PREFIX.length
|
|
105
|
+
decrypt_str = encrypted_str[ENCRYPTED_PREFIX.length..]
|
|
107
106
|
params = decode_aes(decrypt_str)
|
|
108
107
|
|
|
109
108
|
cipher = OpenSSL::Cipher.new(CIPHER).decrypt
|
|
@@ -138,13 +137,13 @@ class SecretKeys::Encryptor
|
|
|
138
137
|
# Receive a cipher object (initialized with key) and data
|
|
139
138
|
def encode_aes(params)
|
|
140
139
|
encoded = params.values.pack(ENCODING_FORMAT)
|
|
141
|
-
# encode base64 and get rid of
|
|
142
|
-
|
|
140
|
+
# encode base64 and get rid of unnecessary '=' padding
|
|
141
|
+
[encoded].pack("m0").tr("=", "")
|
|
143
142
|
end
|
|
144
143
|
|
|
145
144
|
# Passed in an aes encoded string and returns a cipher object
|
|
146
145
|
def decode_aes(str)
|
|
147
|
-
unpacked_data =
|
|
146
|
+
unpacked_data = str.unpack1("m").unpack(ENCODING_FORMAT)
|
|
148
147
|
# Splat the data array apart
|
|
149
148
|
# nonce, auth_tag, encrypted_data = unpacked_data
|
|
150
149
|
CipherParams.new(*unpacked_data)
|
data/lib/secret_keys.rb
CHANGED
|
@@ -15,13 +15,42 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
15
15
|
# Error if the crypto version specified in the file is unsupported
|
|
16
16
|
class VersionError < StandardError; end
|
|
17
17
|
|
|
18
|
+
# Write a file atomically by writing to a temporary file in the same directory and
|
|
19
|
+
# renaming it over the target path so that readers never see a partially written file.
|
|
20
|
+
# The mode of an existing file at the path will be preserved. New files are created
|
|
21
|
+
# readable and writable only by the owner.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @param path [String, Pathname] path of the file to write
|
|
25
|
+
# @param content [String] contents of the file
|
|
26
|
+
# @return [void]
|
|
27
|
+
def self.atomic_write(path, content)
|
|
28
|
+
path = File.expand_path(path.to_s)
|
|
29
|
+
mode = begin
|
|
30
|
+
File.stat(path).mode & 0o7777
|
|
31
|
+
rescue Errno::ENOENT
|
|
32
|
+
0o600
|
|
33
|
+
end
|
|
34
|
+
tmp_path = File.join(File.dirname(path), ".#{File.basename(path)}.tmp.#{Process.pid}.#{rand(1_000_000)}")
|
|
35
|
+
begin
|
|
36
|
+
File.open(tmp_path, "w") do |file| # rubocop:disable Style/FileWrite
|
|
37
|
+
file.write(content)
|
|
38
|
+
end
|
|
39
|
+
File.chmod(mode, tmp_path)
|
|
40
|
+
File.rename(tmp_path, path)
|
|
41
|
+
ensure
|
|
42
|
+
File.unlink(tmp_path) if File.exist?(tmp_path)
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
18
47
|
# Parse a JSON or YAML stream or file with encrypted values. Any values in the ".encrypted" key
|
|
19
48
|
# in the document will be decrypted with the provided encryption key. If values
|
|
20
49
|
# were put into the ".encrypted" key manually and are not yet encrypted, they will be used
|
|
21
50
|
# as is without any decryption.
|
|
22
51
|
#
|
|
23
|
-
# @param [String, #read, Hash]
|
|
24
|
-
# @param [String]
|
|
52
|
+
# @param path_or_stream [String, #read, Hash] path to a JSON/YAML file to load, an IO object, or a Hash (mostly for testing purposes)
|
|
53
|
+
# @param encryption_key [String] secret to use for encryption/decryption
|
|
25
54
|
#
|
|
26
55
|
# @note If no encryption key is passed, this will defautl to env var SECRET_KEYS_ENCRYPTION_KEY
|
|
27
56
|
# or (if that is empty) the value read from the file path in SECRET_KEYS_ENCRYPTION_KEY_FILE.
|
|
@@ -44,11 +73,11 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
44
73
|
def to_h
|
|
45
74
|
@values
|
|
46
75
|
end
|
|
47
|
-
|
|
76
|
+
alias_method :to_hash, :to_h
|
|
48
77
|
|
|
49
78
|
# Mark the key as being encrypted when the JSON is saved.
|
|
50
79
|
#
|
|
51
|
-
# @param [String] key
|
|
80
|
+
# @param key [String] key to mark as needing encryption
|
|
52
81
|
# @return [void]
|
|
53
82
|
def encrypt!(key)
|
|
54
83
|
@secret_keys << key
|
|
@@ -57,7 +86,7 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
57
86
|
|
|
58
87
|
# Mark the key as no longer being decrypted when the JSON is saved.
|
|
59
88
|
#
|
|
60
|
-
# @param [String] key
|
|
89
|
+
# @param key [String] key to mark as not needing encryption
|
|
61
90
|
# @return [void]
|
|
62
91
|
def decrypt!(key)
|
|
63
92
|
@secret_keys.delete(key)
|
|
@@ -66,7 +95,7 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
66
95
|
|
|
67
96
|
# Return true if the key is encrypted.
|
|
68
97
|
#
|
|
69
|
-
# @param [String] key
|
|
98
|
+
# @param key [String] key to check
|
|
70
99
|
# @return [Boolean]
|
|
71
100
|
def encrypted?(key)
|
|
72
101
|
@secret_keys.include?(key)
|
|
@@ -77,8 +106,8 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
77
106
|
# different initialization vector). This can be helpful if you have your secrets in source
|
|
78
107
|
# control so that only changed keys will actually be changed in the file when it is updated.
|
|
79
108
|
#
|
|
80
|
-
# @param [String, Pathname] path
|
|
81
|
-
# @param [String, Symbol]
|
|
109
|
+
# @param path [String, Pathname] path of the file to save. If the file exists, only changed values will be updated.
|
|
110
|
+
# @param format [String, Symbol] output format (YAML or JSON) to use. This will default based on the extension on the file path or the format originally used
|
|
82
111
|
# @return [void]
|
|
83
112
|
def save(path, format: nil)
|
|
84
113
|
# create a copy of the encrypted hash for working on
|
|
@@ -94,11 +123,9 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
94
123
|
format ||= @format
|
|
95
124
|
format = format.to_s.downcase
|
|
96
125
|
|
|
97
|
-
output = (format == "yaml" ? YAML.dump(encrypted) : JSON.pretty_generate(encrypted))
|
|
126
|
+
output = ((format == "yaml") ? YAML.dump(encrypted) : JSON.pretty_generate(encrypted))
|
|
98
127
|
output << $/ unless output.end_with?($/) # ensure file ends with system dependent new line
|
|
99
|
-
|
|
100
|
-
file.write(output)
|
|
101
|
-
end
|
|
128
|
+
self.class.atomic_write(path, output)
|
|
102
129
|
nil
|
|
103
130
|
end
|
|
104
131
|
|
|
@@ -133,9 +160,10 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
133
160
|
|
|
134
161
|
# Change the encryption key in the document. When saving later, this key will be used.
|
|
135
162
|
#
|
|
136
|
-
# @param [String]
|
|
163
|
+
# @param new_encryption_key [String] encryption key to use for future {#save} calls
|
|
137
164
|
# @return [void]
|
|
138
165
|
def encryption_key=(new_encryption_key)
|
|
166
|
+
raise EncryptionKeyError.new("Encryption key not specified") if new_encryption_key.nil? || new_encryption_key.empty?
|
|
139
167
|
@original_encrypted = {}
|
|
140
168
|
update_secret(key: new_encryption_key)
|
|
141
169
|
end
|
|
@@ -168,10 +196,10 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
168
196
|
|
|
169
197
|
hash = {}
|
|
170
198
|
if path_or_stream.is_a?(Hash)
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
hash =
|
|
199
|
+
# Deep copy the hash. Otherwise, we can end up using destructive `#delete`
|
|
200
|
+
# operations and mess up deeply nested values for external code
|
|
201
|
+
# (esp. when loading key: .encrypted)
|
|
202
|
+
hash = deep_dup(path_or_stream)
|
|
175
203
|
elsif path_or_stream
|
|
176
204
|
data = path_or_stream.read
|
|
177
205
|
hash = parse_data(data)
|
|
@@ -179,20 +207,31 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
179
207
|
|
|
180
208
|
encrypted_values = hash.delete(ENCRYPTED)
|
|
181
209
|
if encrypted_values
|
|
182
|
-
|
|
210
|
+
raise ArgumentError, "The #{ENCRYPTED} key must contain a Hash of encrypted values" unless encrypted_values.is_a?(Hash)
|
|
211
|
+
|
|
212
|
+
@original_encrypted = deep_dup(encrypted_values)
|
|
183
213
|
|
|
184
214
|
version = encrypted_values.delete(VERSION_KEY) || CRYPTO_VERSION
|
|
185
|
-
|
|
215
|
+
if version.is_a?(String)
|
|
216
|
+
version = begin
|
|
217
|
+
Integer(version, 10)
|
|
218
|
+
rescue ArgumentError
|
|
219
|
+
version
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
unless version.is_a?(Numeric) && version <= CRYPTO_VERSION
|
|
223
|
+
raise VersionError, "Unsupported file version #{version}. Max supported is #{CRYPTO_VERSION}."
|
|
224
|
+
end
|
|
186
225
|
|
|
187
226
|
file_key = encrypted_values.delete(ENCRYPTION_KEY)
|
|
188
|
-
salt =
|
|
227
|
+
salt = encrypted_values.delete(SALT) || Encryptor.random_salt
|
|
189
228
|
update_secret(salt: salt)
|
|
190
229
|
|
|
191
230
|
# Check that we are using the right key
|
|
192
231
|
if file_key && !encryption_key_matches?(file_key)
|
|
193
232
|
raise EncryptionKeyError.new("Incorrect encryption key")
|
|
194
233
|
end
|
|
195
|
-
@secret_keys = encrypted_values.keys
|
|
234
|
+
@secret_keys = Set.new(encrypted_values.keys)
|
|
196
235
|
hash.merge!(decrypt_values(encrypted_values))
|
|
197
236
|
elsif @salt.nil?
|
|
198
237
|
# if no salt exists, create one.
|
|
@@ -203,15 +242,38 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
203
242
|
end
|
|
204
243
|
|
|
205
244
|
# Attempt to parse the file first using JSON and fallback to YAML
|
|
206
|
-
# @param [String]
|
|
245
|
+
# @param data [String] file data to parse
|
|
207
246
|
# @return [Hash] data parsed to a hash
|
|
208
247
|
def parse_data(data)
|
|
209
248
|
@format = :json
|
|
210
249
|
return {} if data.nil? || data.empty?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
250
|
+
hash = begin
|
|
251
|
+
JSON.parse(data)
|
|
252
|
+
rescue JSON::JSONError
|
|
253
|
+
@format = :yaml
|
|
254
|
+
YAML.safe_load(data)
|
|
255
|
+
end
|
|
256
|
+
# YAML.safe_load returns false instead of nil for empty documents on Ruby < 3.4.
|
|
257
|
+
return {} if hash.nil? || hash == false
|
|
258
|
+
raise ArgumentError, "Data must parse to a Hash of key value pairs" unless hash.is_a?(Hash)
|
|
259
|
+
hash
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Deep copy a parsed data structure of hashes, arrays, and scalar values.
|
|
263
|
+
def deep_dup(value)
|
|
264
|
+
if value.is_a?(Hash)
|
|
265
|
+
copy = {}
|
|
266
|
+
value.each do |key, val|
|
|
267
|
+
copy[deep_dup(key)] = deep_dup(val)
|
|
268
|
+
end
|
|
269
|
+
copy
|
|
270
|
+
elsif value.is_a?(Array)
|
|
271
|
+
value.collect { |val| deep_dup(val) }
|
|
272
|
+
elsif value.is_a?(String)
|
|
273
|
+
value.dup
|
|
274
|
+
else
|
|
275
|
+
value
|
|
276
|
+
end
|
|
215
277
|
end
|
|
216
278
|
|
|
217
279
|
# Recursively encrypt all values.
|
|
@@ -265,23 +327,6 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
265
327
|
@encryptor.decrypt(encrypted_value)
|
|
266
328
|
end
|
|
267
329
|
|
|
268
|
-
# Helper method to test if two values are both encrypted, but result in the same decrypted value.
|
|
269
|
-
def equal_encrypted_values?(value_1, value_2)
|
|
270
|
-
return true if value_1 == value_2
|
|
271
|
-
|
|
272
|
-
decrypt_val_1 = decrypt_value(value_1)
|
|
273
|
-
decrypt_val_2 = decrypt_value(value_2)
|
|
274
|
-
if decrypt_val_1 == decrypt_val_2
|
|
275
|
-
if value_1 == decrypt_val_1 || value_2 == decrypt_val_2
|
|
276
|
-
false
|
|
277
|
-
else
|
|
278
|
-
true
|
|
279
|
-
end
|
|
280
|
-
else
|
|
281
|
-
false
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
330
|
# This is an encrypted known value that can be used determine if the secret has changed.
|
|
286
331
|
def encrypted_known_value
|
|
287
332
|
encrypt_value(KNOWN_VALUE)
|
|
@@ -296,13 +341,12 @@ class SecretKeys < DelegateClass(Hash)
|
|
|
296
341
|
end
|
|
297
342
|
|
|
298
343
|
def yaml_file?(path)
|
|
299
|
-
ext =
|
|
300
|
-
ext == "yaml" || ext == "yml"
|
|
344
|
+
ext = File.extname(path.to_s).downcase
|
|
345
|
+
ext == ".yaml" || ext == ".yml"
|
|
301
346
|
end
|
|
302
347
|
|
|
303
348
|
def json_file?(path)
|
|
304
|
-
|
|
305
|
-
ext == "json"
|
|
349
|
+
File.extname(path.to_s).downcase == ".json"
|
|
306
350
|
end
|
|
307
351
|
|
|
308
352
|
# Update the secret key by updating the salt
|
data/secret_keys.gemspec
CHANGED
|
@@ -7,9 +7,12 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.summary = "Simple mechanism for loading JSON file with encrypted values."
|
|
8
8
|
spec.homepage = "https://github.com/bdurand/secret_keys"
|
|
9
9
|
spec.license = "MIT"
|
|
10
|
-
|
|
11
|
-
spec.metadata
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
spec.metadata = {
|
|
12
|
+
"homepage_uri" => spec.homepage,
|
|
13
|
+
"source_code_uri" => spec.homepage,
|
|
14
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
# Specify which files should be added to the gem when it is released.
|
|
15
18
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
@@ -17,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
|
17
20
|
.
|
|
18
21
|
Appraisals
|
|
19
22
|
Gemfile
|
|
23
|
+
Gemfile.lock
|
|
20
24
|
Rakefile
|
|
21
25
|
gemfiles/
|
|
22
26
|
spec/
|
|
@@ -29,7 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
29
33
|
spec.bindir = "bin"
|
|
30
34
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
31
35
|
|
|
32
|
-
spec.required_ruby_version = ">= 2.
|
|
36
|
+
spec.required_ruby_version = ">= 2.6"
|
|
33
37
|
|
|
34
|
-
spec.add_development_dependency "bundler"
|
|
38
|
+
spec.add_development_dependency "bundler"
|
|
35
39
|
end
|
metadata
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secret_keys
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
8
|
- Winston Durand
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: bundler
|
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
|
17
16
|
requirements:
|
|
18
|
-
- - "
|
|
17
|
+
- - ">="
|
|
19
18
|
- !ruby/object:Gem::Version
|
|
20
|
-
version: '
|
|
19
|
+
version: '0'
|
|
21
20
|
type: :development
|
|
22
21
|
prerelease: false
|
|
23
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
24
23
|
requirements:
|
|
25
|
-
- - "
|
|
24
|
+
- - ">="
|
|
26
25
|
- !ruby/object:Gem::Version
|
|
27
|
-
version: '
|
|
28
|
-
description:
|
|
26
|
+
version: '0'
|
|
29
27
|
email:
|
|
30
28
|
- bbdurand@gmail.com
|
|
31
29
|
- me@winstondurand.com
|
|
@@ -34,7 +32,7 @@ executables:
|
|
|
34
32
|
extensions: []
|
|
35
33
|
extra_rdoc_files: []
|
|
36
34
|
files:
|
|
37
|
-
-
|
|
35
|
+
- CHANGELOG.md
|
|
38
36
|
- MIT_LICENSE.txt
|
|
39
37
|
- README.md
|
|
40
38
|
- VERSION
|
|
@@ -49,9 +47,8 @@ licenses:
|
|
|
49
47
|
- MIT
|
|
50
48
|
metadata:
|
|
51
49
|
homepage_uri: https://github.com/bdurand/secret_keys
|
|
52
|
-
source_code_uri: https://github.com/bdurand/secret_keys
|
|
53
|
-
|
|
54
|
-
post_install_message:
|
|
50
|
+
source_code_uri: https://github.com/bdurand/secret_keys
|
|
51
|
+
changelog_uri: https://github.com/bdurand/secret_keys/blob/main/CHANGELOG.md
|
|
55
52
|
rdoc_options: []
|
|
56
53
|
require_paths:
|
|
57
54
|
- lib
|
|
@@ -59,15 +56,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
59
56
|
requirements:
|
|
60
57
|
- - ">="
|
|
61
58
|
- !ruby/object:Gem::Version
|
|
62
|
-
version: '2.
|
|
59
|
+
version: '2.6'
|
|
63
60
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
61
|
requirements:
|
|
65
62
|
- - ">="
|
|
66
63
|
- !ruby/object:Gem::Version
|
|
67
64
|
version: '0'
|
|
68
65
|
requirements: []
|
|
69
|
-
rubygems_version:
|
|
70
|
-
signing_key:
|
|
66
|
+
rubygems_version: 4.0.3
|
|
71
67
|
specification_version: 4
|
|
72
68
|
summary: Simple mechanism for loading JSON file with encrypted values.
|
|
73
69
|
test_files: []
|