pwl 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.travis.yml +3 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +20 -0
- data/README.md +102 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/bin/pwl +268 -0
- data/lib/pwl/dialog/base.rb +37 -0
- data/lib/pwl/dialog/cocoa.rb +67 -0
- data/lib/pwl/dialog/console.rb +39 -0
- data/lib/pwl/dialog/gnome.rb +27 -0
- data/lib/pwl/dialog.rb +50 -0
- data/lib/pwl/message.rb +46 -0
- data/lib/pwl/password_policy.rb +29 -0
- data/lib/pwl/store.rb +284 -0
- data/lib/pwl.rb +14 -0
- data/pwl.gemspec +94 -0
- data/templates/export.html.erb +61 -0
- data/test/acceptance/test_basics.rb +15 -0
- data/test/acceptance/test_delete.rb +14 -0
- data/test/acceptance/test_dialogs.rb +45 -0
- data/test/acceptance/test_export.rb +42 -0
- data/test/acceptance/test_get.rb +17 -0
- data/test/acceptance/test_init.rb +81 -0
- data/test/acceptance/test_list.rb +30 -0
- data/test/acceptance/test_passwd.rb +23 -0
- data/test/acceptance/test_put.rb +19 -0
- data/test/fixtures/test_all.html +71 -0
- data/test/fixtures/test_empty.html +56 -0
- data/test/helper.rb +79 -0
- data/test/unit/test_error.rb +29 -0
- data/test/unit/test_message.rb +34 -0
- data/test/unit/test_store_construction.rb +62 -0
- data/test/unit/test_store_crud.rb +90 -0
- data/test/unit/test_store_metadata.rb +35 -0
- data/test/unit/test_store_password_policy.rb +61 -0
- data/test/unit/test_store_security.rb +34 -0
- metadata +156 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pwl
|
2
|
+
module Dialog
|
3
|
+
class GnomeDialog < SystemDialog
|
4
|
+
def command
|
5
|
+
"zenity --title \"#{title}\""
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class GnomePasswordDialog < GnomeDialog
|
10
|
+
#
|
11
|
+
# Returns the OS command that is required to ask the user for a password.
|
12
|
+
#
|
13
|
+
def command
|
14
|
+
"#{super} --entry --hide-text --text \"#{prompt}\""
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class GnomeTextDialog < GnomeDialog
|
19
|
+
#
|
20
|
+
# Returns the OS command that is required to ask the user for text input.
|
21
|
+
#
|
22
|
+
def command
|
23
|
+
"#{super} --entry --text \"#{prompt}\""
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/pwl/dialog.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[dialog])
|
2
|
+
|
3
|
+
require 'base'
|
4
|
+
require 'console'
|
5
|
+
require 'gnome'
|
6
|
+
require 'cocoa'
|
7
|
+
|
8
|
+
module Pwl
|
9
|
+
module Dialog
|
10
|
+
PLATFORM_PASSWORD_DIALOGS = {
|
11
|
+
:gnome => GnomePasswordDialog,
|
12
|
+
:mac => CocoaPasswordDialog,
|
13
|
+
}
|
14
|
+
|
15
|
+
PLATFORM_TEXT_DIALOGS = {
|
16
|
+
:gnome => GnomeTextDialog,
|
17
|
+
:mac => CocoaTextDialog,
|
18
|
+
}
|
19
|
+
|
20
|
+
extend self
|
21
|
+
|
22
|
+
class Password
|
23
|
+
class << self
|
24
|
+
# Factory method that creates a new password dialog that suits the current GUI platform.
|
25
|
+
# If no implementation was found for the current platform, a ConsolePasswordDialog is returned.
|
26
|
+
def new(title, prompt)
|
27
|
+
(PLATFORM_PASSWORD_DIALOGS[Dialog.gui_platform] || ConsolePasswordDialog).new(title, prompt)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Text
|
33
|
+
class << self
|
34
|
+
# Factory method that creates a new text (input) dialog that suits the current GUI platform.
|
35
|
+
# If no implementation was found for the current platform, a ConsoleTextDialog is returned.
|
36
|
+
def new(title, prompt)
|
37
|
+
(PLATFORM_TEXT_DIALOGS[Dialog.gui_platform] || ConsoleTextDialog).new(title, prompt)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def gui_platform
|
43
|
+
if ENV['GDMSESSION']
|
44
|
+
:gnome
|
45
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
46
|
+
:mac
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/pwl/message.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
# http://refactormycode.com/codes/281-given-a-hash-of-variables-render-an-erb-template
|
4
|
+
class Hash
|
5
|
+
def to_binding(object = Object.new)
|
6
|
+
object.instance_eval("def binding_for(#{keys.join(",")}) binding end")
|
7
|
+
object.binding_for(*values)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Pwl
|
12
|
+
class ReservedMessageCodeError < StandardError; end
|
13
|
+
|
14
|
+
class Message
|
15
|
+
attr_reader :exit_code
|
16
|
+
|
17
|
+
def initialize(template, exit_code = 0, default_replacements = {})
|
18
|
+
@template = ERB.new(template)
|
19
|
+
@exit_code = exit_code
|
20
|
+
@default_replacements = default_replacements
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s(replacements = {})
|
24
|
+
if !replacements.any? && @default_replacements.any?
|
25
|
+
@template.result(@default_replacements.to_binding)
|
26
|
+
else
|
27
|
+
@template.result(replacements.to_binding)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def error?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ErrorMessage < Message
|
37
|
+
def initialize(template, exit_code, default_replacements = {})
|
38
|
+
raise ReservedMessageCodeError.new("Exit code 0 is reserved for success messages") if 0 == exit_code
|
39
|
+
super(template, exit_code, default_replacements)
|
40
|
+
end
|
41
|
+
|
42
|
+
def error?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Pwl
|
2
|
+
class InvalidMasterPasswordError < StandardError
|
3
|
+
def initialize(reason)
|
4
|
+
super("The master password is not valid: #{reason}")
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class AnyNonEmptyPasswordPolicy
|
9
|
+
def validate!(master_password)
|
10
|
+
raise InvalidMasterPasswordError.new("Password must not be blank") if master_password.blank?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ForbidAllPasswordPolicy
|
15
|
+
def validate!(master_password)
|
16
|
+
raise InvalidMasterPasswordError.new("No password allowed at all")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ReasonableComplexityPasswordPolicy
|
21
|
+
def validate!(pwd)
|
22
|
+
raise InvalidMasterPasswordError.new("May not be blank") if pwd.blank?
|
23
|
+
raise InvalidMasterPasswordError.new("Must have at least eight characters") if 8 > pwd.length
|
24
|
+
raise InvalidMasterPasswordError.new("Must contain at least one integer") unless pwd =~ /.*\d/
|
25
|
+
raise InvalidMasterPasswordError.new("Must contain at least one character (lower or upper case)") unless pwd =~ /.*([a-z]|[A-Z])/
|
26
|
+
raise InvalidMasterPasswordError.new("Special characters are only allowed if their ASCII value is between 0x20 and 0x7E") unless pwd =~ /[\x20-\x7E]/
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/pwl/store.rb
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Pwl
|
4
|
+
class Store
|
5
|
+
class FileAlreadyExistsError < StandardError
|
6
|
+
def initialize(file)
|
7
|
+
super("The file #{file} already exists")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class NotInitializedError < StandardError
|
12
|
+
def initialize(file)
|
13
|
+
super("The store at #{file} was not initialized yet")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class WrongMasterPasswordError < StandardError
|
18
|
+
def initialize
|
19
|
+
super("The master password is wrong")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class FileNotFoundError < StandardError
|
24
|
+
def initialize(file)
|
25
|
+
super("The file #{file} for the store was not found")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class KeyNotFoundError < StandardError
|
30
|
+
def initialize(key)
|
31
|
+
super("No entry was found for #{key}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class BlankError < StandardError
|
36
|
+
def initialize(what)
|
37
|
+
super("#{what} is required")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class BlankKeyError < BlankError
|
42
|
+
def initialize
|
43
|
+
super("Key")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class BlankValueError < BlankError
|
48
|
+
def initialize
|
49
|
+
super("Value")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
alias_method :load, :new
|
55
|
+
|
56
|
+
DEFAULT_PASSWORD_POLICY = ReasonableComplexityPasswordPolicy.new
|
57
|
+
|
58
|
+
#
|
59
|
+
# Constructs a new store (not only the object, but also the file behind it).
|
60
|
+
#
|
61
|
+
def new(file, master_password, options = {})
|
62
|
+
if File.exists?(file) && !options[:force] # don't allow accedidential override of existing file
|
63
|
+
raise FileAlreadyExistsError.new(file)
|
64
|
+
else
|
65
|
+
password_policy.validate!(master_password)
|
66
|
+
store = load(file, master_password)
|
67
|
+
store.reset!
|
68
|
+
end
|
69
|
+
|
70
|
+
store
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Opens an existing store. Throws if the backing file does not exist or isn't initialized.
|
75
|
+
#
|
76
|
+
def open(file, master_password)
|
77
|
+
raise FileNotFoundError.new(file) unless File.exists?(file)
|
78
|
+
store = load(file, master_password)
|
79
|
+
store.authenticate # do not allow openeing without successful authentication
|
80
|
+
store
|
81
|
+
end
|
82
|
+
|
83
|
+
def password_policy
|
84
|
+
@password_policy || DEFAULT_PASSWORD_POLICY
|
85
|
+
end
|
86
|
+
|
87
|
+
def password_policy=(policy)
|
88
|
+
@password_policy = policy
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Create a new store object by loading an existing file.
|
94
|
+
#
|
95
|
+
# Beware: New is overridden; it performs additional actions after before and after #initialize
|
96
|
+
#
|
97
|
+
def initialize(file, master_password)
|
98
|
+
@backend = PStore.new(file, true)
|
99
|
+
@backend.ultra_safe = true
|
100
|
+
@master_password = master_password
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# (Re-) Initialize the database
|
105
|
+
#
|
106
|
+
def reset!
|
107
|
+
@backend.transaction{
|
108
|
+
@backend[:user] = {}
|
109
|
+
@backend[:system] = {}
|
110
|
+
@backend[:system][:created] = DateTime.now
|
111
|
+
@backend[:system][:salt] = encrypt(Random.rand.to_s)
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
#
|
116
|
+
# Check that the master password is correct. This is done to prevent opening an existing but blank store with the wrong password.
|
117
|
+
#
|
118
|
+
def authenticate
|
119
|
+
begin
|
120
|
+
@backend.transaction(true){
|
121
|
+
raise NotInitializedError.new(@backend.path.path) unless @backend[:user] && @backend[:system] && @backend[:system][:created]
|
122
|
+
check_salt!
|
123
|
+
}
|
124
|
+
rescue OpenSSL::Cipher::CipherError
|
125
|
+
raise WrongMasterPasswordError
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Return the value stored under key
|
131
|
+
#
|
132
|
+
def get(key)
|
133
|
+
raise BlankKeyError if key.blank?
|
134
|
+
@backend.transaction{
|
135
|
+
timestamp!(:last_accessed)
|
136
|
+
value = @backend[:user][encrypt(key)]
|
137
|
+
raise KeyNotFoundError.new(key) unless value
|
138
|
+
decrypt(value)
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Store value stored under key
|
144
|
+
#
|
145
|
+
def put(key, value)
|
146
|
+
raise BlankKeyError if key.blank?
|
147
|
+
raise BlankValueError if value.blank?
|
148
|
+
@backend.transaction{
|
149
|
+
timestamp!(:last_modified)
|
150
|
+
@backend[:user][encrypt(key)] = encrypt(value)
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# Delete the value that is stored under key and return it
|
156
|
+
#
|
157
|
+
def delete(key)
|
158
|
+
raise BlankKeyError if key.blank?
|
159
|
+
@backend.transaction{
|
160
|
+
timestamp!(:last_modified)
|
161
|
+
old_value = @backend[:user].delete(encrypt(key))
|
162
|
+
raise KeyNotFoundError.new(key) unless old_value
|
163
|
+
decrypt(old_value)
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Return all keys, optionally filtered by filter
|
169
|
+
#
|
170
|
+
def list(filter = nil)
|
171
|
+
@backend.transaction(true){
|
172
|
+
result = @backend[:user].keys.collect{|k| decrypt(k)}
|
173
|
+
|
174
|
+
if filter.blank?
|
175
|
+
result
|
176
|
+
else
|
177
|
+
result.select{|k,v| k =~ /#{filter}/}
|
178
|
+
end
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# Return all entries
|
184
|
+
#
|
185
|
+
def all
|
186
|
+
result = {}
|
187
|
+
@backend.transaction(true){
|
188
|
+
@backend[:user].each{|k,v| result[decrypt(k)] = decrypt(v)}
|
189
|
+
}
|
190
|
+
result
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# Change the master password to +new_master_password+. Note that we don't take a password confirmation here.
|
195
|
+
# This is up to a UI layer.
|
196
|
+
#
|
197
|
+
def change_password!(new_master_password)
|
198
|
+
self.class.password_policy.validate!(new_master_password)
|
199
|
+
|
200
|
+
@backend.transaction{
|
201
|
+
# Decrypt each key and value with the old master password and encrypt them with the new master password
|
202
|
+
copy = {}
|
203
|
+
@backend[:user].each{|k,v|
|
204
|
+
new_key = Encryptor.encrypt(decrypt(k), :key => new_master_password)
|
205
|
+
new_val = Encryptor.encrypt(decrypt(v), :key => new_master_password)
|
206
|
+
copy[new_key] = new_val
|
207
|
+
}
|
208
|
+
|
209
|
+
# re-write user branch with newly encrypted keys and values
|
210
|
+
@backend[:user] = copy
|
211
|
+
|
212
|
+
# from now on, use the new master password as long as the object lives
|
213
|
+
@master_password = new_master_password
|
214
|
+
|
215
|
+
timestamp!(:last_modified)
|
216
|
+
@backend[:system][:salt] = encrypt(Random.rand.to_s)
|
217
|
+
}
|
218
|
+
end
|
219
|
+
|
220
|
+
#
|
221
|
+
# Return the date when the store was created
|
222
|
+
#
|
223
|
+
def created
|
224
|
+
@backend.transaction(true){@backend[:system][:created]}
|
225
|
+
end
|
226
|
+
|
227
|
+
#
|
228
|
+
# Return the date when the store was last accessed
|
229
|
+
#
|
230
|
+
def last_accessed
|
231
|
+
@backend.transaction(true){@backend[:system][:last_accessed]}
|
232
|
+
end
|
233
|
+
|
234
|
+
#
|
235
|
+
# Return the date when the store was last modified
|
236
|
+
#
|
237
|
+
def last_modified
|
238
|
+
@backend.transaction(true){@backend[:system][:last_modified]}
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# Return the path to the file backing this store
|
243
|
+
#
|
244
|
+
def path
|
245
|
+
@backend.path
|
246
|
+
end
|
247
|
+
|
248
|
+
private
|
249
|
+
|
250
|
+
#
|
251
|
+
# Adds or updates the time-stamp stored under symbol
|
252
|
+
#
|
253
|
+
# This method must run within a PStore read/write transaction.
|
254
|
+
#
|
255
|
+
def timestamp!(sym)
|
256
|
+
@backend[:system][sym] = DateTime.now
|
257
|
+
end
|
258
|
+
|
259
|
+
#
|
260
|
+
# Return the encrypted +value+ (uses the current master password)
|
261
|
+
#
|
262
|
+
def encrypt(value)
|
263
|
+
Encryptor.encrypt(value, :key => @master_password)
|
264
|
+
end
|
265
|
+
|
266
|
+
#
|
267
|
+
# Return the decrypted +value+ (uses the current master password)
|
268
|
+
#
|
269
|
+
def decrypt(value)
|
270
|
+
Encryptor.decrypt(value, :key => @master_password)
|
271
|
+
end
|
272
|
+
|
273
|
+
#
|
274
|
+
# Attempts to decrypt the system salt. Throws if the master password is incorrect.
|
275
|
+
#
|
276
|
+
# This method must run within a PStore transaction (may be read-only).
|
277
|
+
#
|
278
|
+
def check_salt!
|
279
|
+
raise NotInitializedError.new(@backend.path.path) if @backend[:system][:salt].blank?
|
280
|
+
decrypt(@backend[:system][:salt])
|
281
|
+
nil
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
data/lib/pwl.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'pstore'
|
2
|
+
require 'encryptor'
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[pwl])
|
6
|
+
|
7
|
+
require 'message'
|
8
|
+
require 'password_policy'
|
9
|
+
require 'store'
|
10
|
+
require 'dialog'
|
11
|
+
|
12
|
+
module Pwl
|
13
|
+
VERSION = File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION]))
|
14
|
+
end
|
data/pwl.gemspec
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "pwl"
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Nicholas E. Rabenau"]
|
12
|
+
s.date = "2012-03-22"
|
13
|
+
s.description = "pwl is a secure password locker for the commandline"
|
14
|
+
s.email = "nerab@gmx.net"
|
15
|
+
s.executables = ["pwl", "pwl"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE.txt",
|
18
|
+
"README.md"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".document",
|
22
|
+
".travis.yml",
|
23
|
+
"Gemfile",
|
24
|
+
"Gemfile.lock",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"VERSION",
|
29
|
+
"bin/pwl",
|
30
|
+
"lib/pwl.rb",
|
31
|
+
"lib/pwl/dialog.rb",
|
32
|
+
"lib/pwl/dialog/base.rb",
|
33
|
+
"lib/pwl/dialog/cocoa.rb",
|
34
|
+
"lib/pwl/dialog/console.rb",
|
35
|
+
"lib/pwl/dialog/gnome.rb",
|
36
|
+
"lib/pwl/message.rb",
|
37
|
+
"lib/pwl/password_policy.rb",
|
38
|
+
"lib/pwl/store.rb",
|
39
|
+
"pwl.gemspec",
|
40
|
+
"templates/export.html.erb",
|
41
|
+
"test/acceptance/test_basics.rb",
|
42
|
+
"test/acceptance/test_delete.rb",
|
43
|
+
"test/acceptance/test_dialogs.rb",
|
44
|
+
"test/acceptance/test_export.rb",
|
45
|
+
"test/acceptance/test_get.rb",
|
46
|
+
"test/acceptance/test_init.rb",
|
47
|
+
"test/acceptance/test_list.rb",
|
48
|
+
"test/acceptance/test_passwd.rb",
|
49
|
+
"test/acceptance/test_put.rb",
|
50
|
+
"test/fixtures/test_all.html",
|
51
|
+
"test/fixtures/test_empty.html",
|
52
|
+
"test/helper.rb",
|
53
|
+
"test/unit/test_error.rb",
|
54
|
+
"test/unit/test_message.rb",
|
55
|
+
"test/unit/test_store_construction.rb",
|
56
|
+
"test/unit/test_store_crud.rb",
|
57
|
+
"test/unit/test_store_metadata.rb",
|
58
|
+
"test/unit/test_store_password_policy.rb",
|
59
|
+
"test/unit/test_store_security.rb"
|
60
|
+
]
|
61
|
+
s.homepage = "http://github.com/nerab/pwl"
|
62
|
+
s.licenses = ["MIT"]
|
63
|
+
s.require_paths = ["lib"]
|
64
|
+
s.rubygems_version = "1.8.15"
|
65
|
+
s.summary = "Command-line password locker"
|
66
|
+
|
67
|
+
if s.respond_to? :specification_version then
|
68
|
+
s.specification_version = 3
|
69
|
+
|
70
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
71
|
+
s.add_runtime_dependency(%q<encryptor>, [">= 0"])
|
72
|
+
s.add_runtime_dependency(%q<commander>, [">= 0"])
|
73
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 0"])
|
74
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
75
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
76
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
77
|
+
else
|
78
|
+
s.add_dependency(%q<encryptor>, [">= 0"])
|
79
|
+
s.add_dependency(%q<commander>, [">= 0"])
|
80
|
+
s.add_dependency(%q<activesupport>, [">= 0"])
|
81
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
82
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
83
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
84
|
+
end
|
85
|
+
else
|
86
|
+
s.add_dependency(%q<encryptor>, [">= 0"])
|
87
|
+
s.add_dependency(%q<commander>, [">= 0"])
|
88
|
+
s.add_dependency(%q<activesupport>, [">= 0"])
|
89
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
90
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
91
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
<!DOCTYPE HTML>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
5
|
+
<title>Password Manager Export</title>
|
6
|
+
<style type="text/css">
|
7
|
+
table { page-break-inside:auto}
|
8
|
+
tr { page-break-inside:avoid; page-break-after:auto}
|
9
|
+
thead { display:table-header-group}
|
10
|
+
tfoot { display:table-footer-group; font-size: 0.85em;}
|
11
|
+
|
12
|
+
/* adapted from http://coding.smashingmagazine.com/2008/08/13/top-10-css-table-designs/ */
|
13
|
+
body{
|
14
|
+
font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
|
15
|
+
font-size: 12px;
|
16
|
+
margin: 45px;
|
17
|
+
width: 480px;
|
18
|
+
border-collapse: collapse;
|
19
|
+
text-align: left;
|
20
|
+
}
|
21
|
+
th{
|
22
|
+
font-size: 14px;
|
23
|
+
font-weight: bold;
|
24
|
+
padding: 10px 8px;
|
25
|
+
border-bottom: 2px solid;
|
26
|
+
}
|
27
|
+
td{
|
28
|
+
border-bottom: 1px solid;
|
29
|
+
padding: 6px 8px;
|
30
|
+
}
|
31
|
+
</style>
|
32
|
+
</head>
|
33
|
+
<body>
|
34
|
+
<h1>Password Manager Export</h1>
|
35
|
+
<table>
|
36
|
+
<thead>
|
37
|
+
<tr>
|
38
|
+
<th>Name</th>
|
39
|
+
<th>Value</th>
|
40
|
+
</tr>
|
41
|
+
</thead>
|
42
|
+
<tfoot>
|
43
|
+
<tr>
|
44
|
+
<td colspan="2">
|
45
|
+
This <a href="http://rdoc.info/github/nerab/pwl/master/frames">pwl</a> export was created on <%= DateTime.now.strftime('%F %R') %>.
|
46
|
+
<br/>
|
47
|
+
The database at <%= store.path %> was <%= "last modified #{store.last_modified.strftime('%F %R')}" rescue 'never modified' %>.
|
48
|
+
</td>
|
49
|
+
</tr>
|
50
|
+
</tfoot>
|
51
|
+
<tbody>
|
52
|
+
<% store.all.each{|key, value|%>
|
53
|
+
<tr>
|
54
|
+
<td><%= key %></td>
|
55
|
+
<td><%= value %></td>
|
56
|
+
</tr>
|
57
|
+
<% } %>
|
58
|
+
</tbody>
|
59
|
+
</table>
|
60
|
+
</body>
|
61
|
+
</html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestBasics < Test::Pwl::AppTestCase
|
4
|
+
def test_help
|
5
|
+
assert_successful('Rabenau', 'help')
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_no_args
|
9
|
+
assert_error('invalid', '')
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_unknown_command
|
13
|
+
assert_error('invalid', 'foobar')
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
# Tests `pwl delete`
|
4
|
+
class TestDelete < Test::Pwl::AppTestCase
|
5
|
+
def test_delete_blank_key
|
6
|
+
assert_error('may not be blank', 'delete')
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_delete_simple
|
10
|
+
assert_successful('', 'put foo bar')
|
11
|
+
assert_successful('', 'delete foo')
|
12
|
+
assert_error('No entry was found for foo', 'get foo')
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Unit tests for dialogs
|
5
|
+
#
|
6
|
+
# Note that these teste are being excluded when the plain +rake+ file is run because these tests require manual
|
7
|
+
# intervention.
|
8
|
+
#
|
9
|
+
# Run these tests separately with
|
10
|
+
#
|
11
|
+
# rake TEST=test/acceptance/test_dialogs.rb
|
12
|
+
#
|
13
|
+
class TestPasswordDialog < Test::Unit::TestCase
|
14
|
+
CANDIDATES = %w[Homer Marge Bart Lisa Maggie Martin Ralph]
|
15
|
+
|
16
|
+
def test_get_with_spaces
|
17
|
+
expected = "#{CANDIDATES[Random.rand(CANDIDATES.size)]} #{CANDIDATES[Random.rand(CANDIDATES.size)]}"
|
18
|
+
get_text(expected, Pwl::Dialog::Password)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_get_empty
|
22
|
+
assert_equal('', Pwl::Dialog::Password.new(self.class.name, "Please just press Enter without entering any text.").get_input)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_get_without_spaces
|
26
|
+
expected = CANDIDATES[Random.rand(CANDIDATES.size)]
|
27
|
+
get_text(expected, Pwl::Dialog::Password)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_cancel
|
31
|
+
assert_raise Pwl::Dialog::Cancelled do
|
32
|
+
Pwl::Dialog::Password.new(self.class.name, "Please press the Escape key.").get_input
|
33
|
+
end
|
34
|
+
|
35
|
+
assert_raise Pwl::Dialog::Cancelled do
|
36
|
+
Pwl::Dialog::Password.new(self.class.name, "Please press the Cancel button.").get_input
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def get_text(expected, dlg_class)
|
43
|
+
assert_equal(expected, dlg_class.new(self.class.name, "Please enter the string '#{expected}' (without the quotes) and press Enter.").get_input)
|
44
|
+
end
|
45
|
+
end
|