Separate OTP generation into a library.

No functional changes are intended; the main package now imports the library
and uses it, but the implementation is unchanged.

Specific highlights:

- Change the names of the functions to avert stutter following the advice of
  Effective Go: https://golang.org/doc/effective_go.html#package-names

- Reorganize the helpers in main so control flow is easier to follow.

- Add documentation comments.
This commit is contained in:
M. J. Fromberger 2019-03-27 11:37:05 -07:00
parent e16e5ca533
commit 87aa5234c1
2 changed files with 72 additions and 55 deletions

View File

@ -4,72 +4,20 @@ import (
"bytes" "bytes"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256" "crypto/sha256"
"encoding/base32"
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/big"
"os/user" "os/user"
"path" "path"
"strings" "strings"
"syscall" "syscall"
"time"
"github.com/pcarrier/gauth/gauth"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
func TimeStamp() (int64, int) {
time := time.Now().Unix()
return time / 30, int(time % 30)
}
func normalizeSecret(sec string) string {
noPadding := strings.ToUpper(strings.Replace(sec, " ", "", -1))
padLength := 8 - (len(noPadding) % 8)
if padLength < 8 {
return noPadding + strings.Repeat("=", padLength)
}
return noPadding
}
func AuthCode(sec string, ts int64) (string, error) {
key, err := base32.StdEncoding.DecodeString(sec)
if err != nil {
return "", err
}
enc := hmac.New(sha1.New, key)
msg := make([]byte, 8, 8)
msg[0] = (byte)(ts >> (7 * 8) & 0xff)
msg[1] = (byte)(ts >> (6 * 8) & 0xff)
msg[2] = (byte)(ts >> (5 * 8) & 0xff)
msg[3] = (byte)(ts >> (4 * 8) & 0xff)
msg[4] = (byte)(ts >> (3 * 8) & 0xff)
msg[5] = (byte)(ts >> (2 * 8) & 0xff)
msg[6] = (byte)(ts >> (1 * 8) & 0xff)
msg[7] = (byte)(ts >> (0 * 8) & 0xff)
if _, err := enc.Write(msg); err != nil {
return "", err
}
hash := enc.Sum(nil)
offset := hash[19] & 0x0f
trunc := hash[offset : offset+4]
trunc[0] &= 0x7F
res := new(big.Int).Mod(new(big.Int).SetBytes(trunc), big.NewInt(1000000))
return fmt.Sprintf("%06d", res), nil
}
func authCodeOrDie(sec string, ts int64) string {
str, e := AuthCode(sec, ts)
if e != nil {
log.Fatal(e)
}
return str
}
func main() { func main() {
user, e := user.Current() user, e := user.Current()
if e != nil { if e != nil {
@ -83,7 +31,7 @@ func main() {
} }
// Support for 'openssl enc -aes-128-cbc -md sha256 -pass pass:' // Support for 'openssl enc -aes-128-cbc -md sha256 -pass pass:'
if bytes.Compare(cfgContent[:8], []byte{0x53, 0x61, 0x6c, 0x74, 0x65, 0x64, 0x5f, 0x5f}) == 0 { if bytes.HasPrefix(cfgContent, []byte("Salted__")) {
fmt.Printf("Encryption password: ") fmt.Printf("Encryption password: ")
passwd, e := terminal.ReadPassword(int(syscall.Stdin)) passwd, e := terminal.ReadPassword(int(syscall.Stdin))
fmt.Printf("\n") fmt.Printf("\n")
@ -122,7 +70,7 @@ func main() {
log.Fatal(e) log.Fatal(e)
} }
currentTS, progress := TimeStamp() currentTS, progress := gauth.IndexNow()
prevTS := currentTS - 1 prevTS := currentTS - 1
nextTS := currentTS + 1 nextTS := currentTS + 1
@ -150,3 +98,24 @@ func main() {
func leftPad(s string, padStr string, pLen int) string { func leftPad(s string, padStr string, pLen int) string {
return strings.Repeat(padStr, pLen) + s return strings.Repeat(padStr, pLen) + s
} }
// normalizeSecret cleans up whitespace and adds any missing padding to sec to
// use it as an OTP seed.
func normalizeSecret(sec string) string {
noPadding := strings.ToUpper(strings.Replace(sec, " ", "", -1))
padLength := 8 - (len(noPadding) % 8)
if padLength < 8 {
return noPadding + strings.Repeat("=", padLength)
}
return noPadding
}
// authCodeOrDie returns a code for the specified parameters, or aborts if an
// error occurred while generating the code.
func authCodeOrDie(sec string, ts int64) string {
str, e := gauth.Code(sec, ts)
if e != nil {
log.Fatal(e)
}
return str
}

48
gauth/gauth.go Normal file
View File

@ -0,0 +1,48 @@
// Package gauth implements the time-based OTP generation scheme used by Google
// Authenticator.
package gauth
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"fmt"
"math/big"
"time"
)
// IndexNow returns the current 30-second time slice index, and the number of
// seconds remaining until it ends.
func IndexNow() (int64, int) {
time := time.Now().Unix()
return time / 30, int(time % 30)
}
// Code returns the OTP code for the given secret at the specified time slice
// index. It will report an error if the secret is not valid Base32 or if HMAC
// generation fails.
func Code(sec string, ts int64) (string, error) {
key, err := base32.StdEncoding.DecodeString(sec)
if err != nil {
return "", err
}
enc := hmac.New(sha1.New, key)
msg := make([]byte, 8)
msg[0] = (byte)(ts >> (7 * 8) & 0xff)
msg[1] = (byte)(ts >> (6 * 8) & 0xff)
msg[2] = (byte)(ts >> (5 * 8) & 0xff)
msg[3] = (byte)(ts >> (4 * 8) & 0xff)
msg[4] = (byte)(ts >> (3 * 8) & 0xff)
msg[5] = (byte)(ts >> (2 * 8) & 0xff)
msg[6] = (byte)(ts >> (1 * 8) & 0xff)
msg[7] = (byte)(ts >> (0 * 8) & 0xff)
if _, err := enc.Write(msg); err != nil {
return "", err
}
hash := enc.Sum(nil)
offset := hash[19] & 0x0f
trunc := hash[offset : offset+4]
trunc[0] &= 0x7F
res := new(big.Int).Mod(new(big.Int).SetBytes(trunc), big.NewInt(1000000))
return fmt.Sprintf("%06d", res), nil
}