pwkeep 0.0.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.
- 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:
|