diff --git a/.travis.yml b/.travis.yml
index fb4fbaf..887b4e4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,4 +3,7 @@ language: go
 go:
   - 1.7
   - 1.8
-  - 1.9
\ No newline at end of file
+  - 1.9
+  - "1.10"
+  - 1.11
+  - 1.12
diff --git a/gauth.go b/gauth.go
index 1e7d75d..309e771 100644
--- a/gauth.go
+++ b/gauth.go
@@ -4,72 +4,20 @@ import (
 	"bytes"
 	"crypto/aes"
 	"crypto/cipher"
-	"crypto/hmac"
-	"crypto/sha1"
 	"crypto/sha256"
-	"encoding/base32"
 	"encoding/csv"
 	"fmt"
 	"io/ioutil"
 	"log"
-	"math/big"
 	"os/user"
 	"path"
 	"strings"
 	"syscall"
-	"time"
 
+	"github.com/pcarrier/gauth/gauth"
 	"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() {
 	user, e := user.Current()
 	if e != nil {
@@ -83,7 +31,7 @@ func main() {
 	}
 
 	// 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: ")
 		passwd, e := terminal.ReadPassword(int(syscall.Stdin))
 		fmt.Printf("\n")
@@ -122,7 +70,7 @@ func main() {
 		log.Fatal(e)
 	}
 
-	currentTS, progress := TimeStamp()
+	currentTS, progress := gauth.IndexNow()
 	prevTS := currentTS - 1
 	nextTS := currentTS + 1
 
@@ -150,3 +98,24 @@ func main() {
 func leftPad(s string, padStr string, pLen int) string {
 	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
+}
diff --git a/gauth/gauth.go b/gauth/gauth.go
new file mode 100644
index 0000000..5976ec8
--- /dev/null
+++ b/gauth/gauth.go
@@ -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
+}
diff --git a/gauth/gauth_test.go b/gauth/gauth_test.go
new file mode 100644
index 0000000..4bed176
--- /dev/null
+++ b/gauth/gauth_test.go
@@ -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)
+		}
+	}
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..35caacb
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..808e7ec
--- /dev/null
+++ b/go.sum
@@ -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=