Compare commits

...

41 Commits

Author SHA1 Message Date
e2a3081ce5 cmd: add certser command. 2025-11-17 07:18:46 -08:00
3149d958f4 cmd: add certser 2025-11-17 06:55:20 -08:00
f296344acf twofactor: linting fixes 2025-11-16 21:51:38 -08:00
3fb2d88a3f go get rsc.io/qr 2025-11-16 20:44:13 -08:00
150c02b377 Fix subtree. 2025-11-16 18:55:43 -08:00
83f88c49fe Import twofactor. 2025-11-16 18:45:34 -08:00
7c437ac45f Add 'twofactor/' from commit 'c999bf35b0e47de4f63d59abbe0d7efc76c13ced'
git-subtree-dir: twofactor
git-subtree-mainline: 4dc135cfe0
git-subtree-split: c999bf35b0
2025-11-16 18:43:03 -08:00
c999bf35b0 linter fixes. 2025-11-16 18:39:18 -08:00
4dc135cfe0 Update CHANGELOG for v1.11.2. 2025-11-16 13:18:38 -08:00
790113e189 cmd: refactor for code reuse. 2025-11-16 13:15:08 -08:00
0dcd18c6f1 clean up code
- travisci is long dead
- golangci-lint the repo
2024-12-02 13:47:43 -08:00
024d552293 add circle ci config 2024-12-02 13:26:34 -08:00
9cd2ced695 There are different keys for different hash sizes. 2024-12-02 13:16:32 -08:00
619c08a13f Update travis to latest Go versions. 2020-10-31 08:06:11 -07:00
944a57bf0e Switch to go modules. 2020-10-31 07:41:31 -07:00
0857b29624 Actually support clock mocking. 2020-10-31 07:24:34 -07:00
CodeLingo Bot
e95404bfc5 Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2020-10-30 14:57:11 -07:00
ujjwalsh
924654e7c4 Added Support for Linux on Power 2020-10-30 07:50:32 -07:00
9e0979e07f Support clock mocking.
This addresses #15.
2018-12-07 08:23:01 -08:00
Aaron Bieber
bbc82ff8de Pad non-padded secrets. This lets us continue building on <= go1.8.
- Add tests for secrets using various padding methods.
- Add a new method/test to append padding to non-padded secrets.
2018-04-18 13:39:21 -07:00
Aaron Bieber
5fd928f69a Decode using WithPadding as pointed out by @gl-sergei.
This makes us print the same 6 digits as oathtool for non-padded
secrets like "a6mryljlbufszudtjdt42nh5by".
2018-04-18 13:39:21 -07:00
Aaron Bieber
acefe4a3b9 Don't assume our secret is base32 encoded.
According to https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm
secrets are only base32 encoded in gauthenticator and gauth friendly providers.
2018-04-16 13:14:03 -07:00
a1452cebc9 Travis requires a string for Go 1.10. 2018-04-16 13:03:16 -07:00
6e9812e6f5 Vendor dependencies and add more tests. 2018-04-16 13:03:16 -07:00
Aaron Bieber
8c34415c34 add readme 2018-04-16 12:52:39 -07:00
Paul TREHIOU
2cf2c15def Case insensitive algorithm match 2018-04-16 12:43:27 -07:00
Aaron Bieber
eaad1884d4 Make sure our secret is always uppercase
Non-uppercase secrets that are base32 encoded will fial to decode
unless we upper them.
2017-09-17 18:19:23 -07:00
5d57d844d4 Add license (MIT). 2017-04-13 10:02:20 -07:00
Kyle Isom
31b9d175dd Add travis config. 2017-03-20 14:20:49 -07:00
Aaron Bieber
79e106da2e point to new qr location 2017-03-20 13:18:56 -07:00
Kyle Isom
939b1bc272 Updating imports. 2015-08-12 12:29:34 -07:00
Kyle
89e74f390b Add doc.go, finish YubiKey removal. 2014-04-24 20:43:13 -06:00
Kyle
7881b6fdfc Remove test TOTP client. 2014-04-24 20:40:44 -06:00
Kyle
5bef33245f Remove YubiKey (not currently functional). 2014-04-24 20:37:53 -06:00
Kyle
84250b0501 More documentation. 2014-04-24 20:37:00 -06:00
Kyle Isom
459e9f880f Add function to build Google TOTPs from secret 2014-04-23 16:54:16 -07:00
Kyle Isom
0982f47ce3 Add last night's progress.
Basic functionality for HOTP, TOTP, and YubiKey OTP. Still need YubiKey
HMAC, serialisation, check, and scan.
2013-12-20 17:00:01 -07:00
Kyle Isom
1dec15fd11 add missing files
new files are
	oath_test
	totp code
2013-12-19 00:21:26 -07:00
Kyle Isom
2ee9cae5ba Add basic Google Authenticator TOTP client. 2013-12-19 00:20:00 -07:00
Kyle Isom
dc04475120 HOTP and TOTP-SHA-1 working.
why the frak aren't the SHA-256 and SHA-512 variants working
2013-12-19 00:04:26 -07:00
Kyle Isom
dbbd5116b5 Initial import.
Basic HOTP functionality.
2013-12-18 21:48:14 -07:00
23 changed files with 1256 additions and 67 deletions

View File

@@ -12,6 +12,12 @@
version: "2"
output:
sort-order:
- file
- linter
- severity
issues:
# Maximum count of issues with the same text.
# Set to 0 to disable.
@@ -454,6 +460,8 @@ linters:
- -QF1008
# We often explicitly enable old/deprecated ciphers for research.
- -SA1019
# Covered by revive.
- -ST1003
usetesting:
# Enable/disable `os.TempDir()` detections.
@@ -472,6 +480,8 @@ linters:
rules:
- path: 'ahash/ahash.go'
linters: [ staticcheck, gosec ]
- path: 'twofactor/.*.go'
linters: [ exhaustive, mnd, revive ]
- path: 'backoff/backoff_test.go'
linters: [ testpackage ]
- path: 'dbg/dbg_test.go'

View File

@@ -1,5 +1,47 @@
CHANGELOG
v1.13.0 - 2025-11-16
Add:
- cmd/certser: print serial numbers for certificates.
- lib/HexEncode: add a new hex encode function handling multiple output
formats, including with and without colons.
v1.12.4 - 2025-11-16
Changed:
- Linting fixes for twofactor that were previously masked.
v1.12.3 erroneously tagged and pushed
v1.12.2 - 2025-11-16
Changed:
- add rsc.io/qr dependency for twofactor.
v1.12.1 - 2025-11-16
Changed:
- twofactor: Remove go.{mod,sum}.
v1.12.0 - 2025-11-16
Added
- twofactor: the github.com/kisom/twofactor repo has been subtree'd
into this repo.
v1.11.2 - 2025-11-16
Changed
- cmd/ski, cmd/csrpubdump, cmd/tlskeypair: centralize
certificate/private-key/CSR parsing by reusing certlib helpers.
This reduces duplication and improves consistency across commands.
- csr: CSR parsing in the above commands now uses certlib.ParseCSR,
which verifies CSR signatures (behavioral hardening compared to
prior parsing without signature verification).
v1.11.1 - 2025-11-16
Changed

View File

@@ -2,39 +2,52 @@ GOUTILS
This is a collection of small utility code I've written in Go; the `cmd/`
directory has a number of command-line utilities. Rather than keep all
of these in superfluous repositories of their own, or rewriting them
of these in superfluous repositories of their own or rewriting them
for each project, I'm putting them here.
The project can be built with the standard Go tooling, or it can be built
with Bazel.
The project can be built with the standard Go tooling.
Contents:
ahash/ Provides hashes from string algorithm specifiers.
assert/ Error handling, assertion-style.
backoff/ Implementation of an intelligent backoff strategy.
cache/ Implementations of various caches.
lru/ Least-recently-used cache.
mru/ Most-recently-used cache.
certlib/ Library for working with TLS certificates.
cmd/
atping/ Automated TCP ping, meant for putting in cronjobs.
certchain/ Display the certificate chain from a
TLS connection.
ca-signed/ Validate whether a certificate is signed by a CA.
cert-bundler/
Create certificate bundles from a source of PEM
certificates.
cert-revcheck/
Check whether a certificate has been revoked or is
expired.
certchain/ Display the certificate chain from a TLS connection.
certdump/ Dump certificate information.
certexpiry/ Print a list of certificate subjects and expiry times
or warn about certificates expiring within a certain
window.
certverify/ Verify a TLS X.509 certificate, optionally printing
certverify/ Verify a TLS X.509 certificate file, optionally printing
the time to expiry and checking for revocations.
clustersh/ Run commands or transfer files across multiple
servers via SSH.
cruntar/ Untar an archive with hard links, copying instead of
cruntar/ (Un)tar an archive with hard links, copying instead of
linking.
csrpubdump/ Dump the public key from an X.509 certificate request.
data_sync/ Sync the user's homedir to external storage.
diskimg/ Write a disk image to a device.
dumpbytes/ Dump the contents of a file as hex bytes, printing it as
a Go []byte literal.
eig/ EEPROM image generator.
fragment/ Print a fragment of a file.
host/ Go imlpementation of the host(1) command.
jlp/ JSON linter/prettifier.
kgz/ Custom gzip compressor / decompressor that handles 99%
of my use cases.
minmax/ Generate a minmax code for use in uLisp.
parts/ Simple parts database management for my collection of
electronic components.
pem2bin/ Dump the binary body of a PEM-encoded block.
@@ -44,41 +57,45 @@ Contents:
in a bundle.
renfnv/ Rename a file to base32-encoded 64-bit FNV-1a hash.
rhash/ Compute the digest of remote files.
rolldie/ Roll some dice.
showimp/ List the external (e.g. non-stdlib and outside the
current working directory) imports for a Go file.
ski Display the SKI for PEM-encoded TLS material.
sprox/ Simple TCP proxy.
stealchain/ Dump the verified chain from a TLS
connection to a server.
stealchain- Dump the verified chain from a TLS
server/ connection from a client.
stealchain/ Dump the verified chain from a TLS connection to a
server.
stealchain-server/
Dump the verified chain from a TLS connection from
from a client.
subjhash/ Print or match subject info from a certificate.
tlsinfo/ Print information about a TLS connection (the TLS version
and cipher suite).
tlskeypair/ Check whether a TLS certificate and key file match.
utc/ Convert times to UTC.
yamll/ A small YAML linter.
zsearch/ Search for a string in directory of gzipped files.
config/ A simple global configuration system where configuration
data is pulled from a file or an environment variable
transparently.
iniconf/ A simple INI-style configuration system.
dbg/ A debug printer.
die/ Death of a program.
fileutil/ Common file functions.
lib/ Commonly-useful functions for writing Go programs.
log/ A syslog library.
logging/ A logging library.
mwc/ MultiwriteCloser implementation.
rand/ Utilities for working with math/rand.
sbuf/ A byte buffer that can be wiped.
seekbuf/ A read-seekable byte buffer.
syslog/ Syslog-type logging.
tee/ Emulate tee(1)'s functionality in io.Writers.
testio/ Various I/O utilities useful during testing.
testutil/ Various utility functions useful during testing.
twofactor/ Two-factor authentication.
Each program should have a small README in the directory with more
information.
All code here is licensed under the ISC license.
All code here is licensed under the Apache 2.0 license.
Error handling
--------------
@@ -99,7 +116,7 @@ Examples:
```
cert, err := certlib.LoadCertificate(path)
if err != nil {
// sentinel match
// sentinel match:
if errors.Is(err, certerr.ErrEmptyCertificate) {
// handle empty input
}
@@ -116,5 +133,3 @@ if err != nil {
}
}
```
Avoid including sensitive data (keys, passwords, tokens) in error messages.

51
cmd/certser/main.go Normal file
View File

