Permalink
Browse files

Introduce entry instead of password

  • Loading branch information...
1 parent ea6a5d6 commit 13523371bc56dc9c87bc06f83baa3b8b465f5045 @nerab committed Jun 17, 2012
View
@@ -3,7 +3,8 @@ source "http://rubygems.org"
gem 'encryptor', "~> 1.1"
gem 'commander', "~> 4.1"
gem 'activesupport', "~> 3.2"
-gem 'jbuilder', '~> 0.4'
+gem 'activemodel', "~> 3.2"
+gem 'uuid', "~> 2.3"
group :test do
gem "rake", '~> 0.9'
@@ -15,6 +16,7 @@ end
# Include everything needed to run rake, tests, features, etc.
group :development do
gem "rdoc", "~> 3.1"
+ gem "pry" #, "~> 3.1"
gem "bundler", "~> 1.1"
gem "jeweler", "~> 1.8"
end
View
@@ -1,50 +1,64 @@
GEM
remote: http://rubygems.org/
specs:
+ activemodel (3.2.3)
+ activesupport (= 3.2.3)
+ builder (~> 3.0.0)
activesupport (3.2.3)
i18n (~> 0.6)
multi_json (~> 1.0)
- blankslate (2.1.2.4)
+ builder (3.0.0)
+ coderay (1.0.6)
commander (4.1.2)
highline (~> 1.6.11)
encryptor (1.1.3)
git (1.2.5)
highline (1.6.11)
i18n (0.6.0)
- jbuilder (0.4.0)
- activesupport (>= 3.0.0)
- blankslate (>= 2.1.2.4)
jeweler (1.8.3)
bundler (~> 1.0)
git (>= 1.2.5)
rake
rdoc
json (1.7.0)
+ macaddr (1.6.1)
+ systemu (~> 2.5.0)
+ method_source (0.7.1)
multi_json (1.3.4)
nokogiri (1.4.7)
nokogiri-diff (0.1.0)
nokogiri (~> 1.4.1)
tdiff (~> 0.3.2)
+ pry (0.9.9.6)
+ coderay (~> 1.0.5)
+ method_source (~> 0.7.1)
+ slop (>= 2.4.4, < 3)
rake (0.9.2.2)
rdoc (3.12)
json (~> 1.4)
simplecov (0.6.2)
multi_json (~> 1.3)
simplecov-html (~> 0.5.3)
simplecov-html (0.5.3)
+ slop (2.4.4)
+ systemu (2.5.1)
tdiff (0.3.2)
+ uuid (2.3.5)
+ macaddr (~> 1.0)
PLATFORMS
ruby
DEPENDENCIES
+ activemodel (~> 3.2)
activesupport (~> 3.2)
bundler (~> 1.1)
commander (~> 4.1)
encryptor (~> 1.1)
- jbuilder (~> 0.4)
jeweler (~> 1.8)
nokogiri-diff (~> 0.1)
+ pry
rake (~> 0.9)
rdoc (~> 3.1)
simplecov (~> 0.6)
+ uuid (~> 2.3)
View
49 bin/pwl
@@ -22,6 +22,8 @@ EXIT_CODES = {
:list_empty_filter => Pwl::Message.new('No names found that match filter <%= filter %>.', 9, :filter => 'FILTER'),
:validation_new_failed => Pwl::ErrorMessage.new('<%= message %>.', 10, :message => 'Validation of new master password failed'),
:unknown_export_format => Pwl::ErrorMessage.new('<%= format %> is not a known export format.', 11, :format => 'FORMAT'),
+ :inaccessible_field => Pwl::ErrorMessage.new("Field '<%= field %>' is not accessible.", 12, :field => 'FIELD'),
+ :is_dir => Pwl::ErrorMessage.new('File expected, but <%= file %> is a directory. Specify a regular file for the locker.', 13, :file => 'FILE'),
}
program :help, 'Exit Status', "#{program(:name)} sets the following exit status:\n\n" + 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")
@@ -36,6 +38,12 @@ global_option '-V', '--verbose', 'Enable verbose output'
global_option('-f', '--file FILE', 'Determine the file that holds the locker'){|file| locker_file = file}
global_option '-g', '--gui', 'Request the master password using an OS-specific GUI dialog. This option takes precedence over STDIN.'
+class InacessibleFieldError < StandardError
+ def initialize(field)
+ super("The field #{field} is not accessible")
+ end
+end
+
command :init do |c|
c.syntax = "#{program(:name)} #{c.name}"
c.summary = 'Initializes a new locker'
@@ -44,7 +52,10 @@ command :init do |c|
c.example "Initializes a new password locker in /tmp/crackme.txt", "#{program(:name)} #{c.name} --file /tmp/crackme.txt"
c.option '--force', 'Force-overwrite an existing locker file'
c.action do |args, options|
+ msg "Attempting to initialize new locker at #{locker_file}" if options.verbose
+
# Locker checks this too, but we want to fail fast.
+ exit_with(:is_dir, options.verbose, :file => locker_file) if File.exists?(locker_file) && File.directory?(locker_file)
exit_with(:file_exists, options.verbose, :file => locker_file) if File.exists?(locker_file) && !options.force
begin
@@ -53,7 +64,7 @@ command :init do |c|
end while begin
validate!(master_password) # Basic idea from http://stackoverflow.com/questions/136793/is-there-a-do-while-loop-in-ruby
rescue Pwl::InvalidMasterPasswordError => e
- STDERR.puts e.message
+ msg e.message
options.gui || STDIN.tty? # only continue the loop when in interactive mode
end
@@ -66,12 +77,12 @@ command :init do |c|
end
Pwl::Locker.new(locker_file, master_password, {:force => options.force})
- STDERR.puts "Successfully initialized new locker at #{locker_file}" if options.verbose
+ msg "Successfully initialized new locker at #{locker_file}" if options.verbose
end
end
command :get do |c|
- c.syntax = "#{program(:name)} #{c.name} NAME"
+ c.syntax = "#{program(:name)} #{c.name} NAME [FIELD]"
c.summary = 'Retrieves the value for NAME and prints it to STDOUT.'
c.description = 'This command retrieves the value stored under NAME and prints it on STDOUT.'
c.example 'Reads the value stored under the name "foo" and prints it to STDOUT', "#{program(:name)} #{c.name} foo"
@@ -80,13 +91,19 @@ command :get do |c|
exit_with(:file_not_found, options.verbose, :file => locker_file) unless File.exists?(locker_file)
exit_with(:name_blank, options.verbose) if 0 == args.size || args[0].blank?
+ # second argument can be a field other than password
+ field = args.size > 1 ? args[1] : 'password'
+
begin
- result = Pwl::Locker.open(locker_file, get_password("Enter the master password for #{program(:name)}:", options.gui)).get(args[0])
+ locker = Pwl::Locker.open(locker_file, get_password("Enter the master password for #{program(:name)}:", options.gui))
+ result = attr!(locker.get(args[0]), field)
if result.blank?
exit_with(:no_value_found, options.verbose, :name => args[0])
else
puts result
end
+ rescue InacessibleFieldError
+ exit_with(:inaccessible_field, options.verbose, :field => field)
rescue Pwl::Dialog::Cancelled
exit_with(:aborted, options.verbose)
end
@@ -159,7 +176,7 @@ command :add do |c|
end
locker.add(args[0], value)
- STDERR.puts "Successfully stored new value under #{args[0]}." if options.verbose
+ msg "Successfully stored new value under #{args[0]}." if options.verbose
end
end
@@ -179,7 +196,7 @@ command :delete do |c|
end
locker.delete(args[0])
- STDERR.puts "Successfully deleted the value under #{args[0]}." if options.verbose
+ msg "Successfully deleted the value under #{args[0]}." if options.verbose
end
end
@@ -209,7 +226,7 @@ command :passwd do |c|
end while begin
validate!(new_master_password)
rescue Pwl::InvalidMasterPasswordError => e
- STDERR.puts e.message
+ msg e.message
options.gui || STDIN.tty? # only continue the loop when in interactive mode
end
@@ -223,7 +240,7 @@ command :passwd do |c|
end
locker.change_password!(new_master_password)
- STDERR.puts "Successfully changed master password for #{program(:name)}." if options.verbose
+ msg "Successfully changed master password for #{program(:name)}." if options.verbose
end
end
@@ -242,7 +259,7 @@ command :export do |c|
presenter = {:html => Pwl::Presenter::Html, :json => Pwl::Presenter::Json, :yaml => Pwl::Presenter::Yaml}[options.format.to_sym]
exit_with(:unknown_export_format, options.verbose, :format => options.format) if presenter.nil?
-
+
begin
locker = Pwl::Locker.open(locker_file, get_password("Enter the master password for #{program(:name)}:", options.gui))
puts presenter.new(locker).to_s
@@ -252,12 +269,16 @@ command :export do |c|
end
end
+def msg(str)
+ STDERR.puts("#{program(:name)}: #{str}")
+end
+
def exit_with(error_code, verbose, msg_args = {})
msg = EXIT_CODES[error_code]
raise "No message defined for error #{error_code}" if !msg
if msg.error? || verbose # always print errors; messages only when verbose
- STDERR.puts msg.to_s(msg_args)
+ msg msg.to_s(msg_args)
end
exit(msg.exit_code)
@@ -274,3 +295,11 @@ end
def validate!(pwd)
Pwl::Locker.password_policy.validate!(pwd)
end
+
+#
+# Returns the value of the passed attribute name if it is allowed to be retrieved from a locker entry
+#
+def attr!(entry, field)
+ raise InacessibleFieldError.new(field) unless entry.instance_variable_defined?("@#{field}".to_sym)
+ entry.send(field)
+end
View
@@ -8,6 +8,8 @@
require 'message'
require 'password_policy'
require 'locker'
+require 'entry'
+require 'entry_mapper'
require 'dialog'
require 'presenter/html'
require 'presenter/json'
View
@@ -0,0 +1,31 @@
+require 'active_model'
+require 'uuid'
+
+module Pwl
+ class InvalidEntryError < StandardError
+ def initialize(errors)
+ super(errors.to_a.join(', '))
+ end
+ end
+
+ class Entry
+ attr_accessor :uuid, :name, :password
+
+ include ActiveModel::Validations
+ validates_presence_of :name, :uuid, :password
+ 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}/
+
+ def initialize(name = nil)
+ @name = name
+ @uuid = UUID.generate
+ @password = nil
+ end
+
+ #
+ # raises InvalidEntryError if entry is not valid
+ #
+ def validate!
+ raise InvalidEntryError.new(errors) if invalid?
+ end
+ end
+end
View
@@ -0,0 +1,30 @@
+require 'json'
+
+module Pwl
+ #
+ # DataMapper that maps an Entry from and to JSON
+ #
+ class EntryMapper
+ class << self
+ ATTRIBUTES = %w[uuid name password]
+ def from_json(str)
+ json = JSON(str, :symbolize_names => true)
+
+ Entry.new.tap do |entry|
+ ATTRIBUTES.each do |attr|
+ entry.send("#{attr}=", json[attr.to_sym])
+ end
+ end
+ end
+
+ def to_json(entry)
+ entry.validate!
+ result = {}
+ ATTRIBUTES.each do |attr|
+ result.store(attr.to_sym, entry.send(attr))
+ end
+ result.to_json
+ end
+ end
+ end
+end
View
@@ -133,19 +133,26 @@ def get(key)
timestamp!(:last_accessed)
value = @backend[:user][encrypt(key)]
raise KeyNotFoundError.new(key) unless value
- decrypt(value)
+ EntryMapper.from_json(decrypt(value))
}
end
#
- # Store value stored under key
+ # Store entry or value under key
#
- def add(key, value)
- raise BlankKeyError if key.blank?
- raise BlankValueError if value.blank?
+ def add(entry_or_key, value = nil)
+ if value.nil? and entry_or_key.is_a?(Entry) # treat as entry
+ entry = entry_or_key
+ else
+ entry = Entry.new(entry_or_key)
+ entry.password = value
+ end
+
+ entry.validate!
+
@backend.transaction{
timestamp!(:last_modified)
- @backend[:user][encrypt(key)] = encrypt(value)
+ @backend[:user][encrypt(entry.name)] = encrypt(EntryMapper.to_json(entry))
}
end
@@ -158,7 +165,7 @@ def delete(key)
timestamp!(:last_modified)
old_value = @backend[:user].delete(encrypt(key))
raise KeyNotFoundError.new(key) unless old_value
- decrypt(old_value)
+ EntryMapper.from_json(decrypt(old_value))
}
end
@@ -178,12 +185,12 @@ def list(filter = nil)
end
#
- # Return all entries
+ # Return all entries as array
#
def all
- result = {}
+ result = []
@backend.transaction(true){
- @backend[:user].each{|k,v| result[decrypt(k)] = decrypt(v)}
+ @backend[:user].each{|k,v| result << EntryMapper.from_json(decrypt(v))}
}
result
end
@@ -199,6 +206,7 @@ def change_password!(new_master_password)
# Decrypt each key and value with the old master password and encrypt them with the new master password
copy = {}
@backend[:user].each{|k,v|
+ # No need to (de)serialize - the value comes in as JSON and goes out as JSON
new_key = Encryptor.encrypt(decrypt(k), :key => new_master_password)
new_val = Encryptor.encrypt(decrypt(v), :key => new_master_password)
copy[new_key] = new_val
Oops, something went wrong.

0 comments on commit 1352337

Please sign in to comment.