pwl 0.0.2 → 0.0.3

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.
@@ -0,0 +1,98 @@
1
+ module Pwl
2
+ module Commands
3
+ EXIT_CODES = {
4
+ :success => Message.new('Success.'),
5
+ :aborted => Message.new('Aborted by user.', 1),
6
+ :passwords_dont_match => ErrorMessage.new('Passwords do not match.', 2),
7
+ :no_value_found => Message.new('No value found for <%= name %>.', 3, :name => 'NAME'),
8
+ :file_exists => ErrorMessage.new('There already exists a locker at <%= file %>. Use --force to overwrite it or --file to specify a different locker.', 4, :file => 'FILE'),
9
+ :file_not_found => ErrorMessage.new('Locker file <%= file %> could not be found.', 5, :file => 'FILE'),
10
+ :name_blank => ErrorMessage.new('Name may not be blank.', 6),
11
+ :value_blank => ErrorMessage.new('Value may not be blank.', 7),
12
+ :list_empty => Message.new('List is empty.', 8),
13
+ :list_empty_filter => Message.new('No names found that match filter <%= filter %>.', 9, :filter => 'FILTER'),
14
+ :validation_new_failed => ErrorMessage.new('<%= message %>.', 10, :message => 'Validation of new master password failed'),
15
+ :unknown_format => ErrorMessage.new('<%= format %> is not a known format.', 11, :format => 'FORMAT'),
16
+ :inaccessible_field => ErrorMessage.new("Field '<%= field %>' is not accessible.", 12, :field => 'FIELD'),
17
+ :is_dir => ErrorMessage.new('File expected, but <%= file %> is a directory. Specify a regular file for the locker.', 13, :file => 'FILE'),
18
+ }
19
+
20
+ class InacessibleFieldError < StandardError
21
+ def initialize(field)
22
+ super("The field #{field} is not accessible")
23
+ end
24
+ end
25
+
26
+ class Base
27
+ class << self
28
+ def exit_codes_help
29
+ EXIT_CODES.values.sort{|l,r| l.exit_code <=> r.exit_code}.collect{|m| " #{m.exit_code.to_s.rjust(EXIT_CODES.size.to_s.size)}: #{m.to_s}"}.join("\n")
30
+ end
31
+
32
+ def default_locker_file
33
+ File.expand_path("~/.#{program(:name)}.pstore")
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def locker_file(options, init = false)
40
+ result = options.file || self.class.default_locker_file
41
+
42
+ if File.exists?(result) || init
43
+ result
44
+ else
45
+ exit_with(:file_not_found, options.verbose, :file => result)
46
+ end
47
+ end
48
+
49
+ def open_locker(options, master_password = nil)
50
+ # TODO Use DRb at options.url if not nil
51
+ locker_file = locker_file(options)
52
+ msg "Attempting to open locker at #{locker_file}" if options.verbose
53
+
54
+ Locker.open(locker_file, master_password || get_password("Enter the master password for #{program(:name)}:", options.gui))
55
+ end
56
+
57
+ def new_locker(options, master_password)
58
+ # Remote init not allowed. Or maybe it should be?
59
+ Locker.new(locker_file(options, true), master_password, {:force => options.force})
60
+ end
61
+
62
+ def msg(str)
63
+ STDERR.puts("#{program(:name)}: #{str}")
64
+ end
65
+
66
+ def exit_with(error_code, verbose, msg_args = {})
67
+ msg = EXIT_CODES[error_code]
68
+ raise "No message defined for error #{error_code}" if !msg
69
+
70
+ if msg.error? || verbose # always print errors; messages only when verbose
71
+ msg msg.to_s(msg_args)
72
+ end
73
+
74
+ exit(msg.exit_code)
75
+ end
76
+
77
+ def get_password(prompt, gui = false)
78
+ (gui ? Pwl::Dialog::Password.new(program(:name), prompt) : Pwl::Dialog::ConsolePasswordDialog.new(prompt)).get_input
79
+ end
80
+
81
+ def get_text(prompt, gui = false)
82
+ (gui ? Pwl::Dialog::Text.new(program(:name), prompt) : Pwl::Dialog::ConsoleTextDialog.new(prompt)).get_input
83
+ end
84
+
85
+ def validate!(pwd)
86
+ Pwl::Locker.password_policy.validate!(pwd)
87
+ end
88
+
89
+ #
90
+ # Returns the value of the passed attribute name if it is allowed to be retrieved from a locker entry
91
+ #
92
+ def attr!(entry, field)
93
+ raise InacessibleFieldError.new(field) unless entry.instance_variable_defined?("@#{field}".to_sym)
94
+ entry.send(field)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,18 @@
1
+ module Pwl
2
+ module Commands
3
+ class Delete < Base
4
+ def call(args, options)
5
+ exit_with(:name_blank, options.verbose) if 0 == args.size || args[0].blank?
6
+
7
+ begin
8
+ locker = open_locker(options)
9
+ rescue Pwl::Dialog::Cancelled
10
+ exit_with(:aborted, options.verbose)
11
+ end
12
+
13
+ locker.delete(args[0])
14
+ msg "Successfully deleted the value under #{args[0]}." if options.verbose
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ module Pwl
2
+ module Commands
3
+ class Export < Base
4
+ DEFAULT_EXPORT_TEMPLATE = File.join(File.dirname(__FILE__), *%w[.. .. templates export.html.erb])
5
+
6
+ def call(args, options)
7
+ options.default :format => 'html'
8
+
9
+ # TODO See Stats for slightly changed approach using a method
10
+ presenter = {:html => Presenter::Html, :json => Presenter::Json, :yaml => Presenter::Yaml}[options.format.to_sym]
11
+ exit_with(:unknown_export_format, options.verbose, :format => options.format) if presenter.nil?
12
+
13
+ begin
14
+ locker = open_locker(options)
15
+ puts presenter.new(locker).to_s
16
+ rescue Dialog::Cancelled
17
+ exit_with(:aborted, options.verbose)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ module Pwl
2
+ module Commands
3
+ class Get < Base
4
+ def call(args, options)
5
+ exit_with(:name_blank, options.verbose) if 0 == args.size || args[0].blank?
6
+
7
+ # second argument can be a field other than password
8
+ field = args.size > 1 ? args[1] : 'password'
9
+
10
+ begin
11
+ locker = open_locker(options)
12
+ result = attr!(locker.get(args[0]), field)
13
+ if result.blank?
14
+ exit_with(:no_value_found, options.verbose, :name => args[0])
15
+ else
16
+ puts result
17
+ end
18
+ rescue InacessibleFieldError
19
+ exit_with(:inaccessible_field, options.verbose, :field => field)
20
+ rescue Dialog::Cancelled
21
+ exit_with(:aborted, options.verbose)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ module Pwl
2
+ module Commands
3
+ class Init < Base
4
+ def call(args, options)
5
+ locker_file = locker_file(options, true)
6
+ msg "Attempting to initialize new locker at #{locker_file}" if options.verbose
7
+
8
+ # Locker checks this too, but we want to fail fast.
9
+ exit_with(:is_dir, options.verbose, :file => locker_file) if File.exists?(locker_file) && File.directory?(locker_file)
10
+ exit_with(:file_exists, options.verbose, :file => locker_file) if File.exists?(locker_file) && !options.force
11
+
12
+ begin
13
+ begin
14
+ master_password = get_password('Enter new master password:', options.gui)
15
+ end while begin
16
+ validate!(master_password) # Basic idea from http://stackoverflow.com/questions/136793/is-there-a-do-while-loop-in-ruby
17
+ rescue Pwl::InvalidMasterPasswordError => e
18
+ msg e.message
19
+ options.gui || STDIN.tty? # only continue the loop when in interactive mode
20
+ end
21
+
22
+ # Ask for password confirmation if running in interactive mode (terminal)
23
+ if STDIN.tty? && master_password != get_password('Enter master password again:', options.gui)
24
+ exit_with(:passwords_dont_match, options.verbose)
25
+ end
26
+ rescue Pwl::Dialog::Cancelled
27
+ exit_with(:aborted, options.verbose)
28
+ end
29
+
30
+ new_locker(options, master_password)
31
+ msg "Successfully initialized new locker at #{locker_file}" if options.verbose
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ module Pwl
2
+ module Commands
3
+ class List < Base
4
+ def call(args, options)
5
+ options.default :separator => ' '
6
+
7
+ begin
8
+ locker = open_locker(options)
9
+
10
+ if !options.long
11
+ result = locker.list(args[0]).join(options.separator)
12
+ else
13
+ matching_names = locker.list(args[0])
14
+
15
+ result = "total #{matching_names.size}#{$/}"
16
+
17
+ matching_names.each do |name|
18
+ e = locker.get(name)
19
+ result << "#{e.uuid}\t#{e.name}#{$/}"
20
+ end
21
+ end
22
+
23
+ if !result.blank?
24
+ puts result
25
+ else
26
+ if args[0] # filter given
27
+ exit_with(:list_empty_filter, options.verbose, args[0])
28
+ else
29
+ exit_with(:list_empty, options.verbose)
30
+ end
31
+ end
32
+ rescue Dialog::Cancelled
33
+ exit_with(:aborted, options.verbose)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ module Pwl
2
+ module Commands
3
+ class Passwd < Base
4
+ def call(args, options)
5
+ begin
6
+ locker = open_locker(options)
7
+
8
+ if !STDIN.tty? && !options.gui
9
+ # If we are in a pipe and do not run in GUI mode, we accept the new master password as args[0]
10
+ new_master_password = args[0]
11
+
12
+ begin
13
+ validate!(new_master_password)
14
+ rescue Pwl::InvalidMasterPasswordError => e
15
+ exit_with(:validation_new_failed, options.verbose, :message => e.message)
16
+ end
17
+ else
18
+ # If running interactively (console or gui), we loop until we get a valid password or break
19
+ begin
20
+ new_master_password = get_password("Enter the new master password for #{program(:name)}:", options.gui)
21
+ end while begin
22
+ validate!(new_master_password)
23
+ rescue Pwl::InvalidMasterPasswordError => e
24
+ msg e.message
25
+ options.gui || STDIN.tty? # only continue the loop when in interactive mode
26
+ end
27
+
28
+ # Confirm new password
29
+ if new_master_password != get_password("Enter the new master password for #{program(:name)} again:", options.gui)
30
+ exit_with(:passwords_dont_match, options.verbose)
31
+ end
32
+ end
33
+ rescue Pwl::Dialog::Cancelled
34
+ exit_with(:aborted, options.verbose)
35
+ end
36
+
37
+ locker.change_password!(new_master_password)
38
+ msg "Successfully changed master password for #{program(:name)}." if options.verbose
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,59 @@
1
+ module Pwl
2
+ module Commands
3
+ class UnknownFormatError < StandardError; end
4
+
5
+ module StatsPresenter
6
+ class Text
7
+ def present(locker)
8
+ puts "Created: #{locker.created}"
9
+ puts "Last accessed: #{locker.last_accessed}"
10
+ puts "Last modified: #{locker.last_modified}"
11
+ end
12
+ end
13
+
14
+ class Html
15
+ def present(locker)
16
+ puts "TODO"
17
+ end
18
+ end
19
+
20
+ class Json
21
+ def present(locker)
22
+ puts "TODO"
23
+ end
24
+ end
25
+
26
+ class Yaml
27
+ def present(locker)
28
+ puts "TODO"
29
+ end
30
+ end
31
+ end
32
+
33
+ class Stats < Base
34
+ def call(args, options)
35
+ locker = open_locker(options)
36
+
37
+ begin
38
+ puts presenter(options.format).present(locker)
39
+ rescue
40
+ exit_with(:unknown_format, options.verbose, :format => options.format)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def presenter(format)
47
+ if !format || format.is_a?(TrueClass) || 'text' == format
48
+ StatsPresenter::Text.new
49
+ else
50
+ begin
51
+ {:html => StatsPresenter::Html, :json => StatsPresenter::Json, :yaml => StatsPresenter::Yaml}[format.to_sym].new
52
+ rescue
53
+ raise UnknownFormatError.new(format)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ require 'active_model'
2
+ require 'uuid'
3
+
4
+ module Pwl
5
+ class InvalidEntryError < StandardError
6
+ def initialize(errors)
7
+ super(errors.to_a.join(', '))
8
+ end
9
+ end
10
+
11
+ class Entry
12
+ attr_accessor :uuid, :name, :password
13
+
14
+ include ActiveModel::Validations
15
+ validates_presence_of :name, :uuid, :password
16
+ validates_format_of :uuid, :with => /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/
17
+
18
+ def initialize(name = nil)
19
+ @name = name
20
+ @uuid = UUID.generate
21
+ @password = nil
22
+ end
23
+
24
+ #
25
+ # raises InvalidEntryError if entry is not valid
26
+ #
27
+ def validate!
28
+ raise InvalidEntryError.new(errors) if invalid?
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ module Pwl
4
+ #
5
+ # DataMapper that maps an Entry from and to JSON
6
+ #
7
+ class EntryMapper
8
+ class << self
9
+ ATTRIBUTES = %w[uuid name password]
10
+ def from_json(str)
11
+ json = JSON(str, :symbolize_names => true)
12
+
13
+ Entry.new.tap do |entry|
14
+ ATTRIBUTES.each do |attr|
15
+ entry.send("#{attr}=", json[attr.to_sym])
16
+ end
17
+ end
18
+ end
19
+
20
+ def to_json(entry)
21
+ entry.validate!
22
+ result = {}
23
+ ATTRIBUTES.each do |attr|
24
+ result.store(attr.to_sym, entry.send(attr))
25
+ end
26
+ result.to_json
27
+ end
28
+ end
29
+ end
30
+ end
@@ -133,19 +133,26 @@ module Pwl
133
133
  timestamp!(:last_accessed)
