Password Generator and Trainer

The following code allows a password to be generated according to a set of simple rules (length, minimum number of characters from a number of sets). It presents a GUI which a user can then use to "train" on the new password to aid in remembering it by getting it "into ones fingers".

It is fairly simplistic. Any suggested improvements would be gratefully received. Edit any suggested improvements straight inline. --MNO


 #!/bin/sh
 # Emacs: please open this file in -*-Tcl-*- mode
 # the next but one line restarts with wish...
 # DO NOT REMOVE THIS BACKSLASH -> \
         exec wish "$0" ${1+"$@"}
 #
 # Author: Mark Oakden https://wiki.tcl-lang.org/MNO
 # Version: 1.0
 #
 # password generator and drilling program:
 # generate a password according to the rules array and allow the user to
 # test themselves on said password,  displaying statistics on how often
 # they get it right
 #
 # no sanity checks on the supplied rules are done.
 #
 # datasets for password generation:-
 # separate lowercase and UPPERCASE letters so we can demand minimum
 # number of each separately.
 set data(letters) "abcdefghijklmnopqrstuvwxyz"
 set data(LETTERS) "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 set data(numbers) "0123456789"
 set data(punctuation) "!\"£$%^&*()_+-={};':@#~<>,.?/\\|"

 # a simpler set might be, for example:-
 #
 # set data(letters) "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
 # set data(numbers) "0123456789"
 # set data(punctuation) "!\"£$%^&*()_+-={};':@#~<>,.?/\\|"

 # the rules determine characteristics of the randomly generated passwords
 # presently available are:-
 # rules(len) password length
 # rules(<dataset_name>,min) minimum number of characters from <dataset_name>
 # entry on the data array

 # example rules:-
 # password 7 chars long, with at least one U/C char, one l/c char,
 # one number and one punctuation.
 set rules(len) 7
 set rules(letters,min) 1
 set rules(LETTERS,min) 1
 set rules(numbers,min) 1
 set rules(punctuation,min) 1

 # example rules appropriate to the commented "simpler" datasets above:-
 #
 # set rules(len) 7
 # set rules(numbers,min) 1
 # set rules(punctuation,min) 1

 proc initStats {} {
     global stats
     set stats(tries) 0
     set stats(correct) 0
     updateStatsDisplay
 }

 # picks a (pseudo)random char from str
 proc oneCharFrom { str } {
     set len [string length $str]
     set indx [expr {int(rand()*$len)}]
     return [string index $str $indx]
 }

 # for a string of length n,  swap random pairs of chars n times
 # and return the result
 proc shuffle { str } {
     set len [string length $str]
     for { set i 1 } { $i <= $len } { incr i 1 } {
         set indx1 [expr {int(rand()*$len)}]
         set indx2 [expr {int(rand()*$len)}]
         set str [swapStringChars $str $indx1 $indx2]
     }
     return $str
 }

 # given a string, and integers i and j, swap the ith and jth chars of str
 # return the result
 proc swapStringChars { str i j } {
     if { $i == $j } {
         return $str
     }
     if { $i > $j } {
         set t $j
         set j $i
         set i $t
     }
     set pre [string range $str 0 [expr {$i - 1}]]
     set chari [string index $str $i]
     set mid [string range $str [expr {$i + 1}] [expr {$j - 1}]]
     set charj [string index $str $j]
     set end [string range $str [expr {$j + 1}] end]

     set ret ${pre}${charj}${mid}${chari}${end}
     return $ret
 }

 # generate a password
 proc genPw {} {
     global data rules
     # Algorithm
     # 1. foreach dataset with a min parameter, choose exactly min 
     #    random chars from it
     # 2. concatenate results of above into password
     # 3. concatenate all datasets into large dataset
     # 4. choose desired_length-password_length chars from large
     # 5. concatenate (4) and (2)
     # 6. shuffle (5)

     set password {}
     foreach indx [array names rules *,min] {
         set ds_name [lindex [split $indx ,] 0]
         set num $rules($indx)
         for {set i 1} {$i <= $num} {incr i 1} {
             append password [oneCharFrom $data($ds_name)]
         }
     }

     set all_data {}
     foreach set [array names data] {
         append all_data $data($set)
     }

     set rem_len [expr $rules(len) - [string length $password]]
     for {set i 1} {$i <= $rem_len} {incr i 1} {
         append password [oneCharFrom $all_data]
     }

     return [shuffle $password]
 }
 #
 # routines for the GUI
 #
 # get a new password, update stats and GUI
 proc newPass {} {
     global password displaypass pwattempt pwishidden
     set password [genPw]
     set pwattempt {}
     set pwishidden 0
     set displaypass $password
     .pw configure -text $password
     initStats
     update idletasks
     return
 }

 # toggle whether the password is displayed or not
 proc hideOrShowPass {} {
     global password displaypass pwishidden
     set hidden [starString $password]
     if { $pwishidden } {
         set displaypass $password
     } else {
         set displaypass $hidden
     }
     # toggle the hidden state
     set pwishidden [expr {1 - $pwishidden}]
     update idletasks
 }

 # return a string same length as argument str filled with "*"
 proc starString { str } {
     set ret {}
     foreach char [split $str {}] {
           append ret "*"
     }
     return $ret
 }
 # the following works in 8.3 and above, but not in 8.0 or the plugin...
 #proc starString { str } {
 #    return [string repeat "*" [string length $str]]
 #}

 # check a password typed by user, update stats and GUI
 proc testPass {} {
     global pwattempt password feedback stats
     incr stats(tries)
     # would like to use [string equal] in the following but doesn't work
     # in 8.0 or the plugin
     if {[string compare $password $pwattempt] == 0} {
         set feedback "Correct"
         .feedback configure -background green
         incr stats(correct)
     } else {
         set feedback "Wrong"
         .feedback configure -background red
     }
     set pwattempt {}
     updateStatsDisplay
     update idletasks
     return
 }

 # update the string used to display stats in GUI
 proc updateStatsDisplay {} {
     global stats formattedStats
     set formattedStats "$stats(correct)/$stats(tries) "
     if { $stats(tries) != 0 } {
         set perc [expr {100*double($stats(correct))/double($stats(tries))}]
     } else {
         set perc 0
     }
     append formattedStats [format "(%.1f%%)" $perc]
     return
 }
 #
 # set up the GUI
 #
 initStats
 set password [genPw]
 set displaypass $password
 set pwishidden 0
 set formattedStats {0/0 (0%)}
 set feedback {}
 button .newpw -text {New} -command newPass
 label .pw -font {Courier} -textvariable displaypass
 button .hide -text "Show/Hide" -command hideOrShowPass
 entry .try -font {Courier} -show "*" -width $rules(len) -textvariable pwattempt
 label .feedback -textvariable feedback
 label .stats -text "Stats:"
 label .statval -textvariable formattedStats
 button .statreset -text "Reset Stats" -command initStats
 grid .newpw .pw .hide -sticky ew
 grid .try - .feedback -sticky ew
 grid .stats .statval .statreset -sticky ew
 grid columnconfigure . 1 -weight 1
 focus .try
 bind .try <Return> testPass

RLH - I ran it through Nagelfar:

 Line  65: W Expr without braces
 Line  74: W Expr without braces
 Line  75: W Expr without braces
 Line  92: W Expr without braces
 Line  94: W Expr without braces
 Line  94: W Expr without braces
 Line  96: W Expr without braces
 Line 128: W Expr without braces
 Line 161: W Expr without braces
 Line 203: W Expr without braces

yahalom - better fix after pointing to the mistake. I done that.


Another simple password generator can be found at random


And yet another at Pass-word mixer.


Also take a look at the slightly related app Password Gorilla.