uberpass 0.0.3 → 0.0.4
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.
- data/.gitignore +1 -0
- data/README.rdoc +11 -1
- data/lib/uberpass.rb +6 -309
- data/lib/uberpass/cli.rb +284 -0
- data/lib/uberpass/decrypt.rb +18 -0
- data/lib/uberpass/encrypt.rb +21 -0
- data/lib/uberpass/file_handler.rb +115 -0
- data/lib/uberpass/version.rb +1 -1
- data/spec/uberpass_output.rb +58 -0
- data/spec/uberpass_spec.rb +119 -0
- data/uberpass.gemspec +2 -0
- metadata +52 -12
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -9,12 +9,22 @@
|
|
9
9
|
$ openssl genrsa -des3 -out ~/.uberpass/private.pem 2048
|
10
10
|
$ openssl rsa -in ~/.uberpass/private.pem -out ~/.uberpass/public.pem -outform PEM -pubout
|
11
11
|
|
12
|
+
== Dropbox
|
13
|
+
|
14
|
+
$ mv ~/.uberpass ~/Dropbox/uberpass
|
15
|
+
$ ln -s ~/Dropbox/uberpass ~/.uberpass
|
16
|
+
|
12
17
|
== Use
|
13
18
|
|
14
|
-
|
19
|
+
Your personal passwords:
|
15
20
|
|
16
21
|
$ uberpass
|
17
22
|
|
18
23
|
or your work related passwords:
|
19
24
|
|
20
25
|
$ uberpass happy_place
|
26
|
+
|
27
|
+
and getting some help:
|
28
|
+
|
29
|
+
$ uberpass
|
30
|
+
$ uberpass:0.0.4> help
|
data/lib/uberpass.rb
CHANGED
@@ -1,314 +1,11 @@
|
|
1
|
-
require 'uberpass/version'
|
2
1
|
require 'openssl'
|
3
2
|
require 'yaml'
|
4
|
-
require 'ostruct'
|
5
|
-
require 'securerandom'
|
6
|
-
|
7
|
-
module Uberpass
|
8
|
-
class Decrypt
|
9
|
-
attr_reader :decrypted_data
|
10
|
-
|
11
|
-
def initialize(private_key, encrypted_data, encrypted_key, encrypted_iv)
|
12
|
-
key = OpenSSL::PKey::RSA.new(private_key)
|
13
|
-
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
14
|
-
cipher.decrypt
|
15
|
-
cipher.key = key.private_decrypt(encrypted_key)
|
16
|
-
cipher.iv = key.private_decrypt(encrypted_iv)
|
17
|
-
|
18
|
-
@decrypted_data = cipher.update(encrypted_data)
|
19
|
-
@decrypted_data << cipher.final
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class Encrypt
|
24
|
-
attr_reader :encrypted_data, :encrypted_key, :encrypted_iv
|
25
|
-
|
26
|
-
def initialize(public_key, decrypted_data)
|
27
|
-
key = OpenSSL::PKey::RSA.new(public_key)
|
28
|
-
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
29
|
-
cipher.encrypt
|
30
|
-
cipher.key = random_key = cipher.random_key
|
31
|
-
cipher.iv = random_iv = cipher.random_iv
|
32
|
-
|
33
|
-
@encrypted_data = cipher.update(decrypted_data)
|
34
|
-
@encrypted_data << cipher.final
|
35
|
-
|
36
|
-
@encrypted_key = key.public_encrypt(random_key)
|
37
|
-
@encrypted_iv = key.public_encrypt(random_iv)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
class FileHandler
|
42
|
-
class << self
|
43
|
-
attr_accessor :namespace
|
44
|
-
|
45
|
-
def configure
|
46
|
-
yield self
|
47
|
-
end
|
48
|
-
|
49
|
-
def name_spaced_file(file_name)
|
50
|
-
@namespace.nil? ? file_name : "#{file_name}_#{@namespace}"
|
51
|
-
end
|
52
|
-
|
53
|
-
def private_key_file
|
54
|
-
File.expand_path("~/.uberpass/private.pem")
|
55
|
-
end
|
56
|
-
|
57
|
-
def public_key_file
|
58
|
-
File.expand_path("~/.uberpass/public.pem")
|
59
|
-
end
|
60
|
-
|
61
|
-
def passwords_file
|
62
|
-
File.expand_path("~/.uberpass/#{name_spaced_file("passwords")}")
|
63
|
-
end
|
64
|
-
|
65
|
-
def key_file
|
66
|
-
File.expand_path("~/.uberpass/#{name_spaced_file("key")}")
|
67
|
-
end
|
68
|
-
|
69
|
-
def iv_file
|
70
|
-
File.expand_path("~/.uberpass/#{name_spaced_file("iv")}")
|
71
|
-
end
|
72
|
-
|
73
|
-
def write(encryptor)
|
74
|
-
File.open(passwords_file, "w") { |file|
|
75
|
-
file.write(encryptor.encrypted_data)
|
76
|
-
}
|
77
|
-
File.open(key_file, "w") { |file|
|
78
|
-
file.write(encryptor.encrypted_key)
|
79
|
-
}
|
80
|
-
File.open(iv_file, "w") { |file|
|
81
|
-
file.write(encryptor.encrypted_iv)
|
82
|
-
}
|
83
|
-
end
|
84
|
-
|
85
|
-
def decrypted_passwords
|
86
|
-
if File.exists?(passwords_file)
|
87
|
-
YAML::load(
|
88
|
-
Decrypt.new(
|
89
|
-
File.read(private_key_file),
|
90
|
-
File.read(passwords_file),
|
91
|
-
File.read(key_file),
|
92
|
-
File.read(iv_file)
|
93
|
-
).decrypted_data
|
94
|
-
)
|
95
|
-
else
|
96
|
-
{}
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def show(key)
|
101
|
-
Hash[*[key, decrypted_passwords[key]]]
|
102
|
-
end
|
103
|
-
|
104
|
-
def all
|
105
|
-
decrypted_passwords.map do |entry|
|
106
|
-
Hash[*entry]
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def generate_short(key)
|
111
|
-
encrypt key, SecureRandom.urlsafe_base64(8)
|
112
|
-
end
|
113
|
-
|
114
|
-
def generate(key)
|
115
|
-
encrypt key, SecureRandom.urlsafe_base64(24)
|
116
|
-
end
|
117
|
-
|
118
|
-
def encrypt(key, password)
|
119
|
-
passwords = decrypted_passwords
|
120
|
-
entry = passwords[key] = {
|
121
|
-
"password" => password,
|
122
|
-
"created_at" => Time.now
|
123
|
-
}
|
124
|
-
encryptor = Encrypt.new(File.read(public_key_file), passwords.to_yaml)
|
125
|
-
write(encryptor)
|
126
|
-
Hash[*[key, entry]]
|
127
|
-
end
|
128
|
-
|
129
|
-
def rename(old, new)
|
130
|
-
passwords = decrypted_passwords
|
131
|
-
entry = passwords.delete old
|
132
|
-
passwords[new] = entry
|
133
|
-
encryptor = Encrypt.new(File.read(public_key_file), passwords.to_yaml)
|
134
|
-
write(encryptor)
|
135
|
-
Hash[*[new, entry]]
|
136
|
-
end
|
137
|
-
|
138
|
-
def destroy(key)
|
139
|
-
passwords = decrypted_passwords
|
140
|
-
entry = passwords.delete key
|
141
|
-
encryptor = Encrypt.new(File.read(public_key_file), passwords.to_yaml)
|
142
|
-
write(encryptor)
|
143
|
-
Hash[*[key, entry]]
|
144
|
-
end
|
145
|
-
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
class CLI
|
150
|
-
class InvalidActionError < StandardError; end
|
151
|
-
class MissingArgumentError < StandardError; end
|
152
|
-
|
153
|
-
module Actions
|
154
|
-
attr_accessor :actions
|
155
3
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
options[:confirm] = false if options[:confirm].nil?
|
162
|
-
(@actions ||= []) << OpenStruct.new({ :name => args.first, :proc => block }.merge(options))
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
module Formater
|
167
|
-
def bold(obj)
|
168
|
-
"\033[0;1m#{obj}\033[0m"
|
169
|
-
end
|
170
|
-
|
171
|
-
def red(obj)
|
172
|
-
"\033[01;31m#{obj}\033[0m"
|
173
|
-
end
|
174
|
-
|
175
|
-
def green(obj)
|
176
|
-
"\033[0;32m#{obj}\033[0m"
|
177
|
-
end
|
178
|
-
|
179
|
-
def gray(obj)
|
180
|
-
"\033[1;30m#{obj}\033[0m"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
extend Actions
|
185
|
-
include Formater
|
186
|
-
|
187
|
-
register_action :generate, :steps => ["key"] do |key|
|
188
|
-
FileHandler.generate key
|
189
|
-
end
|
190
|
-
|
191
|
-
register_action :generate_short, :steps => ["key"], :short => :gs do |key|
|
192
|
-
FileHandler.generate_short key
|
193
|
-
end
|
194
|
-
|
195
|
-
register_action :destroy, :steps => ["key"], :short => :rm, :confirm => true do |key|
|
196
|
-
FileHandler.destroy key
|
197
|
-
end
|
198
|
-
|
199
|
-
register_action :show, :steps => ["key"] do |key|
|
200
|
-
FileHandler.show key
|
201
|
-
end
|
202
|
-
|
203
|
-
register_action :encrypt, :steps => ["key", "password"] do |key, password|
|
204
|
-
FileHandler.encrypt key, password
|
205
|
-
end
|
206
|
-
|
207
|
-
register_action :rename, :steps => ["key", "new name"] do |old, new|
|
208
|
-
FileHandler.rename old, new
|
209
|
-
end
|
210
|
-
|
211
|
-
register_action :list, :filter => ["password"] do
|
212
|
-
FileHandler.all
|
213
|
-
end
|
214
|
-
|
215
|
-
register_action :dump do
|
216
|
-
FileHandler.all
|
217
|
-
end
|
218
|
-
|
219
|
-
register_action :help do
|
220
|
-
CLI.help
|
221
|
-
[]
|
222
|
-
end
|
223
|
-
|
224
|
-
register_action :exit, :short => :ex do
|
225
|
-
exit
|
226
|
-
end
|
227
|
-
|
228
|
-
def initialize(namespace)
|
229
|
-
FileHandler.configure do |handler|
|
230
|
-
handler.namespace = namespace
|
231
|
-
end
|
232
|
-
line
|
233
|
-
do_action
|
234
|
-
end
|
235
|
-
|
236
|
-
def line(message = nil, format = :bold)
|
237
|
-
parts = []
|
238
|
-
parts << "\nuberpass"
|
239
|
-
parts << "#{VERSION}:"
|
240
|
-
parts << send(format, message) unless message.nil?
|
241
|
-
parts << "> "
|
242
|
-
print parts.join(' ')
|
243
|
-
end
|
244
|
-
|
245
|
-
def self.help
|
246
|
-
print "\nactions:\n"
|
247
|
-
actions.each do |action|
|
248
|
-
print " #{action.name}\n"
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
def confirm_action
|
253
|
-
line "are you sure you? [yn]", :green
|
254
|
-
$stdin.gets.chomp == "y"
|
255
|
-
end
|
256
|
-
|
257
|
-
def do_action
|
258
|
-
input = $stdin.gets.chomp
|
259
|
-
do_action_with_rescue input
|
260
|
-
end
|
261
|
-
|
262
|
-
def do_action_with_rescue(input)
|
263
|
-
args = input.split(/ /)
|
264
|
-
begin
|
265
|
-
action = fetch_action args.slice!(0)
|
266
|
-
action.steps[args.size, action.steps.size].each do |instruction|
|
267
|
-
line instruction, :green
|
268
|
-
arg = $stdin.gets.chomp
|
269
|
-
raise MissingArgumentError, instruction if arg == ""
|
270
|
-
args << arg
|
271
|
-
end
|
272
|
-
if action.confirm
|
273
|
-
pp(action.proc.call(*args), action.filter) if confirm_action
|
274
|
-
else
|
275
|
-
pp(action.proc.call(*args), action.filter)
|
276
|
-
end
|
277
|
-
print "\n"
|
278
|
-
line
|
279
|
-
do_action
|
280
|
-
rescue MissingArgumentError => e
|
281
|
-
line e, :red
|
282
|
-
do_action
|
283
|
-
rescue InvalidActionError => e
|
284
|
-
line e, :red
|
285
|
-
do_action
|
286
|
-
rescue OpenSSL::PKey::RSAError => e
|
287
|
-
line e, :red
|
288
|
-
do_action
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def fetch_action(key)
|
293
|
-
self.class.actions.each do |action|
|
294
|
-
return action if action.name == key.to_sym || action.short == key
|
295
|
-
end
|
296
|
-
raise InvalidActionError, key
|
297
|
-
end
|
4
|
+
require 'uberpass/version'
|
5
|
+
require 'uberpass/decrypt'
|
6
|
+
require 'uberpass/encrypt'
|
7
|
+
require 'uberpass/file_handler'
|
8
|
+
require 'uberpass/cli'
|
298
9
|
|
299
|
-
|
300
|
-
if entry.is_a? Array
|
301
|
-
entry.each do |entry|
|
302
|
-
pp entry, filter
|
303
|
-
end
|
304
|
-
else
|
305
|
-
key = entry.keys.first
|
306
|
-
filter.each do |f|
|
307
|
-
entry[key].delete f
|
308
|
-
end
|
309
|
-
print "\n#{gray entry[key]["created_at"].strftime("%d/%m/%Y")} #{bold key} "
|
310
|
-
print "\n#{entry[key]["password"]}" unless entry[key]["password"].nil?
|
311
|
-
end
|
312
|
-
end
|
313
|
-
end
|
10
|
+
module Uberpass
|
314
11
|
end
|
data/lib/uberpass/cli.rb
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require "highline"
|
3
|
+
|
4
|
+
module Uberpass
|
5
|
+
class CLI
|
6
|
+
class InvalidActionError < StandardError
|
7
|
+
attr_reader :action
|
8
|
+
|
9
|
+
def initialize action
|
10
|
+
@action = action
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"\\'#{action}\\' is not a uberpass command"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class MissingArgumentError < StandardError; end
|
20
|
+
|
21
|
+
module Actions
|
22
|
+
class Register
|
23
|
+
attr_accessor :name, :short, :usage, :description, :confirm
|
24
|
+
attr_writer :proc
|
25
|
+
|
26
|
+
def call *args
|
27
|
+
@proc.call *args
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_accessor :actions
|
32
|
+
|
33
|
+
def register_action
|
34
|
+
register = Register.new
|
35
|
+
yield register
|
36
|
+
(@actions ||= []) << register
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
HighLine.color_scheme = HighLine::ColorScheme.new do |cs|
|
41
|
+
cs[:error] = [:bold, :red]
|
42
|
+
cs[:confirm] = [:green]
|
43
|
+
cs[:date] = [:white]
|
44
|
+
cs[:name] = [:bold]
|
45
|
+
cs[:index] = [:magenta]
|
46
|
+
end
|
47
|
+
|
48
|
+
extend Actions
|
49
|
+
|
50
|
+
register_action do |action|
|
51
|
+
action.name = 'help'
|
52
|
+
action.short = 'help'
|
53
|
+
action.usage = 'help'
|
54
|
+
action.proc = ->(terminal) {
|
55
|
+
HelpDecorator.new(terminal, self.actions).output
|
56
|
+
}
|
57
|
+
action.description = "Lists all commands"
|
58
|
+
end
|
59
|
+
|
60
|
+
register_action do |action|
|
61
|
+
action.name = 'generate'
|
62
|
+
action.short = 'g'
|
63
|
+
action.usage = 'g google'
|
64
|
+
action.proc = ->(terminal, key) {
|
65
|
+
ShowDecorator.new(terminal, FileHandler.generate(key)).output
|
66
|
+
}
|
67
|
+
action.description = "Generates a random password for a given key"
|
68
|
+
end
|
69
|
+
|
70
|
+
register_action do |action|
|
71
|
+
action.name = 'generate_short'
|
72
|
+
action.short = 'gs'
|
73
|
+
action.usage = 'gs [name]'
|
74
|
+
action.proc = ->(terminal, key) {
|
75
|
+
ShowDecorator.new(terminal, FileHandler.generate_short(key)).output
|
76
|
+
}
|
77
|
+
action.description = "Generates a random password but smaller so its easier to type into a phone or a legacy system"
|
78
|
+
end
|
79
|
+
|
80
|
+
register_action do |action|
|
81
|
+
action.name = 'remove'
|
82
|
+
action.short = 'rm'
|
83
|
+
action.usage = 'rm [name]'
|
84
|
+
action.confirm = true
|
85
|
+
action.proc = ->(terminal, key) {
|
86
|
+
ShowDecorator.new(terminal, FileHandler.remove(key)).output
|
87
|
+
}
|
88
|
+
action.description = "Removes and entry"
|
89
|
+
end
|
90
|
+
|
91
|
+
register_action do |action|
|
92
|
+
action.name = 'show'
|
93
|
+
action.short = 's'
|
94
|
+
action.usage = 's [name|index]'
|
95
|
+
action.proc = ->(terminal, key) {
|
96
|
+
entry = if key =~ /^\d+$/
|
97
|
+
FileHandler.all[key.to_i]
|
98
|
+
else
|
99
|
+
FileHandler.show key
|
100
|
+
end
|
101
|
+
ShowDecorator.new(terminal, entry).output
|
102
|
+
}
|
103
|
+
action.description = "Shows an entry"
|
104
|
+
end
|
105
|
+
|
106
|
+
register_action do |action|
|
107
|
+
action.name = 'encrypt'
|
108
|
+
action.short = 'e'
|
109
|
+
action.usage = '[name] << [password]'
|
110
|
+
action.proc = ->(terminal, key, password) {
|
111
|
+
ShowDecorator.new(terminal, FileHandler.encrypt(key, password)).output
|
112
|
+
}
|
113
|
+
action.description = "Encrypts a value"
|
114
|
+
end
|
115
|
+
|
116
|
+
register_action do |action|
|
117
|
+
action.name = 'rename'
|
118
|
+
action.short = 'mv'
|
119
|
+
action.usage = 'mv [name] [new name]'
|
120
|
+
action.proc = ->(terminal, old, new) {
|
121
|
+
ShowDecorator.new(terminal, FileHandler.rename(old, new)).output
|
122
|
+
}
|
123
|
+
action.description = "Rename an entry"
|
124
|
+
end
|
125
|
+
|
126
|
+
register_action do |action|
|
127
|
+
action.name = 'list'
|
128
|
+
action.short = 'ls'
|
129
|
+
action.usage = 'ls'
|
130
|
+
action.proc = ->(terminal) {
|
131
|
+
ListDecorator.new(terminal, FileHandler.all).output
|
132
|
+
}
|
133
|
+
action.description = "Lists all entries"
|
134
|
+
end
|
135
|
+
|
136
|
+
register_action do |action|
|
137
|
+
action.name = 'dump'
|
138
|
+
action.short = 'dump'
|
139
|
+
action.usage = 'dump'
|
140
|
+
action.proc = ->(terminal) {
|
141
|
+
DumpDecorator.new(terminal, FileHandler.all).output
|
142
|
+
}
|
143
|
+
action.description = "Dumps all entries including passwords"
|
144
|
+
end
|
145
|
+
|
146
|
+
def initialize(namespace, input = $stdin, output = $stdout, run_loop = true)
|
147
|
+
@input = input
|
148
|
+
@output = output
|
149
|
+
@run_loop = run_loop
|
150
|
+
@terminal = HighLine.new @input, @output
|
151
|
+
|
152
|
+
pass = @terminal.ask("Enter PEM pass phrase: ") { |q| q.echo = '*' }
|
153
|
+
@output.print "\n"
|
154
|
+
|
155
|
+
FileHandler.configure do |handler|
|
156
|
+
handler.namespace = namespace
|
157
|
+
handler.pass_phrase = pass
|
158
|
+
end
|
159
|
+
do_action if @run_loop
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.help
|
163
|
+
print "\nactions:\n"
|
164
|
+
actions.each do |action|
|
165
|
+
@output.print " #{action.name}\n"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def confirm_action
|
170
|
+
@terminal.agree("<%= color('are you sure?', :confirm) %> ") { |q| q.default = "n" }
|
171
|
+
end
|
172
|
+
|
173
|
+
def do_action
|
174
|
+
input = @terminal.ask "uberpass:#{VERSION}> "
|
175
|
+
return if input.strip == 'exit'
|
176
|
+
@output.print "\n"
|
177
|
+
do_action_with_rescue input
|
178
|
+
@output.print "\n"
|
179
|
+
do_action if @run_loop
|
180
|
+
end
|
181
|
+
|
182
|
+
def do_action_with_rescue(input)
|
183
|
+
args = input.split(/ /).compact
|
184
|
+
if args[1] == '<'
|
185
|
+
action = fetch_action 'encrypt'
|
186
|
+
args.slice!(1)
|
187
|
+
else
|
188
|
+
action = fetch_action args.slice!(0)
|
189
|
+
end
|
190
|
+
|
191
|
+
if action.confirm && @run_loop
|
192
|
+
action.call(@terminal, *args) if confirm_action
|
193
|
+
else
|
194
|
+
action.call(@terminal, *args)
|
195
|
+
end
|
196
|
+
rescue MissingArgumentError, InvalidActionError, OpenSSL::PKey::RSAError => e
|
197
|
+
@terminal.say "<%= color('#{e}', :error) %>"
|
198
|
+
rescue FileHandler::ExistingEntryError => key
|
199
|
+
@terminal.say "<%= color('#{key} is already defined, try removing it first', :error) %>"
|
200
|
+
end
|
201
|
+
|
202
|
+
def fetch_action(key)
|
203
|
+
self.class.actions.each do |action|
|
204
|
+
return action if action.name == key || action.short == key
|
205
|
+
end
|
206
|
+
raise InvalidActionError, key
|
207
|
+
end
|
208
|
+
|
209
|
+
class ListDecorator
|
210
|
+
def initialize terminal, entries
|
211
|
+
@terminal, @entries = terminal, entries
|
212
|
+
end
|
213
|
+
|
214
|
+
def output
|
215
|
+
@entries.each_with_index do |entry, index|
|
216
|
+
key = entry.keys.first
|
217
|
+
output_entry key, entry[key], index
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def output_entry(key, values, index)
|
222
|
+
out = "<%= color('#{values["created_at"].strftime("%d/%m/%Y")}', :date) %>"
|
223
|
+
out << " <%= color('[#{index}]', :index) %>"
|
224
|
+
out << " <%= color('#{key}', :name) %>"
|
225
|
+
@terminal.say out
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class DumpDecorator < ListDecorator
|
230
|
+
def initialize terminal, entries
|
231
|
+
@terminal, @entries = terminal, entries
|
232
|
+
end
|
233
|
+
|
234
|
+
def output
|
235
|
+
@entries.each do |entry|
|
236
|
+
key = entry.keys.first
|
237
|
+
output_entry key, entry[key]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def output_entry(key, value)
|
242
|
+
out = "<%= color('#{key}', :name) %>"
|
243
|
+
out << " #{value["password"]}"
|
244
|
+
@terminal.say out
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
class ShowDecorator
|
249
|
+
def initialize terminal, entry
|
250
|
+
@terminal, @entry = terminal, entry
|
251
|
+
end
|
252
|
+
|
253
|
+
def output
|
254
|
+
key = @entry.keys.first
|
255
|
+
output_entry key, @entry[key]
|
256
|
+
end
|
257
|
+
|
258
|
+
def output_entry(key, values)
|
259
|
+
out = "<%= color('#{values["created_at"].strftime("%d/%m/%Y")}', :date) %>"
|
260
|
+
out << " <%= color('#{key}', :name) %>\n"
|
261
|
+
out << "<%= color('#{values["password"]}', :name) %>"
|
262
|
+
|
263
|
+
@terminal.say out
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
class HelpDecorator
|
268
|
+
def initialize terminal, actions
|
269
|
+
@terminal, @actions = terminal, actions
|
270
|
+
end
|
271
|
+
|
272
|
+
def output
|
273
|
+
@actions.each do |action|
|
274
|
+
next if action.name == 'help'
|
275
|
+
@terminal.say <<HELP
|
276
|
+
<%= color('#{action.name} - #{action.description}', BOLD) %>
|
277
|
+
usage: #{action.usage}
|
278
|
+
|
279
|
+
HELP
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Uberpass
|
4
|
+
class Decrypt
|
5
|
+
attr_reader :decrypted_data
|
6
|
+
|
7
|
+
def initialize(private_key, encrypted_data, encrypted_key, encrypted_iv, pass_phrase = nil)
|
8
|
+
key = OpenSSL::PKey::RSA.new(private_key, pass_phrase)
|
9
|
+
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
10
|
+
cipher.decrypt
|
11
|
+
cipher.key = key.private_decrypt(encrypted_key)
|
12
|
+
cipher.iv = key.private_decrypt(encrypted_iv)
|
13
|
+
|
14
|
+
@decrypted_data = cipher.update(encrypted_data)
|
15
|
+
@decrypted_data << cipher.final
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Uberpass
|
4
|
+
class Encrypt
|
5
|
+
attr_reader :encrypted_data, :encrypted_key, :encrypted_iv
|
6
|
+
|
7
|
+
def initialize(public_key, decrypted_data, pass_phrase = nil)
|
8
|
+
key = OpenSSL::PKey::RSA.new(public_key, pass_phrase)
|
9
|
+
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
10
|
+
cipher.encrypt
|
11
|
+
cipher.key = random_key = cipher.random_key
|
12
|
+
cipher.iv = random_iv = cipher.random_iv
|
13
|
+
|
14
|
+
@encrypted_data = cipher.update(decrypted_data)
|
15
|
+
@encrypted_data << cipher.final
|
16
|
+
|
17
|
+
@encrypted_key = key.public_encrypt(random_key)
|
18
|
+
@encrypted_iv = key.public_encrypt(random_iv)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Uberpass
|
4
|
+
class FileHandler
|
5
|
+
class ExistingEntryError < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :namespace, :pass_phrase
|
9
|
+
|
10
|
+
def configure
|
11
|
+
yield self
|
12
|
+
end
|
13
|
+
|
14
|
+
def name_spaced_file(file_name)
|
15
|
+
@namespace.nil? ? file_name : "#{file_name}_#{@namespace}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def private_key_file
|
19
|
+
File.expand_path("~/.uberpass/private.pem")
|
20
|
+
end
|
21
|
+
|
22
|
+
def public_key_file
|
23
|
+
File.expand_path("~/.uberpass/public.pem")
|
24
|
+
end
|
25
|
+
|
26
|
+
def passwords_file
|
27
|
+
File.expand_path("~/.uberpass/#{name_spaced_file("passwords")}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def key_file
|
31
|
+
File.expand_path("~/.uberpass/#{name_spaced_file("key")}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def iv_file
|
35
|
+
File.expand_path("~/.uberpass/#{name_spaced_file("iv")}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def write(encryptor)
|
39
|
+
File.open(passwords_file, "w") { |file|
|
40
|
+
file.write(encryptor.encrypted_data)
|
41
|
+
}
|
42
|
+
File.open(key_file, "w") { |file|
|
43
|
+
file.write(encryptor.encrypted_key)
|
44
|
+
}
|
45
|
+
File.open(iv_file, "w") { |file|
|
46
|
+
file.write(encryptor.encrypted_iv)
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def decrypted_passwords
|
51
|
+
if File.exists?(passwords_file)
|
52
|
+
YAML::load(
|
53
|
+
Decrypt.new(
|
54
|
+
File.read(private_key_file),
|
55
|
+
File.read(passwords_file),
|
56
|
+
File.read(key_file),
|
57
|
+
File.read(iv_file),
|
58
|
+
pass_phrase
|
59
|
+
).decrypted_data
|
60
|
+
)
|
61
|
+
else
|
62
|
+
{}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def show(key)
|
67
|
+
Hash[*[key, decrypted_passwords[key]]]
|
68
|
+
end
|
69
|
+
|
70
|
+
def all
|
71
|
+
decrypted_passwords.map do |entry|
|
72
|
+
Hash[*entry]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_short(key)
|
77
|
+
encrypt key, SecureRandom.urlsafe_base64(8)
|
78
|
+
end
|
79
|
+
|
80
|
+
def generate(key)
|
81
|
+
encrypt key, SecureRandom.urlsafe_base64(24)
|
82
|
+
end
|
83
|
+
|
84
|
+
def encrypt(key, password)
|
85
|
+
passwords = decrypted_passwords
|
86
|
+
raise ExistingEntryError, key unless passwords[key].nil?
|
87
|
+
entry = passwords[key] = {
|
88
|
+
"password" => password,
|
89
|
+
"created_at" => Time.now
|
90
|
+
}
|
91
|
+
encryptor = Encrypt.new(File.read(public_key_file), passwords.to_yaml, pass_phrase)
|
92
|
+
write(encryptor)
|
93
|
+
Hash[*[key, entry]]
|
94
|
+
end
|
95
|
+
|
96
|
+
def rename(old, new)
|
97
|
+
passwords = decrypted_passwords
|
98
|
+
raise ExistingEntryError, new unless passwords[new].nil?
|
99
|
+
entry = passwords.delete old
|
100
|
+
passwords[new] = entry
|
101
|
+
encryptor = Encrypt.new(File.read(public_key_file), passwords.to_yaml)
|
102
|
+
write(encryptor)
|
103
|
+
Hash[*[new, entry]]
|
104
|
+
end
|
105
|
+
|
106
|
+
def remove(key)
|
107
|
+
passwords = decrypted_passwords
|
108
|
+
entry = passwords.delete key
|
109
|
+
encryptor = Encrypt.new(File.read(public_key_file), passwords.to_yaml)
|
110
|
+
write(encryptor)
|
111
|
+
Hash[*[key, entry]]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/uberpass/version.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$password = File.read(File.join(File.dirname(__FILE__), '..', 'password.txt')).strip
|
3
|
+
$password.freeze
|
4
|
+
|
5
|
+
require 'uberpass'
|
6
|
+
|
7
|
+
input = StringIO.new
|
8
|
+
output = StringIO.new
|
9
|
+
|
10
|
+
input << $password
|
11
|
+
input.rewind
|
12
|
+
|
13
|
+
uberpass = Uberpass::CLI.new 'test', input, output, false
|
14
|
+
|
15
|
+
input.truncate input.rewind
|
16
|
+
|
17
|
+
puts output.string
|
18
|
+
output.truncate output.rewind
|
19
|
+
|
20
|
+
input << 'ls'
|
21
|
+
input.rewind
|
22
|
+
uberpass.do_action
|
23
|
+
input.truncate input.rewind
|
24
|
+
|
25
|
+
puts output.string
|
26
|
+
output.truncate output.rewind
|
27
|
+
|
28
|
+
input << 'dump'
|
29
|
+
input.rewind
|
30
|
+
uberpass.do_action
|
31
|
+
input.truncate input.rewind
|
32
|
+
|
33
|
+
puts output.string
|
34
|
+
output.truncate output.rewind
|
35
|
+
|
36
|
+
input << 'g cia'
|
37
|
+
input.rewind
|
38
|
+
uberpass.do_action
|
39
|
+
input.truncate input.rewind
|
40
|
+
|
41
|
+
puts output.string
|
42
|
+
output.truncate output.rewind
|
43
|
+
|
44
|
+
input << 's cia'
|
45
|
+
input.rewind
|
46
|
+
uberpass.do_action
|
47
|
+
input.truncate input.rewind
|
48
|
+
|
49
|
+
puts output.string
|
50
|
+
output.truncate output.rewind
|
51
|
+
|
52
|
+
input << 'help'
|
53
|
+
input.rewind
|
54
|
+
uberpass.do_action
|
55
|
+
input.truncate input.rewind
|
56
|
+
|
57
|
+
puts output.string
|
58
|
+
output.truncate output.rewind
|
@@ -0,0 +1,119 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$password = File.read(File.join(File.dirname(__FILE__), '..', 'password.txt')).strip
|
3
|
+
$password.freeze
|
4
|
+
|
5
|
+
require 'minitest/spec'
|
6
|
+
require 'minitest/autorun'
|
7
|
+
require 'turn/autorun'
|
8
|
+
require 'uberpass'
|
9
|
+
|
10
|
+
describe Uberpass do
|
11
|
+
before do
|
12
|
+
@input = StringIO.new
|
13
|
+
@output = StringIO.new
|
14
|
+
|
15
|
+
@input << $password
|
16
|
+
@input.rewind
|
17
|
+
|
18
|
+
@uberpass = Uberpass::CLI.new 'test', @input, @output, false
|
19
|
+
|
20
|
+
@input.truncate(@input.rewind)
|
21
|
+
|
22
|
+
Uberpass::FileHandler.all.each do |entry|
|
23
|
+
Uberpass::FileHandler.remove entry.keys.first
|
24
|
+
end
|
25
|
+
|
26
|
+
Uberpass::FileHandler.generate 'twitter'
|
27
|
+
Uberpass::FileHandler.generate 'facebook'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should ask and memorise the password" do
|
31
|
+
prompt = "Enter PEM pass phrase: #{$password.gsub(/\w/, '*')}\n\n"
|
32
|
+
|
33
|
+
assert_equal prompt, @output.string
|
34
|
+
assert_equal $password, Uberpass::FileHandler.pass_phrase
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should list entries" do
|
38
|
+
@output.truncate(@output.rewind)
|
39
|
+
@input << 'ls'
|
40
|
+
@input.rewind
|
41
|
+
|
42
|
+
@uberpass.do_action
|
43
|
+
|
44
|
+
assert_match /facebook/, @output.string
|
45
|
+
assert_match /twitter/, @output.string
|
46
|
+
assert_match Time.now.strftime("%d/%m/%Y"), @output.string
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should raise and catch argument error" do
|
50
|
+
@output.truncate(@output.rewind)
|
51
|
+
@input << 'wadyawant'
|
52
|
+
@input.rewind
|
53
|
+
|
54
|
+
@uberpass.do_action
|
55
|
+
|
56
|
+
assert_match /is not a uberpass command/, @output.string
|
57
|
+
assert_match /'wadyawant'/, @output.string
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should confirm deletion of password" do
|
61
|
+
Uberpass::FileHandler.generate 'linkedin'
|
62
|
+
|
63
|
+
@output.truncate(@output.rewind)
|
64
|
+
@input << 'rm linkedin'
|
65
|
+
@input.rewind
|
66
|
+
|
67
|
+
@uberpass.do_action
|
68
|
+
|
69
|
+
@input.truncate(@input.rewind)
|
70
|
+
@input << 'yes'
|
71
|
+
@input.rewind
|
72
|
+
|
73
|
+
@uberpass.confirm_action
|
74
|
+
|
75
|
+
assert_match /are you sure\?/, @output.string
|
76
|
+
assert_match /linkedin/, @output.string
|
77
|
+
assert_match Time.now.strftime("%d/%m/%Y"), @output.string
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should encrypt an existing password with <" do
|
81
|
+
@output.truncate(@output.rewind)
|
82
|
+
@input << 'google < xxx'
|
83
|
+
@input.rewind
|
84
|
+
|
85
|
+
@uberpass.do_action
|
86
|
+
|
87
|
+
password = Uberpass::FileHandler.show 'google'
|
88
|
+
|
89
|
+
refute_nil password['google']['password']
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should encrypt an existing password with <" do
|
93
|
+
@output.truncate(@output.rewind)
|
94
|
+
@input << 'e foursquare xxx'
|
95
|
+
@input.rewind
|
96
|
+
|
97
|
+
@uberpass.do_action
|
98
|
+
|
99
|
+
password = Uberpass::FileHandler.show 'foursquare'
|
100
|
+
|
101
|
+
refute_nil password['foursquare']['password']
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should rename a key" do
|
105
|
+
Uberpass::FileHandler.generate 'tumbler'
|
106
|
+
|
107
|
+
@output.truncate(@output.rewind)
|
108
|
+
@input << 'mv tumbler tumblr'
|
109
|
+
@input.rewind
|
110
|
+
|
111
|
+
@uberpass.do_action
|
112
|
+
|
113
|
+
password = Uberpass::FileHandler.show 'tumbler'
|
114
|
+
assert_nil password['tumbler']
|
115
|
+
|
116
|
+
password = Uberpass::FileHandler.show 'tumblr'
|
117
|
+
refute_nil password['tumblr']
|
118
|
+
end
|
119
|
+
end
|
data/uberpass.gemspec
CHANGED
@@ -13,7 +13,9 @@ Gem::Specification.new do |s|
|
|
13
13
|
|
14
14
|
s.rubyforge_project = "uberpass"
|
15
15
|
|
16
|
+
s.add_runtime_dependency "highline", "1.6.15"
|
16
17
|
s.add_development_dependency "rake"
|
18
|
+
s.add_development_dependency "turn", "0.9.6"
|
17
19
|
|
18
20
|
s.files = `git ls-files`.split("\n")
|
19
21
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: uberpass
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,19 +9,56 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-03-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: highline
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - '='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.6.15
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - '='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.6.15
|
14
30
|
- !ruby/object:Gem::Dependency
|
15
31
|
name: rake
|
16
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
41
|
none: false
|
18
42
|
requirements:
|
19
43
|
- - ! '>='
|
20
44
|
- !ruby/object:Gem::Version
|
21
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: turn
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - '='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.9.6
|
22
54
|
type: :development
|
23
55
|
prerelease: false
|
24
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.6
|
25
62
|
description: uses open ssl and a cli to generate and retrieve passwords
|
26
63
|
email:
|
27
64
|
- rufuspost@gmail.com
|
@@ -36,7 +73,13 @@ files:
|
|
36
73
|
- Rakefile
|
37
74
|
- bin/uberpass
|
38
75
|
- lib/uberpass.rb
|
76
|
+
- lib/uberpass/cli.rb
|
77
|
+
- lib/uberpass/decrypt.rb
|
78
|
+
- lib/uberpass/encrypt.rb
|
79
|
+
- lib/uberpass/file_handler.rb
|
39
80
|
- lib/uberpass/version.rb
|
81
|
+
- spec/uberpass_output.rb
|
82
|
+
- spec/uberpass_spec.rb
|
40
83
|
- uberpass.gemspec
|
41
84
|
homepage: ''
|
42
85
|
licenses: []
|
@@ -50,22 +93,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
93
|
- - ! '>='
|
51
94
|
- !ruby/object:Gem::Version
|
52
95
|
version: '0'
|
53
|
-
segments:
|
54
|
-
- 0
|
55
|
-
hash: -4545846251252195404
|
56
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
97
|
none: false
|
58
98
|
requirements:
|
59
99
|
- - ! '>='
|
60
100
|
- !ruby/object:Gem::Version
|
61
101
|
version: '0'
|
62
|
-
segments:
|
63
|
-
- 0
|
64
|
-
hash: -4545846251252195404
|
65
102
|
requirements: []
|
66
103
|
rubyforge_project: uberpass
|
67
|
-
rubygems_version: 1.8.
|
104
|
+
rubygems_version: 1.8.23
|
68
105
|
signing_key:
|
69
106
|
specification_version: 3
|
70
107
|
summary: command line key chain
|
71
|
-
test_files:
|
108
|
+
test_files:
|
109
|
+
- spec/uberpass_output.rb
|
110
|
+
- spec/uberpass_spec.rb
|
111
|
+
has_rdoc:
|