Class: Password

Inherits:
String
  • Object
show all
Defined in:
lib/account_engine/password.rb

Overview

Ruby/Password is a collection of password handling routines for Ruby, including an interface to CrackLib for the purposes of testing password strength.

require 'password'

# Define and check a password in code
pw = Password.new( "bigblackcat" )
pw.check

# Get and check a password from the keyboard
begin
  password = Password.get( "New password: " )
  password.check
rescue Password::WeakPassword => reason
  puts reason
  retry
end

# Automatically generate and encrypt a password
password = Password.phonemic( 12, Password:ONE_CASE | Password::ONE_DIGIT )
crypted = password.crypt

Defined Under Namespace

Classes: CryptError, DictionaryError, WeakPassword

Constant Summary collapse

VERSION =
'0.5.3'
DES =

DES algorithm

true
MD5 =

MD5 algorithm (see crypt(3) for more information)

false
ONE_DIGIT =

This flag is used in conjunction with Password.phonemic and states that a password must include a digit.

1
ONE_CASE =

This flag is used in conjunction with Password.phonemic and states that a password must include a capital letter.

1 << 1
PASSWD_CHARS =

Characters that may appear in generated passwords. Password.urandom may also use the characters + and /.

'0123456789' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz'
SALT_CHARS =

Valid salt characters for use by Password#crypt.

'0123456789' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'./'
CONSONANT =

phoneme flags

1
VOWEL =
1 << 1
DIPHTHONG =
1 << 2
NOT_FIRST =

indicates that a given phoneme may not occur first

1 << 3
PHONEMES =
{
  :a	=> VOWEL,
  :ae	=> VOWEL      | DIPHTHONG,
  :ah => VOWEL      | DIPHTHONG,
  :ai => VOWEL      | DIPHTHONG,
  :b	=> CONSONANT,
  :c	=> CONSONANT,
  :ch	=> CONSONANT  | DIPHTHONG,
  :d	=> CONSONANT,
  :e	=> VOWEL,
  :ee	=> VOWEL      | DIPHTHONG,
  :ei	=> VOWEL      | DIPHTHONG,
  :f	=> CONSONANT,
  :g	=> CONSONANT,
  :gh	=> CONSONANT  | DIPHTHONG | NOT_FIRST,
  :h	=> CONSONANT,
  :i	=> VOWEL,
  :ie	=> VOWEL      | DIPHTHONG,
  :j	=> CONSONANT,
  :k	=> CONSONANT,
  :l	=> CONSONANT,
  :m	=> CONSONANT,
  :n	=> CONSONANT,
  :ng	=> CONSONANT  | DIPHTHONG | NOT_FIRST,
  :o	=> VOWEL,
  :oh	=> VOWEL      | DIPHTHONG,
  :oo	=> VOWEL      | DIPHTHONG,
  :p	=> CONSONANT,
  :ph	=> CONSONANT  | DIPHTHONG,
  :qu	=> CONSONANT  | DIPHTHONG,
  :r	=> CONSONANT,
  :s	=> CONSONANT,
  :sh	=> CONSONANT  | DIPHTHONG,
  :t	=> CONSONANT,
  :th	=> CONSONANT  | DIPHTHONG,
  :u	=> VOWEL,
  :v	=> CONSONANT,
  :w	=> CONSONANT,
  :x	=> CONSONANT,
  :y	=> CONSONANT,
  :z	=> CONSONANT
}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.echo(on = true, masked = false) ⇒ Object

Turn local terminal echo on or off. This method is used for securing the display, so that a soon to be entered password will not be echoed to the screen. It is also used for restoring the display afterwards.

If masked is true, the keyboard is put into unbuffered mode, allowing the retrieval of characters one at a time. masked has no effect when on is false. You are unlikely to need this method in the course of normal operations.



166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/account_engine/password.rb', line 166

def Password.echo(on=true, masked=false)
  term = Termios::getattr( $stdin )

  if on
    term.c_lflag |= ( Termios::ECHO | Termios::ICANON )
  else # off
    term.c_lflag &= ~Termios::ECHO
    term.c_lflag &= ~Termios::ICANON if masked
  end

  Termios::setattr( $stdin, Termios::TCSANOW, term )
end

.get(message = "Password: ") ⇒ Object

Get a password from STDIN, using buffered line input and displaying message as the prompt. No output will appear while the password is being typed. Hitting [Enter] completes password entry. If STDIN is not connected to a tty, no prompt will be displayed.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/account_engine/password.rb', line 185

def Password.get(message="Password: ")
  begin
    if $stdin.tty?
	Password.echo false
	print message if message
    end

    pw = Password.new( $stdin.gets || "" )
    pw.chomp!

  ensure
    if $stdin.tty?
	Password.echo true
	print "\n"
    end
  end
end

.get_vowel_or_consonantObject

Determine whether next character should be a vowel or consonant.



241
242
243
# File 'lib/account_engine/password.rb', line 241

def Password.get_vowel_or_consonant
  rand( 2 ) == 1 ? VOWEL : CONSONANT
end

.getc(message = "Password: ", mask = '*') ⇒ Object

Get a password from STDIN in unbuffered mode, i.e. one key at a time. message will be displayed as the prompt and each key press with echo mask to the terminal. There is no need to hit [Enter] at the end.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/account_engine/password.rb', line 208

def Password.getc(message="Password: ", mask='*')
  # Save current buffering mode
  buffering = $stdout.sync

  # Turn off buffering
  $stdout.sync = true

  begin
    Password.echo(false, true)
    print message if message
    pw = ""

    while ( char = $stdin.getc ) != 10 # break after [Enter]
	putc mask
	pw << char
    end

  ensure
    Password.echo true
    print "\n"
  end

  # Restore original buffering mode
  $stdout.sync = buffering

  Password.new( pw )
