@@ -92,10 +92,12 @@ exit 1 } set argv [lrange $argv 2 end] +# We need Tcl 8.6 for [binary encode base64] +package require Tcl 8.6 package require sqlite3 package require platform lappend ::auto_path [file join [file dirname [info script]] lib [platform::identify]] lappend ::auto_path [file join [file dirname [info script]] lib [platform::generic]] @@ -167,10 +169,152 @@ return [list data $ret begin "-----BEGIN PUBLIC KEY-----" end "-----END PUBLIC KEY-----"] } # End backports # Start internal functions +proc _loadDB {dbCmd fileName} { + set ::saveRequired 1 + + if {[file exists $fileName]} { + set fd [open $fileName] + + # Verify that we have a valid file + gets $fd header + + # Ignore the first line if it is a hash-bang as well + if {[string range $header 0 1] == "#!"} { + set ::globalHeader($dbCmd) $header + + gets $fd header + } + + if {$header ne "# oh, ok."} { + # This may be an old SQLite3 DB, convert it + close $fd + + sqlite3 $dbCmd $fileName + + _saveDB $dbCmd $fileName + + $dbCmd close + + return [_loadDB $dbCmd $fileName] + } + + set data [read $fd] + + close $fd + } else { + set data "" + } + + sqlite3 $dbCmd ":memory:" + + $dbCmd eval { + CREATE TABLE IF NOT EXISTS users(name, publicKey BLOB); + CREATE TABLE IF NOT EXISTS passwords(name, encryptedPass BLOB, encryptedKey BLOB, publicKey BLOB, verification BLOB); + } + + $dbCmd transaction { + foreach line [split $data "\n"] { + if {[string trim $line] eq ""} { + continue + } + + set table [lindex $line 0] + set line [lrange $line 1 end] + + set keys [list] + set values [list] + unset -nocomplain valueArray + + foreach {key value} $line { + if {[string index $key 0] == ":"} { + set key [string range $key 1 end] + set valueBase64Encoded 1 + } else { + set valueBase64Encoded 0 + } + + if {$valueBase64Encoded} { + set value [binary decode base64 $value] + } + + if {![regexp {^[a-zA-Z]+$} $key]} { + return -code error "Invalid key name: $key" + } + + switch -- $key { + "name" { + set type "" + set typeInsertChar {$} + + # Convert this to a string-ified value + set value [string range "x$value" 1 end] + } + default { + set type "BLOB" + set typeInsertChar "@" + } + } + + lappend keys $key + + set valueArray($key) $value + + lappend values ${typeInsertChar}valueArray($key) + } + + $dbCmd eval "INSERT INTO $table ([join $keys {, }]) VALUES ([join $values {, }]);" + } + } +} + +proc _saveDB {dbCmd fileName} { + set tmpFileName "${fileName}.[expr rand()]" + + file delete -force -- $tmpFileName + + set fd [open $tmpFileName w] + + if {[info exists ::globalHeader($dbCmd)]} { + puts $fd $::globalHeader($dbCmd) + + unset ::globalHeader($dbCmd) + } + + puts $fd "# oh, ok." + + foreach table [list users passwords] { + unset -nocomplain row + $dbCmd eval "SELECT * FROM $table ORDER BY name;" row { + set outputLine [list $table] + + unset -nocomplain row(*) + + foreach {key value} [array get row] { + if {![regexp {^[a-zA-Z]+$} $value]} { + set key ":$key" + set value [binary encode base64 $value] + } + + lappend outputLine $key $value + } + + puts $fd $outputLine + } + } + + close $fd + + catch { + file attributes $tmpFileName {*}[file attributes $fileName] + } + + file rename -force -- $tmpFileName $fileName +} + proc _listCertificates {} { if {![info exists ::env(PKCS11MODULE)]} { return [list] } @@ -417,10 +561,12 @@ foreach pubkey $pubkeys { puts " |-> $pubkey" } } + + set ::saveRequired 0 } proc listAvailablePasswords {} { set passwordNames [list] foreach slotInfoDict [_listCertificates] { @@ -441,22 +587,28 @@ foreach passwordName $passwordNames { puts "$passwordName - [join [_getUsersForPassword [list $passwordName]] {, }]" } + + set ::saveRequired 0 } proc listPasswords {} { db eval {SELECT DISTINCT name FROM passwords;} row { puts "$row(name) - [join [_getUsersForPassword [list $row(name)]] {, }]" } + + set ::saveRequired 0 } proc listUsers {} { db eval {SELECT DISTINCT name FROM users;} row { puts "$row(name) - [join [_getPasswordsForUser [list $row(name)]] {, }]" } + + set ::saveRequired 0 } proc addUser {userName key} { set keyRaw [binary decode base64 $key] set keyVerify [::pki::pkcs::parse_public_key $keyRaw] @@ -495,10 +647,12 @@ _addPassword $passwordName $password $publicKeys } proc getPassword {passwordName} { puts [_getPassword $passwordName] + + set ::saveRequired 0 } proc updatePassword {passwordName password} { if {$password eq ""} { set password [_prompt "Please enter the new password: "] @@ -568,25 +722,24 @@ set users($row(name)) 1 } } puts [join [array names users] {, }] + + set ::saveRequired 0 } proc help {{action ""}} { _printHelp stdout $action + + set ::saveRequired 0 } # End user CLI functions ### MAIN -sqlite3 db $passwordFile - -db eval { - CREATE TABLE IF NOT EXISTS users(name, publicKey BLOB); - CREATE TABLE IF NOT EXISTS passwords(name, encryptedPass BLOB, encryptedKey BLOB, publicKey BLOB, verification BLOB); -} +_loadDB db $passwordFile if {$action in $validCommands} { if {[catch { $action {*}$argv } error]} { @@ -598,7 +751,12 @@ puts stderr "Invalid action" exit 1 } -exit 0 +if {$::saveRequired} { + _saveDB db $passwordFile +} + +db close +exit 0