perfect_world 0.1.1 → 0.2.0

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