pwl 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/.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
|