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 +151 -0
- data/bin/pwm +101 -0
- data/lib/perfect_world/error.rb +12 -0
- data/lib/perfect_world/manager.rb +95 -0
- data/lib/perfect_world/store.rb +100 -0
- data/lib/perfect_world/version.rb +3 -0
- data/lib/perfect_world.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +115 -0
- metadata.gz.sig +0 -0
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 @@
|
|
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
|