perfect_world 0.1.1 → 0.2.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.tar.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Perfect World Manager (pwm)
2
2
 
3
+ ![build status](https://api.travis-ci.org/ushis/perfect_world.png)
4
+
3
5
  The perfect world manager is the attempt to build a simple but secure
4
6
  password manager for the cli.
5
7
 
data/bin/pwm CHANGED
@@ -2,12 +2,21 @@
2
2
 
3
3
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
4
 
5
- require 'psych'
6
5
  require 'optparse'
7
- require 'gpgme'
8
- require 'perfect_world'
6
+
7
+ require 'psych'
8
+ require 'psych/syntax_error'
9
+
10
+ require 'perfect_world/cli'
11
+ require 'perfect_world/error'
9
12
  require 'perfect_world/version'
10
13
 
14
+ # Suicide helper.
15
+ def die(message, status = 1)
16
+ $stderr.puts message
17
+ exit status
18
+ end
19
+
11
20
  # Lets parse some options.
12
21
  options = {}
13
22
 
@@ -76,39 +85,26 @@ end
76
85
  begin
77
86
  parser.parse!
78
87
  rescue OptionParser::ParseError
79
- $stderr.puts parser.help
80
- exit(1)
88
+ die parser.help
81
89
  end
82
90
 
83
- # Load the config file.
84
- config = File.expand_path(options.fetch(:config, '~/.pwmrc'))
85
- config = File.file?(config) ? Psych.load_file(config) : {}
91
+ # Load config file
92
+ config_file = File.expand_path(options.fetch(:config, '~/.pwmrc'))
86
93
 
87
- # Override config with options.
88
- [:clipboard, :database, :gpgdir, :length, :owner].each do |option|
89
- config[option.to_s] = options.delete(option) unless options[option].nil?
94
+ begin
95
+ config = File.exist?(config_file) ? Psych.load_file(config_file) : {}
96
+ rescue SystemCallError, Psych::Exception, Psych::SyntaxError => e
97
+ die "Couldn't load config file: #{e.message}"
90
98
  end
91
99
 
92
- # Set GPG dir.
93
- if (dir = config.delete('gpgdir'))
94
- GPGME::Engine.home_dir = dir
100
+ # Override config with command line options.
101
+ [:clipboard, :database, :gpgdir, :length, :owner].each do |option|
102
+ config[option.to_s] = options.delete(option) unless options[option].nil?
95
103
  end
96
104
 
97
105
  # Action!
98
- pwm = PerfectWorld::Manager.new(config)
99
-
100
- options.each do |key, value|
101
- case key
102
- when :backup then pwm.save(value)
103
- when :database then pwm.print_config('database')
104
- when :delete then pwm.delete(value)
105
- when :generate then pwm.generate(value)
106
- when :get then pwm.get(value)
107
- when :length then pwm.print_config('length')
108
- when :list then pwm.list
109
- when :owner then pwm.print_config('owner')
110
- end
106
+ begin
107
+ PerfectWorld::Cli.new(config).run(options)
108
+ rescue PerfectWorld::Error => e
109
+ die e.message
111
110
  end
112
-
113
- # Closing time.
114
- pwm.close
data/lib/perfect_world.rb CHANGED
@@ -1 +1 @@
1
- require 'perfect_world/manager'
1
+ require 'perfect_world/db'
@@ -0,0 +1,107 @@
1
+ require 'gpgme'
2
+ require 'clipboard'
3
+
4
+ require 'perfect_world/db'
5
+ require 'perfect_world/error'
6
+ require 'perfect_world/storage'
7
+
8
+ module PerfectWorld
9
+
10
+ class Cli
11
+
12
+ # Default config.
13
+ CONFIG = {
14
+ 'length' => 64,
15
+ 'database' => File.expand_path('~/.pwm.yml.gpg'),
16
+ 'clipboard' => false
17
+ }
18
+
19
+ # Sets the config.
20
+ def initialize(config = {})
21
+ @config = CONFIG.merge(config)
22
+ GPGME::Engine.home_dir = @config['gpgdir'] if @config.key?('gpgdir')
23
+ end
24
+
25
+ # Loads the database and executes the provided commands.
26
+ def run(commands)
27
+ DB.open(@config.fetch('database'), @config['owner']) do |db|
28
+ commands.each { |cmd, arg| send(cmd, db, arg) }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Saves db to another location.
35
+ def backup(db, path)
36
+ db.save(path, @config['owner'])
37
+ end
38
+
39
+ # Prints the path to the currently used database.
40
+ def database(_ = nil, _ = nil)
41
+ print_config('database')
42
+ end
43
+
44
+ # Deletes a password.
45
+ def delete(db, id)
46
+ if db.delete(id)
47
+ puts "Deleted the password for '#{id}'."
48
+ else
49
+ password_not_found(id)
50
+ end
51
+ end
52
+
53
+ # Generats a new password and sends it to stdout.
54
+ def generate(db, id)
55
+ print_or_copy_to_clipboard(id, db.generate(id, @config.fetch('length')))
56
+ rescue Error => e
57
+ raise Error, "Couldn't create password: #{e.message}"
58
+ end
59
+
60
+ # Fetches the password and sends it to stdout.
61
+ def get(db, id)
62
+ print_or_copy_to_clipboard(id, db.fetch(id))
63
+ rescue KeyError
64
+ password_not_found(id)
65
+ end
66
+
67
+ # Prints the currently used password length.
68
+ def length(_ = nil, _ = nil)
69
+ print_config('length')
70
+ end
71
+
72
+ # Lists all passwords.
73
+ def list(db, _ = nil)
74
+ db.each { |id, password| print_password(id, password) }
75
+ end
76
+
77
+ # Prints the current key owner.
78
+ def owner(_ = nil, _ = nil)
79
+ puts Storage.find_key(@config['owner']).email
80
+ end
81
+
82
+ # Raises an error with a proper message.
83
+ def password_not_found(id)
84
+ raise Error, "Couldn't find the password for '#{id}'."
85
+ end
86
+
87
+ # Prints a password or copies it to the clipboard.
88
+ def print_or_copy_to_clipboard(id, password)
89
+ if @config.fetch('clipboard')
90
+ Clipboard.copy(password)
91
+ puts "Copied the password for '#{id}' to your clipboard."
92
+ else
93
+ print_password(id, password)
94
+ end
95
+ end
96
+
97
+ # Prints a password.
98
+ def print_password(id, password)
99
+ puts "#{password} #{id}"
100
+ end
101
+
102
+ # Prints a config entry.
103
+ def print_config(key)
104
+ puts @config[key]
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,108 @@
1
+ require 'perfect_world/error'
2
+ require 'perfect_world/random'
3
+ require 'perfect_world/storage'
4
+
5
+ module PerfectWorld
6
+
7
+ # Handles the password database.
8
+ class DB
9
+
10
+ def self.open(path, owner = nil)
11
+ db = File.exist?(path) ? load(path) : new({})
12
+
13
+ if block_given?
14
+ yield(db)
15
+ db.save(path, owner) if db.changed?
16
+ else
17
+ db
18
+ end
19
+ end
20
+
21
+ # Loads the database.
22
+ def self.load(path)
23
+ passwords = Storage.load(path)
24
+
25
+ valid?(passwords) ? new(passwords) : raise(CorruptedDatabase, path)
26
+ rescue Error => e
27
+ raise Error, "Couldn't load database: #{e.message}"
28
+ end
29
+
30
+ # Checks if a password hash is valid. A valid object is a hash with
31
+ # string values.
32
+ #
33
+ # Returns true if it is valid else false.
34
+ def self.valid?(data)
35
+ data.is_a?(Hash) && data.values.all? { |v| v.is_a?(String) }
36
+ end
37
+
38
+ # Inits the db and sets the initial passwords.
39
+ def initialize(passwords = {})
40
+ @passwords = passwords
41
+ @changed = false
42
+ end
43
+
44
+ # Generates a new password and puts it in the database.
45
+ #
46
+ # store.generate(:google, 32)
47
+ # #=> "B6m/![)A%fqw,\\ti-d`4\"&0>gl+>$0$Z"
48
+ #
49
+ # store[:google]
50
+ # #=> "B6m/![)A%fqw,\\ti-d`4\"&0>gl+>$0$Z"
51
+ #
52
+ # Returns the new password.
53
+ def generate(id, len = 64)
54
+ @passwords[id] = Random.string(len)
55
+ @changed = true
56
+ @passwords[id]
57
+ end
58
+
59
+ # Returns the password for the id or nil, if not found.
60
+ def [](id)
61
+ @passwords[id]
62
+ end
63
+
64
+ # Fetches a password from the database. It behaves like Hash#fetch.
65
+ def fetch(id, *default)
66
+ @passwords.fetch(id, *default)
67
+ end
68
+
69
+ # Deletes the password from the database.
70
+ #
71
+ # store.delete(:google)
72
+ # #=> "B6m/![)A%fqw,\\ti-d`4\"&0>gl+>$0$Z"
73
+ #
74
+ # store[:google]
75
+ # #=> nil
76
+ #
77
+ # Returns nil if not found.
78
+ def delete(id)
79
+ if (password = @passwords.delete(id))
80
+ @changed = true
81
+ password
82
+ end
83
+ end
84
+
85
+ # Returns true, when the database has changed, else false.
86
+ def changed?
87
+ @changed
88
+ end
89
+
90
+ # Iterates over all passwords.
91
+ #
92
+ # store.each do |id, password|
93
+ # puts "#{password} #{id}"
94
+ # end
95
+ #
96
+ # Returns an enumerator, if no block is given.
97
+ def each(&block)
98
+ @passwords.each(&block)
99
+ end
100
+
101
+ # Encryptes the database and writes it to disk.
102
+ def save(path, owner = nil)
103
+ Storage.dump(@passwords, owner, path)
104
+ rescue Error => e
105
+ raise Error, "Couldn't save database: #{e.message}"
106
+ end
107
+ end
108
+ end
@@ -9,4 +9,11 @@ module PerfectWorld
9
9
  super("Couldn't find key for '#{owner}'.")
10
10
  end
11
11
  end
12
+
13
+ # Should be raised if a database corruption is detected.
14
+ class CorruptedDatabase < Error
15
+ def initialize(database)
16
+ super("Database seems corrupted: #{database}")
17
+ end
18
+ end
12
19
  end
@@ -0,0 +1,27 @@
1
+ require 'securerandom'
2
+ require 'perfect_world/error'
3
+
4
+ module PerfectWorld
5
+
6
+ # Random string generator.
7
+ module Random
8
+
9
+ # Generates a random string with of a specified length (64 by default).
10
+ #
11
+ # PerfectWorld::Random.string(32)
12
+ # #=> "XzKk#~c\"Q(e2~8Bb#HO;v$}Jdid16-gO"
13
+ #
14
+ # Returns a string.
15
+ def self.string(len = 64)
16
+ s = String.new
17
+
18
+ while s.length < len
19
+ s << SecureRandom.random_bytes(len * 3).gsub(/[^[:print:]]/, '')
20
+ end
21
+
22
+ s[0..(len - 1)]
23
+ rescue SystemCallError, NotImplementedError => e
24
+ raise Error, e.message
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,87 @@
1
+ require 'psych'
2
+ require 'psych/syntax_error'
3
+ require 'gpgme'
4
+ require 'perfect_world/error'
5
+
6
+ module PerfectWorld
7
+
8
+ class Storage
9
+
10
+ class << self
11
+
12
+ # Loads data from an encrypted file.
13
+ #
14
+ # PerfectWorld::Storage.load('path/to/file.yml.gpg') #=> [1, 2]
15
+ #
16
+ # Returns the data.
17
+ def load(path)
18
+ open(path) do |file|
19
+ deserialize(decrypt(file))
20
+ end
21
+ end
22
+
23
+ # Writes data into an encrypted file.
24
+ #
25
+ # PerfectWorld::Storage.dump([1, 2], 'gpguid', /path/to/file.yml.gpg')
26
+ #
27
+ # Raises PerfectWorld::Error on error.
28
+ def dump(data, recipient, path)
29
+ commit(encrypt(serialize(data), find_key(recipient)), path)
30
+ end
31
+
32
+ # Finds a secret key in the key ring.
33
+ #
34
+ # Returns GPGME::Key or raises PerfectWorld::KeyNotFound.
35
+ def find_key(term = nil)
36
+ GPGME::Key.find(:secrect, term).first || raise(KeyNotFound, term)
37
+ end
38
+
39
+ private
40
+
41
+ def open(path)
42
+ File.open(path, 'r') do |f|
43
+ f.flock(File::LOCK_SH)
44
+ yield(f)
45
+ end
46
+ rescue SystemCallError, IOError => e
47
+ raise Error, e.message
48
+ end
49
+
50
+ def commit(data, path)
51
+ File.open(path, File::RDWR|File::CREAT, 0600) do |f|
52
+ f.flock(File::LOCK_EX)
53
+ f.truncate(0)
54
+ f.write(data)
55
+ end
56
+ rescue SystemCallError, IOError => e
57
+ raise Error, e.message
58
+ end
59
+
60
+ def serialize(data)
61
+ Psych.dump(data)
62
+ rescue Psych::Exception => e
63
+ raise Error, e.message
64
+ end
65
+
66
+ def deserialize(data)
67
+ Psych.load(data)
68
+ rescue Psych::Exception, Psych::SyntaxError => e
69
+ raise Error, e.message
70
+ end
71
+
72
+ def encrypt(data, key)
73
+ GPGME::Crypto.new(key: key).encrypt(data, recipients: key, sign: true)
74
+ rescue GPGME::Error => e
75
+ raise Error, e.message
76
+ end
77
+
78
+ def decrypt(data)
79
+ GPGME::Crypto.new.decrypt(data) do |signature|
80
+ raise(CorruptedDatabase, 'Invalid signature.') unless signature.valid?
81
+ end.to_s
82
+ rescue GPGME::Error => e
83
+ raise Error, e.message
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,3 +1,3 @@
1
1
  module PerfectWorld
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perfect_world
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -36,7 +36,7 @@ cert_chain:
36
36
  bjRVQmxyeTVQVzB6ZzY0VEk5L1AzNEFHUXQ2SStUOHMKQnh0dUdkZ0YyMkpt
37
37
  M3ZYbGlySkJJSStvMDdIeDV3ZjJEUGRDMTRKRU1UUVZoRVFkc2ZwbU9ub2Q0
38
38
  SnpLdWRxNwpES1U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
39
- date: 2013-02-03 00:00:00.000000000 Z
39
+ date: 2013-02-06 00:00:00.000000000 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: gpgme
@@ -78,10 +78,12 @@ executables:
78
78
  extensions: []
79
79
  extra_rdoc_files: []
80
80
  files:
81
- - lib/perfect_world/store.rb
81
+ - lib/perfect_world/cli.rb
82
+ - lib/perfect_world/storage.rb
82
83
  - lib/perfect_world/version.rb
84
+ - lib/perfect_world/random.rb
85
+ - lib/perfect_world/db.rb
83
86
  - lib/perfect_world/error.rb
84
- - lib/perfect_world/manager.rb
85
87
  - lib/perfect_world.rb
86
88
  - bin/pwm
87
89
  - README.md
metadata.gz.sig CHANGED
Binary file
@@ -1,95 +0,0 @@
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
@@ -1,100 +0,0 @@
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