Class: SimplePass

Inherits:
Hash
  • Object
show all
Defined in:
lib/simplepass.rb

Defined Under Namespace

Classes: UI

Constant Summary collapse

VERSION =
"1.0.1"
HEADER =
"This is an encrypted SimplePass password file. Do not edit this file directly.\n"
DIVIDER =
"====BEGIN DATA===\n"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(master_password, file = nil) ⇒ SimplePass

Returns a new instance of SimplePass.



287
288
289
290
291
292
293
# File 'lib/simplepass.rb', line 287

def initialize(master_password, file=nil)
  @master_password = master_password
  @blowfish = Crypt::Blowfish.new(@master_password)
  @file = file
  @header = HEADER
  super({})
end

Instance Attribute Details

#headerObject

Returns the value of attribute header.



286
287
288
# File 'lib/simplepass.rb', line 286

def header
  @header
end

#master_passwordObject

Returns the value of attribute master_password.



286
287
288
# File 'lib/simplepass.rb', line 286

def master_password
  @master_password
end

Class Method Details

.create_databaseObject



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/simplepass.rb', line 248

def self.create_database
  print "Can't find a simplepass.db database in this directory. Create one? (y/n) "
  reply = STDIN.gets.chomp
  puts

  unless reply =~ /y/i
    puts "Ok, good bye."
    exit
  end
  password = set_password
  puts "Creating database simplepass.db and encrypting it with the master password."
  db = SimplePass.new(password, 'simplepass.db')
  db.save!
  db
end

.database_exists?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'lib/simplepass.rb', line 226

def self.database_exists?
  File.exist?('simplepass.db')
end

.edit_entry(entry) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
# File 'lib/simplepass.rb', line 214

def self.edit_entry(entry)
  tf = Tempfile.new('simplepass')
  tf.puts( entry )
  tf.close
  system("#{@options[:editor]} #{tf.path}")
  tf.open 
  entry = tf.read 
  @db.ui.add(entry)
  @db.save!
  tf.close(true)
end

.load(master_password, file) ⇒ Object



318
319
320
321
322
# File 'lib/simplepass.rb', line 318

def self.load(master_password, file)
  simplepass = self.new(master_password, file)
  simplepass.load
  simplepass
end

.process_args(argv) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/simplepass.rb', line 12

def self.process_args(argv)
  @options = options = {
    :editor => ENV['EDITOR'] || 'nano'
  }
  opts = OptionParser.new do |opt|
    opt.program_name = File.basename $0
    opt.version = SimplePass::VERSION
    opt.summary_indent = ' ' * 4
    opt.banner = <<-EOT
Usage: #{opt.program_name} [options] [domain name]

Where domain name is typically a website domain, e.g. yahoo.com. The domain can
also be any other string, e.g. "Gmail account".

For example, you enter 

  #{opt.program_name} gmail.com
  
If your domain name has spaces, remember to quote it like so:

  #{opt.program_name} 'Gmail account'

for the first time, simplepass will launch your text editor (whatever your
EDITOR environment variable has been set to) and present you with a simple form
that you can fill out to save a login, password, and arbitrary notes for that
domain: 

  gmail.com
  login:
  password:
  notes:

You can leave any of the fields blank. So if you want, you can just fill out 
the 'notes' portion for items that are not web logins, such as credit card
numbers and such.

Once you fill out the fields save the file and exit your editor, simplepass will
parse the information, encrypt it, store it in the database, and delete the
temporary file.

The next time you give the command,
  
  #{opt.program_name} gmail.com

