pwl 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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