maquina_credentials 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/CHANGELOG.md +8 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +153 -0
- data/exe/mcr +7 -0
- data/lib/maquina/credentials/cli.rb +122 -0
- data/lib/maquina/credentials.rb +161 -0
- data/lib/maquina_credentials/version.rb +7 -0
- data/lib/maquina_credentials.rb +4 -0
- data/sig/maquina_credentials.rbs +23 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 77da8a494d686b452f4ca9745d69aaa3ce3bff688c7010ef651ed078604e5495
|
|
4
|
+
data.tar.gz: 88d37fea885f62326aa550411ab702856c98079170f2c62caa35aa53b426e77e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ca1c6f06ff9cca2c67bd5438e57397cf9a80cd6ad59da3668d0e8b3d4edac1c2a13ab7af8235c64634f55b353a53d1d7b129a17bb08ace881835ad876335dd08
|
|
7
|
+
data.tar.gz: 5df16514d996d6fed68bb431c8c528c305eb1cedafa67dc47fb0298150b6b1e0559f2f015d065a6398345232a350cae4f41e8ab28a081dc4e41d71d49f18d6a4
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"maquina_credentials" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["mario.chavez@gmail.com"](mailto:"mario.chavez@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mario Alberto Chávez Cárdenas (https://maquina.app)
|
|
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,153 @@
|
|
|
1
|
+
# Maquina Credentials
|
|
2
|
+
|
|
3
|
+
Encrypt and decrypt a single `credentials.yml.enc` file with AES-256-GCM.
|
|
4
|
+
|
|
5
|
+
`maquina_credentials` is a small, dependency-free gem for storing secrets in an
|
|
6
|
+
encrypted file that can safely travel with your source or container image. The
|
|
7
|
+
master key never lives in the file — it is supplied at runtime through the
|
|
8
|
+
`MAQUINA_MASTER_KEY` environment variable. Reading a missing key returns an
|
|
9
|
+
empty string and never raises, so it is safe to call in code paths that may run
|
|
10
|
+
without credentials configured.
|
|
11
|
+
|
|
12
|
+
It is inspired by Rails' encrypted credentials, distilled down to a single file,
|
|
13
|
+
a command line tool, and a plain Ruby API — with no dependency on Rails or
|
|
14
|
+
Active Support.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add it to your `Gemfile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "maquina_credentials"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run `bundle install`. Or install it directly:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
gem install maquina_credentials
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Ruby >= 3.2.0.
|
|
31
|
+
|
|
32
|
+
## Master key
|
|
33
|
+
|
|
34
|
+
Generate a master key once and store it as a secret (never commit it):
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
mcr generate
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This prints a fresh random key built from `SecureRandom.hex(32)`. If you do not
|
|
41
|
+
have the executable handy, the equivalent in plain Ruby is:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
ruby -e "require 'securerandom'; puts SecureRandom.hex(32)"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Provide the key to every command through `MAQUINA_MASTER_KEY`. Any 32-byte key
|
|
48
|
+
is used directly; keys of any other length (including the 64-character hex
|
|
49
|
+
string above) are run through HKDF-SHA256 to derive the encryption key.
|
|
50
|
+
|
|
51
|
+
## Command line
|
|
52
|
+
|
|
53
|
+
The gem installs an `mcr` executable.
|
|
54
|
+
|
|
55
|
+
Read a value (dot-paths traverse nested keys):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
MAQUINA_MASTER_KEY=<key> mcr read database.password
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Write credentials from piped YAML:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
MAQUINA_MASTER_KEY=<key> printf "database:\n password: prod-secret\n" | mcr write
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use a specific encrypted file:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
mcr --file /path/to/credentials.yml.enc read database.password
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Generate a new master key:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
mcr generate
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Show help:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
mcr help
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### File selection
|
|
86
|
+
|
|
87
|
+
The encrypted file is resolved in this order:
|
|
88
|
+
|
|
89
|
+
1. The `--file PATH` option, when given.
|
|
90
|
+
2. The `MAQUINA_CREDENTIALS_FILE` environment variable.
|
|
91
|
+
3. `credentials.yml.enc` in the current working directory.
|
|
92
|
+
|
|
93
|
+
## Ruby API
|
|
94
|
+
|
|
95
|
+
The same class is available through `require "maquina_credentials"`.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
credentials = Maquina::Credentials.new
|
|
99
|
+
credentials.read("anthropic_key")
|
|
100
|
+
credentials.read("database.password") # dot-path traversal
|
|
101
|
+
|
|
102
|
+
# Writing replaces the whole file from a full hash:
|
|
103
|
+
Maquina::Credentials.write({
|
|
104
|
+
anthropic_key: "sk-ant-...",
|
|
105
|
+
database: {password: "prod-secret"}
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- Missing credential paths return an empty string (`""`), never `nil`.
|
|
110
|
+
- All values are returned as strings.
|
|
111
|
+
- A missing or empty `MAQUINA_MASTER_KEY` raises
|
|
112
|
+
`Maquina::Credentials::MasterKeyMissing` (only when a credentials file
|
|
113
|
+
actually exists).
|
|
114
|
+
- A wrong key, tampered, truncated, or otherwise unreadable file raises
|
|
115
|
+
`Maquina::Credentials::DecryptionFailed`.
|
|
116
|
+
- An instance decrypts once and caches the result for the life of the object.
|
|
117
|
+
|
|
118
|
+
## Security notes
|
|
119
|
+
|
|
120
|
+
- Cipher is AES-256-GCM; the wire format is strict-base64 of
|
|
121
|
+
`IV (12 bytes) || ciphertext || GCM auth tag (16 bytes)`.
|
|
122
|
+
- A fresh random IV is generated on every write.
|
|
123
|
+
- Writes are atomic (temp file renamed into place) and the file is set to mode
|
|
124
|
+
`0600`.
|
|
125
|
+
- YAML is parsed with `safe_load(permitted_classes: [])` on both read and write,
|
|
126
|
+
so a tampered file cannot instantiate arbitrary Ruby objects.
|
|
127
|
+
|
|
128
|
+
## Container workflow
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
# 1. Generate the master key once and store it as a secret.
|
|
132
|
+
mcr generate
|
|
133
|
+
|
|
134
|
+
# 2. Write credentials locally.
|
|
135
|
+
MAQUINA_MASTER_KEY=<key> printf "anthropic_key: sk-ant-...\n" | mcr write
|
|
136
|
+
|
|
137
|
+
# 3. Commit credentials.yml.enc into the image — the key never goes in.
|
|
138
|
+
docker build -t my-app .
|
|
139
|
+
|
|
140
|
+
# 4. Inject the key at runtime (or via orchestrator secrets).
|
|
141
|
+
docker run -e MAQUINA_MASTER_KEY=<key> my-app
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run
|
|
147
|
+
`bundle exec rake` to run the tests and Standard Ruby. Use `bin/console` for an
|
|
148
|
+
interactive prompt with the gem preloaded.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
The gem is available as open source under the terms of the
|
|
153
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/exe/mcr
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Maquina
|
|
8
|
+
class Credentials
|
|
9
|
+
class CLI
|
|
10
|
+
def self.start(argv, stdout: $stdout, stderr: $stderr, stdin: $stdin)
|
|
11
|
+
new(argv, stdout: stdout, stderr: stderr, stdin: stdin).run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(argv, stdout:, stderr:, stdin:)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@stderr = stderr
|
|
18
|
+
@stdin = stdin
|
|
19
|
+
@credentials_path = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
command = parser.parse!(@argv).first
|
|
24
|
+
|
|
25
|
+
case command
|
|
26
|
+
when "help", nil
|
|
27
|
+
@stdout.puts parser
|
|
28
|
+
0
|
|
29
|
+
when "generate"
|
|
30
|
+
generate
|
|
31
|
+
when "read"
|
|
32
|
+
read(@argv[1])
|
|
33
|
+
when "write"
|
|
34
|
+
write
|
|
35
|
+
else
|
|
36
|
+
@stderr.puts parser
|
|
37
|
+
command ? 1 : 0
|
|
38
|
+
end
|
|
39
|
+
rescue OptionParser::ParseError => error
|
|
40
|
+
@stderr.puts error.message
|
|
41
|
+
@stderr.puts parser
|
|
42
|
+
1
|
|
43
|
+
rescue MasterKeyMissing
|
|
44
|
+
@stderr.puts "MAQUINA_MASTER_KEY is required"
|
|
45
|
+
1
|
|
46
|
+
rescue DecryptionFailed
|
|
47
|
+
@stderr.puts "Failed to decrypt credentials"
|
|
48
|
+
1
|
|
49
|
+
rescue Psych::Exception
|
|
50
|
+
@stderr.puts "Input must be a valid YAML hash"
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def parser
|
|
57
|
+
@parser ||= OptionParser.new do |options|
|
|
58
|
+
options.banner = <<~TEXT.chomp
|
|
59
|
+
Usage:
|
|
60
|
+
mcr help
|
|
61
|
+
mcr generate
|
|
62
|
+
mcr read KEY [--file PATH]
|
|
63
|
+
mcr write [--file PATH] < credentials.yml
|
|
64
|
+
|
|
65
|
+
Commands:
|
|
66
|
+
help Show this help text.
|
|
67
|
+
generate Print a new random master key to stdout.
|
|
68
|
+
read Print a credential value to stdout.
|
|
69
|
+
write Encrypt YAML from stdin and write it to the credentials file.
|
|
70
|
+
|
|
71
|
+
File selection:
|
|
72
|
+
Use --file PATH to read or write a specific file.
|
|
73
|
+
Without --file, mcr uses credentials.yml.enc in the current directory.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
mcr generate
|
|
77
|
+
mcr read database.password
|
|
78
|
+
mcr --file /tmp/credentials.yml.enc write < credentials.yml
|
|
79
|
+
mcr help
|
|
80
|
+
TEXT
|
|
81
|
+
|
|
82
|
+
options.on("-f", "--file PATH", "Credentials file path") do |path|
|
|
83
|
+
@credentials_path = path
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
options.on("-h", "--help", "Print this help") do
|
|
87
|
+
@stdout.puts options
|
|
88
|
+
exit 0
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def generate
|
|
94
|
+
@stdout.puts SecureRandom.hex(32)
|
|
95
|
+
0
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def read(key)
|
|
99
|
+
unless key
|
|
100
|
+
@stderr.puts "Missing KEY"
|
|
101
|
+
return 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@stdout.puts Credentials.new(credentials_path: @credentials_path).read(key)
|
|
105
|
+
0
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def write
|
|
109
|
+
input = @stdin.read
|
|
110
|
+
hash = YAML.safe_load(input, permitted_classes: [], symbolize_names: false)
|
|
111
|
+
|
|
112
|
+
unless hash.is_a?(Hash)
|
|
113
|
+
@stderr.puts "Input must be a YAML hash"
|
|
114
|
+
return 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Credentials.write(hash, credentials_path: @credentials_path)
|
|
118
|
+
0
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
module Maquina
|
|
9
|
+
class Credentials
|
|
10
|
+
class MasterKeyMissing < StandardError; end
|
|
11
|
+
class DecryptionFailed < StandardError; end
|
|
12
|
+
|
|
13
|
+
DEFAULT_CREDENTIALS_PATH = "credentials.yml.enc"
|
|
14
|
+
ENV_KEY = "MAQUINA_MASTER_KEY"
|
|
15
|
+
FILE_ENV_KEY = "MAQUINA_CREDENTIALS_FILE"
|
|
16
|
+
CIPHER = "aes-256-gcm"
|
|
17
|
+
KEY_LENGTH = 32
|
|
18
|
+
IV_LENGTH = 12
|
|
19
|
+
AUTH_TAG_LENGTH = 16
|
|
20
|
+
HKDF_INFO = "maquina-credentials-v1"
|
|
21
|
+
|
|
22
|
+
def self.write(hash, credentials_path: nil)
|
|
23
|
+
credentials_path = resolve_credentials_path(credentials_path)
|
|
24
|
+
payload = YAML.dump(deep_stringify(hash))
|
|
25
|
+
encrypted = encrypt(payload, master_key)
|
|
26
|
+
tmp_path = "#{credentials_path}.tmp.#{Process.pid}"
|
|
27
|
+
|
|
28
|
+
FileUtils.mkdir_p(File.dirname(credentials_path))
|
|
29
|
+
File.write(tmp_path, encrypted)
|
|
30
|
+
File.rename(tmp_path, credentials_path)
|
|
31
|
+
File.chmod(0o600, credentials_path)
|
|
32
|
+
ensure
|
|
33
|
+
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.encrypt(payload, raw_key)
|
|
37
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
|
38
|
+
cipher.encrypt
|
|
39
|
+
cipher.key = derive_key(raw_key)
|
|
40
|
+
iv = cipher.random_iv
|
|
41
|
+
cipher.iv = iv
|
|
42
|
+
cipher.auth_data = ""
|
|
43
|
+
|
|
44
|
+
ciphertext = cipher.update(payload) + cipher.final
|
|
45
|
+
strict_base64_encode(iv + ciphertext + cipher.auth_tag)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.decrypt(encrypted, raw_key)
|
|
49
|
+
decoded = strict_base64_decode(encrypted)
|
|
50
|
+
raise DecryptionFailed if decoded.bytesize < IV_LENGTH + AUTH_TAG_LENGTH
|
|
51
|
+
|
|
52
|
+
iv = decoded.byteslice(0, IV_LENGTH)
|
|
53
|
+
auth_tag = decoded.byteslice(-AUTH_TAG_LENGTH, AUTH_TAG_LENGTH)
|
|
54
|
+
ciphertext = decoded.byteslice(IV_LENGTH, decoded.bytesize - IV_LENGTH - AUTH_TAG_LENGTH)
|
|
55
|
+
|
|
56
|
+
cipher = OpenSSL::Cipher.new(CIPHER)
|
|
57
|
+
cipher.decrypt
|
|
58
|
+
cipher.key = derive_key(raw_key)
|
|
59
|
+
cipher.iv = iv
|
|
60
|
+
cipher.auth_tag = auth_tag
|
|
61
|
+
cipher.auth_data = ""
|
|
62
|
+
cipher.update(ciphertext) + cipher.final
|
|
63
|
+
rescue ArgumentError, OpenSSL::Cipher::CipherError
|
|
64
|
+
raise DecryptionFailed
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.strict_base64_encode(bytes)
|
|
68
|
+
[bytes].pack("m0")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.strict_base64_decode(encoded)
|
|
72
|
+
unless encoded.match?(/\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?\z/)
|
|
73
|
+
raise ArgumentError
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
encoded.unpack1("m0")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.derive_key(raw_key)
|
|
80
|
+
raw_key = raw_key.b
|
|
81
|
+
return raw_key[0, KEY_LENGTH] if raw_key.bytesize == KEY_LENGTH
|
|
82
|
+
|
|
83
|
+
OpenSSL::KDF.hkdf(
|
|
84
|
+
raw_key,
|
|
85
|
+
salt: "",
|
|
86
|
+
info: HKDF_INFO,
|
|
87
|
+
length: KEY_LENGTH,
|
|
88
|
+
hash: "SHA256"
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.master_key
|
|
93
|
+
key = ENV[ENV_KEY]
|
|
94
|
+
raise MasterKeyMissing if key.nil? || key.empty?
|
|
95
|
+
|
|
96
|
+
key
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.resolve_credentials_path(credentials_path = nil)
|
|
100
|
+
return credentials_path unless credentials_path.nil? || credentials_path.empty?
|
|
101
|
+
|
|
102
|
+
env_path = ENV[FILE_ENV_KEY]
|
|
103
|
+
return env_path unless env_path.nil? || env_path.empty?
|
|
104
|
+
|
|
105
|
+
File.join(Dir.pwd, DEFAULT_CREDENTIALS_PATH)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.deep_stringify(obj)
|
|
109
|
+
case obj
|
|
110
|
+
when Hash
|
|
111
|
+
obj.transform_keys(&:to_s).transform_values { |value| deep_stringify(value) }
|
|
112
|
+
when Array
|
|
113
|
+
obj.map { |value| deep_stringify(value) }
|
|
114
|
+
else
|
|
115
|
+
obj
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def initialize(credentials_path: nil)
|
|
120
|
+
@credentials_path = self.class.resolve_credentials_path(credentials_path)
|
|
121
|
+
@credentials = nil
|
|
122
|
+
@loaded = false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def read(path)
|
|
126
|
+
return "" if path.nil? || path.empty?
|
|
127
|
+
|
|
128
|
+
value = path.split(".").reduce(credentials) do |current, key|
|
|
129
|
+
break unless current.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
current[key]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
value.nil? ? "" : value.to_s
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
attr_reader :credentials_path
|
|
140
|
+
|
|
141
|
+
def credentials
|
|
142
|
+
return @credentials if @loaded
|
|
143
|
+
|
|
144
|
+
@credentials = load_credentials
|
|
145
|
+
@loaded = true
|
|
146
|
+
@credentials
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def load_credentials
|
|
150
|
+
return {} unless File.exist?(credentials_path)
|
|
151
|
+
|
|
152
|
+
decrypted = self.class.decrypt(File.read(credentials_path), self.class.master_key)
|
|
153
|
+
loaded = YAML.safe_load(decrypted, permitted_classes: [], symbolize_names: false)
|
|
154
|
+
raise DecryptionFailed unless loaded.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
self.class.deep_stringify(loaded)
|
|
157
|
+
rescue Psych::Exception
|
|
158
|
+
raise DecryptionFailed
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Maquina
|
|
2
|
+
class Credentials
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
class MasterKeyMissing < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class DecryptionFailed < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.write: (Hash[untyped, untyped] hash, ?credentials_path: String? credentials_path) -> void
|
|
12
|
+
|
|
13
|
+
def self.encrypt: (String payload, String raw_key) -> String
|
|
14
|
+
|
|
15
|
+
def self.decrypt: (String encrypted, String raw_key) -> String
|
|
16
|
+
|
|
17
|
+
def self.resolve_credentials_path: (?String? credentials_path) -> String
|
|
18
|
+
|
|
19
|
+
def initialize: (?credentials_path: String? credentials_path) -> void
|
|
20
|
+
|
|
21
|
+
def read: (String? path) -> String
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: maquina_credentials
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mario Alberto Chávez Cárdenas
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: irb
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.16'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.16'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: standard
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.55'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.55'
|
|
68
|
+
description: A small, dependency-free gem that encrypts and decrypts a single credentials.yml.enc
|
|
69
|
+
file with AES-256-GCM. Ships an `mcr` command line tool for reading and writing
|
|
70
|
+
values, plus a Ruby API. Designed for container and server workflows where the encrypted
|
|
71
|
+
file travels with the image and the master key is injected at runtime via MAQUINA_MASTER_KEY.
|
|
72
|
+
email:
|
|
73
|
+
- mario.chavez@gmail.com
|
|
74
|
+
executables:
|
|
75
|
+
- mcr
|
|
76
|
+
extensions: []
|
|
77
|
+
extra_rdoc_files: []
|
|
78
|
+
files:
|
|
79
|
+
- CHANGELOG.md
|
|
80
|
+
- CODE_OF_CONDUCT.md
|
|
81
|
+
- LICENSE.txt
|
|
82
|
+
- README.md
|
|
83
|
+
- exe/mcr
|
|
84
|
+
- lib/maquina/credentials.rb
|
|
85
|
+
- lib/maquina/credentials/cli.rb
|
|
86
|
+
- lib/maquina_credentials.rb
|
|
87
|
+
- lib/maquina_credentials/version.rb
|
|
88
|
+
- sig/maquina_credentials.rbs
|
|
89
|
+
homepage: https://maquina.app
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
homepage_uri: https://maquina.app
|
|
94
|
+
source_code_uri: https://github.com/maquina-app/maquina_credentials
|
|
95
|
+
documentation_uri: https://github.com/maquina-app/maquina_credentials/blob/main/README.md
|
|
96
|
+
changelog_uri: https://github.com/maquina-app/maquina_credentials/blob/main/CHANGELOG.md
|
|
97
|
+
bug_tracker_uri: https://github.com/maquina-app/maquina_credentials/issues
|
|
98
|
+
rubygems_mfa_required: 'true'
|
|
99
|
+
rdoc_options: []
|
|
100
|
+
require_paths:
|
|
101
|
+
- lib
|
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: 3.2.0
|
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
requirements: []
|
|
113
|
+
rubygems_version: 4.0.14
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: Encrypted credentials for the command line
|
|
116
|
+
test_files: []
|