end

.phonemic(length = 8, flags = nil) ⇒ Object

Generate a memorable password of length characters, using phonemes that a human-being can easily remember. flags is one or more of Password::ONE_DIGIT and Password::ONE_CASE, logically OR’ed together. For example:

pw = Password.phonemic( 8, Password::ONE_DIGIT | Password::ONE_CASE )

This would generate an eight character password, containing a digit and an upper-case letter, such as Ug2shoth.

This method was inspired by the pwgen tool, written by Theodore Ts’o.

Generated passwords may contain any of the characters in Password::PASSWD_CHARS.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/account_engine/password.rb', line 265

def Password.phonemic(length=8, flags=nil)

  pw = nil
  ph_flags = flags

  loop do

    pw = ""

    # Separate the flags integer into an array of individual flags
    feature_flags = [ flags & ONE_DIGIT, flags & ONE_CASE ]

    prev = []
    first = true
    desired = Password.get_vowel_or_consonant

    # Get an Array of all of the phonemes
    phonemes = PHONEMES.keys.map { |ph| ph.to_s }
    nr_phonemes = phonemes.size

    while pw.length < length do

	# Get a random phoneme and its length
	phoneme = phonemes[ rand( nr_phonemes ) ]
	ph_len = phoneme.length

	# Get its flags as an Array
	ph_flags = PHONEMES[ phoneme.to_sym ]
	ph_flags = [ ph_flags & CONSONANT, ph_flags & VOWEL,
     ph_flags & DIPHTHONG, ph_flags & NOT_FIRST ]

	# Filter on the basic type of the next phoneme
	next if ph_flags.include? desired

	# Handle the NOT_FIRST flag
	next if first and ph_flags.include? NOT_FIRST

	# Don't allow a VOWEL followed a vowel/diphthong pair
	next if prev.include? VOWEL and ph_flags.include? VOWEL and
ph_flags.include? DIPHTHONG

	# Don't allow us to go longer than the desired length
	next if ph_len > length - pw.length

	# We've found a phoneme that meets our criteria
	pw << phoneme

	# Handle ONE_CASE
	if feature_flags.include? ONE_CASE

 if (first or ph_flags.include? CONSONANT) and rand( 10 ) < 3
   pw[-ph_len, 1] = pw[-ph_len, 1].upcase
   feature_flags.delete ONE_CASE
 end

	end

	# Is password already long enough?
	break if pw.length >= length

	# Handle ONE_DIGIT
	if feature_flags.include? ONE_DIGIT

 if ! first and rand( 10 ) < 3
   pw << ( rand( 10 ) + ?0 ).chr
   feature_flags.delete ONE_DIGIT

   first = true
   prev = []
   desired = Password.get_vowel_or_consonant
   next
 end

	end

	if desired == CONSONANT
 desired = VOWEL
	elsif prev.include? VOWEL or ph_flags.include? DIPHTHONG or
     rand(10) > 3
 desired = CONSONANT
	else
 desired = VOWEL
	end

	prev = ph_flags
	first = false
    end

    # Try again
    break unless feature_flags.include? ONE_CASE or
   feature_flags.include? ONE_DIGIT

  end

  Password.new( pw )

end

.random(length = 8) ⇒ Object

Generate a random password of length characters. Unlike the Password.phonemic method, no attempt will be made to generate a memorable password. Generated passwords may contain any of the characters in Password::PASSWD_CHARS.



370
371
372
373
374
375
376
377
# File 'lib/account_engine/password.rb', line 370

def Password.random(length=8)
  pw = ""
  nr_chars = PASSWD_CHARS.size

  length.times { pw << PASSWD_CHARS[ rand( nr_chars ) ] }

  Password.new( pw )
end

.urandom(length = 8) ⇒ Object

An alternative to Password.random. It uses the /dev/urandom device to generate passwords, returning nil on systems that do not implement the device. The passwords it generates may contain any of the characters in Password::PASSWD_CHARS, plus the additional characters + and /.



386
387
388
389
390
391
392
393
394
# File 'lib/account_engine/password.rb', line 386

def Password.urandom(length=8)
  return nil unless File.chardev? '/dev/urandom'

  rand_data = nil
  File.open( "/dev/urandom" ) { |f| rand_data = f.read( length ) }

  # Base64 encode it
  Password.new( [ rand_data ].pack( 'm' )[ 0 .. length - 1 ] )
end

Instance Method Details

#crypt(type = DES, salt = '') ⇒ Object

Encrypt a password using type encryption. salt, if supplied, will be used to perturb the encryption algorithm and should be chosen from the Password::SALT_CHARS. If no salt is given, a randomly generated salt will be used.



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/account_engine/password.rb', line 402

def crypt(type=DES, salt='')

  unless ( salt.split( // ) - SALT_CHARS.split( // ) ).empty?
    raise CryptError, 'bad salt'
  end

  salt = Password.random( type ? 2 : 8 ) if salt.empty?

  # (Linux glibc2 interprets a salt prefix of '$1$' as a call to use MD5
  # instead of DES when calling crypt(3))
  salt = '$1$' + salt if type == MD5

  # Pass to crypt in class String (our parent class)
  crypt = super( salt )

  # Raise an exception if MD5 was wanted, but result is not recognisable
  if type == MD5 && crypt !~ /^\$1\$/
    raise CryptError, 'MD5 not implemented'
  end

  crypt
end