donjon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +6 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +37 -0
- data/Rakefile +1 -0
- data/bin/dj +6 -0
- data/donjon.gemspec +28 -0
- data/lib/core_ext/assert.rb +6 -0
- data/lib/core_ext/io_get_password.rb +24 -0
- data/lib/donjon.rb +2 -0
- data/lib/donjon/cli.rb +4 -0
- data/lib/donjon/commands/base.rb +66 -0
- data/lib/donjon/commands/config.rb +41 -0
- data/lib/donjon/commands/user.rb +36 -0
- data/lib/donjon/commands/vault.rb +28 -0
- data/lib/donjon/configurator.rb +166 -0
- data/lib/donjon/database.rb +59 -0
- data/lib/donjon/encrypted_file.rb +87 -0
- data/lib/donjon/repository.rb +6 -0
- data/lib/donjon/settings.rb +60 -0
- data/lib/donjon/shell.rb +19 -0
- data/lib/donjon/user.rb +61 -0
- data/lib/donjon/version.rb +3 -0
- data/spec/donjon/database_spec.rb +76 -0
- data/spec/donjon/encrypted_file_spec.rb +110 -0
- data/spec/donjon/repository_spec.rb +24 -0
- data/spec/donjon/user_spec.rb +98 -0
- data/spec/spec_helper.rb +78 -0
- data/spec/support/keys.rb +7 -0
- data/spec/support/repos.rb +22 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e2e4af5e144eda80bddb067306735fad06605d4a
|
4
|
+
data.tar.gz: bba9c3dd427d5046289498073b47ddbe4d80b306
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: be4415356a0dfacb7b0a8b9389cfb24148688f06fe57d0dea72be24151f49c00d1ae9640df6cc9661d5ae292944b96577a5f1ab41a235598fb682ebc1350ecc3
|
7
|
+
data.tar.gz: e18dfe9d8769cb5c8bdc5b6dd20b96379c39b2208b26f3dae13f951567927af6020eae919ae4485879e70acf1d32a9bea633ef460c9ed39a4963bc5c44978337
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.2
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
9
|
+
|
10
|
+
# vim: set filetype=ruby:
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 HouseTrip Ltd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Donjon
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install it as you would any gem:
|
8
|
+
|
9
|
+
$ gem install donjon
|
10
|
+
|
11
|
+
Then run the interactive setup:
|
12
|
+
|
13
|
+
$ dj vault:init
|
14
|
+
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
Once you've set up a vault (you can use `vault:init` to connect to an existing
|
19
|
+
vault, e.g. on Dropbox).
|
20
|
+
|
21
|
+
```
|
22
|
+
Commands:
|
23
|
+
dj config:get KEY... # Decrypts the value for KEY from the vault
|
24
|
+
dj config:mget [REGEXP] # Decrypts multiple keys (all readable by default)
|
25
|
+
dj config:set KEY=VALUE ... # Encrypts KEY and VALUE in the vault
|
26
|
+
dj help [COMMAND] # Describe available commands or one specific command
|
27
|
+
dj init # Creates a new vault, or connects to an existing vault.
|
28
|
+
dj user:add NAME [PATH] # Adds user and their public key to the vault. Reads from standard input if no path is given.
|
29
|
+
```
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
1. Fork it ( http://github.com/<my-github-username>/donjon/fork )
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
37
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/dj
ADDED
data/donjon.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'donjon/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "donjon"
|
8
|
+
spec.version = Donjon::VERSION
|
9
|
+
spec.authors = ["Julien Letessier"]
|
10
|
+
spec.email = ["julien.letessier@gmail.com"]
|
11
|
+
spec.summary = %q{Secure, multi-user data store.}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency 'thor'
|
21
|
+
spec.add_dependency 'gibberish'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'pry'
|
27
|
+
spec.add_development_dependency 'guard-rspec'
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class IO
|
2
|
+
def get_password(out: $stderr)
|
3
|
+
result = ''
|
4
|
+
noecho do
|
5
|
+
while char = getch
|
6
|
+
case char
|
7
|
+
when /[\r\n]/
|
8
|
+
break
|
9
|
+
when /[\e\b\x7f]/
|
10
|
+
result.replace result.chop
|
11
|
+
out.write "\b \b"
|
12
|
+
when /[\x3\x1A]/ # interrupt, background
|
13
|
+
out.write "\nOk, aborting\n"
|
14
|
+
abort
|
15
|
+
else
|
16
|
+
result << char
|
17
|
+
out.write '*'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
out.write "\n"
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
data/lib/donjon.rb
ADDED
data/lib/donjon/cli.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'donjon/shell'
|
2
|
+
require 'donjon/settings'
|
3
|
+
require 'pathname'
|
4
|
+
require 'pathname'
|
5
|
+
require 'openssl'
|
6
|
+
require 'donjon/user'
|
7
|
+
require 'donjon/database'
|
8
|
+
require 'core_ext/io_get_password'
|
9
|
+
|
10
|
+
module Donjon
|
11
|
+
module Commands
|
12
|
+
class Base < Thor
|
13
|
+
def self.start(args)
|
14
|
+
super(args, shell: Shell.instance)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.decl(method_name)
|
18
|
+
define_method(method_name.to_sym) { |*args| send(method_name.gsub(':','_').to_sym, *args) }
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def settings
|
24
|
+
@settings ||= Settings.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_configured
|
28
|
+
return if settings.configured?
|
29
|
+
say "Oops, I can't run that until you've configured me.", :red
|
30
|
+
say "Run 'vault:init' and I'll help out!"
|
31
|
+
exit 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def repo
|
35
|
+
@repo ||= begin
|
36
|
+
check_configured
|
37
|
+
Repository.new(settings.vault_path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def actor
|
42
|
+
@actor ||= begin
|
43
|
+
check_configured
|
44
|
+
pem_data = Pathname.new(settings.private_key).read
|
45
|
+
password = _get_password("Please enter the password for your private key (#{settings.private_key})")
|
46
|
+
key = OpenSSL::PKey::RSA.new(pem_data, password)
|
47
|
+
User.new(repo: repo, name: settings.user_name, key: key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def database
|
52
|
+
@database ||= begin
|
53
|
+
Database.new(actor: actor)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def _get_password(message)
|
60
|
+
say message, :green
|
61
|
+
$stdout.write('> ')
|
62
|
+
$stdin.get_password
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'donjon/commands/base'
|
2
|
+
|
3
|
+
module Donjon
|
4
|
+
module Commands
|
5
|
+
Base.class_eval do
|
6
|
+
desc 'config:set KEY=VALUE ...', 'Encrypts KEY and VALUE in the vault'
|
7
|
+
decl 'config:set'
|
8
|
+
|
9
|
+
desc 'config:get KEY...', 'Decrypts the value for KEY from the vault'
|
10
|
+
decl 'config:get'
|
11
|
+
|
12
|
+
desc 'config:mget [REGEXP]', 'Decrypts multiple keys (all readable by default)'
|
13
|
+
decl 'config:mget'
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def config_set(*keyvals)
|
18
|
+
keyvals.each do |keyval|
|
19
|
+
m = /([^=]*)=(.*)/.match(keyval)
|
20
|
+
key = m[1]
|
21
|
+
value = m[2]
|
22
|
+
database[key] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def config_get(*keys)
|
27
|
+
keys.each do |key|
|
28
|
+
puts "#{key}: #{database[key]}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def config_mget(regexp = nil)
|
33
|
+
regexp = Regexp.new(regexp) if regexp
|
34
|
+
database.each do |key, value|
|
35
|
+
next if regexp && regexp !~ key
|
36
|
+
puts "#{key}: #{value}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'donjon/commands/base'
|
2
|
+
require 'donjon/user'
|
3
|
+
|
4
|
+
module Donjon
|
5
|
+
module Commands
|
6
|
+
Base.class_eval do
|
7
|
+
desc 'user:add NAME [PATH]', 'Adds user and their public key to the vault. Reads from standard input if no path is given.'
|
8
|
+
decl 'user:add'
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def user_add(name, path = nil)
|
13
|
+
if path == nil
|
14
|
+
say "Please paste #{name}'s public key in PEM format.", :green
|
15
|
+
say "They can obtain it by running e.g.:"
|
16
|
+
say "$ openssl rsa -in ~/.ssh/id_rsa -pubout -outform pem"
|
17
|
+
$stderr.write('> ')
|
18
|
+
key_data = ''
|
19
|
+
while line = $stdin.gets
|
20
|
+
break if line.strip.empty?
|
21
|
+
key_data << line
|
22
|
+
end
|
23
|
+
else
|
24
|
+
key_data = Pathname.new(key_path).expand_path.read
|
25
|
+
end
|
26
|
+
|
27
|
+
key = OpenSSL::PKey::RSA.new(key_data, '').public_key
|
28
|
+
say "Saving #{name}'s public key..."
|
29
|
+
User.new(name: name, key: key, repo: actor.repo).save
|
30
|
+
say "Making the database readable by #{name}..."
|
31
|
+
database.update
|
32
|
+
say "Success! #{name} has been added to the vault.", [:green, :bold]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'pathname'
|
3
|
+
require 'donjon/commands/base'
|
4
|
+
require 'donjon/repository'
|
5
|
+
require 'donjon/settings'
|
6
|
+
require 'donjon/configurator'
|
7
|
+
require 'donjon/shell'
|
8
|
+
|
9
|
+
module Donjon
|
10
|
+
module Commands
|
11
|
+
Base.class_eval do
|
12
|
+
desc "init", 'Creates a new vault, or connects to an existing vault.'
|
13
|
+
|
14
|
+
def init
|
15
|
+
if settings.configured?
|
16
|
+
say 'This vault is already configured :)', :green
|
17
|
+
say 'If you want another one, set DONJONRC to a new configuration file'
|
18
|
+
say "(if it doesn't exist I will create one for you)"
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
Configurator.new(settings: settings).run
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'donjon/user'
|
2
|
+
require 'core_ext/io_get_password'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Donjon
|
6
|
+
class Configurator
|
7
|
+
def initialize(settings:)
|
8
|
+
@settings = settings
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
_get_name
|
13
|
+
_get_repo
|
14
|
+
_get_private_key_path
|
15
|
+
_save_user
|
16
|
+
_thank_user if changes?
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def changes?
|
23
|
+
!!@changes
|
24
|
+
end
|
25
|
+
|
26
|
+
def changes!
|
27
|
+
@changes = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def _thank_user
|
31
|
+
_shell.say "Thanks! All your settings are saved.", [:green, :bold]
|
32
|
+
end
|
33
|
+
|
34
|
+
def _get_repo
|
35
|
+
path = @settings.vault_path ||= begin
|
36
|
+
changes!
|
37
|
+
_shell.say "Where do you want your vault? [~/.donjon]", :green
|
38
|
+
ans = _shell.ask('>')
|
39
|
+
ans = '~/.donjon' if ans.empty?
|
40
|
+
Pathname.new(ans).expand_path.to_s
|
41
|
+
end
|
42
|
+
Donjon::Repository.new(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def _get_name
|
46
|
+
@settings.user_name ||= begin
|
47
|
+
changes!
|
48
|
+
_shell.say "Hi! How do you want to be called? [#{_default_name}]", :green
|
49
|
+
ans = _shell.ask '>'
|
50
|
+
ans.empty? ? _default_name : ans
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def _get_private_key_path
|
55
|
+
@settings.private_key ||= begin
|
56
|
+
changes!
|
57
|
+
_get_key_path.path.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def _get_key_path
|
62
|
+
@_key_path ||= begin
|
63
|
+
_shell.say [
|
64
|
+
'Do you want to use an existing private key or create one?',
|
65
|
+
'If you use an existing key, it needs to be an encrypted, 2048-bit RSA key.',
|
66
|
+
'Use [e]xisting or [c]reate?',
|
67
|
+
].join("\n"), :green
|
68
|
+
ans = _shell.ask('>')
|
69
|
+
|
70
|
+
case ans
|
71
|
+
when /[Ee]/
|
72
|
+
_get_existing_key
|
73
|
+
when /[Cc]/
|
74
|
+
_get_new_key
|
75
|
+
else
|
76
|
+
_shell.say 'No idea what you mean, sorry.', :red
|
77
|
+
exit 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def _get_existing_key
|
83
|
+
_shell.say('What is the path to your key? [~/.ssh/id_rsa]', :green)
|
84
|
+
ans = _shell.ask('>')
|
85
|
+
ans = '~/.ssh/id_rsa' if ans.empty?
|
86
|
+
path = Pathname.new(ans).expand_path
|
87
|
+
if !path.exist?
|
88
|
+
_shell.say 'Sorry, cannot find that file.', :red
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
_check_key(path)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def _check_key(path, password = nil)
|
96
|
+
unless _is_key_encrypted?(path)
|
97
|
+
_shell.say 'That key is not password-protected. Sorry, I\' not taking that risk!', :red
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
|
101
|
+
if password.nil?
|
102
|
+
_shell.say "I'll now try to load your private key.", :green
|
103
|
+
_shell.say "Please enter your key password (I won't store it anywhere)", :green
|
104
|
+
$stdout.write "> "
|
105
|
+
$stdout.flush
|
106
|
+
password = $stdin.get_password
|
107
|
+
end
|
108
|
+
|
109
|
+
key = OpenSSL::PKey::RSA.new(path.read, password)
|
110
|
+
if key.n.num_bits != 2_048
|
111
|
+
_shell.say "Sorry, that key isn't 2,048 bits long.", :red
|
112
|
+
exit 1
|
113
|
+
end
|
114
|
+
|
115
|
+
_shell.say "Okay, that key seems valid.", :green
|
116
|
+
OpenStruct.new key: key.public_key, path: path
|
117
|
+
end
|
118
|
+
|
119
|
+
def _get_new_key
|
120
|
+
_shell.say "Where do you want to store your new key? [~/.ssh/donjon_rsa]", :green
|
121
|
+
ans = _shell.ask('>')
|
122
|
+
ans = '~/.ssh/donjon_rsa' if ans.empty?
|
123
|
+
path = Pathname.new(ans).expand_path
|
124
|
+
|
125
|
+
_shell.say "Please enter a password for your new key (I won't store it anywhere)", :green
|
126
|
+
$stdout.write "> "
|
127
|
+
$stdout.flush
|
128
|
+
password = $stdin.get_password
|
129
|
+
|
130
|
+
command = "ssh-keygen -q -t rsa -b 2048 -N '#{password}' -f #{path}"
|
131
|
+
unless system(command)
|
132
|
+
_shell.say "Sorry, key generation failed!", :red
|
133
|
+
exit 1
|
134
|
+
end
|
135
|
+
|
136
|
+
_check_key(path, password)
|
137
|
+
end
|
138
|
+
|
139
|
+
def _save_user
|
140
|
+
if user = User.find(repo: _get_repo, name: _get_name)
|
141
|
+
if user.key.to_pem.strip != _get_key_path.key.to_pem.strip
|
142
|
+
_shell.say "There's already a user with your name in the vault, and the public keys don't match. I'm afraid I'm a bit stumped.", :red
|
143
|
+
exit 1
|
144
|
+
end
|
145
|
+
return
|
146
|
+
end
|
147
|
+
User.new(name: _get_name, repo: _get_repo, key: _get_key_path.key).save
|
148
|
+
nil
|
149
|
+
end
|
150
|
+
|
151
|
+
def _is_key_encrypted?(path)
|
152
|
+
OpenSSL::PKey::RSA.new(path.read, '')
|
153
|
+
return false
|
154
|
+
rescue OpenSSL::PKey::RSAError
|
155
|
+
return true
|
156
|
+
end
|
157
|
+
|
158
|
+
def _default_name
|
159
|
+
ENV['USER'] || ENV['LOGNAME']
|
160
|
+
end
|
161
|
+
|
162
|
+
def _shell
|
163
|
+
Shell.instance
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|