Skip to content

Instantly share code, notes, and snippets.

@alexaandru
Last active February 4, 2025 18:11
Show Gist options
  • Save alexaandru/88b529fb1474646432812f036633fe37 to your computer and use it in GitHub Desktop.
Save alexaandru/88b529fb1474646432812f036633fe37 to your computer and use it in GitHub Desktop.
OTP cli
[email protected] namespaces="git" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCx0x2FqQPAESrRfU8+TqqikuqB1F4OLDMkGdiZzsN19/59lLGPsZgQfejjHiujCWPRb0Tpz2Jv2mB6QcUVuK/gfiMbltwCdcDNCCa0I42xOgIHoYVatry7D8mZcy+t+e1TALrEUPCod4BEn8oLLnsGy6siDzv2hWINbIs2XLCvsOisa6xERgM1Joezo5+8IshSeP9felv6PqbfKEUWV0zfsoLbxJ0WAAM8T14OA/GIm12Dmg4fNWad70Bp1Yq/0W00vpZ9up+p8kMklZYlK6QHwrIw2gzVqsIL9x3FXgVw7KFRfV1Gp1W7V2S2sumzUW/BIztA0JOWqZnd0CMsSEyX
// Install with:
// export X=88b529fb1474646432812f036633fe37.git
// go install gist.github.com/alexaandru/$X@latest && mv $(which $X) $(dirname $(which $X))/otp
module gist.github.com/alexaandru/88b529fb1474646432812f036633fe37.git
go 1.23.3
require github.com/pquerna/otp v1.4.0
require github.com/boombuler/barcode v1.0.2 // indirect
retract v1.1.0
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
install:
@go install .
@export X=88b529fb1474646432812f036633fe37.git; mv $$(which $$X) $$(dirname $$(which $$X))/otp; \
ln -sf $$(which otp) $$(dirname $$(which otp))/code; \
ln -sf $$(which otp) $$(dirname $$(which otp))/verify
package main
import (
"cmp"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"time"
"github.com/pquerna/otp/totp"
)
type Key struct {
Username,
Secret,
Issuer,
Period,
Algorithm,
Digits string
}
type Keys map[string]Key
var (
otpFile = cmp.Or(os.Getenv("OTP_FILE"), "~/Personal/Other/otp.json")
keys = Keys{}
cmd, scope, target string
)
func clipboardWrite(s string) {
cmd := exec.Command("bash", "-c", fmt.Sprintf("echo %s|xclip", s))
if err := cmd.Run(); err != nil {
fmt.Println("WARN: could not copy to clipboard:", err)
}
}
func code(scope string) (err error) {
k := keys[scope]
code, err := totp.GenerateCode(k.Secret, time.Now())
if err != nil {
return
}
clipboardWrite(code)
fmt.Println(scope, "OK")
return
}
func validate(scope, target string) {
k := keys[scope]
fmt.Println(totp.Validate(target, k.Secret))
}
func main() {
if cmd != "export" && cmd != "complete" {
if _, ok := keys[scope]; !ok {
die("secret not available: " + scope)
}
}
switch cmd {
case "code":
die(code(scope))
case "verify":
validate(scope, target)
case "export":
keys.export()
case "complete":
keys.completion()
default:
die("unknown command: " + cmd)
}
}
func (kx Keys) completion() {
fmt.Println(strings.Join(slices.Collect(maps.Keys(kx)), " "))
}
func parseCmdline() error {
args := slices.Clone(os.Args)
self := filepath.Base(os.Args[0])
if self == "code" || self == "verify" {
args = slices.Concat([]string{self, self}, args[1:])
}
if len(args) == 2 && (args[1] == "export" || args[1] == "complete") {
cmd = args[1]
return nil
}
if len(args) < 3 {
return errors.New("must pass a command and a target/scope")
}
cmd, scope = args[1], args[2]
if cmd == "verify" {
if len(args) < 4 {
return errors.New("verify requires a code")
} else {
target = args[3]
}
}
return nil
}
func die(err any) {
if err == nil {
return
}
fmt.Println("ERROR:", err)
os.Exit(1)
}
func init() {
if strings.HasPrefix(otpFile, "~/") {
home, _ := os.UserHomeDir()
otpFile = filepath.Join(home, otpFile[2:])
}
content, err := os.ReadFile(otpFile)
die(err)
var keys_ []Key
die(json.Unmarshal(content, &keys_))
for _, k := range keys_ {
keys[k.Username] = k
}
die(parseCmdline())
}
package main
import (
"fmt"
"net/url"
)
func (kx Keys) export() {
for _, k := range keys {
fmt.Println(k.export())
}
}
func (k Key) export() string {
return fmt.Sprintf("otpauth://totp/%s?secret=%s&algorithm=%s&digits=%s&period=%s",
url.QueryEscape(k.Username+":"+k.Issuer), k.Secret, k.Algorithm, k.Digits, k.Period)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment