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 +7 -0
- data/.gitignore +59 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +32 -0
- data/README.md +45 -0
- data/Rakefile +7 -0
- data/lib/team-secrets/file_manager.rb +31 -0
- data/lib/team-secrets/key_helper.rb +20 -0
- data/lib/team-secrets/manifest_manager.rb +72 -0
- data/lib/team-secrets/master_key.rb +73 -0
- data/lib/team-secrets/secret_manager.rb +121 -0
- data/lib/team-secrets/user_manager.rb +110 -0
- data/lib/team-secrets.rb +448 -0
- data/spec/file_manager_spec.rb +57 -0
- data/spec/manifest_manager_spec.rb +70 -0
- data/spec/master_key_spec.rb +107 -0
- data/spec/secret_manager_spec.rb +122 -0
- data/spec/support/test_key +30 -0
- data/spec/support/test_key.pub +1 -0
- data/spec/support/test_key.pub.pem +8 -0
- data/spec/user_manager_spec.rb +67 -0
- data/team-secrets.gemspec +19 -0
- metadata +72 -0
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
data/Gemfile
ADDED
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
|
+
[](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,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
|