Skip to content

Commit 13f5f3e

Browse files
committed
Option to display the next TOTP too
1 parent 069461a commit 13f5f3e

File tree

4 files changed

+61
-35
lines changed

4 files changed

+61
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/dist
2+
/twofat

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="https://raw.githubusercontent.com/pepa65/twofat/master/twofat.png" width="96" alt="twofat icon" align="right">
33

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

5657
## Usage
5758
```
58-
twofat v2.1.1 - Manage TOTPs from CLI
59+
twofat v2.2.0 - Manage TOTPs from CLI
5960
The CLI is interactive & colorful, output to Stderr. Password can be piped in.
6061
When output is redirected, only pertinent plain text is sent to Stdout.
6162
* Repo: github.com/pepa65/twofat <pepa65@passchier.net>
6263
* Datafile: ~/.twofat.enc (default, depends on the binary's name)
6364
* Usage: twofat [COMMAND] [ -d | --datafile DATAFILE ]
6465
== COMMAND:
65-
[ show | view ] [REGEX [ -c | --case ]]
66-
Display all TOTPs with NAMEs [matching REGEX] (show/view is optional).
66+
[ show | view ] [REGEX [ -c | --case ]] [ -n | --next ]
67+
Display all TOTPs with NAMEs [matching REGEX] (-n/--next: show next TOTP).
6768
list | ls [REGEX [ -c | --case ]]
6869
List all NAMEs [matching REGEX].
6970
add | insert | entry NAME [TOTP-OPTIONS] [ -f | --force ] [SECRET]

config.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ const (
2525
nonceSize = 12
2626
pwRetry = 3
2727
cls = "\033c"
28-
red = "\033[1m\033[31m"
29-
green = "\033[1m\033[32m"
30-
yellow = "\033[1m\033[33m"
31-
blue = "\033[1m\033[34m"
32-
magenta = "\033[1m\033[35m"
33-
cyan = "\033[1m\033[36m"
28+
red = "\033[1;31m"
29+
green = "\033[1;32m"
30+
yellow = "\033[1;33m"
31+
blue = "\033[1;34m"
32+
magenta = "\033[1;35m"
33+
cyan = "\033[1;36m"
3434
def = "\033[0m"
3535
)
3636

main.go

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
)
2727

2828
const (
29-
version = "2.1.1"
29+
version = "2.2.0"
3030
maxNameLen = 20
3131
period = 30
3232
)
@@ -66,7 +66,7 @@ func wipe(bytes []byte) {
6666
runtime.GC()
6767
}
6868

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

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

187187
fmt.Fprintf(os.Stderr, green+" Entry '"+yellow+name+green+"' %s\n", action)
188188
if redirected {
189-
totp := oneTimePassword(secret, size, algorithm)
189+
totp := oneTimePassword(secret, size, algorithm, 0)
190190
fmt.Println(totp)
191191
wipe(secret)
192192
return
193193
}
194194

195195
signal.Notify(interrupt, os.Interrupt, syscall.SIGINT)
196196
for {
197-
totp := oneTimePassword(secret, size, algorithm)
197+
totp := oneTimePassword(secret, size, algorithm, 0)
198+
ntotp := oneTimePassword(secret, size, algorithm, 1)
198199
left := period - time.Now().Unix()%period
199-
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+yellow+totp+blue+" Validity:"+yellow+
200+
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+green+totp+blue+" Next: "+magenta+ntotp+blue+" Validity:"+yellow+
200201
" %2d"+blue+"s "+def+"[Press "+green+"Ctrl-C"+def+" to exit] ", left)
201202
go func() {
202203
<-interrupt
@@ -221,17 +222,18 @@ func showSingleTotp(secret []byte, size, algorithm string) {
221222
secret = checkBase32(secret)
222223
}
223224
if redirected {
224-
totp := oneTimePassword(secret, size, algorithm)
225+
totp := oneTimePassword(secret, size, algorithm, 0)
225226
wipe(secret)
226227
fmt.Println(totp)
227228
return
228229
}
229230

230231
signal.Notify(interrupt, os.Interrupt, syscall.SIGINT)
231232
for {
232-
totp := oneTimePassword(secret, size, algorithm)
233+
totp := oneTimePassword(secret, size, algorithm, 0)
234+
ntotp := oneTimePassword(secret, size, algorithm, 1)
233235
left := period - time.Now().Unix()%period
234-
fmt.Fprintf(os.Stderr, blue+"\r TOTP: "+yellow+totp+blue+" Validity:"+yellow+" %2d"+blue+"s "+def+"[Press "+green+"Ctrl-C"+def+" to exit] ", left)
236+
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)
235237
go func() {
236238
<-interrupt
237239
fmt.Fprintf(os.Stderr, cls)
@@ -357,13 +359,13 @@ func clipTOTP(name string) {
357359
return
358360
}
359361

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

366-
func showTotps(regex string) {
368+
func showTotps(regex string, next bool) {
367369
db, err := readDb(false)
368370
exitOnError(err, "Failure opening datafile for showing TOTPs")
369371

@@ -376,12 +378,13 @@ func showTotps(regex string) {
376378
}
377379
if redirected {
378380
for _, name := range names {
379-
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm)
381+
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 0)
382+
ntotp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 1)
380383
tag := name
381384
if len(name) > maxNameLen {
382385
tag = name[:maxNameLen]
383386
}
384-
fmt.Printf("%v %v\n", totp, tag)
387+
fmt.Printf("%v (%v) %v\n", totp, ntotp, tag)
385388
}
386389
return
387390
}
@@ -398,7 +401,16 @@ func showTotps(regex string) {
398401

399402
// Check display capabilities
400403
w, h, _ := term.GetSize(int(os.Stdout.Fd()))
401-
cols := (w + 1) / (8 + 1 + maxNameLen + 1)
404+
cols, hdr, hdrspc := 0, "", ""
405+
if next {
406+
cols = (w + 1) / (8 + 1 + 8 + 1 + maxNameLen + 1)
407+
hdr = " TOTP nextTOTP - Name"
408+
hdrspc = fmt.Sprintf(strings.Repeat(" ", maxNameLen-6))
409+
} else {
410+
cols = (w + 1) / (8 + 1 + maxNameLen + 1)
411+
hdr = " TOTP - Name"
412+
hdrspc = fmt.Sprintf(strings.Repeat(" ", maxNameLen-4))
413+
}
402414
if cols < 1 {
403415
exitOnError(errr, "Terminal too narrow to properly display entries")
404416
}
@@ -408,23 +420,28 @@ func showTotps(regex string) {
408420
}
409421

410422
sort.Strings(names)
411-
412423
fmtstr := "%s %-" + fmt.Sprint(maxNameLen) + "s"
413424
for {
414-
fmt.Fprintf(os.Stderr, cls+blue+" TOTP - Name")
425+
fmt.Fprintf(os.Stderr, cls+blue+hdr)
415426
for i := 1; i < cols && i < nn; i++ {
416-
fmt.Fprintf(os.Stderr, strings.Repeat(" ", maxNameLen-1)+"TOTP - Name")
427+
fmt.Fprintf(os.Stderr, hdrspc + hdr)
417428
}
418429
fmt.Fprintln(os.Stderr)
419430
n := 0
420431
for _, name := range names {
421-
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm)
432+
totp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 0)
422433
totp = fmt.Sprintf("%8v", totp)
423434
tag := name
424435
if len(name) > maxNameLen {
425436
tag = name[:maxNameLen]
426437
}
427-
fmt.Fprintf(os.Stderr, fmtstr, green+totp+def, tag)
438+
if next {
439+
ntotp := oneTimePassword(db.Entries[name].Secret, db.Entries[name].Digits, db.Entries[name].Algorithm, 1)
440+
ntotp = fmt.Sprintf("%8v", ntotp)
441+
fmt.Fprintf(os.Stderr, fmtstr, green+totp+magenta+ntotp+def, tag)
442+
} else {
443+
fmt.Fprintf(os.Stderr, fmtstr, green+totp+def, tag)
444+
}
428445
n += 1
429446
if n%cols == 0 {
430447
fmt.Fprintln(os.Stderr)
@@ -638,7 +655,7 @@ func importEntries(filename string) {
638655
func main() {
639656
self, cmd, regex, datafile, name, nname, file := "", "", "", "", "", "", ""
640657
var secret []byte
641-
datafileflag, sizeflag, algorithmflag, size, algorithm, ddash, cas := 0, 0, 0, "6", "SHA1", false, false
658+
datafileflag, sizeflag, algorithmflag, size, algorithm, ddash, cas, next := 0, 0, 0, "6", "SHA1", false, false, false
642659
o, _ := os.Stdout.Stat()
643660
if (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
644661
redirected = false
@@ -677,6 +694,10 @@ func main() {
677694
force = true
678695
continue
679696
}
697+
if arg == "-n" || arg == "--next" {
698+
next = true
699+
continue
700+
}
680701
if arg == "-d" || arg == "--datafile" {
681702
if datafileflag > 0 {
682703
usage("datafile already specified with -d/--datafile")
@@ -708,7 +729,7 @@ func main() {
708729
return
709730

710731
case "show", "view":
711-
cmd = "s" // [REGEX] [-c/--case]
732+
cmd = "s" // [REGEX] [-c/--case] [-n/--next]
712733
case "list", "ls":
713734
cmd = "l" // [REGEX] [-c/--case]
714735
case "rename", "move", "mv":
@@ -820,7 +841,10 @@ func main() {
820841
}
821842
}
822843
// All arguments have been parsed, check
823-
if cas && cmd != "s" && cmd != "l" {
844+
if next && cmd != "" && cmd != "s" {
845+
usage("flag -n/--next can only be given on show/view command")
846+
}
847+
if cas && cmd != "" && cmd != "s" && cmd != "l" {
824848
usage("flag -c/--case can only be given on show/view and list/ls commands")
825849
}
826850
if datafileflag == 1 {
@@ -840,7 +864,7 @@ func main() {
840864
}
841865
switch cmd {
842866
case "", "s":
843-
showTotps(regex)
867+
showTotps(regex, next)
844868
case "l":
845869
showNames(regex)
846870
case "a":
@@ -895,8 +919,8 @@ func usage(err string) {
895919
blue + "Datafile" + def + ": " + magenta + dbPath + def + " (default, depends on the binary's name)\n* " +
896920
blue + "Usage" + def + ": " + magenta + self + def + " [" + green + "COMMAND" + def + "] [ " + yellow + "-d" + def + " | " + yellow + "--datafile " + cyan + " DATAFILE" + def + " ]\n" +
897921
" == " + green + "COMMAND" + def + ":\n" +
898-
"[ " + green + "show" + def + " | " + green + "view" + def + " ] [" + blue + "REGEX" + def + " [ " + yellow + "-c" + def + " | " + yellow + "--case" + def + " ]]\n" +
899-
" Display all TOTPs with " + blue + "NAME" + def + "s [matching " + blue + "REGEX" + def + "] (" + green + "show" + def + "/" + green + "view" + def + " is optional).\n" +
922+
"[ " + green + "show" + def + " | " + green + "view" + def + " ] [" + blue + "REGEX" + def + " [ " + yellow + "-c" + def + " | " + yellow + "--case" + def + " ]] [ " + yellow + "-n" + def + " | " + yellow + "--next" + def + " ]\n" +
923+
" Display all TOTPs with " + blue + "NAME" + def + "s [matching " + blue + "REGEX" + def + "] (" + yellow + "-n" + def + "/" + yellow + "--next" + def + ": show next TOTP).\n" +
900924
green + "list" + def + " | " + green + "ls" + def + " [" + blue + "REGEX" + def + " [ " + yellow + "-c" + def + " | " + yellow + "--case" + def + " ]]\n" +
901925
" List all " + blue + "NAME" + def + "s [matching " + blue + "REGEX" + def + "].\n" +
902926
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" +

0 commit comments

Comments
 (0)