perfect_world 0.1.0

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