dynamo_secret 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eceed7ab952b1bfb775f44552c484acd65854864
4
+ data.tar.gz: 8c1b80312dedd3e880bcbff04ad3836cb577e56e
5
+ SHA512:
6
+ metadata.gz: 6ac8d52f9f3d97d0d4b7d921454484d533858021624d618b673f2b06e45c2f42e0f416cea761e20542a71b2f123d9e91262e589ba7513c36ac19a217f76ddc24
7
+ data.tar.gz: 29eb552532e791d797ca5db55397950e9552bd66f6d74a33cdd0a076222320e28ebb947e6ae66ffbf5f2e8107daf553d611bc516abf6907e9ef4779b8a6afc1e
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Unreleased Changes
2
+
3
+ # 0.1.0 (2017-10-15)
4
+ * initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Robert Bayerl
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # dynamo_secret
2
+ Ruby gem for encrypting secrets with [GnuPG](https://gnupg.org/) and/or
3
+ [KMS](https://aws.amazon.com/kms/) and storing them in
4
+ [DynamoDB](https://aws.amazon.com/dynamodb/).
5
+
6
+ ## Usage
7
+ `dynamo_secret` can be used to store, fetch, update, and delete encrypted
8
+ information. It is intended to be used as a remote password store, but could be
9
+ used for other things as well. Data is organized by site, and can contain
10
+ almost anything.
11
+ Usage:
12
+ ```
13
+ dynamo_secret -l|--list
14
+ dynamo_secret -i|--init [-k|--kms]
15
+ dynamo_secret -g|--get [site] [key1,key2,...]
16
+ dynamo_secret -a|--add [site] [key1,key2,...] [val1,val2,...]
17
+ dynamo_secret -u|--update [site] [key1,key2,...] [val1,val2,...]
18
+ dynamo_secret -d|--delete [site]
19
+ ```
20
+
21
+ ### List
22
+ `dynamo_secret -l` will list all of the sites stored in the DynamoDB table.
23
+
24
+ ### Init
25
+ Before storing secrets the table needs to be created. `dynamo_secret -i [-k]`
26
+ will create the table. If the optional `-k` flag is supplied a KMS key will
27
+ also be created. KMS keys do not qualify for free tier usage and will cost $1
28
+ or more per month.
29
+
30
+ ### Get
31
+ `dynamo_secret -g|--get [site] [key1,key2,...]` will retreive and decrypt
32
+ information stored under the specified site. Specific fields (keys) can also
33
+ be specified if not all fields are wanted or required.
34
+
35
+ ### Add
36
+ `dynamo_secret -a|--add [site] [key1,key2,...] [val1,val2,...]` stores key
37
+ value pairs under `site`. Values may be omitted to keep them out of history
38
+ files, or `-` may be used for extra sensitive secrets.
39
+
40
+ ### Update
41
+ `dynamo_secret -u|--update` works exactly like `--put`, but it replaces the
42
+ specified key value pairs while keeping anything else.
43
+
44
+ ### Delete
45
+ `dynamo_secret -d|--delete [site]` completely removes all records under `site`
46
+ from the DynamoDB table.
data/bin/dynamo_secret ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dynamo_secret'
4
+
5
+ DynamoSecret::CLI.new(ARGV).run
@@ -0,0 +1,98 @@
1
+ module DynamoSecret
2
+ class CLI
3
+ def initialize(args)
4
+ @args = args
5
+ end
6
+
7
+ def run
8
+ load_config
9
+ parse_args(@args)
10
+ perform_action
11
+ end
12
+
13
+ private
14
+
15
+ def ask_key_pairs(keys, values)
16
+ keys = keys.to_s.split(',')
17
+ values = values.to_s.split(',')
18
+ if keys.empty?
19
+ loop do
20
+ key = ask('Key [ENTER to quit]: ')
21
+ break if key == ''
22
+ keys << key
23
+ end
24
+ end
25
+ keys.map.with_index do |key, index|
26
+ { key => values[index].nil? || values[index] == '-' ? ask("Value for #{key}: ") : values[index] }
27
+ end
28
+ end
29
+
30
+ def ask_secret_data
31
+ if @args.count > 3
32
+ $stderr.puts usage
33
+ exit 1
34
+ else
35
+ @config[:secret_data][site] = ask_key_pairs(@args.shift, @args.shift)
36
+ end
37
+ end
38
+
39
+ def load_config
40
+ config_file = "#{ENV['HOME']}/.dynamo_secret.yml"
41
+ @config = File.exist?(config_file) ? YAML.load_file(config_file) : {}
42
+ @config[:secret_data] = {}
43
+ end
44
+
45
+ def parse_args(args)
46
+ OptionParser.new do |opts|
47
+ opts.banner = usage
48
+ opts.version = VERSION
49
+ opts.on('-l', '--list', 'List all sites stored in table') { |_l| @action = 'list' }
50
+ opts.on('-i', '--init', 'Set up DynamoDB and KMS') { |_i| @action = 'init' }
51
+ opts.on('-g', '--get', 'Get a secret') { |_g| @action = 'get' }
52
+ opts.on('-a', '--add', 'Add a new secret') { |_a| @action = 'put' }
53
+ opts.on('-u', '--update', 'Update an existing secret') { |_u| @action = 'update' }
54
+ opts.on('-d', '--delete', 'Remove site from table') { |_d| @action = 'delete' }
55
+ opts.on('-k', '--kms', 'Enable KMS key creation (init only)') { |k| @config[:enable_kms] = k }
56
+ end.parse!(args)
57
+ @args = args
58
+ end
59
+
60
+ def perform_action
61
+ case @action
62
+ when 'init'
63
+ Secret.new(@config).setup
64
+ when 'get'
65
+ @config[:secret_data][site] = []
66
+ Secret.new(@config).get(@args.shift)
67
+ when 'put'
68
+ ask_secret_data
69
+ Secret.new(@config).put
70
+ when 'update'
71
+ ask_secret_data
72
+ Secret.new(@config).update
73
+ when 'delete'
74
+ @config[:secret_data][site] = []
75
+ Secret.new(@config).delete
76
+ when 'list'
77
+ DynamoDB.new(@config).list_secrets
78
+ else
79
+ $stderr.puts usage
80
+ exit 1
81
+ end
82
+ end
83
+
84
+ def site
85
+ @site ||= @args.any? ? @args.shift : ask('Site: ')
86
+ end
87
+
88
+ def usage
89
+ 'Usage:
90
+ dynamo_secret -l|--list
91
+ dynamo_secret -i|--init [-k|--kms]
92
+ dynamo_secret -g|--get [site] [key1,key2,...]
93
+ dynamo_secret -a|--add [site] [key1,key2,...] [val1,val2,...]
94
+ dynamo_secret -u|--update [site] [key1,key2,...] [val1,val2,...]
95
+ dynamo_secret -d|--delete [site]'
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,70 @@
1
+ module DynamoSecret
2
+ class DynamoDB
3
+ def initialize(config)
4
+ @table_name = config[:table_name] || table_name
5
+ @region = config.fetch(:region, region)
6
+ @secret_data = config.fetch(:secret_data, {})
7
+ end
8
+
9
+ def create_table
10
+ client.create_table(
11
+ attribute_definitions: [{ attribute_name: 'Site', attribute_type: 'S' }],
12
+ table_name: @table_name,
13
+ key_schema: [{ attribute_name: 'Site', key_type: 'HASH' }],
14
+ provisioned_throughput: {
15
+ read_capacity_units: 25,
16
+ write_capacity_units: 25
17
+ }
18
+ )
19
+ $stdout.puts "Created new table: #{@table_name}"
20
+ rescue Aws::DynamoDB::Errors::ResourceInUseException
21
+ $stderr.puts "Table #{@table_name} already exists"
22
+ end
23
+
24
+ def delete
25
+ client.delete_item(
26
+ key: {
27
+ 'Site' => @secret_data.map { |k, _v| k }.first
28
+ },
29
+ table_name: @table_name
30
+ )
31
+ end
32
+
33
+ def fetch_secret
34
+ client.get_item(
35
+ key: {
36
+ 'Site' => @secret_data.map { |k, _v| k }.first
37
+ },
38
+ table_name: @table_name
39
+ ).item
40
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
41
+ $stderr.puts "Table #{@table_name} not found"
42
+ exit 1
43
+ end
44
+
45
+ def list_secrets
46
+ client.scan(table_name: @table_name).items.each { |item| $stdout.puts item['Site'] }
47
+ end
48
+
49
+ def put_secret(secret_data)
50
+ client.put_item(
51
+ item: secret_data,
52
+ table_name: @table_name
53
+ )
54
+ end
55
+
56
+ private
57
+
58
+ def client
59
+ @client ||= Aws::DynamoDB::Client.new(region: @region)
60
+ end
61
+
62
+ def region
63
+ ENV.fetch('AWS_REGION', 'us-west-2')
64
+ end
65
+
66
+ def table_name
67
+ "dynamo_secret_#{IAM.new.user_id}"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,24 @@
1
+ module DynamoSecret
2
+ class Gpg
3
+ def decrypt(data)
4
+ crypto.decrypt(data).read
5
+ rescue GPGME::Error::NoData
6
+ $stderr.puts 'Key was found but GPG decrypt failed - skipping'
7
+ data
8
+ end
9
+
10
+ def encrypt(data)
11
+ crypto.encrypt(data, recipients: [key.uids.first.name]).read
12
+ end
13
+
14
+ def key
15
+ @gpg_key ||= GPGME::Key.find(:secret).map { |k| k if k.expires > Date.today.to_time }.first
16
+ end
17
+
18
+ private
19
+
20
+ def crypto
21
+ @crypto ||= GPGME::Crypto.new
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ module DynamoSecret
2
+ class IAM
3
+ def user_id
4
+ @user ||= Aws::IAM::CurrentUser.new(region: 'us-west-2')
5
+ @user.user_name || @user.user_id
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,49 @@
1
+ module DynamoSecret
2
+ class Kms
3
+ def initialize(config)
4
+ @key_name = config[:key_name] || key_name
5
+ @region = config.fetch(:region, region)
6
+ end
7
+
8
+ def create_key
9
+ return $stdout.puts "KMS alias #{@key_name} already exists" if key
10
+ id = client.create_key(tags: [{ tag_key: 'Owner', tag_value: user_id }]).key_metadata.key_id
11
+ client.create_alias(alias_name: "alias/#{@key_name}", target_key_id: id)
12
+ end
13
+
14
+ def decrypt(data)
15
+ client.decrypt(ciphertext_blob: data).plaintext
16
+ rescue Aws::KMS::Errors::InvalidCiphertextException
17
+ $stderr.puts 'Key was found but KMS decrypt failed - skipping'
18
+ data
19
+ end
20
+
21
+ def encrypt(data)
22
+ client.encrypt(key_id: key, plaintext: data).ciphertext_blob
23
+ end
24
+
25
+ def key
26
+ @key ||= client.list_aliases.aliases.map do |kms_alias|
27
+ kms_alias.target_key_id if kms_alias.alias_name == "alias/#{@key_name}"
28
+ end.compact.first
29
+ end
30
+
31
+ private
32
+
33
+ def client
34
+ @client ||= Aws::KMS::Client.new(region: @region)
35
+ end
36
+
37
+ def key_name
38
+ "dynamo_secret_#{user_id}"
39
+ end
40
+
41
+ def region
42
+ ENV.fetch('AWS_REGION', 'us-west-2')
43
+ end
44
+
45
+ def user_id
46
+ @user_id ||= IAM.new.user_id
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,97 @@
1
+ module DynamoSecret
2
+ class Secret
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def delete
8
+ resp = ask("Really delete #{site}? (y/N) ")
9
+ return unless resp.casecmp('y')
10
+ dynamodb.delete
11
+ $stdout.puts "#{site} deleted"
12
+ end
13
+
14
+ def get(fields)
15
+ secret = dynamodb.fetch_secret
16
+ return decrypt(secret, fields) if secret
17
+ $stderr.puts "Could not find record for #{site}"
18
+ exit 1
19
+ end
20
+
21
+ def put
22
+ if gpg.key.nil? && kms.key.nil?
23
+ $stderr.puts 'Refusing to store secrets in plain text'
24
+ exit 1
25
+ elsif dynamodb.fetch_secret
26
+ $stderr.puts "Site #{site} already exists"
27
+ exit 1
28
+ else
29
+ secret = encrypt
30
+ dynamodb.put_secret(secret)
31
+ end
32
+ end
33
+
34
+ def setup
35
+ dynamodb.create_table
36
+ kms.create_key unless @config.fetch(:enable_kms, nil).nil?
37
+ end
38
+
39
+ def update
40
+ secret = dynamodb.fetch_secret.merge(encrypt)
41
+ dynamodb.put_secret(secret)
42
+ end
43
+
44
+ private
45
+
46
+ def decode(data)
47
+ data = Base64.decode64(data)
48
+ data = kms.decrypt(data) if kms.key
49
+ data = gpg.decrypt(data) if gpg.key
50
+ data
51
+ end
52
+
53
+ def decrypt(data, fields)
54
+ headers = [['Key', 'Value'], ['---', '-----']]
55
+ fields ||= [['Site', data['Site']]] + data.map { |k, v| [k, decode(v)] unless k == 'Site' }.compact
56
+ output = if fields.is_a?(Array)
57
+ headers + fields
58
+ else
59
+ headers + data.map { |k, v| [k, decode(v)] if fields.include?(k) }.compact
60
+ end
61
+ widths = output.transpose.map { |x| x.map(&:length).max }.map { |w| "%-#{w}s" }.join(' ')
62
+ output.each { |line| $stdout.puts widths % line }
63
+ end
64
+
65
+ def encode(data)
66
+ data = gpg.encrypt(data) if gpg.key
67
+ data = kms.encrypt(data) if kms.key
68
+ Base64.encode64(data)
69
+ end
70
+
71
+ def dynamodb
72
+ @dynamodb ||= DynamoDB.new(@config)
73
+ end
74
+
75
+ def encrypt
76
+ encrypted_data = {
77
+ 'Site' => site
78
+ }
79
+ @config[:secret_data][site].each do |kv|
80
+ kv.map { |k, v| encrypted_data[k] = encode(v) }
81
+ end
82
+ encrypted_data
83
+ end
84
+
85
+ def gpg
86
+ @gpg ||= Gpg.new
87
+ end
88
+
89
+ def kms
90
+ @kms ||= Kms.new(@config)
91
+ end
92
+
93
+ def site
94
+ @config[:secret_data].map { |k, _v| k }.first
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,3 @@
1
+ module DynamoSecret
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'aws-sdk-dynamodb'
2
+ require 'aws-sdk-iam'
3
+ require 'aws-sdk-kms'
4
+ require 'gpgme'
5
+ require 'highline/import'
6
+ require 'optparse'
7
+ require 'yaml'
8
+ require 'dynamo_secret/cli'
9
+ require 'dynamo_secret/dynamodb'
10
+ require 'dynamo_secret/gpg'
11
+ require 'dynamo_secret/iam'
12
+ require 'dynamo_secret/kms'
13
+ require 'dynamo_secret/secret'
14
+ require 'dynamo_secret/version'
metadata ADDED
@@ -0,0 +1,196 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamo_secret
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rob Bayerl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-dynamodb
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: aws-sdk-iam
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
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: aws-sdk-kms
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
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: gpgme
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: highline
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bump
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: single_cov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Encrypt and decrypt secrets stored in DynamoDB with GPG and/or KMS
154
+ email:
155
+ executables:
156
+ - dynamo_secret
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - CHANGELOG.md
161
+ - LICENSE
162
+ - README.md
163
+ - bin/dynamo_secret
164
+ - lib/dynamo_secret.rb
165
+ - lib/dynamo_secret/cli.rb
166
+ - lib/dynamo_secret/dynamodb.rb
167
+ - lib/dynamo_secret/gpg.rb
168
+ - lib/dynamo_secret/iam.rb
169
+ - lib/dynamo_secret/kms.rb
170
+ - lib/dynamo_secret/secret.rb
171
+ - lib/dynamo_secret/version.rb
172
+ homepage: https://github.com/rbayerl/dynamo_secret
173
+ licenses:
174
+ - MIT
175
+ metadata: {}
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubyforge_project:
192
+ rubygems_version: 2.6.11
193
+ signing_key:
194
+ specification_version: 4
195
+ summary: Store and fetch encrypted secrets in DynamoDB
196
+ test_files: []