Merge pull request #27 from creachadair/librify
Separate OTP generation into a library
This commit is contained in:
commit
61ada0495b
@ -4,3 +4,6 @@ go:
|
|||||||
- 1.7
|
- 1.7
|
||||||
- 1.8
|
- 1.8
|
||||||
- 1.9
|
- 1.9
|
||||||
|
- "1.10"
|
||||||
|
- 1.11
|
||||||
|
- 1.12
|
||||||
|
79
gauth.go
79
gauth.go
@ -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
48
gauth/gauth.go
Normal 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
|
||||||
|
}
|
30
gauth/gauth_test.go
Normal file
30
gauth/gauth_test.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package gauth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pcarrier/gauth/gauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
secret string
|
||||||
|
index int64
|
||||||
|
want string
|
||||||
|
fail bool
|
||||||
|
}{
|
||||||
|
// Manually verified with the Google authenticator app.
|
||||||
|
{"ABCDEFGH", 51790421, "305441", false},
|
||||||
|
|
||||||
|
// Invalid Base32 input for the secret.
|
||||||
|
{"blargh!", 123, "", true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
got, err := gauth.Code(test.secret, test.index)
|
||||||
|
if err != nil && !test.fail {
|
||||||
|
t.Errorf("Code(%q, %d): unexpected error: %v", test.secret, test.index, err)
|
||||||
|
} else if got != test.want {
|
||||||
|
t.Errorf("Code(%q, %d): got %q, want %q", test.secret, test.index, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module github.com/pcarrier/gauth
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
|
||||||
|
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect
|
||||||
|
)
|
10
go.sum
Normal file
10
go.sum
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s=
|
||||||
|
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
Loading…
x
Reference in New Issue
Block a user