leeloo 0.0.16 → 0.4.1
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 +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
|