A Website-Specific Password Generator

NEM 2008-11-27: Here's a Tk version of the Site Password program by Alan Karp at HP Labs [L1 ]. Instead of storing lots of passwords for every website you visit, or using the same password everywhere, this program lets you calculate a password based on a master password and an easy to remember site name. It works by concatenating the master password and the site name, taking the MD5 hash of these and base64 encoding the result. It works quite nicely. The version here improves on the Python version on the site by using Ttk widgets and allowing you to save previously entered site names. This version also support a command-line only mode: simply pass the sitename as an argument:

   sitepw.tcl ?sitename ?-alphanumeric 0|1?? 

(Note: command-line only works on UNIX-like systems that have stty).

http://www.cs.nott.ac.uk/~nem/sitepw.png

Usage

Simply type in your master password and then enter the website name. The site-specific password will appear in the Site Password box. You can copy this password to the clipboard using the Copy button (or select and Ctrl-C as usual). The Add button saves the current website name to a preferences file (don't worry -- the site name is not sufficient to calculate your passwords). On UNIX this prefs file is stored in ~/.sitepw, on Windows in ~/Application Data/sitepw.rc (may need to be localised?), and on Mac in ~/Library/Preferences/sitepw.rc. The Done button exits the application.

#!/usr/bin/env tclsh
# sitepw.tcl
package require Tcl     8.5
package require md5     2.0
package require base64  2.3

proc sitepw args {
    if {[llength $args] >= 1} {
        set pw [readpw]
        puts [password $pw {*}$args]
    } else {
        package require Tk      8.5
        CreateGUI
    }
}

proc readpw {} {
    # Read password from stdin in a safe-ish manner
    puts -nonewline "Password: "
    flush stdout
    exec stty -echo
    gets stdin pw
    exec stty echo
    return $pw
}

# Compute a site-specific password
proc password {master sitename args} {
    array set ops { -alphanumeric 0 -length 12 }
    array set ops $args
    # Compute MD5 hash of inputs to generate unique password
    set sitename [string tolower $sitename]
    set digest [md5::md5 $sitename$master]
    set digest [base64::encode $digest]
    if {$ops(-alphanumeric)} {
        set digest [string map {/ "" + ""} $digest]
    }
    return [string range $digest 0 [expr {$ops(-length)-1}]]
}

proc CreateGUI {} {
    set f [ttk::frame .f]
    ttk::label $f.pl        -text "Master Password:" -anchor e
    ttk::entry $f.passwd    -textvariable ::master -show *
    ttk::label $f.sl        -text "Website:" -anchor e
    ttk::combobox $f.site   -textvariable ::site
    ttk::label $f.gl        -text "Site Password:" -anchor e
    ttk::entry $f.spass     -textvariable ::sitepw
    ttk::checkbutton $f.alpha -text "Letters/numbers only" -variable ::alpha
    
    set tb [ttk::frame .tb]
    ttk::button $tb.done -text "Done" -command Exit
    ttk::button $tb.save -text "Add" -command [list AddSite $f.site]
    ttk::button $tb.copy -text "Copy" \
        -command [list Copy $f.spass]
    
    bind . <Return> [list $tb.copy invoke]

    set ::master ""
    set ::site   ""
    set ::sitepw ""
    set ::alpha  0
    
    LoadSites $f.site
    
    wm protocol . WM_DELETE_WINDOW Exit
    
    grid $f.pl $f.passwd -sticky ew
    grid $f.sl $f.site   -sticky ew
    grid $f.gl $f.spass  -sticky ew
    grid $f.alpha -      

    grid columnconfigure $f 1 -weight 1
    
    grid $tb.done $tb.save $tb.copy -sticky e -padx 10 -pady 2

    grid $f -sticky ew
    grid $tb -sticky ew
    
    grid columnconfigure . 0 -weight 1

    wm resizable . 1 0
    wm title . "Site Password"

    trace add variable ::master write UpdatePW
    trace add variable ::site   write UpdatePW
    trace add variable ::alpha  write UpdatePW
}

# Load list of previously accessed sites into the combobox w
proc LoadSites w {
    catch {
        set in [open [PrefFile] r]
        set sites [split [string trim [read $in]] \n]
        close $in
        $w configure -values $sites
    }
}

proc SaveSites w {
    set out [open [PrefFile] w]
    puts $out [join [$w cget -values] \n]
    close $out
}

proc Exit {} {
    SaveSites .f.site
    exit
}

proc AddSite w {
    set sites [$w cget -values]
    set site [$w get]
    lappend sites $site
    set sites [lsort -unique -dictionary $sites]
    $w configure -values $sites
}

proc PrefFile {} {
    switch -exact [tk windowingsystem] {
        x11     { return [file normalize ~/.sitepw] }
        win     { return [file normalize "~/Application Data/sitepw.rc"] }
        aqua    { return [file normalize ~/Library/Preferences/sitepw.rc] }
    }
}

proc Copy w {
    clipboard clear
    clipboard append [$w get]
}

proc UpdatePW {args} {
    global master site sitepw alpha
    set sitepw [password $master $site -alphanumeric $alpha]
}

sitepw {*}$argv

See also: Password Gorilla