safe 0.2
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 +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:
|