Created
July 19, 2022 00:36
-
-
Save pgaskin/704e027e346a29ff7999ada90b528453 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Command otpprint prints TOTP codes. | |
package main | |
import ( | |
"crypto/hmac" | |
"crypto/sha1" | |
"encoding/base32" | |
"encoding/binary" | |
"fmt" | |
"hash" | |
"math" | |
"os" | |
"strconv" | |
"strings" | |
"time" | |
) | |
func main() { | |
if n := len(os.Args); n < 2 || n > 4 { | |
fmt.Printf("usage: %s secret [text|html] [now|YYYY-MM-DD|unix]\n", os.Args[0]) | |
os.Exit(2) | |
} | |
secret, err := base32.StdEncoding.DecodeString(strings.ToUpper(strings.ReplaceAll(os.Args[1], " ", ""))) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "fatal: invalid base32 secret: %v\n", err) | |
os.Exit(1) | |
} | |
var html bool | |
if len(os.Args) > 2 { | |
switch m := os.Args[2]; m { | |
case "h", "html": | |
html = true | |
case "t", "text": | |
html = false | |
default: | |
fmt.Fprintf(os.Stderr, "fatal: invalid mode %q\n", m) | |
os.Exit(1) | |
} | |
} | |
var dt time.Time | |
if len(os.Args) > 3 && os.Args[3] != "now" { | |
if v, err := strconv.ParseInt(os.Args[3], 10, 64); err != nil { | |
if v, err := time.ParseInLocation("2006-01-02", os.Args[3], time.Local); err != nil { | |
fmt.Fprintf(os.Stderr, "fatal: invalid start date %q\n", os.Args[3]) | |
os.Exit(1) | |
} else { | |
dt = v | |
} | |
} else { | |
dt = time.Unix(v, 0) | |
} | |
} else { | |
dt = time.Now() | |
} | |
yy, mm, dd := dt.Date() | |
if html { | |
fmt.Println(`<!DOCTYPE html>`) | |
fmt.Println(`<html lang="en">`) | |
fmt.Println(`<head>`) | |
fmt.Println(`<meta charset="utf-8">`) | |
fmt.Println(`<title>TOTP</title>`) | |
fmt.Println(`<style>` + strings.TrimSpace(strings.NewReplacer("\t", "", "\n", "").Replace(` | |
* { | |
box-sizing: border-box; | |
} | |
main { | |
width: 8.5in; | |
height: 11in; | |
overflow: hidden; | |
color: #000; | |
} | |
@media screen { | |
html { | |
background: #444; | |
padding: .1in; | |
} | |
main { | |
margin: 0 auto; | |
box-shadow: 0 0 6px 0 #000; | |
background: #fff; | |
} | |
} | |
@page { | |
margin: 0; | |
size: letter; | |
} | |
main { | |
font-family: Roboto; | |
font-size: 6pt; | |
padding: .1in; | |
line-height: 1; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
table { | |
border-collapse: collapse; | |
flex: 0 0 auto; | |
border: 1px solid #000; | |
} | |
th, td { | |
padding: .9pt; | |
white-space: nowrap; | |
} | |
th { | |
font-weight: bold; | |
} | |
tr.rh > * { | |
background: #ddd; | |
} | |
tr.rg > * { | |
border-bottom: 1px solid #000; | |
} | |
`)) + `</style>`) | |
fmt.Println(`<body>`) | |
fmt.Println(`<main>`) | |
fmt.Println(`<table>`) | |
} | |
for i := -1; i < 96; i++ { | |
var ( | |
rh = i == -1 | |
rg = (i+1)%6 == 0 | |
) | |
if html { | |
fmt.Print(`<tr`) | |
if rh || rg { | |
fmt.Print(` class="`) | |
if rh { | |
fmt.Print("rh") | |
} | |
if rh && rg { | |
fmt.Print(" ") | |
} | |
if rg { | |
fmt.Print("rg") | |
} | |
fmt.Print(`"`) | |
} | |
fmt.Print(`>`) | |
} else { | |
if rh { | |
fmt.Print("\033[1m") | |
} | |
if rg { | |
fmt.Print("\033[4m") | |
} | |
} | |
for j := -1; j < 26; j++ { | |
var ld, lm int | |
if i != -1 { | |
lm = i * 10 | |
} | |
if j != -1 { | |
ld = j | |
} | |
lt := time.Date(yy, mm, dd+ld, 6, lm, 0, 0, time.Local) | |
var ct string | |
switch { | |
case i == -1 && j == -1: | |
ct = lt.Format("Jan") | |
case i == -1: | |
ct = lt.Format("Mon _2") | |
case j == -1: | |
ct = lt.Format("15:04") | |
default: | |
ct = hotp(totp(lt, 0), secret, 0, nil) | |
} | |
if html { | |
if i == -1 || j == -1 { | |
fmt.Printf(`<th>%s</th>`, ct) | |
} else { | |
fmt.Printf(`<td>%s</td>`, ct) | |
} | |
} else { | |
if j == -1 { | |
fmt.Printf("% -5s", ct) | |
} else { | |
fmt.Printf(" % -6s", ct) | |
} | |
} | |
} | |
if html { | |
fmt.Println(`</tr>`) | |
} else { | |
fmt.Println("\033[0m") | |
} | |
} | |
if html { | |
fmt.Println(`</table>`) | |
fmt.Println(`</main>`) | |
} | |
} | |
func totp(t time.Time, s time.Duration) uint64 { | |
if t.IsZero() { | |
t = time.Now() | |
} | |
if s == 0 { | |
s = time.Second * 30 | |
} | |
return uint64(math.Floor(float64(t.Unix()) / s.Seconds())) | |
} | |
func hotp(c uint64, k []byte, n int, h func() hash.Hash) string { | |
if n == 0 { | |
n = 6 | |
} | |
if h == nil { | |
h = sha1.New | |
} | |
if n <= 0 || n > 8 { | |
panic("otp: must be 0 < n <= 8") | |
} | |
if len(k) == 0 { | |
panic("otp: key must not be empty") | |
} | |
hsh := hmac.New(h, k) | |
binary.Write(hsh, binary.BigEndian, c) | |
dst := hsh.Sum(nil) | |
off := dst[len(dst)-1] & 0xf | |
val := int64(((int(dst[off]))&0x7f)<<24 | | |
((int(dst[off+1] & 0xff)) << 16) | | |
((int(dst[off+2] & 0xff)) << 8) | | |
((int(dst[off+3]) & 0xff) << 0)) | |
return fmt.Sprintf("%0*d", n, val%int64(math.Pow10(n))) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment