pwkeep 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +41 -0
- data/LICENSE +20 -0
- data/README.md +68 -0
- data/Rakefile +2 -0
- data/bin/pwkeep +6 -0
- data/lib/pwkeep.rb +31 -0
- data/lib/pwkeep/config.rb +10 -0
- data/lib/pwkeep/editor.rb +462 -0
- data/lib/pwkeep/generator.rb +0 -0
- data/lib/pwkeep/main.rb +174 -0
- data/lib/pwkeep/storage.rb +177 -0
- data/pwkeep.gemspec +22 -0
- metadata +190 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pwkeep (0.0.1)
|
5
|
+
bundler
|
6
|
+
colorize
|
7
|
+
hashr
|
8
|
+
highline
|
9
|
+
keepass-password-generator
|
10
|
+
lockfile
|
11
|
+
ruco
|
12
|
+
trollop
|
13
|
+
|
14
|
+
GEM
|
15
|
+
remote: http://rubygems.org/
|
16
|
+
specs:
|
17
|
+
clipboard (1.0.5)
|
18
|
+
colorize (0.6.0)
|
19
|
+
dispel (0.0.3)
|
20
|
+
hashr (0.0.22)
|
21
|
+
highline (1.6.20)
|
22
|
+
keepass-password-generator (0.1.1)
|
23
|
+
language_sniffer (1.0.2)
|
24
|
+
lockfile (2.1.0)
|
25
|
+
plist (3.1.0)
|
26
|
+
rake (10.1.1)
|
27
|
+
ruco (0.2.19)
|
28
|
+
clipboard (>= 0.9.8)
|
29
|
+
dispel
|
30
|
+
language_sniffer
|
31
|
+
textpow (>= 1.3.0)
|
32
|
+
textpow (1.3.1)
|
33
|
+
plist (>= 3.0.1)
|
34
|
+
trollop (2.0)
|
35
|
+
|
36
|
+
PLATFORMS
|
37
|
+
ruby
|
38
|
+
|
39
|
+
DEPENDENCIES
|
40
|
+
pwkeep!
|
41
|
+
rake
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Aki Tuomi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
PassWord Keep (pwkeep)
|
2
|
+
======================
|
3
|
+
|
4
|
+
Simple password storage system.
|
5
|
+
|
6
|
+
Quick start guide
|
7
|
+
=================
|
8
|
+
|
9
|
+
Run pwkeep -i to initialize a new storage into ~/.pwkeep. This will create an RSA key pair.
|
10
|
+
|
11
|
+
If you want to tune the algorithm(s), key sizes and such, you can create ~/.pwkeep/config.yml (see below for syntax).
|
12
|
+
|
13
|
+
If you want to place it somewhere else, set PWKEEP\_HOME environment variable, or use -H (--home) parameter when running pwkeep.
|
14
|
+
|
15
|
+
To add credentials, use
|
16
|
+
|
17
|
+
pwkeep -c -n <name of cred>
|
18
|
+
|
19
|
+
To modify them
|
20
|
+
|
21
|
+
pwkeep -e -n <name>
|
22
|
+
|
23
|
+
And to show
|
24
|
+
|
25
|
+
pwkeep -v -n <name>
|
26
|
+
|
27
|
+
See --help for more options.
|
28
|
+
|
29
|
+
Features
|
30
|
+
========
|
31
|
+
|
32
|
+
Password keep is intended to be simple and easy to use. It uses RSA + AES256 encryption for your credentials. The
|
33
|
+
data is not restricted to usernames and passwords, you can store whatever you want.
|
34
|
+
|
35
|
+
Editing is done with embedded ruco text editor using memory-only backing. No temporary files are used.
|
36
|
+
|
37
|
+
Configuration
|
38
|
+
=============
|
39
|
+
|
40
|
+
The configuration file is a simple YAML formatted file with following syntax (*NOT YET SUPPORTED*)
|
41
|
+
|
42
|
+
```yaml
|
43
|
+
---
|
44
|
+
# less than 1k makes no sense. your files will be at least this / 8 bytes.
|
45
|
+
keysize: 2048
|
46
|
+
iterations: 2000
|
47
|
+
# do not edit the following unless you know what you are doing.
|
48
|
+
cipher: AES-256-CTR
|
49
|
+
```
|
50
|
+
|
51
|
+
File formats
|
52
|
+
============
|
53
|
+
|
54
|
+
The private.pem file contains your private key. It is fully manipulatable with openssl binary without any specialities.
|
55
|
+
|
56
|
+
system-\* files contain actual credentials. The file name consists from system- prefix and hashed system name. The system
|
57
|
+
name is hashed iterations time with chosen hash, SHA512 by default.
|
58
|
+
|
59
|
+
The actual file format is:
|
60
|
+
|
61
|
+
* header (encrypted with your public key)
|
62
|
+
* nil terminated algorithm name
|
63
|
+
* 16 byte iv (algorithm dependant)
|
64
|
+
* 32 byte key (algorithm dependant)
|
65
|
+
* data: encrypted credential with above key+id
|
66
|
+
|
67
|
+
You cannot decrypt this with openssl directly, but you can easily write a program to do this. The header is padded with OAEP
|
68
|
+
padding.
|
data/Rakefile
ADDED
data/bin/pwkeep
ADDED
data/lib/pwkeep.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'trollop'
|
3
|
+
require 'digest/sha2'
|
4
|
+
require 'logger'
|
5
|
+
require 'pathname'
|
6
|
+
require 'colorize'
|
7
|
+
require 'lockfile'
|
8
|
+
require 'openssl'
|
9
|
+
require 'ruco'
|
10
|
+
|
11
|
+
module PWKeep
|
12
|
+
extend self
|
13
|
+
|
14
|
+
def logger
|
15
|
+
unless @logger
|
16
|
+
@logger = Logger.new(STDOUT)
|
17
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
18
|
+
"#{msg}\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@logger
|
22
|
+
end
|
23
|
+
|
24
|
+
class Exception < ::Exception
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'pwkeep/main'
|
29
|
+
require 'pwkeep/generator'
|
30
|
+
require 'pwkeep/storage'
|
31
|
+
require 'pwkeep/editor'
|
@@ -0,0 +1,462 @@
|
|
1
|
+
require 'clipboard'
|
2
|
+
|
3
|
+
module PWKeep
|
4
|
+
def self.run_editor(data, options)
|
5
|
+
ret = [false, data]
|
6
|
+
|
7
|
+
Dispel::Screen.open do |screen|
|
8
|
+
$ruco_screen = screen
|
9
|
+
app = PWKeep::EditorApplication.new(data.to_s,
|
10
|
+
:lines => screen.lines, :columns => screen.columns
|
11
|
+
)
|
12
|
+
|
13
|
+
screen.draw *app.display_info
|
14
|
+
|
15
|
+
Dispel::Keyboard.output do |key|
|
16
|
+
if key == :resize
|
17
|
+
app.resize(screen.lines, screen.columns)
|
18
|
+
else
|
19
|
+
result = app.key key
|
20
|
+
if result == :quit
|
21
|
+
ret = [app.editor.saved_content != data, app.editor.saved_content]
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
screen.draw *app.display_info
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
return ret
|
31
|
+
end
|
32
|
+
|
33
|
+
class EditorApplication
|
34
|
+
attr_reader :editor, :status, :command, :options
|
35
|
+
|
36
|
+
def initialize(data, options)
|
37
|
+
@options = Ruco::OptionAccessor.new(options)
|
38
|
+
@data = data
|
39
|
+
|
40
|
+
setup_actions
|
41
|
+
setup_keys
|
42
|
+
create_components
|
43
|
+
end
|
44
|
+
|
45
|
+
def display_info
|
46
|
+
[view, style_map, cursor]
|
47
|
+
end
|
48
|
+
|
49
|
+
def view
|
50
|
+
[status.view, editor.view, command.view].join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
def style_map
|
54
|
+
status.style_map + editor.style_map + command.style_map
|
55
|
+
end
|
56
|
+
|
57
|
+
def cursor
|
58
|
+
Ruco::Position.new(@focused.cursor.line + @status_lines, @focused.cursor.column)
|
59
|
+
end
|
60
|
+
|
61
|
+
# user typed a key
|
62
|
+
def key(key)
|
63
|
+
# deactivate select_mode if its not re-enabled in this action
|
64
|
+
@select_mode_was_on = @select_mode
|
65
|
+
@select_mode = false
|
66
|
+
|
67
|
+
if bound = @bindings[key]
|
68
|
+
return execute_action(bound)
|
69
|
+
end
|
70
|
+
|
71
|
+
case key
|
72
|
+
|
73
|
+
# move
|
74
|
+
when :down then move_with_select_mode :relative, 1,0
|
75
|
+
when :right then move_with_select_mode :relative, 0,1
|
76
|
+
when :up then move_with_select_mode :relative, -1,0
|
77
|
+
when :left then move_with_select_mode :relative, 0,-1
|
78
|
+
when :page_up then move_with_select_mode :page_up
|
79
|
+
when :page_down then move_with_select_mode :page_down
|
80
|
+
when :"Ctrl+right", :"Alt+f" then move_with_select_mode :jump, :right
|
81
|
+
when :"Ctrl+left", :"Alt+b" then move_with_select_mode :jump, :left
|
82
|
+
|
83
|
+
# select
|
84
|
+
when :"Shift+down" then @focused.selecting { move(:relative, 1, 0) }
|
85
|
+
when :"Shift+right" then @focused.selecting { move(:relative, 0, 1) }
|
86
|
+
when :"Shift+up" then @focused.selecting { move(:relative, -1, 0) }
|
87
|
+
when :"Shift+left" then @focused.selecting { move(:relative, 0, -1) }
|
88
|
+
when :"Ctrl+Shift+left", :"Alt+Shift+left" then @focused.selecting{ move(:jump, :left) }
|
89
|
+
when :"Ctrl+Shift+right", :"Alt+Shift+right" then @focused.selecting{ move(:jump, :right) }
|
90
|
+
when :"Shift+end" then @focused.selecting{ move(:to_eol) }
|
91
|
+
when :"Shift+home" then @focused.selecting{ move(:to_bol) }
|
92
|
+
|
93
|
+
|
94
|
+
# modify
|
95
|
+
when :tab then
|
96
|
+
if @editor.selection
|
97
|
+
@editor.indent
|
98
|
+
else
|
99
|
+
@focused.insert("\t")
|
100
|
+
end
|
101
|
+
when :"Shift+tab" then @editor.unindent
|
102
|
+
when :enter then @focused.insert("\n")
|
103
|
+
when :backspace then @focused.delete(-1)
|
104
|
+
when :delete then @focused.delete(1)
|
105
|
+
|
106
|
+
when :escape then # escape from focused
|
107
|
+
@focused.reset
|
108
|
+
@focused = editor
|
109
|
+
else
|
110
|
+
@focused.insert(key) if key.is_a?(String)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def bind(key, action=nil, &block)
|
115
|
+
raise "Ctrl+m cannot be bound" if key == :"Ctrl+m" # would shadow enter -> bad
|
116
|
+
raise "Cannot bind an action and a block" if action and block
|
117
|
+
@bindings[key] = action || block
|
118
|
+
end
|
119
|
+
|
120
|
+
def action(name, &block)
|
121
|
+
@actions[name] = block
|
122
|
+
end
|
123
|
+
|
124
|
+
def ask(question, options={}, &block)
|
125
|
+
@focused = command
|
126
|
+
command.ask(question, options) do |response|
|
127
|
+
@focused = editor
|
128
|
+
block.call(response)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def loop_ask(question, options={}, &block)
|
133
|
+
ask(question, options) do |result|
|
134
|
+
finished = (block.call(result) == :finished)
|
135
|
+
loop_ask(question, options, &block) unless finished
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def configure(&block)
|
140
|
+
instance_exec(&block)
|
141
|
+
end
|
142
|
+
|
143
|
+
def resize(lines, columns)
|
144
|
+
@options[:lines] = lines
|
145
|
+
@options[:columns] = columns
|
146
|
+
create_components
|
147
|
+
@editor.resize(editor_lines, columns)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def setup_actions
|
153
|
+
@actions = {}
|
154
|
+
|
155
|
+
action :paste do
|
156
|
+
@focused.insert(Clipboard.paste)
|
157
|
+
end
|
158
|
+
|
159
|
+
action :copy do
|
160
|
+
Clipboard.copy(@focused.text_in_selection)
|
161
|
+
end
|
162
|
+
|
163
|
+
action :cut do
|
164
|
+
Clipboard.copy(@focused.text_in_selection)
|
165
|
+
@focused.delete(0)
|
166
|
+
end
|
167
|
+
|
168
|
+
action :save do
|
169
|
+
result = editor.save
|
170
|
+
if result != true
|
171
|
+
ask("#{result.slice(0,100)} -- Enter=Retry Esc=Cancel "){ @actions[:save].call }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
action :quit do
|
176
|
+
if editor.modified?
|
177
|
+
ask("Lose changes? Enter=Yes Esc=Cancel") do
|
178
|
+
editor.store_session
|
179
|
+
:quit
|
180
|
+
end
|
181
|
+
else
|
182
|
+
editor.store_session
|
183
|
+
:quit
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
action :go_to_line do
|
188
|
+
ask('Go to Line: ') do |result|
|
189
|
+
editor.move(:to_line, result.to_i - 1)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
action :delete_line do
|
194
|
+
editor.delete_line
|
195
|
+
end
|
196
|
+
|
197
|
+
action :select_mode do
|
198
|
+
@select_mode = !@select_mode_was_on
|
199
|
+
end
|
200
|
+
|
201
|
+
action :select_all do
|
202
|
+
@focused.move(:to, 0, 0)
|
203
|
+
@focused.selecting do
|
204
|
+
move(:to, 9999, 9999)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
action :find do
|
209
|
+
ask("Find: ", :cache => true) do |result|
|
210
|
+
next if editor.find(result)
|
211
|
+
|
212
|
+
if editor.content.include?(result)
|
213
|
+
ask("No matches found -- Enter=First match ESC=Stop") do
|
214
|
+
editor.move(:to, 0,0)
|
215
|
+
editor.find(result)
|
216
|
+
end
|
217
|
+
else
|
218
|
+
ask("No matches found in entire file", :auto_enter => true){}
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
action :find_and_replace do
|
224
|
+
ask("Find: ", :cache => true) do |term|
|
225
|
+
if editor.find(term)
|
226
|
+
ask("Replace with: ", :cache => true) do |replace|
|
227
|
+
loop_ask("Replace=Enter Skip=s All=a Cancel=Esc", :auto_enter => true) do |ok|
|
228
|
+
case ok
|
229
|
+
when '' # enter
|
230
|
+
editor.insert(replace)
|
231
|
+
when 'a'
|
232
|
+
stop = true
|
233
|
+
editor.insert(replace)
|
234
|
+
editor.insert(replace) while editor.find(term)
|
235
|
+
when 's' # do nothing
|
236
|
+
else
|
237
|
+
stop = true
|
238
|
+
end
|
239
|
+
|
240
|
+
:finished if stop or not editor.find(term)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
action(:undo){ @editor.undo if @focused == @editor }
|
248
|
+
action(:redo){ @editor.redo if @focused == @editor }
|
249
|
+
action(:move_line_up){ @editor.move_line(-1) if @focused == @editor }
|
250
|
+
action(:move_line_down){ @editor.move_line(1) if @focused == @editor }
|
251
|
+
|
252
|
+
action(:move_to_eol){ move_with_select_mode :to_eol }
|
253
|
+
action(:move_to_bol){ move_with_select_mode :to_bol }
|
254
|
+
|
255
|
+
action(:insert_hash_rocket){ @editor.insert(' => ') }
|
256
|
+
end
|
257
|
+
|
258
|
+
def setup_keys
|
259
|
+
@bindings = {}
|
260
|
+
bind :"Ctrl+s", :save
|
261
|
+
bind :"Ctrl+w", :quit
|
262
|
+
bind :"Ctrl+q", :quit
|
263
|
+
bind :"Ctrl+g", :go_to_line
|
264
|
+
bind :"Ctrl+f", :find
|
265
|
+
bind :"Ctrl+r", :find_and_replace
|
266
|
+
bind :"Ctrl+b", :select_mode
|
267
|
+
bind :"Ctrl+a", :select_all
|
268
|
+
bind :"Ctrl+d", :delete_line
|
269
|
+
bind :"Ctrl+l", :insert_hash_rocket
|
270
|
+
bind :"Ctrl+x", :cut
|
271
|
+
bind :"Ctrl+c", :copy
|
272
|
+
bind :"Ctrl+v", :paste
|
273
|
+
bind :"Ctrl+z", :undo
|
274
|
+
bind :"Ctrl+y", :redo
|
275
|
+
bind :"Alt+Ctrl+down", :move_line_down
|
276
|
+
bind :"Alt+Ctrl+up", :move_line_up
|
277
|
+
bind :end, :move_to_eol
|
278
|
+
bind :"Ctrl+e", :move_to_eol # for OsX
|
279
|
+
bind :home, :move_to_bol
|
280
|
+
end
|
281
|
+
|
282
|
+
def load_user_config
|
283
|
+
Ruco.application = self
|
284
|
+
config = File.expand_path(@options[:rc] || "~/.ruco.rb")
|
285
|
+
load config if File.exist?(config)
|
286
|
+
end
|
287
|
+
|
288
|
+
def create_components
|
289
|
+
@status_lines = 1
|
290
|
+
|
291
|
+
editor_options = @options.slice(
|
292
|
+
:columns, :convert_tabs, :convert_newlines, :undo_stack_size, :color_theme
|
293
|
+
).merge(
|
294
|
+
:window => @options.nested(:window),
|
295
|
+
:history => @options.nested(:history),
|
296
|
+
:lines => editor_lines
|
297
|
+
).merge(@options.nested(:editor))
|
298
|
+
|
299
|
+
@editor ||= PWKeep::Editor.new(@data, editor_options)
|
300
|
+
@status = PWKeep::StatusBar.new(@editor, @options.nested(:status_bar).merge(:columns => options[:columns]))
|
301
|
+
@command = Ruco::CommandBar.new(@options.nested(:command_bar).merge(:columns => options[:columns]))
|
302
|
+
command.cursor_line = editor_lines
|
303
|
+
@focused = @editor
|
304
|
+
end
|
305
|
+
|
306
|
+
def editor_lines
|
307
|
+
command_lines = 1
|
308
|
+
@options[:lines] - @status_lines - command_lines
|
309
|
+
end
|
310
|
+
|
311
|
+
def parse_file_and_line(file)
|
312
|
+
if file.to_s.include?(':') and not File.exist?(file)
|
313
|
+
short_file, go_to_line = file.split(':',2)
|
314
|
+
if File.exist?(short_file)
|
315
|
+
file = short_file
|
316
|
+
else
|
317
|
+
go_to_line = nil
|
318
|
+
end
|
319
|
+
end
|
320
|
+
[file, go_to_line]
|
321
|
+
end
|
322
|
+
|
323
|
+
def move_with_select_mode(*args)
|
324
|
+
@select_mode = true if @select_mode_was_on
|
325
|
+
if @select_mode
|
326
|
+
@focused.selecting do
|
327
|
+
move(*args)
|
328
|
+
end
|
329
|
+
else
|
330
|
+
@focused.send(:move, *args)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def execute_action(action)
|
335
|
+
if action.is_a?(Symbol)
|
336
|
+
@actions[action].call
|
337
|
+
else
|
338
|
+
action.call
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class Editor
|
344
|
+
attr_reader :file
|
345
|
+
attr_reader :text_area
|
346
|
+
attr_reader :history
|
347
|
+
attr_reader :saved_content
|
348
|
+
|
349
|
+
private :text_area
|
350
|
+
delegate :view, :style_map, :cursor, :position,
|
351
|
+
:insert, :indent, :unindent, :delete, :delete_line,
|
352
|
+
:redo, :undo, :save_state,
|
353
|
+
:selecting, :selection, :text_in_selection, :reset,
|
354
|
+
:move, :resize, :move_line,
|
355
|
+
:to => :text_area
|
356
|
+
|
357
|
+
def initialize(data, options)
|
358
|
+
@options = options
|
359
|
+
|
360
|
+
content = data
|
361
|
+
@options[:language] ||= LanguageSniffer.detect(@file, :content => content).language
|
362
|
+
content.tabs_to_spaces! if @options[:convert_tabs]
|
363
|
+
|
364
|
+
# cleanup newline formats
|
365
|
+
@newline = content.match("\r\n|\r|\n")
|
366
|
+
@newline = (@newline ? @newline[0] : "\n")
|
367
|
+
content.gsub!(/\r\n?/,"\n")
|
368
|
+
|
369
|
+
@saved_content = content
|
370
|
+
@text_area = Ruco::EditorArea.new(content, @options)
|
371
|
+
@history = @text_area.history
|
372
|
+
restore_session
|
373
|
+
end
|
374
|
+
|
375
|
+
def find(text)
|
376
|
+
move(:relative, 0,0) # reset selection
|
377
|
+
start = text_area.content.index(text, text_area.index_for_position+1)
|
378
|
+
return unless start
|
379
|
+
|
380
|
+
# select the found word
|
381
|
+
finish = start + text.size
|
382
|
+
move(:to_index, finish)
|
383
|
+
selecting{ move(:to_index, start) }
|
384
|
+
|
385
|
+
true
|
386
|
+
end
|
387
|
+
|
388
|
+
def modified?
|
389
|
+
@saved_content != text_area.content
|
390
|
+
end
|
391
|
+
|
392
|
+
def save
|
393
|
+
lines = text_area.send(:lines)
|
394
|
+
lines.each(&:rstrip!) if @options[:remove_trailing_whitespace_on_save]
|
395
|
+
lines << '' if @options[:blank_line_before_eof_on_save] and lines.last.to_s !~ /^\s*$/
|
396
|
+
content = lines * @newline
|
397
|
+
|
398
|
+
@saved_content = content.gsub(/\r?\n/, "\n")
|
399
|
+
|
400
|
+
true
|
401
|
+
rescue Object => e
|
402
|
+
e.message
|
403
|
+
end
|
404
|
+
|
405
|
+
def store_session
|
406
|
+
end
|
407
|
+
|
408
|
+
def content
|
409
|
+
text_area.content.freeze # no modifications allowed
|
410
|
+
end
|
411
|
+
|
412
|
+
private
|
413
|
+
|
414
|
+
def restore_session
|
415
|
+
end
|
416
|
+
|
417
|
+
def session_store
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
class StatusBar
|
422
|
+
def initialize(editor, options)
|
423
|
+
@editor = editor
|
424
|
+
@options = options
|
425
|
+
end
|
426
|
+
|
427
|
+
def view
|
428
|
+
columns = @options[:columns]
|
429
|
+
|
430
|
+
version = "Ruco #{Ruco::VERSION} -- "
|
431
|
+
position = " #{@editor.position.line + 1}:#{@editor.position.column + 1}"
|
432
|
+
indicators = "#{change_indicator}#{writable_indicator}"
|
433
|
+
essential = version + position + indicators
|
434
|
+
space_left = [columns - essential.size, 0].max
|
435
|
+
|
436
|
+
# fit file name into remaining space
|
437
|
+
"#{version}#{indicators}#{' ' * space_left}#{position}"[0, columns]
|
438
|
+
end
|
439
|
+
|
440
|
+
def style_map
|
441
|
+
Dispel::StyleMap.single_line_reversed(@options[:columns])
|
442
|
+
end
|
443
|
+
|
444
|
+
def change_indicator
|
445
|
+
@editor.modified? ? '*' : ' '
|
446
|
+
end
|
447
|
+
|
448
|
+
def writable_indicator
|
449
|
+
true
|
450
|
+
end
|
451
|
+
|
452
|
+
private
|
453
|
+
|
454
|
+
# fill the line with left column and then overwrite the right section
|
455
|
+
def spread(left, right)
|
456
|
+
empty = [@options[:columns] - left.size, 0].max
|
457
|
+
line = left + (" " * empty)
|
458
|
+
line[(@options[:columns] - right.size - 1)..-1] = ' ' + right
|
459
|
+
line
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
File without changes
|
data/lib/pwkeep/main.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
require 'keepass/password'
|
3
|
+
|
4
|
+
module PWKeep
|
5
|
+
class Main
|
6
|
+
attr :opts
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@opts = { :home => ENV['PWKEEP_HOME'] || '~/.pwkeep' } # required value
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@opts = Trollop::options do
|
14
|
+
version "0.0.1 (c) 2014 Aki Tuomi"
|
15
|
+
banner <<-EOS
|
16
|
+
This program is a simple password storage utility. Distributed under MIT license. NO WARRANTY.
|
17
|
+
|
18
|
+
Usage:
|
19
|
+
#{$0} [options]
|
20
|
+
|
21
|
+
Where [options] are
|
22
|
+
|
23
|
+
EOS
|
24
|
+
opt :system, "System name", :type => :string, :short => '-n'
|
25
|
+
opt :initialize, "Initialize storage at ~/.pwkeep (you can change this with PWKEEP_HOME or --home)", :short => '-i'
|
26
|
+
opt :create, "Create new entry", :short => '-c'
|
27
|
+
opt :view, "View entry", :short => '-v'
|
28
|
+
opt :edit, "Edit entry", :short => '-e'
|
29
|
+
opt :delete, "Delete entry", :short => '-d'
|
30
|
+
opt :search, "Search for system or username", :type => :string, :short => '-s'
|
31
|
+
opt :list, "List all known systems", :short => '-l'
|
32
|
+
opt :help, "Show usage", :short => '-h'
|
33
|
+
opt :home, "Home directory", :short => '-H', :type => :string, :default => ( ENV['PWKEEP_HOME'] || '~/.pwkeep' )
|
34
|
+
opt :version, "Show version", :short => '-V'
|
35
|
+
end
|
36
|
+
|
37
|
+
# validate options
|
38
|
+
Trollop::die :system, "must be given for create/show/edit/delete" if opts[:system].nil? and (opts[:edit] or opts[:view] or opts[:delete] or opts[:create])
|
39
|
+
Trollop::die :create, "can only have one mode of operation" if opts[:create] and (opts[:edit] or opts[:view] or opts[:delete] or opts[:search] or opts[:initialize] or opts[:list])
|
40
|
+
Trollop::die :edit, "can only have one mode of operation" if opts[:edit] and (opts[:create] or opts[:view] or opts[:delete] or opts[:search] or opts[:initialize] or opts[:list])
|
41
|
+
Trollop::die :view, "can only have one mode of operation" if opts[:view] and (opts[:edit] or opts[:create] or opts[:delete] or opts[:search] or opts[:initialize] or opts[:list])
|
42
|
+
Trollop::die :delete, "can only have one mode of operation" if opts[:delete] and (opts[:edit] or opts[:view] or opts[:create] or opts[:search] or opts[:initialize] or opts[:list])
|
43
|
+
Trollop::die :search, "can only have one mode of operation" if opts[:search] and (opts[:edit] or opts[:view] or opts[:delete] or opts[:create] or opts[:initialize] or opts[:list])
|
44
|
+
Trollop::die :initialize, "can only have one mode of operation" if opts[:initialize] and (opts[:edit] or opts[:view] or opts[:delete] or opts[:create] or opts[:search] or opts[:list])
|
45
|
+
Trollop::die :list, "can only have one mode of operation" if opts[:list] and (opts[:edit] or opts[:view] or opts[:delete] or opts[:create] or opts[:search] or opts[:initialize])
|
46
|
+
|
47
|
+
Trollop::die "You must choose one mode of operation" unless opts[:create] or opts[:edit] or opts[:view] or opts[:delete] or opts[:search] or opts[:initialize] or opts[:list]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.run
|
51
|
+
Main.new.run
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_config
|
55
|
+
config_file = Pathname.new(@opts[:home]).expand_path.join('config.yml')
|
56
|
+
if config_file.exist?
|
57
|
+
PWKeep::Config.instance.load(config_file)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def run
|
62
|
+
setup
|
63
|
+
load_config
|
64
|
+
@storage = PWKeep::Storage.new(:path => opts[:home])
|
65
|
+
|
66
|
+
begin
|
67
|
+
if opts[:initialize]
|
68
|
+
@storage.create
|
69
|
+
|
70
|
+
if @storage.valid?
|
71
|
+
say("<%= color('WARNING!', BOLD) %> a valid pwkeep storage was found!")
|
72
|
+
say("If you continue, the existing storage becomes <%= color('UNUSABLE', BOLD) %>")
|
73
|
+
unless agree("Continue (y/n)? ")
|
74
|
+
raise PWKeep::Exception, "Storage initialization aborted"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# create a keypair
|
79
|
+
pw_a = ""
|
80
|
+
pw_b = ""
|
81
|
+
while(pw_a == "" or pw_a != pw_b)
|
82
|
+
pw_a = ask("Enter your password: ") { |q| q.echo = false }
|
83
|
+
pw_b = ask("Confirm your password: ") { |q| q.echo = false }
|
84
|
+
say("<%= color('Passwords did not match', RED %>") unless pw_a == pw_b
|
85
|
+
end
|
86
|
+
@storage.keypair_create(pw_b)
|
87
|
+
# this concludes initialization
|
88
|
+
say("<%= color('Password storage initialized', GREEN %>")
|
89
|
+
return
|
90
|
+
end
|
91
|
+
|
92
|
+
raise "Storage not initialized (run with --initialize)" unless @storage.valid?
|
93
|
+
|
94
|
+
if opts[:view]
|
95
|
+
pw = ask("Enter your password:") { |q| q.echo = false }
|
96
|
+
@storage.keypair_load pw
|
97
|
+
|
98
|
+
data = @storage.load_system opts[:system]
|
99
|
+
|
100
|
+
say("Last edited: #{data[:stored_at]}\nSystem: #{data[:system]}\n\n")
|
101
|
+
say(data[:data])
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
if opts[:create]
|
106
|
+
data = "Username: \nPassword: #{KeePass::Password.generate('uullA{6}')}"
|
107
|
+
|
108
|
+
result = PWKeep.run_editor(data, {})
|
109
|
+
|
110
|
+
unless result[0]
|
111
|
+
raise "Not modified"
|
112
|
+
end
|
113
|
+
|
114
|
+
pw = ask("Enter your password:") { |q| q.echo = false }
|
115
|
+
@storage.keypair_load pw
|
116
|
+
@storage.save_system opts[:system], result[1]
|
117
|
+
say("<%= color('Changes stored', GREEN)%>")
|
118
|
+
return
|
119
|
+
end
|
120
|
+
|
121
|
+
if opts[:edit]
|
122
|
+
pw = ask("Enter your password:") { |q| q.echo = false }
|
123
|
+
@storage.keypair_load pw
|
124
|
+
data = @storage.load_system opts[:system]
|
125
|
+
result = PWKeep.run_editor(data[:data], {})
|
126
|
+
unless result[0]
|
127
|
+
raise "Not modified"
|
128
|
+
end
|
129
|
+
@storage.save_system opts[:system], result[1]
|
130
|
+
say("<%= color('Changes stored', GREEN)%>")
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
if opts[:delete]
|
135
|
+
pw = ask("Enter your password:") { |q| q.echo = false }
|
136
|
+
@storage.keypair_load pw
|
137
|
+
data = @storage.load_system opts[:system]
|
138
|
+
# just to be sure
|
139
|
+
unless agree("Are you <%=color('SURE',BOLD)%> you want to delete #{data[:system]}?")
|
140
|
+
@storage.delete data[:system]
|
141
|
+
end
|
142
|
+
say("<%= color('System deleted', YELLOW)%>")
|
143
|
+
return
|
144
|
+
end
|
145
|
+
|
146
|
+
if opts[:search]
|
147
|
+
pw = ask("Enter your password:") { |q| q.echo = false }
|
148
|
+
@storage.keypair_load pw
|
149
|
+
say("All matching systems\n")
|
150
|
+
@storage.list_all_systems.sort.each do |system|
|
151
|
+
if system.match opts[:search]
|
152
|
+
say(" - #{system}")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
return
|
156
|
+
end
|
157
|
+
|
158
|
+
if opts[:list]
|
159
|
+
pw = ask("Enter your password:") { |q| q.echo = false }
|
160
|
+
@storage.keypair_load pw
|
161
|
+
say("All known systems\n")
|
162
|
+
@storage.list_all_systems.sort.each do |system|
|
163
|
+
say(" - #{system}")
|
164
|
+
end
|
165
|
+
return
|
166
|
+
end
|
167
|
+
rescue PWKeep::Exception => e
|
168
|
+
PWKeep::logger.error e.message.colorize(:red)
|
169
|
+
rescue OpenSSL::PKey::RSAError => e
|
170
|
+
PWKeep::logger.error "Cannot load private key".colorize(:red)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'base64'
|
3
|
+
require 'json'
|
4
|
+
require 'hashr'
|
5
|
+
|
6
|
+
module PWKeep
|
7
|
+
|
8
|
+
class Storage
|
9
|
+
def path
|
10
|
+
@options[:path]
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
@options = options
|
15
|
+
|
16
|
+
@options[:cipher] ||= 'AES-256-CTR'
|
17
|
+
@options[:keysize] ||= 2048
|
18
|
+
@options[:iterations] ||= 2000
|
19
|
+
@options[:digest] ||= 'sha512'
|
20
|
+
|
21
|
+
unless @options[:path].class == Pathname
|
22
|
+
@options[:path] = Pathname.new(@options[:path].to_s).expand_path
|
23
|
+
end
|
24
|
+
|
25
|
+
if path.exist?
|
26
|
+
@lockfile = Lockfile.new path.join(".lock").to_s, :retries => 0
|
27
|
+
@lockfile.lock
|
28
|
+
ObjectSpace.define_finalizer(self, proc { @lockfile.unlock })
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create
|
33
|
+
return if path.exist?
|
34
|
+
path.mkdir
|
35
|
+
@lockfile = Lockfile.new path.join(".lock").to_s, :retries => 0
|
36
|
+
@lockfile.lock
|
37
|
+
ObjectSpace.define_finalizer(self, proc { @lockfile.unlock })
|
38
|
+
end
|
39
|
+
|
40
|
+
def keypair_create(password)
|
41
|
+
# ensure it does not exist
|
42
|
+
@key = OpenSSL::PKey::RSA.new @options[:keysize]
|
43
|
+
cipher = OpenSSL::Cipher.new @options[:keycipher]
|
44
|
+
|
45
|
+
path.join('private.pem').open 'w' do |io| io.write @key.export(cipher, password) end
|
46
|
+
end
|
47
|
+
|
48
|
+
def keypair_load(password)
|
49
|
+
key_pem = path.join('private.pem').read
|
50
|
+
@key = OpenSSL::PKey::RSA.new key_pem, password
|
51
|
+
end
|
52
|
+
|
53
|
+
def master_key_load
|
54
|
+
unless @key
|
55
|
+
raise PWKeep::Exception, "RSA private key required"
|
56
|
+
end
|
57
|
+
|
58
|
+
# load the key
|
59
|
+
@master_key = @key.private_decrypt(path.join('master.key').open('rb') { |io| io.read },4)
|
60
|
+
end
|
61
|
+
|
62
|
+
def system_to_hash(system)
|
63
|
+
d = Digest.const_get(@options[:digest].upcase).new
|
64
|
+
|
65
|
+
system_h = system.downcase
|
66
|
+
(0..@options[:iterations]).each do
|
67
|
+
system_h = d.update(system_h).digest
|
68
|
+
d.reset
|
69
|
+
end
|
70
|
+
"system-#{Base64.urlsafe_encode64(system_h)}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def decrypt_system(file)
|
74
|
+
unless @key
|
75
|
+
raise PWKeep::Exception, "Private key required"
|
76
|
+
end
|
77
|
+
# found it, decrypt and load json
|
78
|
+
# the file contains crypto name, iv len, iv, data
|
79
|
+
header = nil
|
80
|
+
data = nil
|
81
|
+
file.open('rb') { |io|
|
82
|
+
header = io.read @options[:keysize]/8
|
83
|
+
data = io.read
|
84
|
+
}
|
85
|
+
|
86
|
+
# header
|
87
|
+
cipher = @key.private_decrypt(header,4).unpack('Z*')[0]
|
88
|
+
cipher = OpenSSL::Cipher.new cipher
|
89
|
+
# re-unpack now that we know the size of the rest of the fields...
|
90
|
+
header = @key.private_decrypt(header,4).unpack("Z*a#{cipher.iv_len}a#{cipher.key_len}")
|
91
|
+
|
92
|
+
cipher.decrypt
|
93
|
+
cipher.iv = header[1]
|
94
|
+
cipher.key = header[2]
|
95
|
+
|
96
|
+
# perform decrypt
|
97
|
+
cipher.update(data) + cipher.final
|
98
|
+
end
|
99
|
+
|
100
|
+
def encrypt_system(file, data)
|
101
|
+
unless @key
|
102
|
+
raise PWKeep::Exception, "Private key required"
|
103
|
+
end
|
104
|
+
|
105
|
+
# encrypt data
|
106
|
+
cipher = OpenSSL::Cipher::Cipher.new @options[:cipher]
|
107
|
+
cipher.encrypt
|
108
|
+
|
109
|
+
# use one time key and iv
|
110
|
+
iv = cipher.random_iv
|
111
|
+
key = cipher.random_key
|
112
|
+
|
113
|
+
header = [cipher.name, iv, key].pack("Z*a#{cipher.iv_len}a#{cipher.key_len}")
|
114
|
+
blob = cipher.update(data) + cipher.final
|
115
|
+
|
116
|
+
# store system name to make search work
|
117
|
+
file.open('wb') do |io|
|
118
|
+
io.write @key.public_encrypt header, 4
|
119
|
+
io.write blob
|
120
|
+
end
|
121
|
+
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_system(system)
|
126
|
+
unless @key
|
127
|
+
raise PWKeep::Exception, "Private key required"
|
128
|
+
end
|
129
|
+
|
130
|
+
system_h = system_to_hash(system)
|
131
|
+
raise "Cannot find #{system}" unless path.join(system_h).exist?
|
132
|
+
|
133
|
+
data = decrypt_system(path.join(system_h))
|
134
|
+
|
135
|
+
unless data[0] == "{" and data[-1] == "}"
|
136
|
+
raise PWKeep::Exception, "Corrupted data file"
|
137
|
+
end
|
138
|
+
|
139
|
+
JSON.load(data).deep_symbolize_keys
|
140
|
+
end
|
141
|
+
|
142
|
+
def save_system(system, data)
|
143
|
+
unless @key
|
144
|
+
raise PWKeep::Exception, "Private key required"
|
145
|
+
end
|
146
|
+
|
147
|
+
# write system
|
148
|
+
system_h = system_to_hash(system)
|
149
|
+
|
150
|
+
data = { :system => system, :data => data, :stored_at => Time.now }
|
151
|
+
encrypt_system(path.join(system_h), data)
|
152
|
+
end
|
153
|
+
|
154
|
+
def valid?
|
155
|
+
path.join('private.pem').exist?
|
156
|
+
end
|
157
|
+
|
158
|
+
def delete(system)
|
159
|
+
data = load_system(system)
|
160
|
+
unless data[:system] == system
|
161
|
+
raise "System not found"
|
162
|
+
end
|
163
|
+
|
164
|
+
path.join(system_to_hash(system)).delete!
|
165
|
+
end
|
166
|
+
|
167
|
+
def list_all_systems
|
168
|
+
systems = []
|
169
|
+
path.entries.each do |s|
|
170
|
+
next unless s.fnmatch? "system-*"
|
171
|
+
systems << JSON.load(decrypt_system(path.join(s)))["system"]
|
172
|
+
end
|
173
|
+
systems
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
data/pwkeep.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'pwkeep'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.authors = [ 'Aki Tuomi']
|
6
|
+
s.email = %w( aki.tuomi@g-works.fi )
|
7
|
+
s.summary = 'Simple password storage gem'
|
8
|
+
s.description = 'A simple password storage utility'
|
9
|
+
s.rubyforge_project = s.name
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.executables = %w( pwkeep )
|
12
|
+
s.require_path = 'lib'
|
13
|
+
s.license = 'MIT'
|
14
|
+
s.add_dependency 'bundler'
|
15
|
+
s.add_dependency 'colorize'
|
16
|
+
s.add_dependency 'highline'
|
17
|
+
s.add_dependency 'trollop'
|
18
|
+
s.add_dependency 'lockfile'
|
19
|
+
s.add_dependency 'hashr'
|
20
|
+
s.add_dependency 'ruco'
|
21
|
+
s.add_dependency 'keepass-password-generator'
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pwkeep
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Aki Tuomi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-01-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
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: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: colorize
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: highline
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: trollop
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: lockfile
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: hashr
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: ruco
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: keepass-password-generator
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
description: A simple password storage utility
|
143
|
+
email:
|
144
|
+
- aki.tuomi@g-works.fi
|
145
|
+
executables:
|
146
|
+
- pwkeep
|
147
|
+
extensions: []
|
148
|
+
extra_rdoc_files: []
|
149
|
+
files:
|
150
|
+
- .gitignore
|
151
|
+
- Gemfile
|
152
|
+
- Gemfile.lock
|
153
|
+
- LICENSE
|
154
|
+
- README.md
|
155
|
+
- Rakefile
|
156
|
+
- bin/pwkeep
|
157
|
+
- lib/pwkeep.rb
|
158
|
+
- lib/pwkeep/config.rb
|
159
|
+
- lib/pwkeep/editor.rb
|
160
|
+
- lib/pwkeep/generator.rb
|
161
|
+
- lib/pwkeep/main.rb
|
162
|
+
- lib/pwkeep/storage.rb
|
163
|
+
- pwkeep.gemspec
|
164
|
+
homepage:
|
165
|
+
licenses:
|
166
|
+
- MIT
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
none: false
|
173
|
+
requirements:
|
174
|
+
- - ! '>='
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
|
+
none: false
|
179
|
+
requirements:
|
180
|
+
- - ! '>='
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
requirements: []
|
184
|
+
rubyforge_project: pwkeep
|
185
|
+
rubygems_version: 1.8.23
|
186
|
+
signing_key:
|
187
|
+
specification_version: 3
|
188
|
+
summary: Simple password storage gem
|
189
|
+
test_files: []
|
190
|
+
has_rdoc:
|