simplepass will display something like this, via the 'less' command: (This is so 

  gmail.com
  login: funnyface
  password: audreyhepburn
  notes: I love gmail! blah... blah...

(We pipe the output to 'less' so as to leave no trace of your password in your
console, where someone can find it by scrolling up.)

The first time you launch simplepass, it will ask you to set a master password.
This password will unlock the simplepass database. This database is stored in
a single file: a partly encrypted text file called simplepass.db. This file will be 
saved in the directory in which you invoked the #{opt.program_name} command.

The top part of this file is a message in plain text and just serves to remind
you of the file's purpose. The bottom part is your password database, which is
nothing more than a YAML string encrypted with the Blowfish encryption
algorithm. Your master password is the the key that decrypts this portion of the
simplepass.db file. Do not edit this file directly; any edits may render the
data un-decryptable.
    EOT
    opt.separator nil
    opt.separator "Options:"
    opt.separator nil

    opt.on("--edit", "-e", 
           "Edit the login information for the domain.",
           "This invokes your text editor.") do |value|
      options[:edit] = true
    end
    opt.separator nil
    opt.on("--remove", "-r",
           "Deletes the entry for the domain.") do |value|
      options[:remove] = true
    end
    opt.on("--list", "-l",
           "List all the domains in the databse.") do |value|
      options[:list] = true
    end
    opt.on("--change-password", "-p",
           "Changes the master password.") do |value|
      options[:change_password] = true
    end
    opt.on("--dump", "-d", 
           "If you specify a domain, this causes the ",
           "login info to be printed out directly in ",
           "the console, instead of via 'less'. If you",
           "don't specify a domain, this decrypts and ",
           "dumps the entire password database into a ",
           "YAML string. This string is printed to ",
           "STDOUT, so you can redirect it into a file.",
           "This is to ensure that you can get all your",
           "login data out whenever you want.") do |value|
      options[:dump] = true
    end
  end

  opts.parse! argv
  unless argv.first || options[:dump] || options[:list] || options[:change_password]
    raise OptionParser::InvalidArgument, "You must specify a domain name."
  end
  options
 rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e
  puts opts
  puts
  puts e
  exit 1
end

.run(argv = ARGV) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/simplepass.rb', line 126

def self.run(argv = ARGV)
  options = process_args argv
  domain = argv.shift
  @db = db = if database_exists?
    unlock_database
  else
    create_database
  end
  if options[:change_password]
    puts "To change the master password:"
    password = set_password
    @db.change_master_password(password)
    @db.save!
    puts "Master password changed."
  elsif options[:list]
    (domains=db.ui.list).each_with_index do |x,i|
      puts "%2d %s" %[i,x]
    end
    loop do
      print "Type a number to view an entry, or return to exit: "
      reply = STDIN.gets.chomp
      unless reply =~ /\d+/
        puts "Ok, good bye."
        exit
      end
      # lookup the domain
      output = db.ui.decrypt(domains[reply.to_i])
      if output
        IO.popen("less", "w") do |pipe|
          pipe.puts output
        end
      end
    end
  elsif options[:remove]
    puts db.ui.remove(domain)
  elsif options[:dump]
    puts db.ui.dump(domain)
  else
    # lookup the domain
    output = db.ui.decrypt(domain)
    if output
      if options[:edit]
        output = UI::EDIT_HEADER + output
        edit_entry( output )
      else
        IO.popen("less", "w") do |pipe|
          pipe.puts output
        end
      end
    else
      # if no entry, create one
      # We loop so user can add multiple entries without having to enter the
      # password every time.
      loop do
        puts "Creating entry."
        sleep 1
        edit_entry( UI::EDIT_HEADER + db.ui.new_entry(domain) )
        puts "Added entry for #{domain}"
        print "Add another? (y/n) "
        reply = STDIN.gets.chomp
        unless reply =~ /y/i
          puts "Ok, good bye."
          exit
        end
        puts
        print "Domain name for new entry: "
        domain = STDIN.gets.chomp
      end
    end
  end

  exit
  # create database if no database in current dir
  # ask for master password
  # ask for password confirm
  
  if options[:dump]
    # handle dump
  end
  domain = ARGV.shift
  match = decrypt(domain)
  if match
    # show in less
  else
    # create entry
  end
end

.set_passwordObject



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/simplepass.rb', line 264

def self.set_password
  password = nil
  loop do
    print "Please type a master password (will not show what you type): "
    `stty -echo`
    password = STDIN.gets.chomp
    puts 
    print "Please confirm by typing it again: "
    password_confirm = STDIN.gets.chomp
    puts
    unless password == password_confirm
      puts "Sorry, you passwords do not match."
    else
      `stty echo`
      break
    end
  end
  password
end

.unlock_databaseObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/simplepass.rb', line 230

def self.unlock_database
  loop do
    print "Master password: "
    `stty -echo`
    password = STDIN.gets.chomp
    `stty echo`
    puts
    exit if password == ''
    db = SimplePass.new(password, 'simplepass.db')
    begin
      db.load
      break db
    rescue Exception
      puts "Sorry, that password isn't correct." 
    end
  end
end

Instance Method Details

#change_master_password(password) ⇒ Object



295
296
297
298
# File 'lib/simplepass.rb', line 295

def change_master_password(password)
  @master_password = password
  @blowfish = Crypt::Blowfish.new(@master_password)
end

#decrypt(encrypted_string) ⇒ Object



304
305
306
# File 'lib/simplepass.rb', line 304

def decrypt(encrypted_string)
  @blowfish.decrypt_string(encrypted_string)
end

#encrypt(plain_string) ⇒ Object



300
301
302
# File 'lib/simplepass.rb', line 300

def encrypt(plain_string)
  @blowfish.encrypt_string(plain_string)
end

#loadObject



308
309
310
311
312
313
314
315
316
# File 'lib/simplepass.rb', line 308

def load
  File.open(@file, "r") do |file|
    content = file.read
    # strip the header
    @header, data = content.split(DIVIDER, 2)
    hash = YAML::load(decrypt(data))
    hash.each_pair {|k,v| self[k] = v}
  end
end

#save!Object Also known as: save



324
325
326
327
328
329
330
331
332
# File 'lib/simplepass.rb', line 324

def save!
  File.open(@file, "w") do |file|
    # Write an informative header
    file.write(@header + DIVIDER)
    # Dump the encrypted data
    dump = YAML::dump(self)
    file.write(encrypt(dump))
  end
end

#uiObject



335
336
337
338
# File 'lib/simplepass.rb', line 335

def ui
  @ui ||= UI.new(self)
  @ui
end