itamae-secrets 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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CONTRIBUTING.md +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/Rakefile +6 -0
- data/bin/itamae-secrets +4 -0
- data/itamae-secrets.gemspec +26 -0
- data/lib/itamae/secrets.rb +12 -0
- data/lib/itamae/secrets/aes_key.rb +63 -0
- data/lib/itamae/secrets/cli.rb +120 -0
- data/lib/itamae/secrets/decryptor.rb +77 -0
- data/lib/itamae/secrets/encryptor.rb +86 -0
- data/lib/itamae/secrets/keychain.rb +32 -0
- data/lib/itamae/secrets/store.rb +81 -0
- data/lib/itamae/secrets/version.rb +5 -0
- data/script/console +14 -0
- data/script/setup +7 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: df379b9fa313eb7a3516694a0efc764d086d23cf
|
4
|
+
data.tar.gz: 08e47d784901105a67f80ac1b1340e61e8e6524a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bffd0e6a9b7def10679ef9783cbc908cc909e1cfa778463a31f3a25eec6afc630c270fc92445c2caa6dde0802df4529231211b46e07d708dcc2393d6fa86a446
|
7
|
+
data.tar.gz: 740a9e5f8bd2d130769b94a7399aad5769d9631a2b1cdb79a8a6e29dacbc9025027e48ce9c74f6613257d225436f522d8fefaef3034fbe851a6e4caec54cc7d9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
__Security issues?__ Send me directly at `security@sorah.jp`. My GPG key is available here: <http://sorah.jp/id.html> ([SSL](https://github.com/sorah/sorah.jp/tree/master/source/pgp-pubkeys))
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Shota Fukumori (sora_h)
|
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,92 @@
|
|
1
|
+
# Itamae::Secrets - Encrypted Data Bag for Itamae
|
2
|
+
|
3
|
+
This is [itamae](https://github.com/itamae-kitchen/itamae) plugin that provides store for secrets, like encrypted data bag in chef.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'itamae-secrets'
|
9
|
+
```
|
10
|
+
|
11
|
+
or
|
12
|
+
|
13
|
+
```
|
14
|
+
$ gem install itamae-secrets
|
15
|
+
```
|
16
|
+
|
17
|
+
## Basic
|
18
|
+
|
19
|
+
- `itamae-secrets` command for storing data or manually reading
|
20
|
+
- specify base directory to `--base` option
|
21
|
+
- you should exclude `${base}/keys` from checking into VCS. (`.gitignore` it!)
|
22
|
+
|
23
|
+
## Walkthrough
|
24
|
+
|
25
|
+
### Generate a key
|
26
|
+
|
27
|
+
##### randomly
|
28
|
+
|
29
|
+
```
|
30
|
+
$ itamae-secrets newkey --base=./secret --method=aes-random
|
31
|
+
```
|
32
|
+
|
33
|
+
##### from passphrase
|
34
|
+
|
35
|
+
```
|
36
|
+
$ itamae-secrets newkey --base=./secret --method=aes-passphrase
|
37
|
+
```
|
38
|
+
|
39
|
+
Both generates `./secret/keys/default`. Make sure `./secret/keys` be excluded from VCS.
|
40
|
+
|
41
|
+
### Store value
|
42
|
+
|
43
|
+
```
|
44
|
+
$ itamae-secrets set --base=./secret awesome_secret value
|
45
|
+
```
|
46
|
+
|
47
|
+
(when omit `value`, it'll read from STDIN until EOF. You can also use `--noecho` if you want hide value in your terminal's buffer completely.)
|
48
|
+
|
49
|
+
### Reading data from itamae
|
50
|
+
|
51
|
+
on your itamae recipe, do:
|
52
|
+
|
53
|
+
``` ruby
|
54
|
+
require 'itamae/secrets'
|
55
|
+
node[:secrets] = Itamae::Secrets(File.join(__dir__, 'secrets'))
|
56
|
+
|
57
|
+
# Use it
|
58
|
+
p node[:secrets][:awesome_secret]
|
59
|
+
```
|
60
|
+
|
61
|
+
### Reading data fro CLI
|
62
|
+
|
63
|
+
```
|
64
|
+
$ itamae-secrets get --base=./secret awesome_secret
|
65
|
+
```
|
66
|
+
|
67
|
+
### Remembering `--base`
|
68
|
+
|
69
|
+
```
|
70
|
+
$ echo 'base: ./secret' >> .itamae-secrets.yml
|
71
|
+
```
|
72
|
+
|
73
|
+
## Development
|
74
|
+
|
75
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
76
|
+
|
77
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
78
|
+
|
79
|
+
## Contributing
|
80
|
+
|
81
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/itamae-secrets.
|
82
|
+
|
83
|
+
__Security issues?__ Send me directly at `security@sorah.jp`. My GPG key is available here: <http://sorah.jp/id.html> ([SSL](https://github.com/sorah/sorah.jp/tree/master/source/pgp-pubkeys))
|
84
|
+
|
85
|
+
|
86
|
+
## License
|
87
|
+
|
88
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
89
|
+
|
90
|
+
## To-dos
|
91
|
+
|
92
|
+
- [ ] Missing test :(
|
data/Rakefile
ADDED
data/bin/itamae-secrets
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'itamae/secrets/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "itamae-secrets"
|
8
|
+
spec.version = Itamae::Secrets::VERSION
|
9
|
+
spec.authors = ["Shota Fukumori (sora_h)"]
|
10
|
+
spec.email = ["her@sorah.jp"]
|
11
|
+
|
12
|
+
spec.summary = %q{Encrypted Data Bag for itamae}
|
13
|
+
spec.homepage = "https://github.com/sorah/itamae-secrets"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "bin"
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'thor'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Itamae
|
5
|
+
module Secrets
|
6
|
+
class AesKey
|
7
|
+
AES1_KEY_LEN = OpenSSL::Cipher.new('aes-256-gcm').key_len
|
8
|
+
|
9
|
+
def self.key_len_for_type(type)
|
10
|
+
case type
|
11
|
+
when 'aes1'
|
12
|
+
AES1_KEY_LEN
|
13
|
+
else
|
14
|
+
raise ArgumentError, "unknown type #{type.inspect}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.generate_random(name)
|
19
|
+
key_len = key_len_for_type('aes1')
|
20
|
+
new name, 'aes1', OpenSSL::Random.random_bytes(key_len)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.generate_pkcs5(name, passphrase)
|
24
|
+
key_len = key_len_for_type('aes1')
|
25
|
+
|
26
|
+
salt = OpenSSL::Digest::SHA256.digest(name)
|
27
|
+
key = OpenSSL::PKCS5.pbkdf2_hmac(passphrase, salt, 30000, key_len, OpenSSL::Digest::SHA256.new)
|
28
|
+
|
29
|
+
new name, 'aes1', key
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.load_json(json)
|
33
|
+
data = JSON.parse(json)
|
34
|
+
new(data['name'], data['type'], data['key'].unpack('m*')[0])
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(name, type, key)
|
38
|
+
raise ArgumentError, "name must not contain slashes, commas, backslackes" if name.include?("\\") || name.include?(?/) || name.include?(?:)
|
39
|
+
@name = name
|
40
|
+
@type = type
|
41
|
+
@key = key
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :name, :type, :key
|
45
|
+
|
46
|
+
def algorithm_compatible?(algorithm)
|
47
|
+
algorithm == 'aes-256-gcm'
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
key
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_json
|
55
|
+
{
|
56
|
+
name: name,
|
57
|
+
type: type,
|
58
|
+
key: [key].pack('m*'),
|
59
|
+
}.to_json
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
require 'itamae/secrets/aes_key'
|
6
|
+
require 'itamae/secrets/store'
|
7
|
+
|
8
|
+
module Itamae
|
9
|
+
module Secrets
|
10
|
+
class Cli < Thor
|
11
|
+
class_option :base, type: :string, desc: 'path to base directory for storing secrets and keys'
|
12
|
+
|
13
|
+
|
14
|
+
desc 'newkey [KEYNAME]', 'generate then save key'
|
15
|
+
method_option :method, type: :string, required: true, desc: 'generating method (aes-file, aes-passphrase)'
|
16
|
+
method_option :confirm_passphrase, type: :boolean, default: true, desc: 'Confirm passphrase when asking'
|
17
|
+
|
18
|
+
def newkey(name='default')
|
19
|
+
if keychain.exist?(name)
|
20
|
+
raise ArgumentError, "key #{name} already exists"
|
21
|
+
end
|
22
|
+
|
23
|
+
key = case options[:method] || config['generate_method']
|
24
|
+
when 'aes-random'
|
25
|
+
AesKey.generate_random(name)
|
26
|
+
when 'aes-passphrase'
|
27
|
+
passphrase = ask_noecho('Passphrase:', true)
|
28
|
+
AesKey.generate_pkcs5(name, passphrase)
|
29
|
+
else
|
30
|
+
raise ArgumentError, "Unknown method: #{name.inspect}"
|
31
|
+
end
|
32
|
+
|
33
|
+
keychain.save(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'set VARNAME [VALUE]', 'store value (when VALUE is omitted, read from STDIN)'
|
37
|
+
method_option :key, type: :string, default: 'default', desc: 'key name'
|
38
|
+
method_option :noecho, type: :boolean, desc: 'Ask one-line value with noecho when stdin is tty'
|
39
|
+
def set(name, value = nil)
|
40
|
+
value ||= if options[:noecho]
|
41
|
+
ask_noecho("#{name}:", false)
|
42
|
+
else
|
43
|
+
$stdin.read.chomp
|
44
|
+
end
|
45
|
+
|
46
|
+
store.store(name, value, options[:key])
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'get VARNAME', 'read value'
|
50
|
+
def get(name)
|
51
|
+
value = store[name]
|
52
|
+
if value
|
53
|
+
puts value
|
54
|
+
else
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def store
|
62
|
+
@store ||= Store.new base_dir
|
63
|
+
end
|
64
|
+
|
65
|
+
def keychain
|
66
|
+
store.keychain
|
67
|
+
end
|
68
|
+
|
69
|
+
def base_dir
|
70
|
+
unless config['base']
|
71
|
+
raise ArgumentError, 'Missing --base'
|
72
|
+
end
|
73
|
+
Pathname.new config['base']
|
74
|
+
end
|
75
|
+
|
76
|
+
def config
|
77
|
+
@config ||= config_file.merge(
|
78
|
+
'base' => config_file['base'] || options[:base],
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def config_file
|
83
|
+
@config_file ||= if File.exist?('./.itamae-secrets.yml')
|
84
|
+
YAML.load_file('./.itamae-secrets.yml')
|
85
|
+
else
|
86
|
+
{}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def ask_noecho(prompt, confirm = true)
|
91
|
+
io_console = false
|
92
|
+
begin
|
93
|
+
require 'io/console'
|
94
|
+
io_console = true
|
95
|
+
rescue LoadError
|
96
|
+
end
|
97
|
+
|
98
|
+
get = -> do
|
99
|
+
if $stdin.tty?
|
100
|
+
$stdin.noecho { $stdin.gets.chomp }
|
101
|
+
else
|
102
|
+
$stdin.gets.chomp
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
loop do
|
107
|
+
$stdout.print "#{prompt} "
|
108
|
+
value = get.call
|
109
|
+
|
110
|
+
break value unless confirm
|
111
|
+
|
112
|
+
$stdout.print "(confirm) #{prompt} "
|
113
|
+
break value if value == get.call
|
114
|
+
|
115
|
+
$stderr.puts "Confirmation didn't match..."
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Itamae
|
4
|
+
module Secrets
|
5
|
+
class Decryptor
|
6
|
+
ALGORITHM = 'aes-256-gcm'
|
7
|
+
|
8
|
+
def self.load_json(json, key = nil)
|
9
|
+
data = JSON.parse(json)
|
10
|
+
|
11
|
+
raise ArgumentError, "unknown version #{data['version'].inspect}" if data['version'] != 1
|
12
|
+
raise ArgumentError, "unknown version #{data['algorithm'].inspect}" if data['algorithm'] != ALGORITHM
|
13
|
+
|
14
|
+
new(
|
15
|
+
data['ciphertext'],
|
16
|
+
data['auth_tag'],
|
17
|
+
data['iv'],
|
18
|
+
data['key_name'],
|
19
|
+
key
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(ciphertext, auth_tag, iv, key_name, key = nil)
|
24
|
+
ensure_algorithm_key_compatiblity!(key) if key
|
25
|
+
@ciphertext = ciphertext
|
26
|
+
@auth_tag = auth_tag
|
27
|
+
@iv = iv
|
28
|
+
@key_name = key_name
|
29
|
+
@key = key
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :ciphertext, :auth_tag, :iv, :key_name
|
33
|
+
attr_accessor :key
|
34
|
+
|
35
|
+
def key=(other)
|
36
|
+
raise "can't overwrite" if @key
|
37
|
+
ensure_algorithm_key_compatiblity!(other)
|
38
|
+
@key = other
|
39
|
+
end
|
40
|
+
|
41
|
+
def plaintext
|
42
|
+
@plaintext ||= begin
|
43
|
+
txt = cipher.update(ciphertext.unpack('m*')[0])
|
44
|
+
txt << cipher.final
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def version
|
49
|
+
1
|
50
|
+
end
|
51
|
+
|
52
|
+
def algorithm
|
53
|
+
ALGORITHM
|
54
|
+
end
|
55
|
+
|
56
|
+
def cipher
|
57
|
+
@cipher ||= OpenSSL::Cipher.new(algorithm).tap do |c|
|
58
|
+
raise 'key is required to proceed' unless key
|
59
|
+
c.decrypt
|
60
|
+
c.key = key.to_s
|
61
|
+
c.iv = iv.unpack('m*')[0]
|
62
|
+
c.auth_data = ''
|
63
|
+
c.auth_tag = auth_tag.unpack('m*')[0]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def ensure_algorithm_key_compatiblity!(key)
|
70
|
+
unless key.algorithm_compatible?(algorithm)
|
71
|
+
raise ArgumentError, "#{key.type} is not compatible"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Itamae
|
4
|
+
module Secrets
|
5
|
+
class Encryptor
|
6
|
+
ALGORITHM = 'aes-256-gcm'
|
7
|
+
|
8
|
+
def initialize(plaintext, key = nil, iv = nil)
|
9
|
+
ensure_algorithm_key_compatiblity!(key) if key
|
10
|
+
@key = key
|
11
|
+
@iv = iv
|
12
|
+
@plaintext = plaintext
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :key, :plaintext
|
16
|
+
|
17
|
+
def key=(other)
|
18
|
+
raise "can't overwrite" if @key
|
19
|
+
ensure_algorithm_key_compatiblity!(other)
|
20
|
+
@key = other
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
{
|
25
|
+
version: version,
|
26
|
+
algorithm: algorithm,
|
27
|
+
key_name: key.name,
|
28
|
+
ciphertext: ciphertext,
|
29
|
+
iv: iv,
|
30
|
+
auth_tag: auth_tag,
|
31
|
+
}.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
alias data to_s
|
35
|
+
|
36
|
+
def ciphertext
|
37
|
+
@ciphertext ||= begin
|
38
|
+
data = cipher.update(plaintext)
|
39
|
+
data << cipher.final
|
40
|
+
@auth_tag = cipher.auth_tag
|
41
|
+
[data].pack('m*')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def iv
|
46
|
+
@iv && [@iv].pack('m*')
|
47
|
+
end
|
48
|
+
|
49
|
+
def auth_tag
|
50
|
+
if @auth_tag
|
51
|
+
[@auth_tag].pack('m*')
|
52
|
+
else
|
53
|
+
raise '[BUG] auth_tag not exists'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def version
|
58
|
+
1
|
59
|
+
end
|
60
|
+
|
61
|
+
def algorithm
|
62
|
+
ALGORITHM
|
63
|
+
end
|
64
|
+
|
65
|
+
def cipher
|
66
|
+
@cipher ||= OpenSSL::Cipher.new(algorithm).tap do |c|
|
67
|
+
raise 'key is required to proceed' unless key
|
68
|
+
c.encrypt
|
69
|
+
c.key = key.to_s
|
70
|
+
# XXX: avoid generate IV here, but consider if extract to a method like #iv, it have to know Cipher#iv_len...
|
71
|
+
@iv ||= c.random_iv
|
72
|
+
c.iv = @iv
|
73
|
+
c.auth_data = ''
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def ensure_algorithm_key_compatiblity!(key)
|
80
|
+
unless key.algorithm_compatible?(algorithm)
|
81
|
+
raise ArgumentError, "#{key.type} is not compatible"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'itamae/secrets/aes_key'
|
3
|
+
|
4
|
+
module Itamae
|
5
|
+
module Secrets
|
6
|
+
class Keychain
|
7
|
+
class KeyNotFound < StandardError; end
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = Pathname.new(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
def exist?(name)
|
16
|
+
@path.join(name).exist?
|
17
|
+
end
|
18
|
+
|
19
|
+
def load(name)
|
20
|
+
AesKey.load_json @path.join(name).read
|
21
|
+
rescue File::ENOENT
|
22
|
+
raise KeyNotFound, "Couldn't find key #{name.inspect}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def save(key)
|
26
|
+
open(@path.join(key.name), 'w', 0600) do |io|
|
27
|
+
io.puts key.to_json
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'itamae/secrets/keychain'
|
4
|
+
require 'itamae/secrets/encryptor'
|
5
|
+
require 'itamae/secrets/decryptor'
|
6
|
+
|
7
|
+
module Itamae
|
8
|
+
module Secrets
|
9
|
+
class Store
|
10
|
+
def initialize(base_dir)
|
11
|
+
@base_dir = Pathname.new(base_dir)
|
12
|
+
ensure_base_dir!
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :base_dir
|
16
|
+
|
17
|
+
def keychain_path
|
18
|
+
base_dir.join('keys')
|
19
|
+
end
|
20
|
+
|
21
|
+
def values_path
|
22
|
+
base_dir.join('values')
|
23
|
+
end
|
24
|
+
|
25
|
+
def keychain
|
26
|
+
@keychain ||= Keychain.new(keychain_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](name)
|
30
|
+
name = name.to_s
|
31
|
+
validate_name!(name)
|
32
|
+
|
33
|
+
value_path = values_path.join(name)
|
34
|
+
|
35
|
+
if value_path.exist?
|
36
|
+
encrypted_data = Decryptor.load_json(value_path.read)
|
37
|
+
encrypted_data.key = keychain.load(encrypted_data.key_name)
|
38
|
+
JSON.parse(encrypted_data.plaintext)['value']
|
39
|
+
else
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(*args)
|
45
|
+
case args.size
|
46
|
+
when 2
|
47
|
+
store(*args)
|
48
|
+
when 3
|
49
|
+
store(args[0], args[2], args[1])
|
50
|
+
else
|
51
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 2..3)"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def store(name, value, key = 'default')
|
56
|
+
name = name.to_s
|
57
|
+
validate_name!(name)
|
58
|
+
value_path = values_path.join(name)
|
59
|
+
|
60
|
+
encrypted_data = Encryptor.new({value: value}.to_json, keychain.load(key))
|
61
|
+
|
62
|
+
open(value_path, 'w', 0600) do |io|
|
63
|
+
io.puts encrypted_data.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def ensure_base_dir!
|
70
|
+
base_dir.mkpath
|
71
|
+
base_dir.join('keys').mkpath
|
72
|
+
base_dir.join('values').mkpath
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_name!(name)
|
76
|
+
# XXX: dupe
|
77
|
+
raise ArgumentError, "name must not contain slashes, commas, backslackes" if name.include?("\\") || name.include?(?/) || name.include?(?:)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "itamae/secrets"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/script/setup
ADDED
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: itamae-secrets
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shota Fukumori (sora_h)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-25 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: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '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: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- her@sorah.jp
|
72
|
+
executables:
|
73
|
+
- itamae-secrets
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- ".rspec"
|
79
|
+
- ".travis.yml"
|
80
|
+
- CONTRIBUTING.md
|
81
|
+
- Gemfile
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- bin/itamae-secrets
|
86
|
+
- itamae-secrets.gemspec
|
87
|
+
- lib/itamae/secrets.rb
|
88
|
+
- lib/itamae/secrets/aes_key.rb
|
89
|
+
- lib/itamae/secrets/cli.rb
|
90
|
+
- lib/itamae/secrets/decryptor.rb
|
91
|
+
- lib/itamae/secrets/encryptor.rb
|
92
|
+
- lib/itamae/secrets/keychain.rb
|
93
|
+
- lib/itamae/secrets/store.rb
|
94
|
+
- lib/itamae/secrets/version.rb
|
95
|
+
- script/console
|
96
|
+
- script/setup
|
97
|
+
homepage: https://github.com/sorah/itamae-secrets
|
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: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 2.4.5
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Encrypted Data Bag for itamae
|
121
|
+
test_files: []
|