team-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e7ac9a7e715809aaea125c89cc5c7a340b7638ae
4
+ data.tar.gz: 5f420a2dc9e61768dcab3b255fabc60520198b89
5
+ SHA512:
6
+ metadata.gz: d602632ed1ca49c6cb89ad589fc33bddc20a5ed36899f7121cf81ac0ea6c51ef40835722b631b00f02851d5717ba456d2c182a53f4d77f901ed670db4c7c005b
7
+ data.tar.gz: 17d5c369d624b4bf9ec4691b12cb21da3dc9edb488032b9799769e5f14d2ee07b2ff263a1bfa7bc9a56ce9f90ea62bab94084a400cf7332f541ad8baed50bf14
data/.gitignore ADDED
@@ -0,0 +1,59 @@
1
+ # Script-generated
2
+ *.yml
3
+ *.yaml
4
+ users/
5
+
6
+ # Mac
7
+ .DS_Store
8
+
9
+ # General
10
+ *.gem
11
+ *.rbc
12
+ /.config
13
+ /coverage/
14
+ /InstalledFiles
15
+ /pkg/
16
+ /spec/reports/
17
+ /spec/examples.txt
18
+ /test/tmp/
19
+ /test/version_tmp/
20
+ /tmp/
21
+
22
+ # Used by dotenv library to load environment variables.
23
+ # .env
24
+
25
+ ## Specific to RubyMotion:
26
+ .dat*
27
+ .repl_history
28
+ build/
29
+ *.bridgesupport
30
+ build-iPhoneOS/
31
+ build-iPhoneSimulator/
32
+
33
+ ## Specific to RubyMotion (use of CocoaPods):
34
+ #
35
+ # We recommend against adding the Pods directory to your .gitignore. However
36
+ # you should judge for yourself, the pros and cons are mentioned at:
37
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
38
+ #
39
+ # vendor/Pods/
40
+
41
+ ## Documentation cache and generated files:
42
+ /.yardoc/
43
+ /_yardoc/
44
+ /doc/
45
+ /rdoc/
46
+
47
+ ## Environment normalization:
48
+ /.bundle/
49
+ /vendor/bundle
50
+ /lib/bundler/man/
51
+
52
+ # for a library or gem, you might want to ignore these files since the code is
53
+ # intended to run in multiple environments; otherwise, check them in:
54
+ # Gemfile.lock
55
+ # .ruby-version
56
+ # .ruby-gemset
57
+
58
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
59
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.0
4
+ os: linux
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "rake"
4
+
5
+ gem "gli"
6
+ gem "hacer"
7
+
8
+ gem "rspec"
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.2.5)
5
+ gli (2.14.0)
6
+ hacer (0.0.3)
7
+ rake (12.0.0)
8
+ rspec (3.5.0)
9
+ rspec-core (~> 3.5.0)
10
+ rspec-expectations (~> 3.5.0)
11
+ rspec-mocks (~> 3.5.0)
12
+ rspec-core (3.5.4)
13
+ rspec-support (~> 3.5.0)
14
+ rspec-expectations (3.5.0)
15
+ diff-lcs (>= 1.2.0, < 2.0)
16
+ rspec-support (~> 3.5.0)
17
+ rspec-mocks (3.5.0)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.5.0)
20
+ rspec-support (3.5.0)
21
+
22
+ PLATFORMS
23
+ ruby
24
+
25
+ DEPENDENCIES
26
+ gli
27
+ hacer
28
+ rake
29
+ rspec
30
+
31
+ BUNDLED WITH
32
+ 1.13.7
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # Team-Secrets #
2
+
3
+ [![Build Status](https://travis-ci.org/firelit/secrets.svg?branch=master)](https://travis-ci.org/firelit/secrets)
4
+
5
+ This is a command line utility for sharing secrets (passwords, api keys, etc) among a team and with servers. Store your passwords in a git repository and track changes without keeping sensitive data laying around all plain-text.
6
+
7
+ ## How It Works ##
8
+
9
+ All secrets are encrypted with symmetric AES encryption and stored in a YAML file. The encryption key is then encrypted for each user of the system using asymetric public-key encryption. Then each user can decrypt the master key and reveal secrets or add new ones when needed. Everything is signed to prevent tampering and encryption keys are rotated when users are added or removed (so that new users can decrypt past secrets and old users can't decrypt new secrets).
10
+
11
+ With the tag feature, you can filter credentials which has many different use cases. For isntance, use tags to differentiate between secrets used in different environments, such as `DEV`, `QA` and `PROD`.
12
+
13
+ ## Installing ##
14
+
15
+ To use, install as a global gem.
16
+
17
+ ```
18
+ gem install team-secrets
19
+ ```
20
+
21
+ ## How To Use ##
22
+
23
+ To start a new repo for your secrets:
24
+ ```
25
+ team-secrets init
26
+ ```
27
+
28
+ You'll be the first user and you'll be prompted for a user name to use and the path to your public key. Your public key will be added to the project, along with the initial YAML files.
29
+
30
+ You can then add new users:
31
+ ```
32
+ team-secrets users add
33
+ ```
34
+
35
+ And, new secrets:
36
+ ```
37
+ team-secrets secrets add
38
+ ```
39
+
40
+ Then, commit your changes and push to your central repository. Anyone you add will be able to access the secrets and manage users with through their private key.
41
+
42
+ Retrieve all secrets:
43
+ ```
44
+ team-secrets secrets list
45
+ ```
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:default) do |task|
6
+ task.pattern = 'spec/*.rb'
7
+ end
@@ -0,0 +1,31 @@
1
+ require 'yaml'
2
+
3
+ class FileManager
4
+
5
+ attr_accessor :data
6
+
7
+ def loadFile(file = nil)
8
+ if block_given?
9
+ string_data = yield
10
+ else
11
+ raise 'No file given' if file.nil?
12
+ string_data = File.read(file)
13
+ end
14
+
15
+ @data = YAML.load(string_data)
16
+ end
17
+
18
+ def writeFile(file = nil)
19
+ yaml = @data.to_yaml
20
+
21
+ if block_given?
22
+ yield yaml
23
+ else
24
+ raise 'No file given' if file.nil?
25
+ File.write(file, yaml)
26
+ end
27
+
28
+ yaml
29
+ end
30
+
31
+ end
@@ -0,0 +1,20 @@
1
+ class KeyHelper
2
+
3
+ def self.getPublicKey(file_path)
4
+
5
+ key_string = File.read(file_path)
6
+
7
+ if (key_string[0..7] == 'ssh-rsa ')
8
+ # Test the file conversion
9
+ unless system("ssh-keygen -f #{file_path} -e -m pem > /dev/null 2>&1")
10
+ raise 'Could not convert ssh-rsa public key to PEM format for OpenSSL'
11
+ end
12
+
13
+ key_string = `ssh-keygen -f #{file_path} -e -m pem`
14
+ end
15
+
16
+ key_string
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,72 @@
1
+ require_relative './file_manager'
2
+
3
+ class ManifestManager < FileManager
4
+
5
+ def initialize(master_key)
6
+ unless (master_key.decrypted.is_a? String) && master_key.decrypted.length
7
+ raise 'Master key must be decrypted'
8
+ end
9
+
10
+ @@working_dir = Dir.pwd
11
+ @master_key = master_key
12
+ @data = @data || {}
13
+ end
14
+
15
+ def validate
16
+
17
+ unless File.exists?(@@working_dir +'/manifest.yaml')
18
+ raise 'Required manifest.yaml does not exist'
19
+ end
20
+
21
+ loadFile(@@working_dir +'/manifest.yaml')
22
+
23
+ unless @data.is_a? Object
24
+ raise 'No valid data in manifest.yaml'
25
+ end
26
+
27
+ if @data[:secrets_file].nil? || @data[:users_file].nil?
28
+ raise 'Manifest.yaml must list a secrets_file and users_file'
29
+ end
30
+
31
+ @data.each do |key, value|
32
+
33
+ unless value.is_a? Object
34
+ raise "#{key} does not have required data"
35
+ end
36
+
37
+ unless File.exists?(value[:path])
38
+ raise "#{key} does not exist"
39
+ end
40
+
41
+ file_string = File.read @data[key][:path]
42
+ signature = @master_key.sign file_string
43
+
44
+ unless signature == value[:signature]
45
+ raise "#{key} signature does not match"
46
+ end
47
+
48
+ end
49
+
50
+ true
51
+ end
52
+
53
+ def update
54
+ ['users', 'secrets'].each do |file|
55
+
56
+ file_name = file +'.yaml'
57
+ absolute = @@working_dir +'/'+ file_name
58
+
59
+ unless File.exists?(absolute)
60
+ raise "#{file_name}.yaml does not exist, cannot update manifest"
61
+ end
62
+
63
+ signature = @master_key.sign File.read(absolute)
64
+
65
+ @data[(file + '_file').to_sym] = {
66
+ path: file_name,
67
+ signature: signature
68
+ }
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,73 @@
1
+ require 'openssl'
2
+
3
+ class MasterKey
4
+
5
+ attr_reader :encrypted, :decrypted
6
+
7
+ CONFIG = {
8
+ cipher: 'aes-256-cbc',
9
+ key_len: 32,
10
+ iv_len: 16
11
+ }
12
+
13
+ def initialize(key, encrypted = true)
14
+ @defaultCipher = 'AES-256-CBC'
15
+
16
+ if (encrypted)
17
+ @encrypted = key
18
+ else
19
+ @decrypted = key
20
+ end
21
+ end
22
+
23
+ def self.generate
24
+ cipher = OpenSSL::Cipher.new(CONFIG[:cipher])
25
+ cipher.encrypt
26
+ self.new(cipher.random_key, false)
27
+ end
28
+
29
+ def encryptWithPublicKey(public_key)
30
+ key = OpenSSL::PKey::RSA.new public_key
31
+ raise 'Not a public key' unless key.public?
32
+ @encrypted = key.public_encrypt @decrypted
33
+ end
34
+
35
+ def decryptWithPrivateKey(private_key, pass_phrase = nil)
36
+ key = OpenSSL::PKey::RSA.new private_key, pass_phrase
37
+ raise 'Not a private key' unless key.private?
38
+ @decrypted = key.private_decrypt @encrypted
39
+ end
40
+
41
+ def encryptSecret(secret)
42
+ cipher = OpenSSL::Cipher.new(CONFIG[:cipher])
43
+ cipher.encrypt
44
+ cipher.key = @decrypted
45
+ iv = cipher.random_iv
46
+ iv + cipher.update(secret) + cipher.final
47
+ end
48
+
49
+ def decryptSecret(secret)
50
+ decipher = OpenSSL::Cipher.new(CONFIG[:cipher])
51
+ decipher.decrypt
52
+ decipher.key = @decrypted
53
+ iv_len = CONFIG[:iv_len]
54
+ iv = secret[0..(iv_len-1)]
55
+ secret = secret[iv_len..-1]
56
+ decipher.iv = iv
57
+ decipher.update(secret) + decipher.final
58
+ end
59
+
60
+ def sign(string)
61
+ raise 'Must first decrypt master key with private key' unless (@decrypted.is_a? String) || @decrypted.length
62
+ self.class.bin_to_hex OpenSSL::HMAC.digest('sha256', @decrypted, string)
63
+ end
64
+
65
+ def self.bin_to_hex(s)
66
+ s.each_byte.map { |b| b.to_s(16).rjust(2,'0') }.join
67
+ end
68
+
69
+ def self.hex_to_bin(b)
70
+ [b].pack('H*')
71
+ end
72
+
73
+ end
@@ -0,0 +1,121 @@
1
+ require 'openssl'
2
+ require_relative 'file_manager'
3
+
4
+ class SecretManager < FileManager
5
+
6
+ attr_accessor :working_dir, :master_key
7
+
8
+ def initialize(master_key = nil)
9
+ @@working_dir = Dir.pwd
10
+ @@master_key = master_key unless master_key.nil?
11
+ @data = @data || []
12
+ end
13
+
14
+ # - name: SEND_GRID
15
+ # tags:
16
+ # - PROD
17
+ # - DEV
18
+ # account: my_user_name
19
+ # secret: 010b606069e6e63cd24c9cf60d08351775539...
20
+ # added: 2017-01-14 09:02:16.906998000 -05:00
21
+
22
+ # Add a secret
23
+ def add(secret_name, secret, account = nil, tags = [], notes = nil)
24
+ unless find(secret_name, tags, false).empty?
25
+ raise 'Secret already exists with these tags'
26
+ end
27
+
28
+ tags = [tags] unless tags.is_a? Array
29
+
30
+ secret_data = {
31
+ name: secret_name,
32
+ tags: tags,
33
+ account: account, # like the user name or email, if applicable
34
+ secret: @@master_key.class.bin_to_hex( @@master_key.encryptSecret(secret) ),
35
+ notes: notes,
36
+ added: Time.now
37
+ }
38
+
39
+ @data.push secret_data
40
+ end
41
+
42
+ # Remove a secret, must have all tags given
43
+ def remove(secret_name, tags = [])
44
+ tags = [tags] unless tags.is_a? Array
45
+ removed = 0
46
+
47
+ @data.keep_if do |secret_data|
48
+ if (secret_data[:name] == secret_name) && (tags.empty? || (tags - secret_data[:tags]).empty?)
49
+ removed += 1
50
+ false
51
+ else
52
+ true
53
+ end
54
+ end
55
+
56
+ removed
57
+ end
58
+
59
+ # Search for a secret, must have all tags given
60
+ def find(secret_name, tags = [], decrypt = true)
61
+ tags = [tags] unless tags.is_a? Array
62
+
63
+ return_data = []
64
+ @data.each do |secret_data|
65
+ if (secret_data[:name] == secret_name)
66
+ next unless tags.empty? || (tags - secret_data[:tags]).empty?
67
+ # If no tags or secret has all tags
68
+
69
+ this_secret = secret_data.dup
70
+ if decrypt
71
+ this_secret[:secret] = @@master_key.decryptSecret(@@master_key.class.hex_to_bin this_secret[:secret])
72
+ end
73
+
74
+ return_data.push(this_secret)
75
+ end
76
+ end
77
+ return_data
78
+ end
79
+
80
+ # Get decrypted secret, array of secrets if mutliple matches
81
+ def getSecret(secret_name, tags = [])
82
+ res = find(secret_name, tags)
83
+ return nil if res.empty?
84
+ res = res.map {|x| x[:secret]}
85
+ return res[0] if res.length == 1
86
+ res
87
+ end
88
+
89
+ # Change the encryption key for all secrets
90
+ def rotateMasterKey(new_master_key)
91
+ @data = @data.map do |secret_data|
92
+ secret = secret_data[:secret]
93
+ plain_text = @@master_key.decryptSecret( @@master_key.class.hex_to_bin secret )
94
+ secret = @@master_key.class.bin_to_hex( new_master_key.encryptSecret(plain_text) )
95
+ secret_data[:secret] = secret
96
+ secret_data
97
+ end
98
+
99
+ @@master_key = new_master_key
100
+ end
101
+
102
+ # Get all decrypted secrets
103
+ def getAll(tags = [])
104
+ tags = [tags] unless tags.is_a? Array
105
+ return_data = []
106
+
107
+ @data.each do |secret_data|
108
+ if (tags.empty? || (tags - secret_data[:tags]).empty?) # If no tags or secret has all tags
109
+ return_data.push(
110
+ name: secret_data[:name],
111
+ tags: secret_data[:tags] || [],
112
+ account: secret_data[:account],
113
+ secret: @@master_key.decryptSecret(@@master_key.class.hex_to_bin secret_data[:secret])
114
+ )
115
+ end
116
+ end
117
+
118
+ return_data
119
+ end
120
+
121
+ end
@@ -0,0 +1,110 @@
1
+ require_relative './file_manager'
2
+ require_relative './key_helper'
3
+ require_relative './master_key'
4
+
5
+ class UserManager < FileManager
6
+
7
+ attr_accessor :working_dir, :user_dir, :master_key
8
+
9
+ HASH_ALG = :sha256
10
+
11
+ def initialize(master_key = nil)
12
+ @@working_dir = Dir.pwd
13
+ @@user_dir = @@working_dir + '/users'
14
+ @data = @data || []
15
+ @master_key = master_key unless master_key.nil?
16
+ end
17
+
18
+ # Add a user
19
+ # - Store public key
20
+ # - Add to listing
21
+ def add(user_name, public_key_file)
22
+ unless find(user_name).nil?
23
+ raise 'User already exists, delete existing user record to replace'
24
+ end
25
+
26
+ public_key = KeyHelper.getPublicKey(public_key_file)
27
+ key_file_hash = self.class.calcHash(HASH_ALG, public_key)
28
+
29
+ unique_file_name = self.class.calcHash(HASH_ALG, key_file_hash + user_name)
30
+ key_file = 'users/' + unique_file_name[0..10] + '.pem'
31
+
32
+ Dir.mkdir(@@user_dir) unless File.exists?(@@user_dir)
33
+ File.write(@@working_dir +'/'+ key_file, public_key)
34
+
35
+ # - user: george
36
+ # public_key: users/eb0545f9010.pem
37
+ # added: 2017-01-14 09:02:16.906998000 -05:00
38
+ # sha256: eb0545f9010b606069e6e63cd24c9cf60d08351775539d878055cbf3330afafa
39
+ # lock_Box: 010b606069e6e63cd24c9cf60d08351775539...
40
+
41
+ user_data = {
42
+ user: user_name,
43
+ public_key: key_file, # folder & file, relative to working directory
44
+ added: Time.now,
45
+ lock_box: 'error - should be replaced'
46
+ }
47
+
48
+ user_data[HASH_ALG] = key_file_hash
49
+
50
+ @data.push user_data
51
+ rotateMasterKey
52
+
53
+ end
54
+
55
+ # Remove a user
56
+ # - Remove public key
57
+ # - Remove from listing
58
+ def remove(user_name)
59
+ @data = @data.keep_if do |user_data|
60
+ next true if user_data[:user] != user_name
61
+ File.delete(@@working_dir +'/'+ user_data[:public_key])
62
+ false
63
+ end
64
+ rotateMasterKey
65
+ end
66
+
67
+ # Search for a user
68
+ def find(user_name)
69
+ @data.each do |user_data|
70
+ return user_data if user_data[:user] == user_name
71
+ end
72
+ nil
73
+ end
74
+
75
+ # List all user names
76
+ def all
77
+ ret = []
78
+ @data.each { |user_data| ret.push user_data[:user] }
79
+ ret
80
+ end
81
+
82
+ # Rotate master key
83
+ # - Create a new master key
84
+ # - Update all lock boxes
85
+ def rotateMasterKey
86
+ @master_key = MasterKey.generate
87
+
88
+ @data.map! do |user_data|
89
+ public_key = getUserKey user_data[:public_key], user_data[HASH_ALG]
90
+ user_data[:lock_box] = MasterKey.bin_to_hex @master_key.encryptWithPublicKey(public_key)
91
+ user_data
92
+ end
93
+ end
94
+
95
+ # Get the user's public key as a string
96
+ def getUserKey(file_name, check_hash)
97
+ raise 'User key doesn\'t exist' unless File.exists?(@@working_dir +'/'+ file_name)
98
+ file_data = File.read(@@working_dir +'/'+ file_name)
99
+ unless (self.class.calcHash(HASH_ALG, file_data) == check_hash)
100
+ raise('Key digest mismatch for '+ file_name)
101
+ end
102
+ file_data
103
+ end
104
+
105
+ def self.calcHash(algo, string)
106
+ raise 'Hash algorithim not supported' unless algo == HASH_ALG
107
+ Digest::SHA256.hexdigest string
108
+ end
109
+
110
+ end