...
 
Commits (8)
sudo: false
language: go
go:
- 1.8
- 1.9
- "1.10"
- master
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "rsc.io/qr"
packages = [
".",
"coding",
"gf256"
]
revision = "48b2ede4844e13f1a2b7ce4d2529c9af7e359fc5"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "b705d306da5a78e76b7ff289744770eef56328a4f0d1c615c1a233d056283651"
solver-name = "gps-cdcl"
solver-version = 1
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "rsc.io/qr"
[prune]
go-tests = true
unused-packages = true
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.
## `twofactor`
[![GoDoc](https://godoc.org/github.com/gokyle/twofactor?status.svg)](https://godoc.org/github.com/gokyle/twofactor)
[![Build Status](https://travis-ci.org/gokyle/twofactor.svg?branch=master)](https://travis-ci.org/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.
```
golang-github-gokyle-twofactor (1.0.1-1) unstable; urgency=medium
[ Michael Stapelberg ]
* update debian/gitlab-ci.yml (using salsa.debian.org/go-team/ci/cmd/ci)
[ Michael Meskes ]
* New upstream version 1.0.1
* Bumped Standards-Version
-- Michael Meskes <meskes@debian.org> Sun, 22 Apr 2018 17:06:39 +0200
golang-github-gokyle-twofactor (0.0~git20170918.eaad188-1) unstable; urgency=medium
* Initial release (Closes: #890198)
-- Michael Meskes <meskes@debian.org> Sat, 10 Feb 2018 13:01:18 +0100
Source: golang-github-gokyle-twofactor
Section: devel
Priority: optional
Maintainer: Debian Go Packaging Team <pkg-go-maintainers@lists.alioth.debian.org>
Uploaders: Michael Meskes <meskes@debian.org>
Build-Depends: debhelper (>= 11),
dh-golang,
golang-rsc-qr-dev,
golang-any
Standards-Version: 4.1.4
Homepage: https://github.com/gokyle/twofactor
Vcs-Browser: https://salsa.debian.org/go-team/packages/golang-github-gokyle-twofactor
Vcs-Git: https://salsa.debian.org/go-team/packages/golang-github-gokyle-twofactor.git
XS-Go-Import-Path: github.com/gokyle/twofactor
Testsuite: autopkgtest-pkg-go
Package: golang-github-gokyle-twofactor-dev
Architecture: all
Depends: ${shlibs:Depends},
${misc:Depends},
golang-rsc-qr-dev
Description: Two-factor authentication
Package twofactor implements two-factor authentication.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: twofactor
Source: https://github.com/gokyle/twofactor
Files: *
Copyright: 2017 Kyle Isom <kyle@imap.cc>
License: Expat
Files: debian/*
Copyright: 2018 Michael Meskes <meskes@debian.org>
License: Expat
Comment: Debian packaging is licensed under the same terms as upstream
License: Expat
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.
[DEFAULT]
pristine-tar = True
# auto-generated, DO NOT MODIFY.
# The authoritative copy of this file lives at:
# https://salsa.debian.org/go-team/ci/blob/master/cmd/ci/gitlabciyml.go
# TODO: publish under debian-go-team/ci
image: stapelberg/ci2
test_the_archive:
artifacts:
paths:
- before-applying-commit.json
- after-applying-commit.json
script:
# Create an overlay to discard writes to /srv/gopath/src after the build:
- "rm -rf /cache/overlay/{upper,work}"
- "mkdir -p /cache/overlay/{upper,work}"
- "mount -t overlay overlay -o lowerdir=/srv/gopath/src,upperdir=/cache/overlay/upper,workdir=/cache/overlay/work /srv/gopath/src"
- "export GOPATH=/srv/gopath"
- "export GOCACHE=/cache/go"
# Build the world as-is:
- "ci-build -exemptions=/var/lib/ci-build/exemptions.json > before-applying-commit.json"
# Copy this package into the overlay:
- "GBP_CONF_FILES=:debian/gbp.conf gbp buildpackage --git-no-pristine-tar --git-ignore-branch --git-ignore-new --git-export-dir=/tmp/export --git-no-overlay --git-tarball-dir=/nonexistant --git-cleaner=/bin/true --git-builder='dpkg-buildpackage -S -d --no-sign'"
- "pgt-gopath -dsc /tmp/export/*.dsc"
# Rebuild the world:
- "ci-build -exemptions=/var/lib/ci-build/exemptions.json > after-applying-commit.json"
- "ci-diff before-applying-commit.json after-applying-commit.json"
#!/usr/bin/make -f
%:
dh $@ --buildsystem=golang --with=golang
version=3
opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/golang-github-gokyle-twofactor-\$1\.tar\.gz/,\
uversionmangle=s/(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$/\$1~\$2\$3/ \
https://github.com/gokyle/twofactor/tags .*/v?(\d\S*)\.tar\.gz
// 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
package twofactor
import (
"crypto"
"crypto/sha1"
"encoding/base32"
"io"
"net/url"
"strconv"
"strings"
)
// HOTP represents an RFC-4226 Hash-based One Time Password instance.
type HOTP struct {
*OATH
}
// Type returns OATH_HOTP.
func (otp *HOTP) Type() Type {
return OATH_HOTP
}
// 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,
},
}
}
// 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 = 0
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)
}
package twofactor
import (
"fmt"
"testing"
)
var testKey = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
func newZeroHOTP() *HOTP {
return NewHOTP(testKey, 0, 6)
}
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 := 0; i < len(rfcHotpExpected); i++ {
if otp.Counter() != uint64(i) {
fmt.Printf("twofactor: invalid counter (should be %d, is %d",
i, otp.Counter())
t.FailNow()
}
code := otp.OTP()
if code == "" {
fmt.Printf("twofactor: failed to produce an OTP\n")
t.FailNow()
} else if code != rfcHotpExpected[i] {
fmt.Printf("twofactor: invalid OTP\n")
fmt.Printf("\tExpected: %s\n", rfcHotpExpected[i])
fmt.Printf("\t Actual: %s\n", code)
fmt.Printf("\t Counter: %d\n", otp.counter)
t.FailNow()
}
}
}
// 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 := 0; i < len(rfcHotpExpected); i++ {
code := otp.OTP()
if code == "" {
fmt.Printf("twofactor: failed to produce an OTP\n")
t.FailNow()
} else if code == rfcHotpExpected[i] {
fmt.Printf("twofactor: should not have received a valid OTP\n")
t.FailNow()
}
}
}
package twofactor
import (
"crypto"
"crypto/hmac"
"encoding/base32"
"encoding/binary"
"fmt"
"hash"
"net/url"
"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", fmt.Sprintf("%d", o.Counter()))
}
if o.Size() != defaultSize {
v.Add("digits", fmt.Sprintf("%d", o.Size()))
}
switch {
case o.algo == crypto.SHA256:
v.Add("algorithm", "SHA256")
case o.algo == 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,
}
// The 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
}
package twofactor
import (
"fmt"
"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 {
fmt.Printf("hotp: expected truncate -> %d, saw %d\n",
truncExpect, result)
t.FailNow()
}
sha1Hmac[19]++
if result := truncate(sha1Hmac); result == truncExpect {
fmt.Println("hotp: expected truncation to fail")
t.FailNow()
}
}
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")
)
// Type 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(URL string) (OTP, string, error) {
u, err := url.Parse(URL)
if err != nil {
return nil, "", err
}
if u.Scheme != "otpauth" {
return nil, "", ErrInvalidURL
}
switch {
case u.Host == "totp":
return totpFromURL(u)
case u.Host == "hotp":
return hotpFromURL(u)
default:
return nil, "", ErrInvalidURL
}
}
package twofactor
import "fmt"
import "io"
import "testing"
func TestHOTPString(t *testing.T) {
hotp := NewHOTP(nil, 0, 6)
hotpString := otpString(hotp)
if hotpString != "OATH-HOTP, 6" {
fmt.Println("twofactor: invalid OTP string")
t.FailNow()
}
}
// 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)
if err != nil {
fmt.Printf("hotp: failed to parse HOTP URL\n")
t.FailNow()
} else if id != ident {
fmt.Printf("hotp: bad label\n")
fmt.Printf("\texpected: %s\n", ident)
fmt.Printf("\t actual: %s\n", id)
t.FailNow()
} else if otp2.Counter() != otp.Counter() {
fmt.Printf("hotp: OTP counters aren't synced\n")
fmt.Printf("\toriginal: %d\n", otp.Counter())
fmt.Printf("\t second: %d\n", otp2.Counter())
t.FailNow()
}
code1 := otp.OTP()
code2 := otp2.OTP()
if code1 != code2 {
fmt.Printf("hotp: mismatched OTPs\n")
fmt.Printf("\texpected: %s\n", code1)
fmt.Printf("\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 {
fmt.Printf("hotp: failed to generate QR code PNG (%v)\n", err)
t.FailNow()
}
// 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 {
fmt.Printf("hotp: failed to read identity (%v)\n", err)
t.FailNow()
} else if _, err = otp.QR(string(tooBigIdent)); err == nil {
fmt.Println("hotp: QR code should fail to encode oversized URL")
t.FailNow()
}
}
// 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 {
fmt.Println("hotp: URL should have parsed successfully")
fmt.Printf("\turl was: %s\n", urlList[i])
t.FailNow()
fmt.Printf("\t%s, %s\n", o.OTP(), id)
} else {
code2 := o.OTP()
if code2 != codeList[i] {
fmt.Printf("hotp: mismatched OTPs\n")
fmt.Printf("\texpected: %s\n", codeList[i])
fmt.Printf("\t actual: %s\n", code2)
t.FailNow()
}
}
}
}
// 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 {
fmt.Println("hotp: URL should not have parsed successfully")
fmt.Printf("\turl was: %s\n", urlList[i])
t.FailNow()
}
}
}
package twofactor
import (
"crypto"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base32"
"hash"
"io"
"net/url"
"strconv"
"strings"
"time"
)
// TOTP represents an RFC 6238 Time-based One-Time Password instance.
type TOTP struct {
*OATH
step uint64
}
// 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(time.Now().Unix()))
}
// NewOTP 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,
}
}
// 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)
}
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)
}
// 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
}
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 != "" {
if strings.EqualFold(algorithm, "SHA256") {
algo = crypto.SHA256
} else if strings.EqualFold(algorithm, "SHA512") {
algo = crypto.SHA512
} else if !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)
}
package twofactor
import (
"crypto"
"fmt"
"testing"
)
var rfcTotpKey = []byte("12345678901234567890")
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, 0, 30, 8, tc.Algo)
if otp.otpCounter(tc.Time) != tc.T {
fmt.Println("twofactor: invalid T")
fmt.Printf("\texpected: %d\n", tc.T)
fmt.Printf("\t actual: %d\n", otp.otpCounter(tc.Time))
t.FailNow()
}
if code := otp.otp(otp.otpCounter(tc.Time)); code != tc.Code {
fmt.Println("twofactor: invalid TOTP")
fmt.Printf("\texpected: %s\n", tc.Code)
fmt.Printf("\t actual: %s\n", code)
t.FailNow()
}
}
}
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
}
package twofactor
import (
"encoding/base32"
"fmt"
"math/rand"
"strings"
"testing"
)
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 i := 0; i < 300; i++ {
b := randString()
origEncoding := string(b)
modEncoding := strings.Replace(string(b), "=", "", -1)
str, err := base32.StdEncoding.DecodeString(origEncoding)
if err != nil {
fmt.Println("Can't decode: ", string(b))
t.FailNow()
}
paddedEncoding := Pad(modEncoding)
if origEncoding != paddedEncoding {
fmt.Println("Padding failed:")
fmt.Printf("Expected: '%s'", origEncoding)
fmt.Printf("Got: '%s'", paddedEncoding)
t.FailNow()
} else {
mstr, err := base32.StdEncoding.DecodeString(paddedEncoding)
if err != nil {
fmt.Println("Can't decode: ", paddedEncoding)
t.FailNow()
}
if string(mstr) != string(str) {
fmt.Println("Re-padding failed:")
fmt.Printf("Expected: '%s'", str)
fmt.Printf("Got: '%s'", mstr)
t.FailNow()
}
}
}
}