Skip to content

Commit

Permalink
Option to display the next TOTP too
Browse files Browse the repository at this point in the history
  • Loading branch information
pepa65 committed Dec 8, 2024
1 parent 069461a commit 13f5f3e
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 35 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/dist
/twofat
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="https://raw.githubusercontent.com/pepa65/twofat/master/twofat.png" width="96" alt="twofat icon" align="right">

## Manage TOTPs from CLI
* **v2.1.1**
* **v2.2.0**
* Repo: [github.com/pepa65/twofat](https://github.com/pepa65/twofat)
* After: [github.com/slandx/tfat](https://github.com/slandx/tfat)
* Contact: github.com/pepa65
Expand All @@ -18,6 +18,7 @@
For even more security, run like: `GODEBUG=clobberfree=1 twofat`
* Datafile password can be changed.
* Display TOTPs of names matching regex, which auto-refresh.
* Option to display the next TOTP as well.
* Add, rename, delete entry, reveal secret, copy TOTP to clipboard.
* Import & export entries from & to standardized OTPAUTH_URI file.
* Adjusts to terminal width for display. NAME truncated to 20 on display
Expand Down Expand Up @@ -55,15 +56,15 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o twofat.exe

## Usage
```
twofat v2.1.1 - Manage TOTPs from CLI
twofat v2.2.0 - Manage TOTPs from CLI
The CLI is interactive & colorful, output to Stderr. Password can be piped in.
When output is redirected, only pertinent plain text is sent to Stdout.
* Repo: github.com/pepa65/twofat <pepa65@passchier.net>
* Datafile: ~/.twofat.enc (default, depends on the binary's name)
* Usage: twofat [COMMAND] [ -d | --datafile DATAFILE ]
== COMMAND:
[ show | view ] [REGEX [ -c | --case ]]
Display all TOTPs with NAMEs [matching REGEX] (show/view is optional).
[ show | view ] [REGEX [ -c | --case ]] [ -n | --next ]
Display all TOTPs with NAMEs [matching REGEX] (-n/--next: show next TOTP).
list | ls [REGEX [ -c | --case ]]
List all NAMEs [matching REGEX].
add | insert | entry NAME [TOTP-OPTIONS] [ -f | --force ] [SECRET]
Expand Down
12 changes: 6 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ const (
nonceSize = 12
pwRetry = 3
cls = "\033c"
red = "\033[1m\033[31m"
green = "\033[1m\033[32m"
yellow = "\033[1m\033[33m"
blue = "\033[1m\033[34m"
magenta = "\033[1m\033[35m"
cyan = "\033[1m\033[36m"
red = "\033[1;31m"
green = "\033[1;32m"
yellow = "\033[1;33m"
blue = "\033[1;34m"
magenta = "\033[1;35m"
cyan = "\033[1;36m"
def = "\033[0m"
)

Expand Down
74 changes: 49 additions & 25 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
)

const (
version = "2.1.1"
version = "2.2.0"
maxNameLen = 20
period = 30
)
Expand Down Expand Up @@ -66,7 +66,7 @@ func wipe(bytes []byte) {
runtime.GC()
}

func oneTimePassword(secret []byte, size, algorithm string) string {
func oneTimePassword(secret []byte, size, algorithm string, epoch int64) string {
decsecret := make([]byte, base32.StdEncoding.WithPadding(base32.NoPadding).DecodedLen(len(secret)))
_, err := base32.StdEncoding.WithPadding(base32.NoPadding).Decode(decsecret, secret)
//wipe(secret)
Expand All @@ -75,7 +75,7 @@ func oneTimePassword(secret []byte, size, algorithm string) string {
os.Exit(2)
}

value := toBytes(time.Now().Unix() / period)
value := toBytes((time.Now().Unix() + 30*epoch) / period)
var hash []byte
switch algorithm {
case "SHA1": // Sign the value using HMAC-SHA1
Expand Down Expand Up @@ -186,17 +186,18 @@ func addEntry(name string, secret []byte, size, algorithm string, clearscr bool)

fmt.Fprintf(os.Stderr, green+" Entry '"+yellow+name+green+"' %s\n", action)
if redirected {
totp := oneTimePassword(secret, size, algorithm)
totp := oneTimePassword(secret, size, algorithm, 0)
fmt.Println(totp)
wipe(secret)
return
}

signal.Notify(interrupt, os.Interrupt, syscall.SIGINT)
for {
totp := oneTimePassword(secret, size, algorithm)
totp := oneTimePassword(secret, size, algorithm, 0)
ntotp := oneTimePassword(secret, size, algorithm, 1)
left := period - time.Now().Unix()%period
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+yellow+totp+blue+" Validity:"+yellow+
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+green+totp+blue+" Next: "+magenta+ntotp+blue+" Validity:"+yellow+
" %2d"+blue+"s "+def+"[Press "+green+"Ctrl-C"+def+" to exit] ", left)
go func() {
<-interrupt
Expand All @@ -221,17 +222,18 @@ func showSingleTotp(secret []byte, size, algorithm string) {
secret = checkBase32(secret)
}
if redirected {
totp := oneTimePassword(secret, size, algorithm)
totp := oneTimePassword(secret, size, algorithm, 0)
wipe(secret)
fmt.Println(totp)
return
}

signal.Notify(interrupt, os.Interrupt, syscall.SIGINT)
for {
totp := oneTimePassword(secret, size, algorithm)
totp := oneTimePassword(secret, size, algorithm, 0)
ntotp := oneTimePassword(secret, size, algorithm, 1)
left := period - time.Now().Unix()%period
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+yellow+totp+blue+" Validity:"+yellow+" %2d"+blue+"s "+def+"[Press "+green+"Ctrl-C"+def+" to exit] ", left)
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+green+totp+blue+" Next: "+magenta+ntotp+blue+" Validity:"+yellow+" %2d"+blue+"s "+def+"[Press "+green+"Ctrl-C"+def+" to exit] ", left)
go func() {
<-interrupt
fmt.Fprintf(os.Stderr, cls)
Expand Down Expand Up @@ -357,13 +359,13 @@ func clipTOTP(name string) {
return
}

totp := oneTimePassword(secret, db.Entries[name].Digits, db.Entries[name].Algorithm)
totp := oneTimePassword(secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 0)
clipboard.WriteAll(totp)
left := period - time.Now().Unix()%period
fmt.Fprintf(os.Stderr, green+"TOTP of "+yellow+"'"+name+"'"+green+" copied to clipboard, valid for"+yellow+" %d "+green+"s\n", left)
}

func showTotps(regex string) {
func showTotps(regex string, next bool) {
db, err := readDb(false)
exitOnError(err, "Failure opening datafile for showing TOTPs")

Expand All @@ -376,12 +378,13 @@ func showTotps(regex string) {
}
if redirected {
for _, name := range names {
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm)
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 0)
ntotp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 1)
tag := name
if len(name) > maxNameLen {
tag = name[:maxNameLen]
}
fmt.Printf("%v %v\n", totp, tag)
fmt.Printf("%v (%v) %v\n", totp, ntotp, tag)
}
return
}
Expand All @@ -398,7 +401,16 @@ func showTotps(regex string) {

// Check display capabilities
w, h, _ := term.GetSize(int(os.Stdout.Fd()))
cols := (w + 1) / (8 + 1 + maxNameLen + 1)
cols, hdr, hdrspc := 0, "", ""
if next {
cols = (w + 1) / (8 + 1 + 8 + 1 + maxNameLen + 1)
hdr = " TOTP nextTOTP - Name"
hdrspc = fmt.Sprintf(strings.Repeat(" ", maxNameLen-6))
} else {
cols = (w + 1) / (8 + 1 + maxNameLen + 1)
hdr = " TOTP - Name"
hdrspc = fmt.Sprintf(strings.Repeat(" ", maxNameLen-4))
}
if cols < 1 {
exitOnError(errr, "Terminal too narrow to properly display entries")
}
Expand All @@ -408,23 +420,28 @@ func showTotps(regex string) {
}

sort.Strings(names)

fmtstr := "%s %-" + fmt.Sprint(maxNameLen) + "s"
for {
fmt.Fprintf(os.Stderr, cls+blue+" TOTP - Name")
fmt.Fprintf(os.Stderr, cls+blue+hdr)
for i := 1; i < cols && i < nn; i++ {
fmt.Fprintf(os.Stderr, strings.Repeat(" ", maxNameLen-1)+"TOTP - Name")
fmt.Fprintf(os.Stderr, hdrspc + hdr)
}
fmt.Fprintln(os.Stderr)
n := 0
for _, name := range names {
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm)
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 0)
totp = fmt.Sprintf("%8v", totp)
tag := name
if len(name) > maxNameLen {
tag = name[:maxNameLen]
}
fmt.Fprintf(os.Stderr, fmtstr, green+totp+def, tag)
if next {
ntotp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 1)
ntotp = fmt.Sprintf("%8v", ntotp)
fmt.Fprintf(os.Stderr, fmtstr, green+totp+magenta+ntotp+def, tag)
} else {
fmt.Fprintf(os.Stderr, fmtstr, green+totp+def, tag)
}
n += 1
if n%cols == 0 {
fmt.Fprintln(os.Stderr)
Expand Down Expand Up @@ -638,7 +655,7 @@ func importEntries(filename string) {
func main() {
self, cmd, regex, datafile, name, nname, file := "", "", "", "", "", "", ""
var secret []byte
datafileflag, sizeflag, algorithmflag, size, algorithm, ddash, cas := 0, 0, 0, "6", "SHA1", false, false
datafileflag, sizeflag, algorithmflag, size, algorithm, ddash, cas, next := 0, 0, 0, "6", "SHA1", false, false, false
o, _ := os.Stdout.Stat()
if (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
redirected = false
Expand Down Expand Up @@ -677,6 +694,10 @@ func main() {
force = true
continue
}
if arg == "-n" || arg == "--next" {
next = true
continue
}
if arg == "-d" || arg == "--datafile" {
if datafileflag > 0 {
usage("datafile already specified with -d/--datafile")
Expand Down Expand Up @@ -708,7 +729,7 @@ func main() {
return

case "show", "view":
cmd = "s" // [REGEX] [-c/--case]
cmd = "s" // [REGEX] [-c/--case] [-n/--next]
case "list", "ls":
cmd = "l" // [REGEX] [-c/--case]
case "rename", "move", "mv":
Expand Down Expand Up @@ -820,7 +841,10 @@ func main() {
}
}
// All arguments have been parsed, check
if cas && cmd != "s" && cmd != "l" {
if next && cmd != "" && cmd != "s" {
usage("flag -n/--next can only be given on show/view command")
}
if cas && cmd != "" && cmd != "s" && cmd != "l" {
usage("flag -c/--case can only be given on show/view and list/ls commands")
}
if datafileflag == 1 {
Expand All @@ -840,7 +864,7 @@ func main() {
}
switch cmd {
case "", "s":
showTotps(regex)
showTotps(regex, next)
case "l":
showNames(regex)
case "a":
Expand Down Expand Up @@ -895,8 +919,8 @@ func usage(err string) {
blue + "Datafile" + def + ": " + magenta + dbPath + def + " (default, depends on the binary's name)\n* " +
blue + "Usage" + def + ": " + magenta + self + def + " [" + green + "COMMAND" + def + "] [ " + yellow + "-d" + def + " | " + yellow + "--datafile " + cyan + " DATAFILE" + def + " ]\n" +
" == " + green + "COMMAND" + def + ":\n" +
"[ " + green + "show" + def + " | " + green + "view" + def + " ] [" + blue + "REGEX" + def + " [ " + yellow + "-c" + def + " | " + yellow + "--case" + def + " ]]\n" +
" Display all TOTPs with " + blue + "NAME" + def + "s [matching " + blue + "REGEX" + def + "] (" + green + "show" + def + "/" + green + "view" + def + " is optional).\n" +
"[ " + green + "show" + def + " | " + green + "view" + def + " ] [" + blue + "REGEX" + def + " [ " + yellow + "-c" + def + " | " + yellow + "--case" + def + " ]] [ " + yellow + "-n" + def + " | " + yellow + "--next" + def + " ]\n" +
" Display all TOTPs with " + blue + "NAME" + def + "s [matching " + blue + "REGEX" + def + "] (" + yellow + "-n" + def + "/" + yellow + "--next" + def + ": show next TOTP).\n" +
green + "list" + def + " | " + green + "ls" + def + " [" + blue + "REGEX" + def + " [ " + yellow + "-c" + def + " | " + yellow + "--case" + def + " ]]\n" +
" List all " + blue + "NAME" + def + "s [matching " + blue + "REGEX" + def + "].\n" +
green + "add" + def + " | " + green + "insert" + def + " | " + green + "entry " + blue + "NAME" + def + " [" + yellow + "TOTP-OPTIONS" + def + "] [ " + yellow + "-f" + def + " | " + yellow + "--force" + def + " ] [" + blue + "SECRET" + def + "]\n" +
Expand Down

0 comments on commit 13f5f3e

Please sign in to comment.