perfect_world 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Perfect World Manager (pwm)
2
+
3
+ The perfect world manager is the attempt to build a simple but secure
4
+ password manager for the cli.
5
+
6
+ This is work in progress and has not been audited by security experts.
7
+ Do NOT use for your actual passwords yet!
8
+
9
+ ## How?
10
+
11
+ The procedure is pretty simple.
12
+
13
+ To retrieve passwords a gpg encrypted Yaml file is opened, decrypted,
14
+ deserialized and the password is displayed to the user.
15
+
16
+ To add new passwords the encrypted file is opened and decrypted. The new
17
+ password is added to the data (a simple Ruby hash), the data is serialized
18
+ to Yaml, encrypted by gpg and written to disk.
19
+
20
+ This should make it even possible to sync the password "database" between
21
+ machines using Dropbox or other "untrusted" services. (I actually trust
22
+ Dropbox, but they should not get my passwords.)
23
+
24
+ ## Concerns
25
+
26
+ I am no security expert. Maybe my concerns are no problem at all and there
27
+ are some other problems i couldn't even think of.
28
+
29
+ ### Is the config file trusted user input?
30
+
31
+ At the moment pwm uses Psych to parse the config file. Given the recent
32
+ problems with Psych related to arbitrary code execution, my concerns are not
33
+ unfounded. But if an attacker has access to a config file in my home folder
34
+ she probably also has access to my private gpg key and can read the whole
35
+ "database" anyway.
36
+
37
+ ### The clipboard feature
38
+
39
+ pwm has the ability to copy passwords to the users clipboard. It's a very
40
+ nice feature. You don't have to select/copy the password from the command
41
+ line, what kinda sucks. But it begs for some questions. What are the different
42
+ clipboard implementations (xclip, xsel, clip) actually doing with the
43
+ data? And more important, who can access the data in the clipboard? Well,
44
+ every programm on the machine i guess, just like pwm. What about the crazy
45
+ old flash hacks? Are they still working? That would be a huge problem.
46
+
47
+ # Install
48
+
49
+ If you are still considering to download pwm, read on.
50
+
51
+ ## Dependecies
52
+
53
+ You need an installed and set up version of gnupg. It should be available
54
+ in the package repo of your linux distribution. There are also several ways
55
+ to install it on a Mac. I tested the code with version 2.0.19.
56
+
57
+ In addition you will need at least Ruby 1.9.
58
+
59
+ ## pwm
60
+
61
+ Install the gem.
62
+
63
+ gem install perfect_world
64
+
65
+ Or clone the repo.
66
+
67
+ git clone https://github.com/ushis/perfect_world.git
68
+ cd perfect_world
69
+ bundle install
70
+ rake spec
71
+ rake build
72
+
73
+ # Usage
74
+
75
+ $ pwm --help
76
+ Usage: pwm [options]
77
+
78
+ Options:
79
+ -b, --backup FILE Writes a backup to another database.
80
+ -c, --config FILE Specifies the path to the config file.
81
+ -C, --clipboard Copies the password to the clipboard.
82
+ -d, --delete ID Deletes the password.
83
+ -D, --database [FILE] Prints or sets the used database.
84
+ -g, --get ID Prints the password for an ID.
85
+ -G, --generate ID Generates and stores a new passord.
86
+ -h, --help Prints this message and exits.
87
+ -l, --list Lists all passwords.
88
+ -L, --length [LENGTH] Prints or sets the length of new passwords.
89
+ -o, --owner [OWNER] Prints or sets the encryption recipient.
90
+
91
+ ## Examples
92
+
93
+ Let's create some passwords.
94
+
95
+ $ pwm --generate github
96
+ 9&sq'8Gz.Bpb8#%M.T-Xyi#&.sDcTYFE.=qFyEbld Z[wA'By75y?NA?qUy}U>xd github
97
+ $ pwm --generate google
98
+ 8UN:'I1^M)H\kj'U{4l!.tK3\v9V+}L4$XNal \rzE@c\["&u#@#TRINt5"Jj[6A google
99
+
100
+ And retrieve them.
101
+
102
+ $ pwm --list
103
+ 9&sq'8Gz.Bpb8#%M.T-Xyi#&.sDcTYFE.=qFyEbld Z[wA'By75y?NA?qUy}U>xd github
104
+ 8UN:'I1^M)H\kj'U{4l!.tK3\v9V+}L4$XNal \rzE@c\["&u#@#TRINt5"Jj[6A google
105
+
106
+ Or just one.
107
+
108
+ $ pwm --get github
109
+ 9&sq'8Gz.Bpb8#%M.T-Xyi#&.sDcTYFE.=qFyEbld Z[wA'By75y?NA?qUy}U>xd github
110
+
111
+ Directly to the clipboard.
112
+
113
+ $ pwm --clipboard --get google
114
+ Copied the password for 'google' to your clipboard.
115
+
116
+ And delete one.
117
+
118
+ $ pwm --delete google
119
+ Deleted the password for 'google'.
120
+
121
+ ## Config file
122
+
123
+ pwm looks for the config file at ```~/.pwmrc``` by default. This can be
124
+ changed with the ```--config``` switch. It contains straight forward Yaml.
125
+
126
+ ---
127
+ owner: ushi. # Used as ecryption recipient by gpg.
128
+ length: 64 # Length of the generated password.
129
+ database: ~/.pwm.yml.gpg # Path to the password database.
130
+
131
+ # License (MIT)
132
+
133
+ Copyright (c) 2013 ushi
134
+
135
+ Permission is hereby granted, free of charge, to any person obtaining a copy
136
+ of this software and associated documentation files (the "Software"), to deal
137
+ in the Software without restriction, including without limitation the rights
138
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
139
+ copies of the Software, and to permit persons to whom the Software is
140
+ furnished to do so, subject to the following conditions:
141
+
142
+ The above copyright notice and this permission notice shall be included in
143
+ all copies or substantial portions of the Software.
144
+
145
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
146
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
147
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
148
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
149
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
150
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
151
+ IN THE SOFTWARE.
data/bin/pwm ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'psych'
6
+ require 'optparse'
7
+ require 'perfect_world'
8
+
9
+
10
+ # Lets parse some options.
11
+ options = {}
12
+
13
+ parser = OptionParser.new do |parser|
14
+ parser.banner = "Usage: #{$0} [options]"
15
+
16
+ parser.separator ''
17
+ parser.separator 'Options:'
18
+
19
+ parser.on('-b', '--backup FILE', 'Writes a backup to another database.') do |f|
20
+ options[:backup] = f
21
+ end
22
+
23
+ parser.on('-c', '--config FILE', 'Specifies the path to the config file.') do |f|
24
+ options[:config] = f
25
+ end
26
+
27
+ parser.on('-C', '--clipboard', "Copies the password to the clipboard.") do |c|
28
+ options[:clipboard] = c
29
+ end
30
+
31
+ parser.on('-d', '--delete ID', 'Deletes the password.') do |id|
32
+ options[:delete] = id
33
+ end
34
+
35
+ parser.on('-D', '--database [FILE]', 'Prints or sets the used database.') do |f|
36
+ options[:database] = f
37
+ end
38
+
39
+ parser.on('-g', '--get ID', 'Prints the password for an ID.') do |id|
40
+ options[:get] = id
41
+ end
42
+
43
+ parser.on('-G', '--generate ID', 'Generates and stores a new passord.') do |id|
44
+ options[:generate] = id
45
+ end
46
+
47
+ parser.on('-h', '--help', 'Prints this message and exits.') do
48
+ puts parser.help; exit
49
+ end
50
+
51
+ parser.on('-l', '--list', 'Lists all passwords.') do |l|
52
+ options[:list] = l
53
+ end
54
+
55
+ parser.on('-L', '--length [LENGTH]', Integer, "Prints or sets the length of new passwords.") do |l|
56
+ options[:length] = l
57
+ end
58
+
59
+ parser.on('-o', '--owner [OWNER]', 'Prints or sets the encryption recipient.') do |o|
60
+ options[:owner] = o
61
+ end
62
+
63
+ parser.separator ''
64
+ parser.separator 'Please report bugs at: http://github.com/ushis/perfect_world/issues'
65
+ end
66
+
67
+ begin
68
+ parser.parse!
69
+ rescue OptionParser::ParseError
70
+ $stderr.puts parser.help
71
+ exit(1)
72
+ end
73
+
74
+
75
+ # Load the config file and init the manager.
76
+ config = File.expand_path(options.fetch(:config, '~/.pwmrc'))
77
+ config = File.file?(config) ? Psych.load_file(config) : {}
78
+
79
+ [:database, :length, :owner, :clipboard].each do |option|
80
+ config[option.to_s] = options.delete(option) unless options[option].nil?
81
+ end
82
+
83
+ pwm = PerfectWorld::Manager.new(config)
84
+
85
+
86
+ # Action!
87
+ options.each do |key, value|
88
+ case key
89
+ when :backup then pwm.save(value)
90
+ when :database then pwm.print_config('database')
91
+ when :delete then pwm.delete(value)
92
+ when :generate then pwm.generate(value)
93
+ when :get then pwm.get(value)
94
+ when :length then pwm.print_config('length')
95
+ when :list then pwm.list
96
+ when :owner then pwm.print_config('owner')
97
+ end
98
+ end
99
+
100
+ # Closing time.
101
+ pwm.close
@@ -0,0 +1,12 @@
1
+ module PerfectWorld
2
+
3
+ # Base error for all perfect world errors.
4
+ class Error < StandardError; end
5
+
6
+ # Should be raised, when gpg key could not be found.
7
+ class KeyNotFound < Error
8
+ def initialize(owner)
9
+ super("Couldn't find key for '#{owner}'.")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,95 @@
1
+ require 'clipboard'
2
+ require 'perfect_world/store'
3
+ require 'perfect_world/error'
4
+
5
+ module PerfectWorld
6
+
7
+ # Manages our perfec world store.
8
+ class Manager
9
+
10
+ # Default options.
11
+ OPTIONS = {
12
+ 'length' => 64,
13
+ 'database' => File.expand_path('~/.pwm.yml.gpg'),
14
+ 'clipboard' => false
15
+ }
16
+
17
+ # Inits our perfect world.
18
+ def initialize(options = {})
19
+ @options = OPTIONS.merge(options)
20
+ db = @options.fetch('database')
21
+ @store = File.file?(db) ? Store.load(db) : Store.new({})
22
+ rescue Error => e
23
+ die("Couldn't load database: " << e.message)
24
+ end
25
+
26
+ # Fetches the password and sends it to stdout.
27
+ def get(id)
28
+ print_or_copy_to_clipboard(id, @store.fetch(id))
29
+ rescue KeyError
30
+ not_found(id)
31
+ end
32
+
33
+ # Generats a new password and sends it to stdout.
34
+ def generate(id)
35
+ print_or_copy_to_clipboard(id, @store.create(id, @options.fetch('length')))
36
+ rescue Error => e
37
+ $stderr.puts("Couldn't create password: " << e.message)
38
+ end
39
+
40
+ # Deletes a password.
41
+ def delete(id)
42
+ if @store.delete(id)
43
+ puts "Deleted the password for '#{id}'."
44
+ else
45
+ not_found(id)
46
+ end
47
+ end
48
+
49
+ # Lists all passwords.
50
+ def list
51
+ @store.each { |id, password| print_password(id, password) }
52
+ end
53
+
54
+ # Prints the path to the currently used database.
55
+ def print_config(key)
56
+ puts @options[key]
57
+ end
58
+
59
+ def close
60
+ save(@options.fetch('database')) if @store.changed?
61
+ end
62
+
63
+ def save(database)
64
+ @store.save(database, @options['owner'])
65
+ rescue Error => e
66
+ die("Couldn't save database: " << e.message)
67
+ end
68
+
69
+ private
70
+
71
+ def die(message)
72
+ $stderr.puts(message)
73
+ exit(1)
74
+ end
75
+
76
+ # Sends a not found message to stderr.
77
+ def not_found(id)
78
+ $stderr.puts "Couldn't find the password for '#{id}'."
79
+ end
80
+
81
+ def print_or_copy_to_clipboard(id, password)
82
+ if @options.fetch('clipboard')
83
+ Clipboard.copy(password)
84
+ puts "Copied the password for '#{id}' to your clipboard."
85
+ else
86
+ print_password(id, password)
87
+ end
88
+ end
89
+
90
+ # Prints a password.
91
+ def print_password(id, password)
92
+ puts "#{password} #{id}"
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,100 @@
1
+ require 'securerandom'
2
+ require 'psych'
3
+ require 'gpgme'
4
+ require 'perfect_world/error'
5
+
6
+ module PerfectWorld
7
+
8
+ # Handles the password database.
9
+ class Store
10
+
11
+ # Loads the database and returns a new store.
12
+ def self.load(database)
13
+ File.open(database, 'r') do |f|
14
+ f.flock(File::LOCK_SH)
15
+ new(Psych.load(GPGME::Crypto.new.decrypt(f).to_s))
16
+ end
17
+ rescue SystemCallError, IOError, GPGME::Error, Psych::Exception => e
18
+ raise Error, e.message
19
+ end
20
+
21
+ # Inits the store and sets the initial passwords.
22
+ def initialize(passwords = {})
23
+ @passwords = passwords
24
+ @changed = false
25
+ end
26
+
27
+ # Generates a new password and puts it in the store.
28
+ #
29
+ # Returns the new password.
30
+ def create(id, len = 64)
31
+ @passwords[id] = generate(len)
32
+ @changed = true
33
+ @passwords[id]
34
+ end
35
+
36
+ # Returns the password for the id or nil, if not found.
37
+ def [](id)
38
+ @passwords[id]
39
+ end
40
+
41
+ # Fetches a password from the store. It behaves like Hash#fetch.
42
+ def fetch(id, *default)
43
+ @passwords.fetch(id, *default)
44
+ end
45
+
46
+ # Deletes the password from the store.
47
+ #
48
+ # Returns nil if not found.
49
+ def delete(id)
50
+ if (password = @passwords.delete(id))
51
+ @changed = true
52
+ password
53
+ end
54
+ end
55
+
56
+ # Returns true, when the store has changed, else false.
57
+ def changed?
58
+ @changed
59
+ end
60
+
61
+ # Iterates over all passwords.
62
+ def each(&block)
63
+ @passwords.each(&block)
64
+ end
65
+
66
+ # Encryptes the database and writes it to disk.
67
+ def save(database, owner = nil)
68
+ key = key_for(owner)
69
+
70
+ File.open(database, File::RDWR|File::CREAT, 0600) do |f|
71
+ yaml = Psych.dump(@passwords)
72
+ f.flock(File::LOCK_EX)
73
+ f.truncate(0)
74
+ f << GPGME::Crypto.new(key: key).encrypt(yaml, recipients: key)
75
+ end
76
+ rescue SystemCallError, IOError, Psych::Exception, GPGME::Error => e
77
+ raise Error, e.message
78
+ end
79
+
80
+ # Generates a new password and returns it.
81
+ def generate(len = 64)
82
+ password = ''
83
+
84
+ begin
85
+ password << SecureRandom.random_bytes(len * 3).gsub(/[^[:print:]]/, '')
86
+ end while password.length < len
87
+
88
+ password[0..(len - 1)]
89
+ rescue SystemCallError, NotImplementedError => e
90
+ raise Error, e.message
91
+ end
92
+
93
+ private
94
+
95
+ # Returns the key for an owner.
96
+ def key_for(owner)
97
+ GPGME::Key.find(:secret, owner).first || raise(KeyNotFound, owner)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module PerfectWorld
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1 @@
1
+ require 'perfect_world/manager'
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perfect_world
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - ushi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain:
12
+ - !binary |-
13
+ LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMakNDQWhhZ0F3SUJB
14
+ Z0lCQURBTkJna3Foa2lHOXcwQkFRVUZBREE5TVEwd0N3WURWUVFEREFSMWMy
15
+ aHAKTVJjd0ZRWUtDWkltaVpQeUxHUUJHUllIY0c5eWEySnZlREVUTUJFR0Nn
16
+ bVNKb21UOGl4a0FSa1dBMjVsZERBZQpGdzB4TXpBeU1ESXlNekEyTVRSYUZ3
17
+ MHhOREF5TURJeU16QTJNVFJhTUQweERUQUxCZ05WQkFNTUJIVnphR2t4CkZ6
18
+ QVZCZ29Ka2lhSmsvSXNaQUVaRmdkd2IzSnJZbTk0TVJNd0VRWUtDWkltaVpQ
19
+ eUxHUUJHUllEYm1WME1JSUIKSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4
20
+ QU1JSUJDZ0tDQVFFQXdmU1ovSHJTNFBSV0M2eGw5UGFyejlwegpzSXFKWGFZ
21
+ WlBJNWJxeFVsdm1IVkdZL0lJclpZOUZSUjFLdDhFZjlGbDJxaWw4ZStPSW9J
22
+ REkxbW5XSi9BM25uCkRKa1Ntd2I0MVQ5M2laQno3NER5VU9xbVBGYkxSUm1G
23
+ aGV6ekJ2T0pPUVM1OGtHMysvcmxONTR3UjI5L3BWbEgKVUs5NlByRVhzdmRH
24
+ VmpuN09WM2M3eTJUZDNheXdFVWJpSFVtRjVJaFBDcW5FOXpaZVI0U2pGQ3Uz
25
+ VXFKVUE3TApzNjdhNURtTTlLdTcwMWxoYnpGL2RtZGd1MU1SamJnbngrZnVx
26
+ OW9HcVpaTUhhWVpRb0xndTNBaDlaVlRhWjdCCnMxdVVuOTlRUitqdGxMNWlI
27
+ T0U2TktISE16UjFaNWxtaEY0aUFqSXRjbzAzZEE5K2p1eHBFTm1PK24xdjVR
28
+ SUQKQVFBQm96a3dOekFKQmdOVkhSTUVBakFBTUIwR0ExVWREZ1FXQkJRd0pN
29
+ NkF2M0psSVlBM0xGTkZEQ2RMTFgzYQp1akFMQmdOVkhROEVCQU1DQkxBd0RR
30
+ WUpLb1pJaHZjTkFRRUZCUUFEZ2dFQkFFd2c2bVpYd2g4KzE3WlFXbkxQCnk0
31
+ NmxodGNON0t6RHc0cjdNbGp3THdoT3EzNmEvSkR1aHFNSDN4NFNjb3o5dzRX
32
+ aWZEVE5EbitQemJEQWQrVW8KakVUNjJCTTJRK29veGN4djh4eVRFK1VycXF3
33
+ eTNTTVdjQi9ONnhwYzB3ZDdVWWtVMmc0ZUg3SFo4Q2dBM0pzagpTUU40NkxT
34
+ WTZOa3dXZzRacUZCS25OYVpjSWlrSHJCclhHZXg4TTdZZG9yRlFZMmZIQml5
35
+ cXg5ZEVraDlRZjFOCkJBVVlUTlVLQVFzT0R3ZkYrU2U4SmNMZlJGbjQ2QUNB
36
+ bjRVQmxyeTVQVzB6ZzY0VEk5L1AzNEFHUXQ2SStUOHMKQnh0dUdkZ0YyMkpt
37
+ M3ZYbGlySkJJSStvMDdIeDV3ZjJEUGRDMTRKRU1UUVZoRVFkc2ZwbU9ub2Q0
38
+ SnpLdWRxNwpES1U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
39
+ date: 2013-02-02 00:00:00.000000000 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: gpgme
43
+ requirement: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: 2.0.1
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: 2.0.1
57
+ - !ruby/object:Gem::Dependency
58
+ name: clipboard
59
+ requirement: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ~>
63
+ - !ruby/object:Gem::Version
64
+ version: 1.0.1
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ~>
71
+ - !ruby/object:Gem::Version
72
+ version: 1.0.1
73
+ description: An attempt to build a simple but secure password manager.
74
+ email:
75
+ - ushi@porkbox.net
76
+ executables:
77
+ - pwm
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - lib/perfect_world.rb
82
+ - lib/perfect_world/version.rb
83
+ - lib/perfect_world/error.rb
84
+ - lib/perfect_world/manager.rb
85
+ - lib/perfect_world/store.rb
86
+ - bin/pwm
87
+ - README.md
88
+ homepage: http://github.com/ushis/perfect_world
89
+ licenses: []
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ segments:
101
+ - 0
102
+ hash: 1106683263290744133
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: 1.3.6
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Password Manager for the CLI.
115
+ test_files: []
metadata.gz.sig ADDED
Binary file