@@ -0,0 +1,51 @@
package main
import (
"crypto/x509"
"flag"
"fmt"
"strings"
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
)
const displayInt lib.HexEncodeMode = iota
func parseDisplayMode(mode string) lib.HexEncodeMode {
mode = strings.ToLower(mode)
if mode == "int" {
return displayInt
}
return lib.ParseHexEncodeMode(mode)
}
func serialString(cert *x509.Certificate, mode lib.HexEncodeMode) string {
if mode == displayInt {
return cert.SerialNumber.String()
}
return lib.HexEncode(cert.SerialNumber.Bytes(), mode)
}
func main() {
displayAs := flag.String("d", "int", "display mode (int, hex, uhex)")
showExpiry := flag.Bool("e", false, "show expiry date")
flag.Parse()
displayMode := parseDisplayMode(*displayAs)
for _, arg := range flag.Args() {
cert, err := certlib.LoadCertificate(arg)
die.If(err)
fmt.Printf("%s: %s", arg, serialString(cert, displayMode))
if *showExpiry {
fmt.Printf(" (%s)", cert.NotAfter.Format("2006-01-02"))
}
fmt.Println()
}
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"os"
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die"
)
@@ -19,14 +20,7 @@ func main() {
in, err := os.ReadFile(fileName)
die.If(err)
if p, _ := pem.Decode(in); p != nil {
if p.Type != "CERTIFICATE REQUEST" {
die.With("INVALID FILE TYPE")
}
in = p.Bytes
}
csr, err := x509.ParseCertificateRequest(in)
csr, _, err := certlib.ParseCSR(in)
die.If(err)
out, err := x509.MarshalPKIXPublicKey(csr.PublicKey)

View File

@@ -2,7 +2,6 @@ package main
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha1" // #nosec G505
@@ -16,6 +15,7 @@ import (
"os"
"strings"
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/goutils/lib"
)
@@ -83,28 +83,19 @@ func parse(path string) ([]byte, string, string) {
}
func parseKey(data []byte) ([]byte, string) {
privInterface, err := x509.ParsePKCS8PrivateKey(data)
priv, err := certlib.ParsePrivateKeyDER(data)
if err != nil {
privInterface, err = x509.ParsePKCS1PrivateKey(data)
if err != nil {
privInterface, err = x509.ParseECPrivateKey(data)
if err != nil {
die.With("couldn't parse private key.")
}
}
die.If(err)
}
var priv crypto.Signer
var kt string
switch p := privInterface.(type) {
case *rsa.PrivateKey:
priv = p
switch priv.Public().(type) {
case *rsa.PublicKey:
kt = keyTypeRSA
case *ecdsa.PrivateKey:
priv = p
case *ecdsa.PublicKey:
kt = keyTypeECDSA
default:
die.With("unknown private key type %T", privInterface)
die.With("unknown private key type %T", priv)
}
public, err := x509.MarshalPKIXPublicKey(priv.Public())
@@ -134,7 +125,8 @@ func parseCertificate(data []byte) ([]byte, string) {
}
func parseCSR(data []byte) ([]byte, string) {
csr, err := x509.ParseCertificateRequest(data)
// Use certlib to support both PEM and DER and to centralize validation.
csr, _, err := certlib.ParseCSR(data)
die.If(err)
pub := csr.PublicKey

View File

@@ -13,6 +13,7 @@ import (
"fmt"
"os"
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die"
)
@@ -124,34 +125,14 @@ func loadKey(path string) (crypto.Signer, error) {
}
in = bytes.TrimSpace(in)
p, _ := pem.Decode(in)
if p != nil {
if p, _ := pem.Decode(in); p != nil {
if !validPEMs[p.Type] {
return nil, errors.New("invalid private key file type " + p.Type)
}
in = p.Bytes
return certlib.ParsePrivateKeyPEM(in)
}
priv, err := x509.ParsePKCS8PrivateKey(in)
if err != nil {
priv, err = x509.ParsePKCS1PrivateKey(in)
if err != nil {
priv, err = x509.ParseECPrivateKey(in)
if err != nil {
return nil, err
}
}
}
switch p := priv.(type) {
case *rsa.PrivateKey:
return p, nil
case *ecdsa.PrivateKey:
return p, nil
default:
// should never reach here
return nil, errors.New("invalid private key")
}
return certlib.ParsePrivateKeyDER(in)
}
func main() {

1
go.mod
View File

@@ -22,4 +22,5 @@ require (
github.com/kr/pretty v0.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
rsc.io/qr v0.2.0 // indirect
)

2
go.sum
View File

@@ -44,3 +44,5 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@@ -2,9 +2,11 @@
package lib
import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
@@ -109,3 +111,111 @@ func Duration(d time.Duration) string {
s += fmt.Sprintf("%dh%s", hours, d)
return s
}
type HexEncodeMode uint8
const (
// HexEncodeLower prints the bytes as lowercase hexadecimal.
HexEncodeLower HexEncodeMode = iota + 1
// HexEncodeUpper prints the bytes as uppercase hexadecimal.
HexEncodeUpper
// HexEncodeLowerColon prints the bytes as lowercase hexadecimal
// with colons between each pair of bytes.
HexEncodeLowerColon
// HexEncodeUpperColon prints the bytes as uppercase hexadecimal
// with colons between each pair of bytes.
HexEncodeUpperColon
)
func (m HexEncodeMode) String() string {
switch m {
case HexEncodeLower:
return "lower"
case HexEncodeUpper:
return "upper"
case HexEncodeLowerColon:
return "lcolon"
case HexEncodeUpperColon:
return "ucolon"
default:
panic("invalid hex encode mode")
}
}
func ParseHexEncodeMode(s string) HexEncodeMode {
switch strings.ToLower(s) {
case "lower":
return HexEncodeLower
case "upper":
return HexEncodeUpper
case "lcolon":
return HexEncodeLowerColon
case "ucolon":
return HexEncodeUpperColon
}
panic("invalid hex encode mode")
}
func hexColons(s string) string {
if len(s)%2 != 0 {
fmt.Fprintf(os.Stderr, "hex string: %s\n", s)
fmt.Fprintf(os.Stderr, "hex length: %d\n", len(s))
panic("invalid hex string length")
}
n := len(s)
if n <= 2 {
return s
}
pairCount := n / 2
if n%2 != 0 {
pairCount++
}
var b strings.Builder
b.Grow(n + pairCount - 1)
for i := 0; i < n; i += 2 {
b.WriteByte(s[i])
if i+1 < n {
b.WriteByte(s[i+1])
}
if i+2 < n {
b.WriteByte(':')
}
}
return b.String()
}
func hexEncode(b []byte) string {
s := hex.EncodeToString(b)
if len(s)%2 != 0 {
s = "0" + s
}
return s
}
// HexEncode encodes the given bytes as a hexadecimal string.
func HexEncode(b []byte, mode HexEncodeMode) string {
str := hexEncode(b)
switch mode {
case HexEncodeLower:
return str
case HexEncodeUpper:
return strings.ToUpper(str)
case HexEncodeLowerColon:
return hexColons(str)
case HexEncodeUpperColon:
return strings.ToUpper(hexColons(str))
default:
panic("invalid hex encode mode")
}
}

79
lib/lib_test.go Normal file
View File

@@ -0,0 +1,79 @@
package lib_test
import (
"testing"
"git.wntrmute.dev/kyle/goutils/lib"
)
func TestHexEncode_LowerUpper(t *testing.T) {
b := []byte{0x0f, 0xa1, 0x00, 0xff}
gotLower := lib.HexEncode(b, lib.HexEncodeLower)
if gotLower != "0fa100ff" {
t.Fatalf("lib.HexEncode lower: expected %q, got %q", "0fa100ff", gotLower)
}
gotUpper := lib.HexEncode(b, lib.HexEncodeUpper)
if gotUpper != "0FA100FF" {
t.Fatalf("lib.HexEncode upper: expected %q, got %q", "0FA100FF", gotUpper)
}
}
func TestHexEncode_ColonModes(t *testing.T) {
// Includes leading zero nibble and a zero byte to verify padding and separators
b := []byte{0x0f, 0xa1, 0x00, 0xff}
gotLColon := lib.HexEncode(b, lib.HexEncodeLowerColon)
if gotLColon != "0f:a1:00:ff" {
t.Fatalf("lib.HexEncode colon lower: expected %q, got %q", "0f:a1:00:ff", gotLColon)
}
gotUColon := lib.HexEncode(b, lib.HexEncodeUpperColon)
if gotUColon != "0F:A1:00:FF" {
t.Fatalf("lib.HexEncode colon upper: expected %q, got %q", "0F:A1:00:FF", gotUColon)
}
}
func TestHexEncode_EmptyInput(t *testing.T) {
var b []byte
if got := lib.HexEncode(b, lib.HexEncodeLower); got != "" {
t.Fatalf("empty lower: expected empty string, got %q", got)
}
if got := lib.HexEncode(b, lib.HexEncodeUpper); got != "" {
t.Fatalf("empty upper: expected empty string, got %q", got)
}
if got := lib.HexEncode(b, lib.HexEncodeLowerColon); got != "" {
t.Fatalf("empty colon lower: expected empty string, got %q", got)
}
if got := lib.HexEncode(b, lib.HexEncodeUpperColon); got != "" {
t.Fatalf("empty colon upper: expected empty string, got %q", got)
}
}
func TestHexEncode_SingleByte(t *testing.T) {
b := []byte{0x0f}
if got := lib.HexEncode(b, lib.HexEncodeLower); got != "0f" {
t.Fatalf("single byte lower: expected %q, got %q", "0f", got)
}
if got := lib.HexEncode(b, lib.HexEncodeUpper); got != "0F" {
t.Fatalf("single byte upper: expected %q, got %q", "0F", got)
}
// For a single byte, colon modes should not introduce separators
if got := lib.HexEncode(b, lib.HexEncodeLowerColon); got != "0f" {
t.Fatalf("single byte colon lower: expected %q, got %q", "0f", got)
}
if got := lib.HexEncode(b, lib.HexEncodeUpperColon); got != "0F" {
t.Fatalf("single byte colon upper: expected %q, got %q", "0F", got)
}
}
func TestHexEncode_InvalidModePanics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic for invalid mode, but function returned normally")
}
}()
// 0 is not a valid lib.HexEncodeMode (valid modes start at 1)
_ = lib.HexEncode([]byte{0x01}, lib.HexEncodeMode(0))
}

33
twofactor/README.md Normal file
View File

@@ -0,0 +1,33 @@
## `twofactor`
[![GoDoc](https://godoc.org/github.com/gokyle/twofactor?status.svg)](https://godoc.org/github.com/gokyle/twofactor)
### Author
`twofactor` was written by Kyle Isom <kyle@tyrfingr.is>.
### License
```
Copyright (c) 2017 Kyle Isom <kyle@imap.cc>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

5
twofactor/doc.go Normal file
View File

@@ -0,0 +1,5 @@
// Package twofactor implements two-factor authentication.
//
// Currently supported are RFC 4226 HOTP one-time passwords and
// RFC 6238 TOTP SHA-1 one-time passwords.
package twofactor

103
twofactor/hotp.go Normal file
View File

@@ -0,0 +1,103 @@
package twofactor
import (
"crypto"
"crypto/sha1" // #nosec G505 - required by RFC
"encoding/base32"
"io"
"net/url"
"strconv"
"strings"
)
// HOTP represents an RFC-4226 Hash-based One Time Password instance.
type HOTP struct {
*OATH
}
// NewHOTP takes the key, the initial counter value, and the number
// of digits (typically 6 or 8) and returns a new HOTP instance.
func NewHOTP(key []byte, counter uint64, digits int) *HOTP {
return &HOTP{
OATH: &OATH{
key: key,
counter: counter,
size: digits,
hash: sha1.New,
algo: crypto.SHA1,
},
}
}
// Type returns OATH_HOTP.
func (otp *HOTP) Type() Type {
return OATH_HOTP
}
// OTP returns the next OTP and increments the counter.
func (otp *HOTP) OTP() string {
code := otp.OATH.OTP(otp.counter)
otp.counter++
return code
}
// URL returns an HOTP URL (i.e. for putting in a QR code).
func (otp *HOTP) URL(label string) string {
return otp.OATH.URL(otp.Type(), label)
}
// SetProvider sets up the provider component of the OTP URL.
func (otp *HOTP) SetProvider(provider string) {
otp.provider = provider
}
// GenerateGoogleHOTP generates a new HOTP instance as used by
// Google Authenticator.
func GenerateGoogleHOTP() *HOTP {
key := make([]byte, sha1.Size)
if _, err := io.ReadFull(PRNG, key); err != nil {
return nil
}
return NewHOTP(key, 0, 6)
}
func hotpFromURL(u *url.URL) (*HOTP, string, error) {
label := u.Path[1:]
v := u.Query()
secret := strings.ToUpper(v.Get("secret"))
if secret == "" {
return nil, "", ErrInvalidURL
}
var digits = 6
if sdigit := v.Get("digits"); sdigit != "" {
tmpDigits, err := strconv.ParseInt(sdigit, 10, 8)
if err != nil {
return nil, "", err
}
digits = int(tmpDigits)
}
var counter uint64
if scounter := v.Get("counter"); scounter != "" {
var err error
counter, err = strconv.ParseUint(scounter, 10, 64)
if err != nil {
return nil, "", err
}
}
key, err := base32.StdEncoding.DecodeString(Pad(secret))
if err != nil {
// assume secret isn't base32 encoded
key = []byte(secret)
}
otp := NewHOTP(key, counter, digits)
return otp, label, nil
}
// QR generates a new QR code for the HOTP.
func (otp *HOTP) QR(label string) ([]byte, error) {
return otp.OATH.QR(otp.Type(), label)
}

View File

@@ -0,0 +1,58 @@
package twofactor
import (
"testing"
)
var testKey = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
var rfcHotpKey = []byte("12345678901234567890")
var rfcHotpExpected = []string{
"755224",
"287082",
"359152",
"969429",
"338314",
"254676",
"287922",
"162583",
"399871",
"520489",
}
// This test runs through the test cases presented in the RFC, and
// ensures that this implementation is in compliance.
func TestHotpRFC(t *testing.T) {
otp := NewHOTP(rfcHotpKey, 0, 6)
for i := range rfcHotpExpected {
if otp.Counter() != uint64(i) {
t.Fatalf("twofactor: invalid counter (should be %d, is %d",
i, otp.Counter())
}
code := otp.OTP()
if code == "" {
t.Fatal("twofactor: failed to produce an OTP")
} else if code != rfcHotpExpected[i] {
t.Logf("twofactor: invalid OTP\n")
t.Logf("\tExpected: %s\n", rfcHotpExpected[i])
t.Logf("\t Actual: %s\n", code)
t.Fatalf("\t Counter: %d\n", otp.counter)
}
}
}
// This test uses a different key than the test cases in the RFC,
// but runs through the same test cases to ensure that they fail as
// expected.
func TestHotpBadRFC(t *testing.T) {
otp := NewHOTP(testKey, 0, 6)
for i := range rfcHotpExpected {
code := otp.OTP()
switch code {
case "":
t.Error("twofactor: failed to produce an OTP")
case rfcHotpExpected[i]:
t.Error("twofactor: should not have received a valid OTP")
}
}
}

150
twofactor/oath.go Normal file
View File

@@ -0,0 +1,150 @@
package twofactor
import (
"crypto"
"crypto/hmac"
"encoding/base32"
"encoding/binary"
"fmt"
"hash"
"net/url"
"strconv"
"rsc.io/qr"
)
const defaultSize = 6
// OATH provides a baseline structure for the two OATH algorithms.
type OATH struct {
key []byte
counter uint64
size int
hash func() hash.Hash
algo crypto.Hash
provider string
}
// Size returns the output size (in characters) of the password.
func (o *OATH) Size() int {
return o.size
}
// Counter returns the OATH token's counter.
func (o *OATH) Counter() uint64 {
return o.counter
}
// SetCounter updates the OATH token's counter to a new value.
func (o *OATH) SetCounter(counter uint64) {
o.counter = counter
}
// Key returns the token's secret key.
func (o *OATH) Key() []byte {
return o.key
}
// Hash returns the token's hash function.
func (o *OATH) Hash() func() hash.Hash {
return o.hash
}
// URL constructs a URL appropriate for the token (i.e. for use in a
// QR code).
func (o *OATH) URL(t Type, label string) string {
secret := base32.StdEncoding.EncodeToString(o.key)
u := url.URL{}
v := url.Values{}
u.Scheme = "otpauth"
switch t {
case OATH_HOTP:
u.Host = "hotp"
case OATH_TOTP:
u.Host = "totp"
}
u.Path = label
v.Add("secret", secret)
if o.Counter() != 0 && t == OATH_HOTP {
v.Add("counter", strconv.FormatUint(o.Counter(), 10))
}
if o.Size() != defaultSize {
v.Add("digits", strconv.Itoa(o.Size()))
}
switch o.algo {
case crypto.SHA256:
v.Add("algorithm", "SHA256")
case crypto.SHA512:
v.Add("algorithm", "SHA512")
}
if o.provider != "" {
v.Add("provider", o.provider)
}
u.RawQuery = v.Encode()
return u.String()
}
var digits = []int64{
0: 1,
1: 10,
2: 100,
3: 1000,
4: 10000,
5: 100000,
6: 1000000,
7: 10000000,
8: 100000000,
9: 1000000000,
10: 10000000000,
}
// OTP top-level type should provide a counter; for example, HOTP
// will provide the counter directly while TOTP will provide the
// time-stepped counter.
func (o *OATH) OTP(counter uint64) string {
var ctr [8]byte
binary.BigEndian.PutUint64(ctr[:], counter)
var mod int64 = 1
if len(digits) > o.size {
for i := 1; i <= o.size; i++ {
mod *= 10
}
} else {
mod = digits[o.size]
}
h := hmac.New(o.hash, o.key)
h.Write(ctr[:])
dt := truncate(h.Sum(nil)) % mod
fmtStr := fmt.Sprintf("%%0%dd", o.size)
return fmt.Sprintf(fmtStr, dt)
}
// truncate contains the DT function from the RFC; this is used to
// deterministically select a sequence of 4 bytes from the HMAC
// counter hash.
func truncate(in []byte) int64 {
offset := int(in[len(in)-1] & 0xF)
p := in[offset : offset+4]
var binCode int32
binCode = int32((p[0] & 0x7f)) << 24
binCode += int32((p[1] & 0xff)) << 16
binCode += int32((p[2] & 0xff)) << 8
binCode += int32((p[3] & 0xff))
return int64(binCode) & 0x7FFFFFFF
}
// QR generates a byte slice containing the a QR code encoded as a
// PNG with level Q error correction.
func (o *OATH) QR(t Type, label string) ([]byte, error) {
u := o.URL(t, label)
code, err := qr.Encode(u, qr.Q)
if err != nil {
return nil, err
}
return code.PNG(), nil
}

View File

@@ -0,0 +1,27 @@
package twofactor
import (
"testing"
)
var sha1Hmac = []byte{
0x1f, 0x86, 0x98, 0x69, 0x0e,
0x02, 0xca, 0x16, 0x61, 0x85,
0x50, 0xef, 0x7f, 0x19, 0xda,
0x8e, 0x94, 0x5b, 0x55, 0x5a,
}
var truncExpect int64 = 0x50ef7f19
// This test runs through the truncation example given in the RFC.
func TestTruncate(t *testing.T) {
if result := truncate(sha1Hmac); result != truncExpect {
t.Fatalf("hotp: expected truncate -> %d, saw %d\n",
truncExpect, result)
}
sha1Hmac[19]++
if result := truncate(sha1Hmac); result == truncExpect {
t.Fatal("hotp: expected truncation to fail")
}
}

86
twofactor/otp.go Normal file
View File

@@ -0,0 +1,86 @@
package twofactor
import (
"crypto/rand"
"errors"
"fmt"
"hash"
"net/url"
)
type Type uint
const (
OATH_HOTP = iota
OATH_TOTP
)
// PRNG is an io.Reader that provides a cryptographically secure
// random byte stream.
var PRNG = rand.Reader
var (
ErrInvalidURL = errors.New("twofactor: invalid URL")
ErrInvalidAlgo = errors.New("twofactor: invalid algorithm")
)
// OTP represents a one-time password token -- whether a
// software taken (as in the case of Google Authenticator) or a
// hardware token (as in the case of a YubiKey).
type OTP interface {
// Returns the current counter value; the meaning of the
// returned value is algorithm-specific.
Counter() uint64
// Set the counter to a specific value.
SetCounter(uint64)
// the secret key contained in the OTP
Key() []byte
// generate a new OTP
OTP() string
// the output size of the OTP
Size() int
// the hash function used by the OTP
Hash() func() hash.Hash
// Returns the type of this OTP.
Type() Type
}
func otpString(otp OTP) string {
var typeName string
switch otp.Type() {
case OATH_HOTP:
typeName = "OATH-HOTP"
case OATH_TOTP:
typeName = "OATH-TOTP"
default:
typeName = "UNKNOWN"
}
return fmt.Sprintf("%s, %d", typeName, otp.Size())
}
// FromURL constructs a new OTP token from a URL string.
func FromURL(otpURL string) (OTP, string, error) {
u, err := url.Parse(otpURL)
if err != nil {
return nil, "", err
}
if u.Scheme != "otpauth" {
return nil, "", ErrInvalidURL
}
switch u.Host {
case "totp":
return totpFromURL(u)
case "hotp":
return hotpFromURL(u)
default:
return nil, "", ErrInvalidURL
}
}

View File

@@ -0,0 +1,126 @@
package twofactor
import (
"io"
"testing"
)
func TestHOTPString(t *testing.T) {
hotp := NewHOTP(nil, 0, 6)
hotpString := otpString(hotp)
if hotpString != "OATH-HOTP, 6" {
t.Fatal("twofactor: invalid OTP string")
}
}
// This test generates a new OTP, outputs the URL for that OTP,
// and attempts to parse that URL. It verifies that the two OTPs
// are the same, and that they produce the same output.
func TestURL(t *testing.T) {
var ident = "testuser@foo"
otp := NewHOTP(testKey, 0, 6)
url := otp.URL("testuser@foo")
otp2, id, err := FromURL(url)
switch {
case err != nil:
t.Fatal("hotp: failed to parse HOTP URL\n")
case id != ident:
t.Logf("hotp: bad label\n")
t.Logf("\texpected: %s\n", ident)
t.Fatalf("\t actual: %s\n", id)
case otp2.Counter() != otp.Counter():
t.Logf("hotp: OTP counters aren't synced\n")
t.Logf("\toriginal: %d\n", otp.Counter())
t.Fatalf("\t second: %d\n", otp2.Counter())
}
code1 := otp.OTP()
code2 := otp2.OTP()
if code1 != code2 {
t.Logf("hotp: mismatched OTPs\n")
t.Logf("\texpected: %s\n", code1)
t.Fatalf("\t actual: %s\n", code2)
}
// There's not much we can do test the QR code, except to
// ensure it doesn't fail.
_, err = otp.QR(ident)
if err != nil {
t.Fatalf("hotp: failed to generate QR code PNG (%v)\n", err)
}
// This should fail because the maximum size of an alphanumeric
// QR code with the lowest-level of error correction should
// max out at 4296 bytes. 8k may be a bit overkill... but it
// gets the job done. The value is read from the PRNG to
// increase the likelihood that the returned data is
// uncompressible.
var tooBigIdent = make([]byte, 8192)
_, err = io.ReadFull(PRNG, tooBigIdent)
if err != nil {
t.Fatalf("hotp: failed to read identity (%v)\n", err)
} else if _, err = otp.QR(string(tooBigIdent)); err == nil {
t.Fatal("hotp: QR code should fail to encode oversized URL")
}
}
// This test makes sure we can generate codes for padded and non-padded
// entries.
func TestPaddedURL(t *testing.T) {
var urlList = []string{
"otpauth://hotp/?secret=ME",
"otpauth://hotp/?secret=MEFR",
"otpauth://hotp/?secret=MFRGG",
"otpauth://hotp/?secret=MFRGGZA",
"otpauth://hotp/?secret=a6mryljlbufszudtjdt42nh5by=======",
"otpauth://hotp/?secret=a6mryljlbufszudtjdt42nh5by",
"otpauth://hotp/?secret=a6mryljlbufszudtjdt42nh5by%3D%3D%3D%3D%3D%3D%3D",
}
var codeList = []string{
"413198",
"770938",
"670717",
"402378",
"069864",
"069864",
"069864",
}
for i := range urlList {
if o, id, err := FromURL(urlList[i]); err != nil {
t.Log("hotp: URL should have parsed successfully (id=", id, ")")
t.Logf("\turl was: %s\n", urlList[i])
t.Fatalf("\t%s, %s\n", o.OTP(), id)
} else {
code2 := o.OTP()
if code2 != codeList[i] {
t.Logf("hotp: mismatched OTPs\n")
t.Logf("\texpected: %s\n", codeList[i])
t.Fatalf("\t actual: %s\n", code2)
}
}
}
}
// This test attempts a variety of invalid urls against the parser
// to ensure they fail.
func TestBadURL(t *testing.T) {
var urlList = []string{
"http://google.com",
"",
"-",
"foo",
"otpauth:/foo/bar/baz",
"://",
"otpauth://hotp/?digits=",
"otpauth://hotp/?secret=MFRGGZDF&digits=ABCD",
"otpauth://hotp/?secret=MFRGGZDF&counter=ABCD",
}
for i := range urlList {
if _, _, err := FromURL(urlList[i]); err == nil {
t.Log("hotp: URL should not have parsed successfully")
t.Fatalf("\turl was: %s\n", urlList[i])
}
}
}

172
twofactor/totp.go Normal file
View File

@@ -0,0 +1,172 @@
package twofactor
import (
"crypto"
"crypto/sha1" // #nosec G505 - required by RFC
"crypto/sha256"
"crypto/sha512"
"encoding/base32"
"hash"
"io"
"net/url"
"strconv"
"strings"
"github.com/benbjohnson/clock"
)
var timeSource = clock.New()
// TOTP represents an RFC 6238 Time-based One-Time Password instance.
type TOTP struct {
*OATH
step uint64
}
// NewTOTP takes a new key, a starting time, a step, the number of
// digits of output (typically 6 or 8) and the hash algorithm to
// use, and builds a new OTP.
func NewTOTP(key []byte, start uint64, step uint64, digits int, algo crypto.Hash) *TOTP {
h := hashFromAlgo(algo)
if h == nil {
return nil
}
return &TOTP{
OATH: &OATH{
key: key,
counter: start,
size: digits,
hash: h,
algo: algo,
},
step: step,
}
}
// NewGoogleTOTP takes a secret as a base32-encoded string and
// returns an appropriate Google Authenticator TOTP instance.
func NewGoogleTOTP(secret string) (*TOTP, error) {
key, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return nil, err
}
return NewTOTP(key, 0, 30, 6, crypto.SHA1), nil
}
// NewTOTPSHA1 will build a new TOTP using SHA-1.
func NewTOTPSHA1(key []byte, start uint64, step uint64, digits int) *TOTP {
return NewTOTP(key, start, step, digits, crypto.SHA1)
}
// Type returns OATH_TOTP.
func (otp *TOTP) Type() Type {
return OATH_TOTP
}
func (otp *TOTP) otp(counter uint64) string {
return otp.OATH.OTP(counter)
}
// OTP returns the OTP for the current timestep.
func (otp *TOTP) OTP() string {
return otp.otp(otp.OTPCounter())
}
// URL returns a TOTP URL (i.e. for putting in a QR code).
func (otp *TOTP) URL(label string) string {
return otp.OATH.URL(otp.Type(), label)
}
// SetProvider sets up the provider component of the OTP URL.
func (otp *TOTP) SetProvider(provider string) {
otp.provider = provider
}
func (otp *TOTP) otpCounter(t uint64) uint64 {
return (t - otp.counter) / otp.step
}
// OTPCounter returns the current time value for the OTP.
func (otp *TOTP) OTPCounter() uint64 {
return otp.otpCounter(uint64(timeSource.Now().Unix() & 0x7FFFFFFF)) //#nosec G115 - masked out overflow bits
}
func hashFromAlgo(algo crypto.Hash) func() hash.Hash {
switch algo {
case crypto.SHA1:
return sha1.New
case crypto.SHA256:
return sha256.New
case crypto.SHA512:
return sha512.New
}
return nil
}
// GenerateGoogleTOTP produces a new TOTP token with the defaults expected by
// Google Authenticator.
func GenerateGoogleTOTP() *TOTP {
key := make([]byte, sha1.Size)
if _, err := io.ReadFull(PRNG, key); err != nil {
return nil
}
return NewTOTP(key, 0, 30, 6, crypto.SHA1)
}
func totpFromURL(u *url.URL) (*TOTP, string, error) {
label := u.Path[1:]
v := u.Query()
secret := strings.ToUpper(v.Get("secret"))
if secret == "" {
return nil, "", ErrInvalidURL
}
var algo = crypto.SHA1
if algorithm := v.Get("algorithm"); algorithm != "" {
switch {
case strings.EqualFold(algorithm, "SHA256"):
algo = crypto.SHA256
case strings.EqualFold(algorithm, "SHA512"):
algo = crypto.SHA512
case !strings.EqualFold(algorithm, "SHA1"):
return nil, "", ErrInvalidAlgo
}
}
var digits = 6
if sdigit := v.Get("digits"); sdigit != "" {
tmpDigits, err := strconv.ParseInt(sdigit, 10, 8)
if err != nil {
return nil, "", err
}
digits = int(tmpDigits)
}
var period uint64 = 30
if speriod := v.Get("period"); speriod != "" {
var err error
period, err = strconv.ParseUint(speriod, 10, 64)
if err != nil {
return nil, "", err
}
}
key, err := base32.StdEncoding.DecodeString(Pad(secret))
if err != nil {
// assume secret isn't base32 encoded
key = []byte(secret)
}
otp := NewTOTP(key, 0, period, digits, algo)
return otp, label, nil
}
// QR generates a new TOTP QR code.
func (otp *TOTP) QR(label string) ([]byte, error) {
return otp.OATH.QR(otp.Type(), label)
}
func SetClock(c clock.Clock) {
timeSource = c
}

View File

@@ -0,0 +1,85 @@
package twofactor
import (
"crypto"
"testing"
"time"
"github.com/benbjohnson/clock"
)
var rfcTotpKey = map[crypto.Hash][]byte{
crypto.SHA1: []byte("12345678901234567890"),
crypto.SHA256: []byte("12345678901234567890123456789012"),
crypto.SHA512: []byte("1234567890123456789012345678901234567890123456789012345678901234"),
}
var rfcTotpStep uint64 = 30
var rfcTotpTests = []struct {
Time uint64
Code string
T uint64
Algo crypto.Hash
}{
{59, "94287082", 1, crypto.SHA1},
{59, "46119246", 1, crypto.SHA256},
{59, "90693936", 1, crypto.SHA512},
{1111111109, "07081804", 37037036, crypto.SHA1},
{1111111109, "68084774", 37037036, crypto.SHA256},
{1111111109, "25091201", 37037036, crypto.SHA512},
{1111111111, "14050471", 37037037, crypto.SHA1},
{1111111111, "67062674", 37037037, crypto.SHA256},
{1111111111, "99943326", 37037037, crypto.SHA512},
{1234567890, "89005924", 41152263, crypto.SHA1},
{1234567890, "91819424", 41152263, crypto.SHA256},
{1234567890, "93441116", 41152263, crypto.SHA512},
{2000000000, "69279037", 66666666, crypto.SHA1},
{2000000000, "90698825", 66666666, crypto.SHA256},
{2000000000, "38618901", 66666666, crypto.SHA512},
{20000000000, "65353130", 666666666, crypto.SHA1},
{20000000000, "77737706", 666666666, crypto.SHA256},
{20000000000, "47863826", 666666666, crypto.SHA512},
}
func TestTotpRFC(t *testing.T) {
for _, tc := range rfcTotpTests {
otp := NewTOTP(rfcTotpKey[tc.Algo], 0, rfcTotpStep, 8, tc.Algo)
if otp.otpCounter(tc.Time) != tc.T {
t.Logf("twofactor: invalid TOTP (t=%d, h=%d)\n", tc.Time, tc.Algo)
t.Logf("\texpected: %d\n", tc.T)
t.Errorf("\t actual: %d\n", otp.otpCounter(tc.Time))
}
if code := otp.otp(otp.otpCounter(tc.Time)); code != tc.Code {
t.Logf("twofactor: invalid TOTP (t=%d, h=%d)\n", tc.Time, tc.Algo)
t.Logf("\texpected: %s\n", tc.Code)
t.Errorf("\t actual: %s\n", code)
}
}
}
func TestTOTPTime(t *testing.T) {
otp := GenerateGoogleTOTP()
testClock := clock.NewMock()
testClock.Add(2 * time.Minute)
SetClock(testClock)
code := otp.OTP()
testClock.Add(-1 * time.Minute)
if newCode := otp.OTP(); newCode == code {
t.Errorf("twofactor: TOTP: previous code %s shouldn't match code %s", newCode, code)
}
testClock.Add(2 * time.Minute)
if newCode := otp.OTP(); newCode == code {
t.Errorf("twofactor: TOTP: future code %s shouldn't match code %s", newCode, code)
}
testClock.Add(-1 * time.Minute)
if newCode := otp.OTP(); newCode != code {
t.Errorf("twofactor: TOTP: current code %s shouldn't match code %s", newCode, code)
}
}

16
twofactor/util.go Normal file
View File

@@ -0,0 +1,16 @@
package twofactor
import (
"strings"
)
// Pad calculates the number of '='s to add to our encoded string
// to make base32.StdEncoding.DecodeString happy.
func Pad(s string) string {
if !strings.HasSuffix(s, "=") && len(s)%8 != 0 {
for len(s)%8 != 0 {
s += "="
}
}
return s
}

51
twofactor/util_test.go Normal file
View File

@@ -0,0 +1,51 @@
package twofactor_test
import (
"encoding/base32"
"math/rand"
"strings"
"testing"
"git.wntrmute.dev/kyle/goutils/twofactor"
)
const letters = "1234567890!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func randString() string {
b := make([]byte, rand.Intn(len(letters)))
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return base32.StdEncoding.EncodeToString(b)
}
func TestPadding(t *testing.T) {
for range 300 {
b := randString()
origEncoding := b
modEncoding := strings.ReplaceAll(b, "=", "")
str, err := base32.StdEncoding.DecodeString(origEncoding)
if err != nil {
t.Fatal("Can't decode: ", b)
}
paddedEncoding := twofactor.Pad(modEncoding)
if origEncoding != paddedEncoding {
t.Log("Padding failed:")
t.Logf("Expected: '%s'", origEncoding)
t.Fatalf("Got: '%s'", paddedEncoding)
} else {
var mstr []byte
mstr, err = base32.StdEncoding.DecodeString(paddedEncoding)
if err != nil {
t.Fatal("Can't decode: ", paddedEncoding)
}
if string(mstr) != string(str) {
t.Log("Re-padding failed:")
t.Logf("Expected: '%s'", str)
t.Fatalf("Got: '%s'", mstr)
}
}
}
}