safe 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +154 -0
- data/bin/safe.rb +110 -0
- data/lib/safeentry.rb +53 -0
- data/lib/safefile.rb +212 -0
- data/lib/safeutils.rb +76 -0
- data/test/tc_safeentry.rb +37 -0
- data/test/tc_safefile.rb +120 -0
- data/test/tc_safeutils.rb +45 -0
- metadata +68 -0
data/README
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
=== Safe ===
|
2
|
+
Safe is a command line password management program. It securely stores all your
|
3
|
+
user IDs and passwords using a single password as the master key. It stores the
|
4
|
+
master key as a hash (SHA2), and uses it to encrypt the rest of the data using
|
5
|
+
Blowfish.
|
6
|
+
|
7
|
+
== License ==
|
8
|
+
Safe is licensed under the BSD License. Copyright (c) 2007, Rob Warner
|
9
|
+
|
10
|
+
All rights reserved.
|
11
|
+
|
12
|
+
Redistribution and use in source and binary forms, with or without modification,
|
13
|
+
are permitted provided that the following conditions are met:
|
14
|
+
|
15
|
+
* Redistributions of source code must retain the above copyright notice,
|
16
|
+
this list of conditions and the following disclaimer.
|
17
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
18
|
+
this list of conditions and the following disclaimer in the documentation
|
19
|
+
and/or other materials provided with the distribution.
|
20
|
+
* Neither the name of Rob Warner nor the names of its contributors may be
|
21
|
+
used to endorse or promote products derived from this software without
|
22
|
+
specific prior written permission.
|
23
|
+
|
24
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
25
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
26
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
27
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
28
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
29
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
30
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
31
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
32
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
33
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
34
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
35
|
+
|
36
|
+
== Installation ==
|
37
|
+
To install safe, you must have Ruby and Ruby Gems installed. From the command
|
38
|
+
line, type:
|
39
|
+
|
40
|
+
gem install safe --include-dependencies
|
41
|
+
|
42
|
+
safe depends on two gems:
|
43
|
+
|
44
|
+
1) crypt
|
45
|
+
2) highline
|
46
|
+
|
47
|
+
Also, you must set up an environment variable called SAFE_DIR that points to the
|
48
|
+
directory in which your encrypted password file (called .safe.xml) will be
|
49
|
+
stored. Examples:
|
50
|
+
|
51
|
+
Linux/Mac OS X:
|
52
|
+
export SAFE_DIR=~
|
53
|
+
|
54
|
+
Windows:
|
55
|
+
set SAFE_DIR=c:\data
|
56
|
+
|
57
|
+
== Version History ==
|
58
|
+
0.1 -- May 12, 2007
|
59
|
+
0.2 -- August 19, 2007
|
60
|
+
|
61
|
+
See Release-Notes.txt for more information
|
62
|
+
|
63
|
+
== Usage ==
|
64
|
+
Run safe by typing:
|
65
|
+
|
66
|
+
safe.rb
|
67
|
+
|
68
|
+
The first time you run safe, you will be prompted for a password, and a new
|
69
|
+
password file (.safe.xml) will be created in the directory specified by
|
70
|
+
SAFE_DIR, like this:
|
71
|
+
|
72
|
+
$ safe.rb
|
73
|
+
Password: ****
|
74
|
+
Creating new file . . .
|
75
|
+
|
76
|
+
If you have not set up the SAFE_DIR environment variable, you will see this
|
77
|
+
message:
|
78
|
+
|
79
|
+
Set environment variable SAFE_DIR to the directory for your safe file
|
80
|
+
|
81
|
+
To correct, set the SAFE_DIR environment variable.
|
82
|
+
|
83
|
+
If you have set up SAFE_DIR to point to a non-existent directory, you will see
|
84
|
+
this message:
|
85
|
+
|
86
|
+
Environment variable SAFE_DIR does not point to an existing directory
|
87
|
+
(/home/foo)
|
88
|
+
|
89
|
+
where "/home/foo" is the value of SAFE_DIR. To correct, point SAFE_DIR to an
|
90
|
+
existing directory.
|
91
|
+
|
92
|
+
If you type:
|
93
|
+
|
94
|
+
safe.rb -h
|
95
|
+
|
96
|
+
the available options display. They are:
|
97
|
+
|
98
|
+
Usage: safe.rb [options]
|
99
|
+
|
100
|
+
Options:
|
101
|
+
-a, --add NAME Add an entry
|
102
|
+
-c, --count Print the count of entries
|
103
|
+
-d, --delete NAME Delete an entry
|
104
|
+
-i, --import FILE Import a file in <name,ID,password> format
|
105
|
+
-l, --list NAME List an entry
|
106
|
+
-v, --version Print version
|
107
|
+
-p, --password Change password
|
108
|
+
|
109
|
+
For example, to add an entry to your password file for Rubyforge.org, you type:
|
110
|
+
|
111
|
+
safe.rb -a Rubyforge.org
|
112
|
+
|
113
|
+
You will be prompted for your master password, and then your Rubyforge.org
|
114
|
+
user ID and password, like this:
|
115
|
+
|
116
|
+
Password: ****
|
117
|
+
Rubyforge.org User ID: myuserid
|
118
|
+
Rubyforge.org Password: ****
|
119
|
+
|
120
|
+
NOTE: If NAME contains spaces, you must surround it in quotes, like this:
|
121
|
+
|
122
|
+
safe.rb -a "A Cool Site"
|
123
|
+
|
124
|
+
To list your Rubyforge.org password, you type:
|
125
|
+
|
126
|
+
safe.rb -l Rubyforge.org
|
127
|
+
|
128
|
+
You will be prompted for your master password, and then your Rubyforge.org
|
129
|
+
user ID and password will display, like this:
|
130
|
+
|
131
|
+
Rubyforge.org myuserid mypassword
|
132
|
+
|
133
|
+
NOTE: The NAME you pass to -l is not case sensitive, and will find all entries
|
134
|
+
beginning with NAME. For example, typing:
|
135
|
+
|
136
|
+
safe.rb -l r
|
137
|
+
|
138
|
+
will list all entries beginning with "r" or "R."
|
139
|
+
|
140
|
+
To delete your Rubyforge.org password, you type:
|
141
|
+
|
142
|
+
safe.rb -d Rubyforge.org
|
143
|
+
|
144
|
+
You will be prompted for your master password, and then your Rubyforge.org
|
145
|
+
password is irretrievably deleted. You will not be asked to confirm the
|
146
|
+
deletion.
|
147
|
+
|
148
|
+
== Backups ==
|
149
|
+
Please back up your .safe.xml file! See License section for disclaimer
|
150
|
+
information.
|
151
|
+
|
152
|
+
== Contact ==
|
153
|
+
You can contact me, Rob Warner, at rwarner@grailbox.com.
|
154
|
+
|
data/bin/safe.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
########################################################################
|
4
|
+
# safe: Safe Storage for Passwords.
|
5
|
+
# Stores user IDs and passwords, encrypted by a password.
|
6
|
+
#
|
7
|
+
# Copyright (c) 2007, Rob Warner
|
8
|
+
#
|
9
|
+
# All rights reserved.
|
10
|
+
#
|
11
|
+
# Redistribution and use in source and binary forms, with or without
|
12
|
+
# modification, are permitted provided that the following conditions are met:
|
13
|
+
#
|
14
|
+
# * Redistributions of source code must retain the above copyright notice,
|
15
|
+
# this list of conditions and the following disclaimer.
|
16
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
17
|
+
# this list of conditions and the following disclaimer in the documentation
|
18
|
+
# and/or other materials provided with the distribution.
|
19
|
+
# * Neither the name of Rob Warner nor the names of its contributors may be
|
20
|
+
# used to endorse or promote products derived from this software without
|
21
|
+
# specific prior written permission.
|
22
|
+
#
|
23
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
24
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
25
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
26
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
27
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
28
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
29
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
30
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
31
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
32
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
33
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
########################################################################
|
35
|
+
require 'rubygems'
|
36
|
+
require 'optparse'
|
37
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safeentry')
|
38
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safefile')
|
39
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safeutils')
|
40
|
+
|
41
|
+
# TODO support list <NAME> without -l flag
|
42
|
+
|
43
|
+
class Safe
|
44
|
+
SAFE_VERSION = '0.2'
|
45
|
+
|
46
|
+
# Gets the command line
|
47
|
+
def self.get_command_line
|
48
|
+
options = {}
|
49
|
+
options[:action] = 'list'
|
50
|
+
opts = OptionParser.new
|
51
|
+
opts.banner = 'Usage: safe.rb [options]'
|
52
|
+
opts.separator ''
|
53
|
+
opts.separator 'Options:'
|
54
|
+
opts.on('-a', '--add NAME', 'Add an entry') do |val|
|
55
|
+
options[:name] = val
|
56
|
+
options[:action] = 'add'
|
57
|
+
end
|
58
|
+
opts.on('-c', '--count', 'Print the count of entries') do
|
59
|
+
options[:action] = 'count'
|
60
|
+
end
|
61
|
+
opts.on('-d', '--delete NAME', 'Delete an entry') do |val|
|
62
|
+
options[:name] = val
|
63
|
+
options[:action] = 'delete'
|
64
|
+
end
|
65
|
+
opts.on('-i', '--import FILE', 'Import a file in <name,ID,password> format') do |val|
|
66
|
+
options[:name] = val
|
67
|
+
options[:action] = 'import'
|
68
|
+
end
|
69
|
+
opts.on('-l', '--list NAME', 'List an entry') do |val|
|
70
|
+
options[:name] = val
|
71
|
+
options[:action] = 'list'
|
72
|
+
end
|
73
|
+
opts.on('-v', '--version', 'Print version') do
|
74
|
+
options[:action] = 'version'
|
75
|
+
end
|
76
|
+
opts.on('-p', '--password', 'Change password') do
|
77
|
+
options[:action] = 'password'
|
78
|
+
end
|
79
|
+
opts.parse! rescue abort [$!.message, opts].join("\n")
|
80
|
+
options
|
81
|
+
end
|
82
|
+
|
83
|
+
# Main
|
84
|
+
def self.run
|
85
|
+
options = get_command_line
|
86
|
+
if options[:action] == 'version'
|
87
|
+
puts "Safe version #{SAFE_VERSION}, Copyright (C) 2007 Rob Warner"
|
88
|
+
else
|
89
|
+
begin
|
90
|
+
dir = SafeUtils::get_safe_dir
|
91
|
+
rescue
|
92
|
+
abort $!.message
|
93
|
+
end
|
94
|
+
password = SafeUtils::get_password
|
95
|
+
file = SafeFile.new(password, dir)
|
96
|
+
|
97
|
+
# Perform the requested action
|
98
|
+
case options[:action]
|
99
|
+
when 'list': file.list(options[:name])
|
100
|
+
when 'add': file.add(options[:name])
|
101
|
+
when 'delete': file.delete(options[:name])
|
102
|
+
when 'count': puts file.count
|
103
|
+
when 'password': file.change_password
|
104
|
+
when 'import': SafeUtils::import(file, options[:name])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
Safe.run
|
data/lib/safeentry.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
########################################################################
|
2
|
+
# safeentry.rb
|
3
|
+
# Copyright (c) 2007, Rob Warner
|
4
|
+
#
|
5
|
+
# All rights reserved.
|
6
|
+
#
|
7
|
+
# Redistribution and use in source and binary forms, with or without
|
8
|
+
# modification, are permitted provided that the following conditions are met:
|
9
|
+
#
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer.
|
12
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
14
|
+
# and/or other materials provided with the distribution.
|
15
|
+
# * Neither the name of Rob Warner nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
20
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
21
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
22
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
########################################################################
|
31
|
+
|
32
|
+
##
|
33
|
+
# SafeEntry
|
34
|
+
# A single entry (name, ID, password) in the "safe"
|
35
|
+
##
|
36
|
+
class SafeEntry
|
37
|
+
attr_accessor :name, :id, :password
|
38
|
+
|
39
|
+
def initialize(name, id, password)
|
40
|
+
@name = name
|
41
|
+
@id = id
|
42
|
+
@password = password
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"#@name\t#@id\t#@password"
|
47
|
+
end
|
48
|
+
|
49
|
+
def <=>(other)
|
50
|
+
self.name.upcase <=> other.name.upcase
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
data/lib/safefile.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
########################################################################
|
2
|
+
# safefile.rb
|
3
|
+
# Copyright (c) 2007, Rob Warner
|
4
|
+
#
|
5
|
+
# All rights reserved.
|
6
|
+
#
|
7
|
+
# Redistribution and use in source and binary forms, with or without
|
8
|
+
# modification, are permitted provided that the following conditions are met:
|
9
|
+
#
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer.
|
12
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
14
|
+
# and/or other materials provided with the distribution.
|
15
|
+
# * Neither the name of Rob Warner nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
20
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
21
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
22
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
########################################################################
|
31
|
+
require 'rubygems'
|
32
|
+
require 'crypt/blowfish'
|
33
|
+
require 'digest/sha2'
|
34
|
+
require 'rexml/document'
|
35
|
+
require 'fileutils'
|
36
|
+
require 'base64'
|
37
|
+
|
38
|
+
include REXML
|
39
|
+
|
40
|
+
##
|
41
|
+
# SafeFile
|
42
|
+
# The file containing the safe entries
|
43
|
+
##
|
44
|
+
class SafeFile
|
45
|
+
attr_accessor :hash, :salt, :password, :filename, :entries
|
46
|
+
|
47
|
+
def initialize(password, dir, filename = '.safe.xml')
|
48
|
+
@entries = Hash.new
|
49
|
+
@filename = "#{dir}/#{filename}"
|
50
|
+
@password = password
|
51
|
+
load
|
52
|
+
end
|
53
|
+
|
54
|
+
# Loads the file
|
55
|
+
def load
|
56
|
+
f = File.open(@filename, File::CREAT)
|
57
|
+
begin
|
58
|
+
if f.lstat.size == 0
|
59
|
+
create_file(f)
|
60
|
+
else
|
61
|
+
decrypt_file(f)
|
62
|
+
end
|
63
|
+
rescue
|
64
|
+
# TODO Determine whether error is can't create file, and print appropriate error
|
65
|
+
puts $!
|
66
|
+
ensure
|
67
|
+
f.close unless f.nil?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Decrypts the file
|
72
|
+
def decrypt_file(file)
|
73
|
+
begin
|
74
|
+
doc = Document.new file
|
75
|
+
root = doc.root
|
76
|
+
@hash = root.elements["hash"].text
|
77
|
+
@salt = root.elements["salt"].text
|
78
|
+
if authorized?
|
79
|
+
crypt_key = Crypt::Blowfish.new(@password)
|
80
|
+
e_entries = root.elements["entries"]
|
81
|
+
e_entries.elements.each("entry") do |entry|
|
82
|
+
fields = crypt_key.decrypt_string(Base64::decode64(entry.cdatas[0].to_s)).split("\t")
|
83
|
+
if fields.length == 3
|
84
|
+
insert(fields[0], fields[1], fields[2])
|
85
|
+
else
|
86
|
+
puts "Cannot parse #{fields}, discarding."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
else
|
90
|
+
abort('The password you entered is not valid')
|
91
|
+
end
|
92
|
+
rescue ParseException
|
93
|
+
puts "Cannot parse #{file.path}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Creates a new file
|
98
|
+
def create_file(file)
|
99
|
+
puts 'Creating new file . . .'
|
100
|
+
generate_hash_and_salt
|
101
|
+
save
|
102
|
+
end
|
103
|
+
|
104
|
+
# Saves the file
|
105
|
+
def save
|
106
|
+
crypt_key = Crypt::Blowfish.new(@password)
|
107
|
+
doc = Document.new
|
108
|
+
root = Element.new "safe"
|
109
|
+
doc.add_element root
|
110
|
+
|
111
|
+
root.add_element create_element("hash", @hash)
|
112
|
+
root.add_element create_element("salt", @salt)
|
113
|
+
|
114
|
+
e_entries = Element.new "entries"
|
115
|
+
@entries.each_value do |entry|
|
116
|
+
e = Element.new "entry"
|
117
|
+
CData.new Base64.encode64(crypt_key.encrypt_string(entry.to_s)), true, e
|
118
|
+
e_entries.add_element e
|
119
|
+
end
|
120
|
+
root.add_element e_entries
|
121
|
+
|
122
|
+
f = File.new(@filename, 'w')
|
123
|
+
doc.write f, 0
|
124
|
+
f.close
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds an entry
|
128
|
+
def add(name)
|
129
|
+
if can_insert? name
|
130
|
+
print "#{name} User ID: "
|
131
|
+
id = gets.chomp!
|
132
|
+
pw = SafeUtils::get_password("#{name} Password: ")
|
133
|
+
insert(name, id, pw)
|
134
|
+
save
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Inserts an entry
|
139
|
+
def insert(name, id, pw)
|
140
|
+
@entries.delete(name)
|
141
|
+
@entries[name] = SafeEntry.new(name, id, pw)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Determines whether we can insert this entry--if it exists, we prompt for the OK
|
145
|
+
def can_insert?(name)
|
146
|
+
proceed = true
|
147
|
+
if @entries.has_key?(name)
|
148
|
+
print "Overwrite existing #{name} (y/N)? "
|
149
|
+
proceed = (gets.chomp == 'y')
|
150
|
+
end
|
151
|
+
proceed
|
152
|
+
end
|
153
|
+
|
154
|
+
# Deletes an entry
|
155
|
+
def delete(name)
|
156
|
+
if @entries.has_key?(name)
|
157
|
+
@entries.delete(name)
|
158
|
+
save
|
159
|
+
else
|
160
|
+
puts "Entry #{name} not found"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Lists the desired entries
|
165
|
+
def list(name)
|
166
|
+
output = Array.new
|
167
|
+
@entries.each_value do |entry|
|
168
|
+
# TODO Inconsistent case handling
|
169
|
+
if name == nil || entry.name.upcase =~ /^#{name.upcase}/
|
170
|
+
output << entry
|
171
|
+
end
|
172
|
+
end
|
173
|
+
output.sort.each do |entry|
|
174
|
+
puts entry
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns the count of entries in the file
|
179
|
+
def count()
|
180
|
+
@entries.length
|
181
|
+
end
|
182
|
+
|
183
|
+
# Changes the file's password
|
184
|
+
def change_password
|
185
|
+
new_password = SafeUtils::get_password('Enter new password: ')
|
186
|
+
rpt_password = SafeUtils::get_password('Confirm password: ')
|
187
|
+
if new_password == rpt_password
|
188
|
+
@password = new_password
|
189
|
+
generate_hash_and_salt
|
190
|
+
save
|
191
|
+
else
|
192
|
+
puts 'Passwords do not match'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def generate_hash_and_salt
|
197
|
+
@salt = [Array.new(20){rand(256).chr}.join].pack('m').chomp
|
198
|
+
@hash = Digest::SHA256.hexdigest(@password + @salt)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns whether the password is authorized
|
202
|
+
def authorized?
|
203
|
+
Digest::SHA256.hexdigest(@password + @salt) == @hash
|
204
|
+
end
|
205
|
+
|
206
|
+
# Helper method to create an XML element with a name and text
|
207
|
+
def create_element(name, text)
|
208
|
+
e = Element.new name
|
209
|
+
e.text = text
|
210
|
+
e
|
211
|
+
end
|
212
|
+
end
|
data/lib/safeutils.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
########################################################################
|
2
|
+
# safeutils.rb
|
3
|
+
# Copyright (c) 2007, Rob Warner
|
4
|
+
#
|
5
|
+
# All rights reserved.
|
6
|
+
#
|
7
|
+
# Redistribution and use in source and binary forms, with or without
|
8
|
+
# modification, are permitted provided that the following conditions are met:
|
9
|
+
#
|
10
|
+
# * Redistributions of source code must retain the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer.
|
12
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
14
|
+
# and/or other materials provided with the distribution.
|
15
|
+
# * Neither the name of Rob Warner nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
20
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
21
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
22
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
23
|
+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
24
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
25
|
+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
########################################################################
|
31
|
+
require 'highline/import'
|
32
|
+
|
33
|
+
##
|
34
|
+
# SafeUtils
|
35
|
+
# Utility methods for the Safe application
|
36
|
+
##
|
37
|
+
class SafeUtils
|
38
|
+
# Gets the directory for the data file
|
39
|
+
def SafeUtils.get_safe_dir(var = 'SAFE_DIR')
|
40
|
+
dir = ENV[var]
|
41
|
+
raise "Set environment variable #{var} to the directory for your safe file" unless dir
|
42
|
+
begin
|
43
|
+
d = Dir.new(dir)
|
44
|
+
rescue
|
45
|
+
raise "Environment variable #{var} does not point to an existing directory (#{dir})"
|
46
|
+
end
|
47
|
+
dir
|
48
|
+
end
|
49
|
+
|
50
|
+
# Prompts the user for a password
|
51
|
+
def SafeUtils.get_password(prompt = 'Password: ', mask = '*')
|
52
|
+
ask(prompt) { |q| q.echo = mask }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Imports a file in the format:
|
56
|
+
# name,ID,password
|
57
|
+
# name,ID,password
|
58
|
+
# etc.
|
59
|
+
def SafeUtils.import(safe_file, import_file)
|
60
|
+
begin
|
61
|
+
open(import_file).each do |line|
|
62
|
+
puts "Importing #{line}"
|
63
|
+
fields = line.split(',')
|
64
|
+
if fields.length == 3
|
65
|
+
safe_file.insert(fields[0], fields[1], fields[2])
|
66
|
+
else
|
67
|
+
puts "Cannot import; number of fields = #{fields.length}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
safe_file.save
|
71
|
+
rescue
|
72
|
+
puts $!
|
73
|
+
raise "Problem importing file #{import_file}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
########################################################################
|
2
|
+
# tc_safeentry.rb
|
3
|
+
# Copyright (c) 2007 Rob Warner.
|
4
|
+
# All rights reserved. This program and the accompanying materials
|
5
|
+
# are made available under the terms of the Eclipse Public License v1.0
|
6
|
+
# which accompanies this distribution, and is available at
|
7
|
+
# http://www.eclipse.org/legal/epl-v10.html
|
8
|
+
########################################################################
|
9
|
+
|
10
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safeentry')
|
11
|
+
require 'test/unit'
|
12
|
+
|
13
|
+
class SafeDirTest < Test::Unit::TestCase
|
14
|
+
def test_to_s
|
15
|
+
se = SafeEntry.new('name', 'id', 'password')
|
16
|
+
assert_equal "name\tid\tpassword", se.to_s
|
17
|
+
|
18
|
+
se = SafeEntry.new('', '', '')
|
19
|
+
assert_equal "\t\t", se.to_s
|
20
|
+
|
21
|
+
se = SafeEntry.new(' ', ' ', ' ')
|
22
|
+
assert_equal " \t \t ", se.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_sort
|
26
|
+
test = Array.new
|
27
|
+
test << SafeEntry.new('apple', 'baker', 'charlie')
|
28
|
+
test << SafeEntry.new('zoo', 'yacht', 'x-ray')
|
29
|
+
test << SafeEntry.new('aardvark', 'kangaroo', 'wallaby')
|
30
|
+
test << SafeEntry.new('123', '456', '789')
|
31
|
+
test.sort!
|
32
|
+
assert_equal '123', test[0].name
|
33
|
+
assert_equal 'aardvark', test[1].name
|
34
|
+
assert_equal 'apple', test[2].name
|
35
|
+
assert_equal 'zoo', test[3].name
|
36
|
+
end
|
37
|
+
end
|
data/test/tc_safefile.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
########################################################################
|
2
|
+
# tc_safefile.rb
|
3
|
+
# Copyright (c) 2007 Rob Warner.
|
4
|
+
# All rights reserved. This program and the accompanying materials
|
5
|
+
# are made available under the terms of the Eclipse Public License v1.0
|
6
|
+
# which accompanies this distribution, and is available at
|
7
|
+
# http://www.eclipse.org/legal/epl-v10.html
|
8
|
+
########################################################################
|
9
|
+
|
10
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safefile')
|
11
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safeentry')
|
12
|
+
require 'test/unit'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
class SafeFileTest < Test::Unit::TestCase
|
16
|
+
def test_insert_and_delete
|
17
|
+
f = SafeFile.new("password", ".")
|
18
|
+
assert_equal "./.safe.xml", f.filename
|
19
|
+
assert_equal 0, f.entries.length
|
20
|
+
|
21
|
+
f.insert('apple', 'baker', 'charlie')
|
22
|
+
assert_equal 1, f.entries.length
|
23
|
+
|
24
|
+
f.insert('apple', 'charlie', 'baker')
|
25
|
+
assert_equal 1, f.entries.length
|
26
|
+
|
27
|
+
f.insert('foo', 'bar', 'baz')
|
28
|
+
assert_equal 2, f.entries.length
|
29
|
+
|
30
|
+
f.delete('bar')
|
31
|
+
assert_equal 2, f.entries.length
|
32
|
+
|
33
|
+
f.delete('foo')
|
34
|
+
assert_equal 1, f.entries.length
|
35
|
+
|
36
|
+
f.delete('apple')
|
37
|
+
assert_equal 0, f.entries.length
|
38
|
+
|
39
|
+
f.delete('apple')
|
40
|
+
assert_equal 0, f.entries.length
|
41
|
+
|
42
|
+
FileUtils::rm("./.safe.xml")
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_entry
|
46
|
+
f = SafeFile.new("password", ".")
|
47
|
+
f.insert('apple', 'baker', 'charlie')
|
48
|
+
e = f.entries['apple']
|
49
|
+
assert_equal 'apple', e.name
|
50
|
+
assert_equal 'baker', e.id
|
51
|
+
assert_equal 'charlie', e.password
|
52
|
+
assert_equal "apple\tbaker\tcharlie", e.to_s
|
53
|
+
|
54
|
+
FileUtils::rm("./.safe.xml")
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_modify_entry
|
58
|
+
f = SafeFile.new("password", ".")
|
59
|
+
f.insert('apple', 'baker', 'charlie')
|
60
|
+
e = f.entries['apple']
|
61
|
+
assert_equal 'apple', e.name
|
62
|
+
assert_equal 'baker', e.id
|
63
|
+
assert_equal 'charlie', e.password
|
64
|
+
assert_equal "apple\tbaker\tcharlie", e.to_s
|
65
|
+
|
66
|
+
f.insert('apple', 'foo', 'bar')
|
67
|
+
e = f.entries['apple']
|
68
|
+
assert_equal 'apple', e.name
|
69
|
+
assert_equal 'foo', e.id
|
70
|
+
assert_equal 'bar', e.password
|
71
|
+
assert_equal "apple\tfoo\tbar", e.to_s
|
72
|
+
|
73
|
+
FileUtils::rm("./.safe.xml")
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_authorized
|
77
|
+
f = SafeFile.new("password", ".")
|
78
|
+
assert_equal true, f.authorized?
|
79
|
+
|
80
|
+
f.password = "foo"
|
81
|
+
assert_equal false, f.authorized?
|
82
|
+
|
83
|
+
FileUtils::rm("./.safe.xml")
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_count
|
87
|
+
f = SafeFile.new("password", ".")
|
88
|
+
|
89
|
+
assert_equal 0, f.count
|
90
|
+
|
91
|
+
f.insert('apple', 'baker', 'charlie')
|
92
|
+
assert_equal 1, f.count
|
93
|
+
|
94
|
+
f.insert('apple1', 'baker', 'charlie')
|
95
|
+
assert_equal 2, f.count
|
96
|
+
|
97
|
+
f.insert('apple1', 'baker', 'charlie')
|
98
|
+
assert_equal 2, f.count
|
99
|
+
|
100
|
+
f.insert('apple2', 'baker', 'charlie')
|
101
|
+
assert_equal 3, f.count
|
102
|
+
|
103
|
+
f.delete('apple')
|
104
|
+
assert_equal 2, f.count
|
105
|
+
|
106
|
+
f.delete('apple')
|
107
|
+
assert_equal 2, f.count
|
108
|
+
|
109
|
+
f.delete('apple1')
|
110
|
+
assert_equal 1, f.count
|
111
|
+
|
112
|
+
f.delete('apple2')
|
113
|
+
assert_equal 0, f.count
|
114
|
+
|
115
|
+
f.delete('apple2')
|
116
|
+
assert_equal 0, f.count
|
117
|
+
|
118
|
+
FileUtils::rm("./.safe.xml")
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
########################################################################
|
2
|
+
# tc_safeutils.rb
|
3
|
+
# Copyright (c) 2007 Rob Warner.
|
4
|
+
# All rights reserved. This program and the accompanying materials
|
5
|
+
# are made available under the terms of the Eclipse Public License v1.0
|
6
|
+
# which accompanies this distribution, and is available at
|
7
|
+
# http://www.eclipse.org/legal/epl-v10.html
|
8
|
+
########################################################################
|
9
|
+
|
10
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'safeutils')
|
11
|
+
require 'test/unit'
|
12
|
+
require 'fileutils'
|
13
|
+
|
14
|
+
class SafeUtilsTest < Test::Unit::TestCase
|
15
|
+
def test_get_safe_dir
|
16
|
+
ev = 'MY_SAFE_ENV_VAR'
|
17
|
+
|
18
|
+
# Make sure we're not clobbering an environment variable
|
19
|
+
temp = ENV[ev]
|
20
|
+
assert_equal nil, temp
|
21
|
+
|
22
|
+
# Test when the variable isn't set
|
23
|
+
begin
|
24
|
+
dir = SafeUtils::get_safe_dir(ev)
|
25
|
+
rescue
|
26
|
+
assert_equal "Set environment variable #{ev} to the directory for your safe file", $!.message
|
27
|
+
end
|
28
|
+
|
29
|
+
# Test when the directory doesn't exist
|
30
|
+
ENV[ev] = 'Does not exist'
|
31
|
+
begin
|
32
|
+
dir = SafeUtils::get_safe_dir(ev)
|
33
|
+
rescue
|
34
|
+
assert_equal "Environment variable #{ev} does not point to an existing directory (#{ENV[ev]})", $!.message
|
35
|
+
end
|
36
|
+
|
37
|
+
# Test when the directory exists
|
38
|
+
ENV[ev] = '.'
|
39
|
+
dir = SafeUtils::get_safe_dir(ev)
|
40
|
+
assert_equal '.', dir
|
41
|
+
|
42
|
+
# Make sure the directory exists
|
43
|
+
FileUtils::cd dir
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: safe
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: "0.2"
|
7
|
+
date: 2007-08-19 00:00:00 -04:00
|
8
|
+
summary: A command-line password storage program
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: rwarner@grailbox.com
|
12
|
+
homepage: http://grailbox.com/safe
|
13
|
+
rubyforge_project:
|
14
|
+
description: safe safely stores all your user IDs and passwords, encrypted by a password of your choosing.
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: false
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Rob Warner
|
31
|
+
files:
|
32
|
+
- README
|
33
|
+
- lib/safeentry.rb
|
34
|
+
- lib/safefile.rb
|
35
|
+
- lib/safeutils.rb
|
36
|
+
test_files:
|
37
|
+
- test/tc_safefile.rb
|
38
|
+
- test/tc_safeentry.rb
|
39
|
+
- test/tc_safeutils.rb
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
extra_rdoc_files: []
|
43
|
+
|
44
|
+
executables:
|
45
|
+
- safe.rb
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
requirements: []
|
49
|
+
|
50
|
+
dependencies:
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: crypt
|
53
|
+
version_requirement:
|
54
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: 0.0.0
|
59
|
+
version:
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: highline
|
62
|
+
version_requirement:
|
63
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.0.0
|
68
|
+
version:
|