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