leeloo 0.0.16 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/leeloo.rb +5 -3
- data/lib/leeloo/command.rb +127 -120
- data/lib/leeloo/controller.rb +122 -0
- data/lib/leeloo/keystore.rb +188 -21
- data/lib/leeloo/output.rb +149 -0
- data/lib/leeloo/preferences.rb +100 -0
- data/lib/leeloo/secret.rb +93 -57
- data/lib/leeloo/server.rb +37 -0
- data/lib/leeloo/version.rb +1 -1
- metadata +40 -24
- data/lib/leeloo/config.rb +0 -62
data/lib/leeloo/keystore.rb
CHANGED
@@ -1,40 +1,207 @@
|
|
1
|
-
require 'fileutils'
|
2
1
|
require 'gpgme'
|
2
|
+
require 'fileutils'
|
3
3
|
require 'git'
|
4
|
+
require 'base64'
|
4
5
|
|
5
6
|
module Leeloo
|
7
|
+
|
8
|
+
class KeystoreFactory
|
9
|
+
def self.create keystore
|
10
|
+
keystore_created = nil
|
11
|
+
case keystore["cypher"]
|
12
|
+
when "gpg"
|
13
|
+
keystore_created = GpgPrivateLocalFileSystemKeystore.new keystore["name"], keystore["path"]
|
14
|
+
else
|
15
|
+
keystore_created = PrivateLocalFileSystemKeystore.new keystore["name"], keystore["path"]
|
16
|
+
end
|
17
|
+
|
18
|
+
case keystore["vc"]
|
19
|
+
when "git"
|
20
|
+
GitKeystoreDecorator.new keystore_created
|
21
|
+
else
|
22
|
+
keystore_created
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
6
27
|
class Keystore
|
7
28
|
|
8
|
-
|
9
|
-
|
29
|
+
attr_reader :name
|
30
|
+
|
31
|
+
def initialize name
|
32
|
+
@name = name
|
33
|
+
end
|
34
|
+
|
35
|
+
def secrets
|
36
|
+
# returns the secrets list
|
10
37
|
end
|
11
38
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
39
|
+
def secret_of path
|
40
|
+
# returns a secret object
|
41
|
+
end
|
42
|
+
|
43
|
+
def secret_from_name name
|
44
|
+
# returns a secret object
|
45
|
+
end
|
46
|
+
|
47
|
+
def sync
|
48
|
+
# synchronizes the keystore
|
49
|
+
end
|
50
|
+
|
51
|
+
def init
|
52
|
+
# initialize the keystore
|
53
|
+
end
|
54
|
+
|
55
|
+
def footprint name
|
56
|
+
# footprint a given secret path
|
57
|
+
end
|
16
58
|
|
17
|
-
|
18
|
-
|
59
|
+
def secret_from_footprint footprint
|
60
|
+
# returns a secret object
|
61
|
+
end
|
62
|
+
|
63
|
+
def == keystore
|
64
|
+
self.name == keystore.name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class PrivateLocalFileSystemKeystore < Keystore
|
69
|
+
|
70
|
+
attr_reader :path
|
71
|
+
|
72
|
+
def initialize name, path
|
73
|
+
super name
|
74
|
+
@path = path
|
75
|
+
FileUtils.mkdir_p "#{@path}/secrets"
|
76
|
+
end
|
77
|
+
|
78
|
+
def secrets
|
79
|
+
find_secrets "#{@path}/secrets"
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_secrets path
|
83
|
+
elements = []
|
84
|
+
Dir.glob("#{path}/**") do |element|
|
85
|
+
elements << secret_of(element) unless Dir.exist? element
|
86
|
+
elements << find_secrets(element) if Dir.exist? element
|
19
87
|
end
|
88
|
+
return elements.flatten
|
89
|
+
end
|
90
|
+
|
91
|
+
def == keystore
|
92
|
+
self.name == keystore.name && self.path == keystore.path
|
93
|
+
end
|
20
94
|
|
21
|
-
|
22
|
-
|
23
|
-
|
95
|
+
def secret_of path
|
96
|
+
name = path.gsub("#{@path}/secrets/", "")
|
97
|
+
LocalFileSystemSecret.new path, name
|
24
98
|
end
|
25
99
|
|
26
|
-
def
|
27
|
-
|
28
|
-
g.add_remote 'origin', remote
|
100
|
+
def secret_from_name name
|
101
|
+
secret_of "#{path}/secrets/#{name}"
|
29
102
|
end
|
30
103
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
104
|
+
def footprint_of name
|
105
|
+
secret = secret_from_name name
|
106
|
+
{ "footprint" => secret.footprint, "keystore" => self.name, "secret" => secret.name }
|
107
|
+
end
|
108
|
+
|
109
|
+
def secret_from_footprint footprint
|
110
|
+
secret = secret_from_name footprint["secret"]
|
111
|
+
unless secret.footprint == footprint["footprint"]
|
112
|
+
raise "footprint is not valid"
|
36
113
|
end
|
37
|
-
|
114
|
+
secret
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
class GpgPrivateLocalFileSystemKeystore < PrivateLocalFileSystemKeystore
|
120
|
+
|
121
|
+
def initialize name, path
|
122
|
+
super name, path
|
123
|
+
FileUtils.mkdir_p "#{@path}/keys"
|
124
|
+
|
125
|
+
@recipients = []
|
126
|
+
Dir.glob("#{path}/keys/*") { |key| @recipients << File.basename(key) }
|
127
|
+
@recipients.each { |key| GPGME::Key.import(File.open("#{path}/keys/#{key}")) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def init
|
131
|
+
super
|
132
|
+
GPGME::Key.find(:public, nil, ).each { |key| key.export(:output => File.open("#{path}/keys/#{key.uids.first.email}", "w+")) }
|
133
|
+
end
|
134
|
+
|
135
|
+
def secret_of path
|
136
|
+
name = path.gsub("#{@path}/secrets/", "").gsub(".gpg", "")
|
137
|
+
GpgLocalFileSystemSecret.new path, name, @recipients
|
138
|
+
end
|
139
|
+
|
140
|
+
def secret_from_name name
|
141
|
+
secret_of "#{path}/secrets/#{name}.gpg"
|
142
|
+
end
|
143
|
+
|
144
|
+
def footprint_of name
|
145
|
+
footprint = super name
|
146
|
+
footprint["sign"] = Base64.strict_encode64 GPGME::Crypto.new.sign(footprint["footprint"]).to_s
|
147
|
+
footprint
|
148
|
+
end
|
149
|
+
|
150
|
+
def secret_from_footprint footprint
|
151
|
+
data = GPGME::Crypto.new.verify(Base64.strict_decode64 footprint["sign"]) { |signature| signature.valid? }
|
152
|
+
if data.read == footprint["footprint"]
|
153
|
+
super footprint
|
154
|
+
else
|
155
|
+
raise "signature is not valid"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class GitKeystoreDecorator < Keystore
|
161
|
+
def initialize keystore
|
162
|
+
@keystore = keystore
|
163
|
+
Git.init @keystore.path
|
164
|
+
@git = Git.open keystore.path
|
165
|
+
end
|
166
|
+
|
167
|
+
def secret_of element
|
168
|
+
GitSecretDecorator.new(@git, element)
|
169
|
+
end
|
170
|
+
|
171
|
+
def secret_from_name element
|
172
|
+
secret_of @keystore.secret_from_name(element)
|
173
|
+
end
|
174
|
+
|
175
|
+
def secrets
|
176
|
+
@keystore.secrets
|
177
|
+
end
|
178
|
+
|
179
|
+
def name
|
180
|
+
@keystore.name
|
181
|
+
end
|
182
|
+
|
183
|
+
def path
|
184
|
+
@keystore.path
|
185
|
+
end
|
186
|
+
|
187
|
+
def footprint_of element
|
188
|
+
@keystore.footprint_of element
|
189
|
+
end
|
190
|
+
|
191
|
+
def secret_from_footprint footprint
|
192
|
+
@keystore.secret_from_footprint footprint
|
193
|
+
end
|
194
|
+
|
195
|
+
def sync
|
196
|
+
@git.pull
|
197
|
+
@keystore.sync
|
198
|
+
@git.push
|
199
|
+
end
|
200
|
+
|
201
|
+
def init
|
202
|
+
@keystore.init
|
203
|
+
@git.add
|
204
|
+
@git.commit "keystore #{@keystore.name} added"
|
38
205
|
end
|
39
206
|
|
40
207
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'clipboard'
|
2
|
+
require 'tty-table'
|
3
|
+
require 'tty-tree'
|
4
|
+
require 'json'
|
5
|
+
require 'base64'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
module Leeloo
|
9
|
+
|
10
|
+
class Output
|
11
|
+
|
12
|
+
def render_preferences preferences
|
13
|
+
end
|
14
|
+
|
15
|
+
def render_secrets secrets
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_secret secret
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_text text
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_footprint footprint
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_share footprint
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Ascii < Output
|
32
|
+
|
33
|
+
def render_preferences preferences
|
34
|
+
preferences.keystores.each { |keystore| puts keystore.name }
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_secrets secrets
|
38
|
+
secrets.sort_by(&:name).each {|secret| puts secret.name}
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_secret secret
|
42
|
+
begin
|
43
|
+
puts secret.read
|
44
|
+
rescue => exception
|
45
|
+
puts "#{secret.name} doesn't exist"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def render_text text
|
50
|
+
puts text
|
51
|
+
end
|
52
|
+
|
53
|
+
def render_footprint footprint
|
54
|
+
puts "token:"
|
55
|
+
puts Base64.strict_encode64 footprint.to_json
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Terminal < Ascii
|
60
|
+
|
61
|
+
def render_preferences preferences
|
62
|
+
rows = []
|
63
|
+
default_keystore = preferences.default
|
64
|
+
preferences.keystores.each do |keystore|
|
65
|
+
is_default = '*' if keystore.name == default_keystore
|
66
|
+
rows << [keystore.name, keystore.path, is_default ]
|
67
|
+
end
|
68
|
+
puts TTY::Table.new(header: ['Name', 'Path', 'Default'], rows: rows).render(:ascii)
|
69
|
+
end
|
70
|
+
|
71
|
+
def render_secrets secrets
|
72
|
+
hash = {:secrets => []}
|
73
|
+
secrets.sort_by(&:name).each { |secret| sort(hash[:secrets], secret.name) }
|
74
|
+
puts TTY::Tree.new(hash).render
|
75
|
+
end
|
76
|
+
|
77
|
+
def sort array, element
|
78
|
+
if element
|
79
|
+
e = element.split("/", 2)
|
80
|
+
if e.length > 1
|
81
|
+
found = false
|
82
|
+
array.each do |a|
|
83
|
+
if a.is_a? Hash
|
84
|
+
if a[e.first]
|
85
|
+
found = true
|
86
|
+
sort(a[e.first], e.last)
|
87
|
+
break
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
unless found
|
93
|
+
array << { e.first => sort([], e.last) }
|
94
|
+
end
|
95
|
+
else
|
96
|
+
array << e.last
|
97
|
+
end
|
98
|
+
end
|
99
|
+
array
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class ClipboardOutputDecorator < Output
|
104
|
+
|
105
|
+
def initialize output
|
106
|
+
@output = output
|
107
|
+
end
|
108
|
+
|
109
|
+
def render_preferences preferences
|
110
|
+
@output.render_preferences preferences
|
111
|
+
end
|
112
|
+
|
113
|
+
def render_secrets secrets
|
114
|
+
@output.render_secrets secrets
|
115
|
+
end
|
116
|
+
|
117
|
+
def render_text text
|
118
|
+
@output.render_text text
|
119
|
+
end
|
120
|
+
|
121
|
+
def render_secret secret
|
122
|
+
|
123
|
+
Signal.trap("INT") do
|
124
|
+
Clipboard.clear
|
125
|
+
abort "cleared"
|
126
|
+
end
|
127
|
+
|
128
|
+
Clipboard.copy secret.read
|
129
|
+
wait = Thread.new do
|
130
|
+
puts "cleaning in 30s"
|
131
|
+
30.times {
|
132
|
+
print "."
|
133
|
+
sleep 1
|
134
|
+
}
|
135
|
+
end
|
136
|
+
wait.join
|
137
|
+
Clipboard.clear
|
138
|
+
end
|
139
|
+
|
140
|
+
def render_footprint footprint
|
141
|
+
@output.render_footprint footprint
|
142
|
+
end
|
143
|
+
|
144
|
+
def render_share footprint
|
145
|
+
@output.render_share footprint
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Leeloo
|
4
|
+
class Preferences
|
5
|
+
|
6
|
+
attr_reader :default
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@keystores = []
|
10
|
+
@default = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def load
|
14
|
+
# this method loads all preferences
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_default_keystore name
|
19
|
+
@default = name
|
20
|
+
end
|
21
|
+
|
22
|
+
def keystore name=nil
|
23
|
+
keystores.find { |k| k.name == (name||@default) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def keystores
|
27
|
+
@keystores.map { |k| KeystoreFactory::create k }
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_keystore keystore
|
31
|
+
unless @keystores.include? keystore
|
32
|
+
@keystores << keystore
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove_keystore name
|
37
|
+
abort "you can not remove default keystore" if name == @default
|
38
|
+
keystore = @keystores.find { |k| k["name"] == name }
|
39
|
+
if keystore != nil
|
40
|
+
@keystores.delete keystore
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class PrivateLocalFileSystemPreferences < Preferences
|
46
|
+
|
47
|
+
DEFAULT_PATH = "#{Dir.home}/.leeloo"
|
48
|
+
|
49
|
+
def load(path=DEFAULT_PATH)
|
50
|
+
@path = path
|
51
|
+
|
52
|
+
if File.exist? "#{path}/keystores"
|
53
|
+
@keystores = YAML.load_file "#{path}/keystores"
|
54
|
+
end
|
55
|
+
|
56
|
+
if File.exist? "#{path}/config"
|
57
|
+
config = YAML.load_file "#{path}/config"
|
58
|
+
set_default_keystore config["keystore"]
|
59
|
+
else
|
60
|
+
default_keystore = {
|
61
|
+
'name' => "private",
|
62
|
+
'path' => "#{path}/private",
|
63
|
+
'cypher' => "gpg",
|
64
|
+
'vc' => "git"
|
65
|
+
}
|
66
|
+
add_keystore default_keystore
|
67
|
+
set_default_keystore "private"
|
68
|
+
keystore_of("private").init
|
69
|
+
end
|
70
|
+
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def keystore_of name
|
75
|
+
keystore = @keystores.find { |keystore| keystore["name"] == name }
|
76
|
+
KeystoreFactory::create keystore
|
77
|
+
end
|
78
|
+
|
79
|
+
def set_default_keystore name
|
80
|
+
super name
|
81
|
+
config = {
|
82
|
+
"keystore" => name
|
83
|
+
}
|
84
|
+
File.write("#{@path}/config", config.to_yaml)
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_keystore keystore
|
88
|
+
super keystore
|
89
|
+
FileUtils.mkdir_p keystore["path"]
|
90
|
+
File.write("#{@path}/keystores", @keystores.to_yaml)
|
91
|
+
end
|
92
|
+
|
93
|
+
def remove_keystore name
|
94
|
+
super name
|
95
|
+
File.write("#{@path}/keystores", @keystores.to_yaml)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|