fun_with_passwords 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.document +5 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +22 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/bin/fwpass +5 -0
- data/lib/fun_with/passwords/command_line.rb +184 -0
- data/lib/fun_with/passwords/command_line_result.rb +56 -0
- data/lib/fun_with/passwords/console.rb +42 -0
- data/lib/fun_with/passwords/crypt.rb +38 -0
- data/lib/fun_with/passwords/file_store.rb +78 -0
- data/lib/fun_with/passwords/keychain.rb +105 -0
- data/lib/fun_with/passwords/password.rb +14 -0
- data/lib/fun_with_passwords.rb +19 -0
- data/test/data/kvpair_successes.txt +9 -0
- data/test/helper.rb +31 -0
- data/test/test_command_line.rb +76 -0
- data/test/test_crypt.rb +13 -0
- data/test/test_fun_with_passwords.rb +15 -0
- data/test/test_keychain.rb +56 -0
- metadata +207 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YmE0NWE2NGNiZWY2MWRlMjcyNmNiYzFhMjU1MDM4YThkYmY0ZTJjZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YTNlZTk5MTFkNjg1YzhjNWI5OTkwYTZiNmQyZmEyZTYxMDFmZjYzNw==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDVkZWMxZTY2MmVkMTQ3MWU2MDcxZjU2MTI2MzRhNTliZGZkOWE3NGQ1M2Fk
|
10
|
+
NWJiYWE1MTcxMGQwODdhYjE2YTc0NzZiY2NjMDU3ZjlhMjI1NTBmY2ZmN2Yy
|
11
|
+
MDBjMDkxYzFhNjk3YjYzNDZiZDFjNjYzMTgxZDYzYTJiN2I3NGU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZWY1OGZiYjU5ODY2ODUzMjJmMGNiMmVkOTE4MGU2YWJmN2JkNjZiNmFiYTZm
|
14
|
+
ZmFlNWQ2YWQyNzdhNmM1YmM2NGEzNzhjMDhjYzQ1ZTFjM2E1YTEzYTAwYzI0
|
15
|
+
MWFhZWY3NjYxMjQ1NDFiN2Y0ODFlYWZmMDFmZTgxNTgzOGRkMmU=
|
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", "~> 3.5"
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", "~> 1.5"
|
12
|
+
gem "jeweler", "~> 2.0"
|
13
|
+
gem "debugger", "~> 1.6"
|
14
|
+
end
|
15
|
+
|
16
|
+
gem "fun_with_files", "~> 0"
|
17
|
+
gem "fun_with_configurations", "~> 0"
|
18
|
+
gem "fun_with_version_strings", "~> 0"
|
19
|
+
gem "highline", "~> 1.6"
|
20
|
+
gem "xdg", "~> 2.2"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Bryce Anderson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
= fun_with_passwords
|
2
|
+
|
3
|
+
Creates an encrypted password store that your applications can access.
|
4
|
+
|
5
|
+
Keychain.
|
6
|
+
|
7
|
+
|
8
|
+
== Contributing to fun_with_passwords
|
9
|
+
|
10
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
11
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
12
|
+
* Fork the project.
|
13
|
+
* Start a feature/bugfix branch.
|
14
|
+
* Commit and push until you are happy with your contribution.
|
15
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
16
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
17
|
+
|
18
|
+
== Copyright
|
19
|
+
|
20
|
+
Copyright (c) 2013 Bryce Anderson. See LICENSE.txt for
|
21
|
+
further details.
|
22
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "fun_with_passwords"
|
18
|
+
gem.homepage = "http://github.com/darthschmoo/fun_with_passwords"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = "Create an encrypted password store that your scripts can access."
|
21
|
+
gem.description = "Because passwords are obnoxious."
|
22
|
+
gem.email = "keeputahweird@gmail.com"
|
23
|
+
gem.authors = ["Bryce Anderson"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
Rake::TestTask.new(:test) do |test|
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default => :test
|
37
|
+
|
38
|
+
require 'rdoc/task'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "fun_with_passwords #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.2
|
data/bin/fwpass
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
module FunWith
|
2
|
+
module Passwords
|
3
|
+
ACTION_KEYWORDS_TO_ACTIONS = {
|
4
|
+
"init" => :initialize,
|
5
|
+
"initialize" => :initialize,
|
6
|
+
"new" => :initialize,
|
7
|
+
"create" => :initialize,
|
8
|
+
"add" => :add,
|
9
|
+
"insert" => :add,
|
10
|
+
"remove" => :remove,
|
11
|
+
"rm" => :remove,
|
12
|
+
"rmv" => :remove,
|
13
|
+
"drop" => :remove,
|
14
|
+
"delete" => :delete,
|
15
|
+
"del" => :delete,
|
16
|
+
"display" => :display,
|
17
|
+
"disp" => :display,
|
18
|
+
"reveal" => :display,
|
19
|
+
"show" => :display,
|
20
|
+
"view" => :display,
|
21
|
+
"help" => :help,
|
22
|
+
"h" => :help,
|
23
|
+
"rekey" => :rekey
|
24
|
+
}
|
25
|
+
|
26
|
+
class CommandLine
|
27
|
+
def initialize( args )
|
28
|
+
@args = args
|
29
|
+
@result = CommandLineResult.new( @args.dup ) # false shuts it up
|
30
|
+
|
31
|
+
parse_args()
|
32
|
+
|
33
|
+
@console = Console.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def verbose( verbosity = nil )
|
37
|
+
@verbose = verbosity unless verbosity.nil?
|
38
|
+
@result.verbose( @verbose )
|
39
|
+
!!@verbose
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
return @result if @result.failed?
|
44
|
+
|
45
|
+
case @action
|
46
|
+
when :initialize
|
47
|
+
if @file.exist?
|
48
|
+
@result.puts "File #{@file} already exists"
|
49
|
+
@result.fail!
|
50
|
+
return @result
|
51
|
+
end
|
52
|
+
|
53
|
+
get_master_key()
|
54
|
+
@keychain = Keychain.new( :keys => {}, :master_key => @master_key, :file => @file )
|
55
|
+
@keychain.save
|
56
|
+
@result.puts( "Saved new password file at #{@file}" )
|
57
|
+
when :add
|
58
|
+
unlock()
|
59
|
+
if @keychain[@key].nil? || Console.new.confirm( "Replace current password for key #{@key}?" )
|
60
|
+
@keychain[@key] = @pass
|
61
|
+
@keychain.save
|
62
|
+
end
|
63
|
+
when :remove
|
64
|
+
unlock()
|
65
|
+
pass = @keychain.delete( @key )
|
66
|
+
|
67
|
+
if pass.fwf_blank?
|
68
|
+
@result.puts "No such key as #{@key}. No action taken."
|
69
|
+
return @result
|
70
|
+
end
|
71
|
+
|
72
|
+
@keychain.save
|
73
|
+
when :display
|
74
|
+
unlock()
|
75
|
+
for key, pass in @keychain
|
76
|
+
@result.puts "#{key} : #{pass}"
|
77
|
+
end
|
78
|
+
when :rekey
|
79
|
+
unlock()
|
80
|
+
new_key = @new_master_key || Console.ask_for_asterisks( "Enter the NEW master key for #{@file}? " )
|
81
|
+
@keychain.save( new_key )
|
82
|
+
when :help
|
83
|
+
print_help
|
84
|
+
end
|
85
|
+
|
86
|
+
return @result
|
87
|
+
rescue OpenSSL::Cipher::CipherError => e
|
88
|
+
@result.stderr( "Cipher error, probably bad master password given. message: #{e.message}")
|
89
|
+
@result.fail!
|
90
|
+
@result
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
def parse_args()
|
96
|
+
if @args.length == 0
|
97
|
+
@action = :help
|
98
|
+
else
|
99
|
+
@action = ACTION_KEYWORDS_TO_ACTIONS[ (keyword = @args.shift) ]
|
100
|
+
|
101
|
+
|
102
|
+
case @action
|
103
|
+
when nil
|
104
|
+
@result.puts_error( "Unrecognized keyword #{keyword}" )
|
105
|
+
@result.fail!
|
106
|
+
print_help
|
107
|
+
when :add
|
108
|
+
@key, @pass = key_and_pass( @args.shift )
|
109
|
+
when :remove
|
110
|
+
@key = @args.shift
|
111
|
+
when :display
|
112
|
+
# do nothing
|
113
|
+
else
|
114
|
+
# do nothing
|
115
|
+
end
|
116
|
+
|
117
|
+
while @args.length > 0
|
118
|
+
arg = @args.shift
|
119
|
+
|
120
|
+
if m = /^--file=(.*)$/.match(arg)
|
121
|
+
@file = unquote( m[1] ).fwf_filepath.expand
|
122
|
+
elsif m = /--master=(.*)$/.match(arg)
|
123
|
+
@master_key = unquote( m[1] )
|
124
|
+
elsif m = /--new_master=(.*)$/.match(arg)
|
125
|
+
@new_master_key = unquote( m[1] )
|
126
|
+
elsif arg == "--quiet"
|
127
|
+
@verbose = false
|
128
|
+
@result.verbose( false )
|
129
|
+
else
|
130
|
+
@result.puts_error( "Unrecognized option #{arg}" )
|
131
|
+
@result.fail!
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
@file ||= FileStore.default_file
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
def unquote( str )
|
141
|
+
if str =~ /^(?<q>["']).*(?<q>)$/
|
142
|
+
str[1..-2]
|
143
|
+
else
|
144
|
+
str
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def print_help
|
149
|
+
@result.puts "\tfwpass new (opts) (create new password file)"
|
150
|
+
@result.puts "\tfwpass add <KEY>=<PASS> (opts) (add a key to an existing password file)"
|
151
|
+
@result.puts "\tfwpass rm <KEY> (opts) (remove a key from a password file)"
|
152
|
+
@result.puts "\tfwpass show (opts) (show contents of password file)"
|
153
|
+
@result.puts "\tfwpass rekey (opts) (swap the master key for a new one. must know the current key)"
|
154
|
+
@result.puts "\tOptions:"
|
155
|
+
@result.puts "\t\t--file=<PASSWORD_FILE> (default file: #{FileStore.default_file})"
|
156
|
+
@result.puts "\t\t--master=<PASSWORD> ('master key' to unlock the password file)"
|
157
|
+
@result.puts "\t\t--new_master=<PASSWORD> (the new 'master key' that will unlock the file hereafter) (option ignored except by rekey command)"
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_master_key
|
161
|
+
@master_key ||= @console.ask_for_master_key(@file)
|
162
|
+
end
|
163
|
+
|
164
|
+
def unlock
|
165
|
+
get_master_key()
|
166
|
+
@keychain = Keychain.new( :master_key => @master_key, :file => @file ).unlock
|
167
|
+
end
|
168
|
+
|
169
|
+
def key_and_pass( str )
|
170
|
+
# matches things like "key"='password' key="P4s5w0rd"
|
171
|
+
# regexp = /^(?<q1>["']?)(?<key>.*)(?<q1>)=(?<q2>["']?)(?<pass>.*)(?<q2>)$/
|
172
|
+
regexp = /^(?<key>(?<q1>["']?).*?(?<q1>))=(?<pass>(?<q2>["']?).*(?<q2>))$/ # wrong: reruns expression
|
173
|
+
regexp = /^(?<key>(?<q1>["']?).*?\k<q1>)=(?<pass>(?<q2>["']?).*\k<q2>)$/ # right: looks for the matched text
|
174
|
+
match_data = regexp.match( str )
|
175
|
+
|
176
|
+
if match_data
|
177
|
+
[unquote(match_data["key"]), unquote(match_data["pass"])]
|
178
|
+
else
|
179
|
+
[nil, nil]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FunWith
|
2
|
+
module Passwords
|
3
|
+
class CommandLineResult
|
4
|
+
attr_reader :success, :output, :errors, :args
|
5
|
+
|
6
|
+
def initialize( args )
|
7
|
+
@args = args
|
8
|
+
@errors = []
|
9
|
+
@output = ""
|
10
|
+
end
|
11
|
+
|
12
|
+
def stderr( msg )
|
13
|
+
STDERR.puts( msg ) if @verbose
|
14
|
+
@errors << msg
|
15
|
+
end
|
16
|
+
|
17
|
+
def stdout( msg )
|
18
|
+
STDOUT.write( msg ) if @verbose
|
19
|
+
@output << msg
|
20
|
+
end
|
21
|
+
|
22
|
+
def puts( msg )
|
23
|
+
stdout( msg + "\n" )
|
24
|
+
end
|
25
|
+
|
26
|
+
def puts_error( msg )
|
27
|
+
stderr( msg + "\n" )
|
28
|
+
end
|
29
|
+
|
30
|
+
def fail!
|
31
|
+
@success = false
|
32
|
+
end
|
33
|
+
|
34
|
+
def failed?
|
35
|
+
@success == false
|
36
|
+
end
|
37
|
+
|
38
|
+
def succeed!
|
39
|
+
@success = true
|
40
|
+
end
|
41
|
+
|
42
|
+
def success?
|
43
|
+
@success
|
44
|
+
end
|
45
|
+
|
46
|
+
def verbose( verbosity = nil )
|
47
|
+
@verbose = !!verbosity unless verbosity.nil?
|
48
|
+
@verbose
|
49
|
+
end
|
50
|
+
|
51
|
+
def verbose?
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module FunWith
|
2
|
+
module Passwords
|
3
|
+
class Console
|
4
|
+
attr_accessor :pre_inputs
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@pre_inputs = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def ask_for_password( key = nil )
|
11
|
+
key ||= ask( "Enter the key associated with the password: ")
|
12
|
+
pass = ask_for_asterisks( "Enter the password for key #{key}: " )
|
13
|
+
[key, pass]
|
14
|
+
end
|
15
|
+
|
16
|
+
def ask_for_master_key( file )
|
17
|
+
ask_for_asterisks( "Enter the master key to unlock #{file}: " )
|
18
|
+
end
|
19
|
+
|
20
|
+
def ask_for_new_master_key( file )
|
21
|
+
ask_for_asterisks( "Enter the NEW master key for #{file}: " )
|
22
|
+
end
|
23
|
+
|
24
|
+
def confirm( q )
|
25
|
+
ask( "#{q} (Y/N)").upcase == "Y"
|
26
|
+
end
|
27
|
+
|
28
|
+
def ask_for_asterisks( msg )
|
29
|
+
ask( msg ){ |q| q.echo = "*" }
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
def ask( *args, &block )
|
34
|
+
if @pre_inputs.fwf_blank?
|
35
|
+
HighLine.ask( *args, &block )
|
36
|
+
else
|
37
|
+
@pre_inputs.shift
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module FunWith
|
2
|
+
module Passwords
|
3
|
+
class Crypt
|
4
|
+
IV = "b2f7zaf14kagpd3j76ulddjxytvhjnzvna7diacozrui7afx4d7kj0cxj4ch1of1z7in376vah4kwkarwls0vbtosraovy7d4ci"
|
5
|
+
|
6
|
+
def self.decrypt( encrypted_message, key )
|
7
|
+
cipher = OpenSSL::Cipher::AES256.new( :CBC )
|
8
|
+
cipher.decrypt
|
9
|
+
cipher.key = self.stretch_key(key)
|
10
|
+
cipher.iv = IV
|
11
|
+
|
12
|
+
msg = cipher.update( encrypted_message )
|
13
|
+
msg << cipher.final
|
14
|
+
msg
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.encrypt( plaintext, key )
|
18
|
+
cipher = OpenSSL::Cipher::AES256.new( :CBC )
|
19
|
+
cipher.encrypt
|
20
|
+
cipher.key = self.stretch_key(key)
|
21
|
+
cipher.iv = IV
|
22
|
+
|
23
|
+
encrypted_message = cipher.update( plaintext )
|
24
|
+
encrypted_message << cipher.final
|
25
|
+
encrypted_message
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
protected
|
30
|
+
# Only advantage of doing this is lengthening short, insecure master passwords to
|
31
|
+
# randomish-looking key of length needed by the crypto cipher. Short passwords?
|
32
|
+
# Still insecure. Film at 11.
|
33
|
+
def self.stretch_key( key )
|
34
|
+
(Digest::MD5.hexdigest(key) + Digest::MD5.hexdigest(key.reverse) ).to_i(16).to_s(36)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module FunWith
|
2
|
+
module Passwords
|
3
|
+
|
4
|
+
# The FileStore knows only where to save itself, and how to encrypt
|
5
|
+
# and decrypt the file. The contents of a file are a mystery for
|
6
|
+
# Keychain to deal with
|
7
|
+
class FileStore
|
8
|
+
DEFAULT_DIR = XDG['CONFIG'].fwf_filepath.join("fun_with_passwords")
|
9
|
+
DEFAULT_EXT = "aes256.dat"
|
10
|
+
DEFAULT_FILENAME = "password_store"
|
11
|
+
|
12
|
+
attr_accessor :password_file, :key_chain
|
13
|
+
|
14
|
+
def self.default_file
|
15
|
+
DEFAULT_DIR.join( "#{DEFAULT_FILENAME}.#{DEFAULT_EXT}" )
|
16
|
+
end
|
17
|
+
|
18
|
+
# if the key is nil, waits until a password is requested or added to decrypt password file
|
19
|
+
def initialize( filename = nil )
|
20
|
+
@password_file = expand_filepath( filename )
|
21
|
+
@password_file.touch
|
22
|
+
end
|
23
|
+
|
24
|
+
# sends back a Keychain that knows where its store is
|
25
|
+
def unlock( master_key )
|
26
|
+
if @password_file.file? && !@password_file.empty?
|
27
|
+
YAML.load( Crypt.decrypt( @password_file.read, master_key ) )
|
28
|
+
else
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
#
|
33
|
+
# def initialize_key_chain_if_needed( keychain_keys, master_key )
|
34
|
+
# unless @key_chain
|
35
|
+
# @key_chain = Keychain.new( :keys => keychain_keys, :master_key => master_key )
|
36
|
+
# @key_chain.file_store = self
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
|
40
|
+
def save( yaml, master_key )
|
41
|
+
encrypted_message = Crypt.encrypt( yaml, master_key )
|
42
|
+
@password_file.write( encrypted_message )
|
43
|
+
true
|
44
|
+
rescue Exception => e
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
# Just the filename? Send it to the default directory.
|
50
|
+
# nil? Use the default filename and default directory.
|
51
|
+
# Only a directory given? append default filename
|
52
|
+
# It's an existing file? Overwrite, I suppose.
|
53
|
+
# doesn't exist? Assume filename if a dot is included
|
54
|
+
def expand_filepath( filename = nil )
|
55
|
+
if filename.nil?
|
56
|
+
return DEFAULT_DIR.join( "#{DEFAULT_FILENAME}.#{DEFAULT_EXT}" )
|
57
|
+
end
|
58
|
+
|
59
|
+
file = filename.fwf_filepath
|
60
|
+
|
61
|
+
if file.directory?
|
62
|
+
return file.join( "#{DEFAULT_FILENAME}.#{DEFAULT_EXT}" )
|
63
|
+
elsif file.file?
|
64
|
+
return file
|
65
|
+
end
|
66
|
+
|
67
|
+
file = file.expand
|
68
|
+
|
69
|
+
if file.directory?
|
70
|
+
return file.join( "#{DEFAULT_FILENAME}.#{DEFAULT_EXT}" )
|
71
|
+
else
|
72
|
+
file.touch
|
73
|
+
return file
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module FunWith
|
2
|
+
module Passwords
|
3
|
+
class Keychain
|
4
|
+
NAMESPACE_CHAR = ":"
|
5
|
+
|
6
|
+
attr_accessor :file_store
|
7
|
+
|
8
|
+
# Load an existing
|
9
|
+
def self.load( master_key, opts = {} )
|
10
|
+
file = opts[:file] || FileStore.default_file
|
11
|
+
master_key = Console.new.ask_for_master_key if master_key == :new
|
12
|
+
self.new( :file => file, :master_key => master_key ).unlock
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize( opts = {} )
|
16
|
+
@options = opts
|
17
|
+
set_keys
|
18
|
+
set_master_key
|
19
|
+
set_file_store
|
20
|
+
end
|
21
|
+
|
22
|
+
# if no new master key is given, save with the old one.
|
23
|
+
def save( master_key = nil)
|
24
|
+
@master_key = master_key if master_key
|
25
|
+
if @file_store
|
26
|
+
@file_store.save( @keys.to_yaml, @master_key )
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def unlock( master_key = nil )
|
33
|
+
@master_key = master_key if master_key
|
34
|
+
|
35
|
+
if @file_store
|
36
|
+
set_keys( @file_store.unlock( @master_key ) )
|
37
|
+
end
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_options
|
43
|
+
if @opts[:interactive]
|
44
|
+
@ask_on_fail = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# After @keys is set, subsequent set_keys(nil) calls have no effect.
|
49
|
+
def set_keys( hash = nil )
|
50
|
+
if hash
|
51
|
+
@keys = hash
|
52
|
+
else
|
53
|
+
@keys ||= @options[:keys] || {}
|
54
|
+
@options.delete(:keys)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_master_key( key = nil )
|
59
|
+
if key
|
60
|
+
@master_key = key
|
61
|
+
else
|
62
|
+
@master_key ||= @options.delete(:master_key)
|
63
|
+
@master_key ||= Console.new.ask_for_master_key if @ask_on_fail
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_file_store
|
68
|
+
@file_store = FileStore.new( @options[:file] ) if @options[:file]
|
69
|
+
end
|
70
|
+
|
71
|
+
def []=( key, password )
|
72
|
+
@keys[key] = password
|
73
|
+
end
|
74
|
+
|
75
|
+
def []( key )
|
76
|
+
password = @keys[key]
|
77
|
+
|
78
|
+
if !password && @ask_on_fail
|
79
|
+
password = Console.new.ask_for_password( key )
|
80
|
+
self[key] = password
|
81
|
+
end
|
82
|
+
|
83
|
+
password
|
84
|
+
end
|
85
|
+
|
86
|
+
def each( &block )
|
87
|
+
if block_given?
|
88
|
+
@keys.each(&block)
|
89
|
+
else
|
90
|
+
@keys.each
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def delete( key )
|
95
|
+
@keys.delete( key )
|
96
|
+
end
|
97
|
+
|
98
|
+
def printout
|
99
|
+
self.each do |key, password|
|
100
|
+
puts "#{key}: #{password}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'fun_with_files'
|
2
|
+
require 'fun_with_configurations'
|
3
|
+
require 'fun_with_version_strings'
|
4
|
+
require 'highline/import'
|
5
|
+
require 'xdg'
|
6
|
+
require 'openssl'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module FunWith
|
10
|
+
module Passwords
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
FunWith::Files::RootPath.rootify( FunWith::Passwords, __FILE__.fwf_filepath.dirname.up )
|
15
|
+
FunWith::VersionStrings.versionize( FunWith::Passwords )
|
16
|
+
|
17
|
+
FunWith::Passwords.root( "lib","fun_with" ).requir
|
18
|
+
|
19
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
k=v|k|v
|
2
|
+
key=value|key|value
|
3
|
+
'key'='value'|key|value
|
4
|
+
key='value'|key|value
|
5
|
+
key="value"|key|value
|
6
|
+
"key"=value|key|value
|
7
|
+
"key"='value'|key|value
|
8
|
+
"site=flow.xmission.com"='xjJ3~=""'|site=flow.xmission.com|xjJ3~=""
|
9
|
+
godzilla:v:tokyo="&&^32='*h<>!="|godzilla:v:tokyo|&&^32='*h<>!=
|
data/test/helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'debugger'
|
4
|
+
begin
|
5
|
+
Bundler.setup(:default, :development)
|
6
|
+
rescue Bundler::BundlerError => e
|
7
|
+
$stderr.puts e.message
|
8
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
9
|
+
exit e.status_code
|
10
|
+
end
|
11
|
+
require 'test/unit'
|
12
|
+
require 'shoulda'
|
13
|
+
|
14
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
15
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
16
|
+
|
17
|
+
require 'fun_with_passwords'
|
18
|
+
|
19
|
+
class Test::Unit::TestCase
|
20
|
+
end
|
21
|
+
|
22
|
+
class FunWith::Passwords::TestCase < Test::Unit::TestCase
|
23
|
+
include FunWith::Passwords
|
24
|
+
def tmpdir( &block )
|
25
|
+
FunWith::Files::FilePath.tmpdir do |tmp|
|
26
|
+
@tmpdir = tmp
|
27
|
+
warn( "temporary dir not writable. Some tests may fail." ) unless @tmpdir.directory? && @tmpdir.writable?
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestCommandLine < FunWith::Passwords::TestCase
|
4
|
+
should "test basics" do
|
5
|
+
tmpdir do
|
6
|
+
file = @tmpdir.join( 'passwords.aes256.dat' )
|
7
|
+
file_arg = "--file=#{file}"
|
8
|
+
master_arg = "--master=pass1"
|
9
|
+
master_arg2 = "--master=pass2"
|
10
|
+
new_master_arg = "--new_master=pass2"
|
11
|
+
bad_password = "--master=wrong"
|
12
|
+
refute file.exist?
|
13
|
+
# how to simulate user entering password...
|
14
|
+
run_console( ["new", file_arg, master_arg, "--quiet"] )
|
15
|
+
assert file.exist?
|
16
|
+
|
17
|
+
run_console( ["add", "site1:mysql=pass1", file_arg, master_arg, "--quiet"] )
|
18
|
+
run_console( ["add", "site2:mysql=pass2", file_arg, master_arg, "--quiet"] )
|
19
|
+
|
20
|
+
run_console( ["show", file_arg, master_arg, "--quiet"] ) do |result|
|
21
|
+
assert_equal "site1:mysql : pass1\nsite2:mysql : pass2", result.output.strip
|
22
|
+
end
|
23
|
+
|
24
|
+
run_console( ["rekey", file_arg, master_arg, new_master_arg])
|
25
|
+
|
26
|
+
run_console( ["rm", "site1:mysql", file_arg, master_arg2, "--quiet"] )
|
27
|
+
run_console( ["rm", "site2:mysql", file_arg, master_arg2, "--quiet"] )
|
28
|
+
|
29
|
+
run_console( ["rm", "site2:mysql", file_arg, bad_password, "--quiet"] ) do |result|
|
30
|
+
assert_match /Cipher error/, result.errors.first
|
31
|
+
refute result.success?
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# puts "\tfwpass new <OPTIONAL_FILENAME> (create new password file)"
|
36
|
+
# puts "\tfwpass add key=password <OPTIONAL_FILENAME> (add a key to an existing password file)"
|
37
|
+
# puts "\tfwpass rm key <OPTIONAL_FILENAME> (remove a key from a password file)"
|
38
|
+
# puts "\tfwpass show <OPTIONAL_FILENAME> (show contents of password file)"
|
39
|
+
# puts "\tfwpass rekey <OPTIONAL_FILENAME> (swap the master key for a new one. must know the current key)"
|
40
|
+
# puts "\t(default file: #{FileStore.default_file})"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
should "properly parse key=value pairs" do
|
45
|
+
successes = FunWith::Passwords.root("test", "data", "kvpair_successes.txt").read.split("\n")
|
46
|
+
|
47
|
+
cmd = CommandLine.new(["help"])
|
48
|
+
|
49
|
+
for line in successes
|
50
|
+
str, expected_key, expected_pass = line.split("|")
|
51
|
+
|
52
|
+
actual_key, actual_pass = cmd.send( :key_and_pass, str )
|
53
|
+
|
54
|
+
refute actual_key.nil?
|
55
|
+
refute actual_pass.nil?
|
56
|
+
refute actual_key.fwf_blank?
|
57
|
+
refute actual_pass.fwf_blank?
|
58
|
+
|
59
|
+
assert_equal( expected_key, actual_key )
|
60
|
+
assert_equal( expected_pass, actual_pass )
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
def run_console( args, &block )
|
68
|
+
result = CommandLine.new( args ).run
|
69
|
+
|
70
|
+
if block_given?
|
71
|
+
yield result
|
72
|
+
else
|
73
|
+
result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/test/test_crypt.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestCrypt < FunWith::Passwords::TestCase
|
4
|
+
should "encrypt and decrypt using Crypt functions" do
|
5
|
+
message = "secret message"
|
6
|
+
key = "secret key"
|
7
|
+
|
8
|
+
encrypted_message = Crypt.encrypt( message, key )
|
9
|
+
assert_not_equal( message, encrypted_message )
|
10
|
+
decrypted_message = Crypt.decrypt( encrypted_message, key )
|
11
|
+
assert_equal( message, decrypted_message )
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestFunWithPasswords < FunWith::Passwords::TestCase
|
4
|
+
should "test basics" do
|
5
|
+
assert defined?( FunWith::Passwords ) # utterly useless test
|
6
|
+
assert defined?( FunWith::Passwords::Console )
|
7
|
+
assert defined?( FunWith::Passwords::Crypt )
|
8
|
+
end
|
9
|
+
|
10
|
+
should "test fun_with_files integrating properly" do
|
11
|
+
assert defined?(FunWith::Files)
|
12
|
+
assert defined?(FunWith::Files::FilePath)
|
13
|
+
assert [].fwf_blank?
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestKeychain < FunWith::Passwords::TestCase
|
4
|
+
KEYHASH = { "hey:sailor" => "password",
|
5
|
+
"orangutan" => "marigold",
|
6
|
+
"hitch:hiker:steve" => "hoboname"
|
7
|
+
}
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@keychain = Keychain.new( :keys => KEYHASH.clone, :master_key => "master_key" )
|
11
|
+
end
|
12
|
+
|
13
|
+
should "get the correct keys" do
|
14
|
+
assert_equal( "password", @keychain["hey:sailor"] )
|
15
|
+
assert_equal( "marigold", @keychain["orangutan"] )
|
16
|
+
end
|
17
|
+
|
18
|
+
should "update a key" do
|
19
|
+
@keychain["orangutan"] = "password_orangutan"
|
20
|
+
@keychain["hey:sailor"] = "password_sailor"
|
21
|
+
|
22
|
+
assert_equal( "password_orangutan", @keychain["orangutan"] )
|
23
|
+
assert_equal( "password_sailor", @keychain["hey:sailor"] )
|
24
|
+
assert_equal( @keychain["hitch:hiker:steve"], "hoboname")
|
25
|
+
end
|
26
|
+
|
27
|
+
should "test each(){}" do
|
28
|
+
expected = [["hey:sailor", "password"], ["orangutan", "marigold"], ["hitch:hiker:steve", "hoboname"]]
|
29
|
+
|
30
|
+
i = 0
|
31
|
+
@keychain.each do |item|
|
32
|
+
assert_equal( expected[i], item )
|
33
|
+
i += 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
should "write passwords to a file and read them back" do
|
38
|
+
tmpdir do
|
39
|
+
assert @keychain.file_store.nil?
|
40
|
+
pwfile = @tmpdir.join("password_store.aes256.dat")
|
41
|
+
assert !pwfile.exist?
|
42
|
+
|
43
|
+
@keychain.file_store = FileStore.new(@tmpdir)
|
44
|
+
assert_equal( pwfile, @keychain.file_store.password_file )
|
45
|
+
|
46
|
+
@keychain.save
|
47
|
+
assert @keychain.file_store.password_file.exist?
|
48
|
+
|
49
|
+
@keychain = Keychain.load( "master_key", :file => pwfile )
|
50
|
+
@keychain.unlock
|
51
|
+
|
52
|
+
assert_equal( "password", @keychain["hey:sailor"] )
|
53
|
+
assert_equal( "marigold", @keychain["orangutan"] )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fun_with_passwords
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bryce Anderson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fun_with_files
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fun_with_configurations
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fun_with_version_strings
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: highline
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.6'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: xdg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.2'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: shoulda
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.5'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rdoc
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.12'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.12'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: bundler
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.5'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.5'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: jeweler
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ~>
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '2.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '2.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: debugger
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ~>
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.6'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ~>
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '1.6'
|
153
|
+
description: Because passwords are obnoxious.
|
154
|
+
email: keeputahweird@gmail.com
|
155
|
+
executables:
|
156
|
+
- fwpass
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files:
|
159
|
+
- LICENSE.txt
|
160
|
+
- README.rdoc
|
161
|
+
files:
|
162
|
+
- .document
|
163
|
+
- Gemfile
|
164
|
+
- LICENSE.txt
|
165
|
+
- README.rdoc
|
166
|
+
- Rakefile
|
167
|
+
- VERSION
|
168
|
+
- bin/fwpass
|
169
|
+
- lib/fun_with/passwords/command_line.rb
|
170
|
+
- lib/fun_with/passwords/command_line_result.rb
|
171
|
+
- lib/fun_with/passwords/console.rb
|
172
|
+
- lib/fun_with/passwords/crypt.rb
|
173
|
+
- lib/fun_with/passwords/file_store.rb
|
174
|
+
- lib/fun_with/passwords/keychain.rb
|
175
|
+
- lib/fun_with/passwords/password.rb
|
176
|
+
- lib/fun_with_passwords.rb
|
177
|
+
- test/data/kvpair_successes.txt
|
178
|
+
- test/helper.rb
|
179
|
+
- test/test_command_line.rb
|
180
|
+
- test/test_crypt.rb
|
181
|
+
- test/test_fun_with_passwords.rb
|
182
|
+
- test/test_keychain.rb
|
183
|
+
homepage: http://github.com/darthschmoo/fun_with_passwords
|
184
|
+
licenses:
|
185
|
+
- MIT
|
186
|
+
metadata: {}
|
187
|
+
post_install_message:
|
188
|
+
rdoc_options: []
|
189
|
+
require_paths:
|
190
|
+
- lib
|
191
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ! '>='
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
196
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ! '>='
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
requirements: []
|
202
|
+
rubyforge_project:
|
203
|
+
rubygems_version: 2.2.2
|
204
|
+
signing_key:
|
205
|
+
specification_version: 4
|
206
|
+
summary: Create an encrypted password store that your scripts can access.
|
207
|
+
test_files: []
|