134
134
  value = @backend[:user][encrypt(key)]
135
135
  raise KeyNotFoundError.new(key) unless value
136
- decrypt(value)
136
+ EntryMapper.from_json(decrypt(value))
137
137
  }
138
138
  end
139
139
 
140
140
  #
141
- # Store value stored under key
141
+ # Store entry or value under key
142
142
  #
143
- def add(key, value)
144
- raise BlankKeyError if key.blank?
145
- raise BlankValueError if value.blank?
143
+ def add(entry_or_key, value = nil)
144
+ if value.nil? and entry_or_key.is_a?(Entry) # treat as entry
145
+ entry = entry_or_key
146
+ else
147
+ entry = Entry.new(entry_or_key)
148
+ entry.password = value
149
+ end
150
+
151
+ entry.validate!
152
+
146
153
  @backend.transaction{
147
154
  timestamp!(:last_modified)
148
- @backend[:user][encrypt(key)] = encrypt(value)
155
+ @backend[:user][encrypt(entry.name)] = encrypt(EntryMapper.to_json(entry))
149
156
  }
150
157
  end
151
158
 
@@ -158,7 +165,7 @@ module Pwl
158
165
  timestamp!(:last_modified)
159
166
  old_value = @backend[:user].delete(encrypt(key))
160
167
  raise KeyNotFoundError.new(key) unless old_value
161
- decrypt(old_value)
168
+ EntryMapper.from_json(decrypt(old_value))
162
169
  }
163
170
  end
164
171
 
@@ -178,12 +185,12 @@ module Pwl
178
185
  end
179
186
 
180
187
  #
181
- # Return all entries
188
+ # Return all entries as array
182
189
  #
183
190
  def all
184
- result = {}
191
+ result = []
185
192
  @backend.transaction(true){
186
- @backend[:user].each{|k,v| result[decrypt(k)] = decrypt(v)}
193
+ @backend[:user].each{|k,v| result << EntryMapper.from_json(decrypt(v))}
187
194
  }
188
195
  result
189
196
  end
@@ -199,6 +206,7 @@ module Pwl
199
206
  # Decrypt each key and value with the old master password and encrypt them with the new master password
200
207
  copy = {}
201
208
  @backend[:user].each{|k,v|
209
+ # No need to (de)serialize - the value comes in as JSON and goes out as JSON
202
210
  new_key = Encryptor.encrypt(decrypt(k), :key => new_master_password)
203
211
  new_val = Encryptor.encrypt(decrypt(v), :key => new_master_password)
204
212
  copy[new_key] = new_val