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 +0 -0
- data/README.md +2 -0
- data/bin/pwm +26 -30
- data/lib/perfect_world.rb +1 -1
- data/lib/perfect_world/cli.rb +107 -0
- data/lib/perfect_world/db.rb +108 -0
- data/lib/perfect_world/error.rb +7 -0
- data/lib/perfect_world/random.rb +27 -0
- data/lib/perfect_world/storage.rb +87 -0
- data/lib/perfect_world/version.rb +1 -1
- metadata +6 -4
- metadata.gz.sig +0 -0
- data/lib/perfect_world/manager.rb +0 -95
- data/lib/perfect_world/store.rb +0 -100
data.tar.gz.sig
CHANGED
Binary file
|
data/README.md
CHANGED
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
|
-
|
8
|
-
require '
|
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
|
-
|
80
|
-
exit(1)
|
88
|
+
die parser.help
|
81
89
|
end
|
82
90
|
|
83
|
-
# Load
|
84
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
-
#
|
93
|
-
|
94
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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/
|
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
|
data/lib/perfect_world/error.rb
CHANGED
@@ -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
|
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.
|
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-
|
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/
|
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
|
data/lib/perfect_world/store.rb
DELETED
@@ -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
|