用Golang创建一个用于双因素认证(2FA)的一次性密码(OTP)库

1,609 阅读11分钟

在这个例子中,我们将在Golang中创建一个一次性密码(OTP)库。它将利用基于时间的一次性密码(TOTP)基于HMAC的一次性密码(HOTP)版本。这两个版本都尊重其IETF标准,即RFC 6238RFC 4226。最后,尽管有些代码部分类似于现有的公共库*(如果你遵循标准,这是不可避免的*),这个例子没有使用任何公共库。

我们在这里使用了两个默认值。第一个是,OTP长度6个字符,第二个是,周期30秒。另外,你应该

  • 缓存成功验证的代码,以防止被重复使用,例如90秒

  • 在3次失败的尝试后阻止验证,例如30秒

  • 将秘密与用户记录一起存储在一个持久性存储器中

  • 将HOTP计数器与用户记录的秘密一起存储在一个持久性存储器中

  • 如果用户决定禁用2FA,将用户的OTP记录从持久性存储中删除。

关于HOTP的建议 - 鉴于这是一个基于计数器的算法,允许用户扫描QR码来使用移动应用程序,使得计数器在客户端和服务器之间的漂移更加不可避免。例如,用户在应用程序中手动刷新代码,但从未提交,这在客户端增加了计数器,但在服务器端没有。如果您通过电子邮件或短信将代码发送给用户,使客户端和服务器端的计数器一致,那么HOTP就非常理想。CreateHOTPCode 功能就是专门用于这种目的的。

qr.go

package otp

import (
	"fmt"

	"rsc.io/qr"
)

// NewQR creates a new QR PNG from an OTP URI.
func NewQR(uri string) ([]byte, error) {
	code, err := qr.Encode(uri, qr.Q)
	if err != nil {
		return nil, fmt.Errorf("encode: %w", err)
	}

	return code.PNG(), nil
}
package otp

import (
	"testing"
)

func Test_NewQR(t *testing.T) {
	qr, err := NewQR("otpauth://type/label?parameters")
	if err != nil {
		t.Error("an error was not expected")
	}
	if len(qr) != 1445 {
		t.Error("expected 1445 byte slice but got", len(qr))
	}
}

secret.go

package otp

import (
	"crypto/rand"
	"encoding/base32"
	"fmt"
)

// REF: https://datatracker.ietf.org/doc/html/rfc3548#section-5
const secretChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

// NewSecret creates a Base32 encoded arbitrary secret from a fixed length of 16
// byte slice without having a padding sign `=` at the end.
func NewSecret() (string, error) {
	bytes := make([]byte, 16)

	if _, err := rand.Read(bytes); err != nil {
		return "", fmt.Errorf("read: %w", err)
	}

	for i, b := range bytes {
		bytes[i] = secretChars[b%byte(len(secretChars))]
	}

	return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(bytes), nil
}
package otp

import (
	"encoding/base32"
	"testing"
)

func Test_NewSecret(t *testing.T) {
	// Encoded secret
	encSec, err := NewSecret()
	if err != nil {
		t.Error("an error was not expected while encoding but got", err.Error())
	}
	if len(encSec) != 26 {
		t.Error("expected 26 characters long secret but got", len(encSec))
	}
	if encSec[:26] == "=" {
		t.Error("did not expect padding")
	}

	// Decodeded secret
	decSec, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(encSec)
	if err != nil {
		t.Error("an error was not expected while decoding but got", err.Error())
	}
	if len(decSec) != 16 {
		t.Error("expected 16 byte slice but got", len(decSec))
	}
}

otp.go

// TOTP: https://en.wikipedia.org/wiki/One-time_password
//       https://datatracker.ietf.org/doc/html/rfc6238
// HOTP: https://en.wikipedia.org/wiki/HMAC-based_one-time_password
//       https://datatracker.ietf.org/doc/html/rfc4226
// The Google Authenticator: https://github.com/google/google-authenticator/wiki
package otp

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base32"
	"encoding/binary"
	"fmt"
	"time"
)

const (
	// length defines the OTP code in character length.
	length = 6
	// period defines the TTL of a TOTP code in seconds.
	period = 30
)

type OTP struct {
	// Issuer represents the service provider. It is you! e.g. your service,
	// your application, your organisation so on.
	Issuer string
	// Account represents the service user. It is the user! e.g. username, email
	// address so on.
	Account string
	// Secret is an arbitrary key value encoded in Base32 and belongs to the
	// service user.
	Secret string
	// Window is used for time (TOTP) and counter (HOTP) synchronization. Given
	// that the possible time and counter drifts between client and server, this
	// parameter helps overcome such issue. TOTP uses backward and forward time
	// window whereas HOTP uses look-ahead counter window that depends on the
	// Counter parameter.
	// Resynchronisation is an official recommended practise, however the
	// lower the better.
	// 0 = not recommended as synchronization is disabled
	//   TOTP: current time
	//   HOTP: current counter
	// 1 = recommended option
	//   TOTP: previous - current - next
	//   HOTP: current counter - next counter
	// 2 = being overcautious
	//   TOTP: previous,previous - current - next,next
	//   HOTP: current counter - next counter - next counter
	// * = Higher numbers may cause denial-of-service attacks.
	// REF: https://datatracker.ietf.org/doc/html/rfc6238#page-7
	// REF: https://datatracker.ietf.org/doc/html/rfc4226#page-11
	Window int
	// Counter is required for HOTP only and used for provisioning the code. Set
	// it to 0 if you with to use TOTP. Start from 1 for HOTP then fetch and use
	// the one in the persistent storage. The server counter is incremented only
	// after a successful code verification, however the counter on the code is
	// incremented every time a new code is requested by the user which causes
	// counters being out of sync. For that reason, time-synchronization should
	// be enabled.
	// REF: https://datatracker.ietf.org/doc/html/rfc4226#page-11
	Counter int
}

// CreateURI builds the authentication URI which is used to create a QR code.
// If the counter is set to 0, the algorithm is assumed to be TOTP, otherwise
// HOTP.
// REF: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
func (o *OTP) CreateURI() string {
	algorithm := "totp"
	counter := ""
	if o.Counter != 0 {
		algorithm = "hotp"
		counter = fmt.Sprintf("&counter=%d", o.Counter)
	}

	return fmt.Sprintf("otpauth://%s/%s:%s?secret=%s&issuer=%s%s",
		algorithm,
		o.Issuer,
		o.Account,
		o.Secret,
		o.Issuer,
		counter,
	)
}

// CreateHOTPCode creates a new HOTP with a specific counter. This method is
// ideal if you are planning to send manually created code via email, SMS etc.
// The user should not be present a QR code for this option otherwise there is
// a high posibility that the client and server counters will be out of sync,
// unless the user will be forced to rescan a newly generaed QR with up to date
// counter value.
func (o *OTP) CreateHOTPCode(counter int) (string, error) {
	val, err := o.createCode(counter)
	if err != nil {
		return "", fmt.Errorf("create code: %w", err)
	}

	o.Counter = counter
	return val, nil
}

// VerifyCode talks to an algorithm specific validator to verify the integrity
// of the code. If the counter is set to 0, the algorithm is assumed to be TOTP,
// otherwise HOTP.
func (o *OTP) VerifyCode(code string) (bool, error) {
	if len(code) != length {
		return false, fmt.Errorf("invalid length")
	}

	if o.Counter != 0 {
		ok, err := o.verifyHOTP(code)
		if err != nil {
			return false, fmt.Errorf("verify HOTP: %w", err)
		}
		if !ok {
			return false, nil
		}
		return true, nil
	}

	ok, err := o.verifyTOTP(code)
	if err != nil {
		return false, fmt.Errorf("verify TOTP: %w", err)
	}
	if !ok {
		return false, nil
	}

	return true, nil
}

// Depending on the given windows size, we handle clock resynchronisation. If
// the window size is set to 0, resynchronisation is disabled and we just use
// the current time. Otherwise, backward and forward window is taken into
// account as well.
func (o *OTP) verifyTOTP(code string) (bool, error) {
	curr := int(time.Now().UTC().Unix() / period)
	back := curr
	forw := curr
	if o.Window != 0 {
		back -= o.Window
		forw += o.Window
	}

	for i := back; i <= forw; i++ {
		val, err := o.createCode(i)
		if err != nil {
			return false, fmt.Errorf("create code: %w", err)
		}
		if val == code {
			return true, nil
		}
	}

	return false, nil
}

// Depending on the given windows size, we handle counter resynchronisation. If
// the window size is set to 0, resynchronisation is disabled and we just use
// the current counter. Otherwise, look-ahead counter window is used. When the
// look-ahead window is used, we calculate the next codes and determine if there
// is a match by utilising counter resynchronisation.
func (o *OTP) verifyHOTP(code string) (bool, error) {
	size := 0
	if o.Window != 0 {
		size = o.Window
	}

	for i := 0; i <= size; i++ {
		val, err := o.createCode(o.Counter + i)
		if err != nil {
			return false, fmt.Errorf("create code: %w", err)
		}
		if val == code {
			o.Counter += i + 1
			return true, nil
		}
	}

	o.Counter++
	return false, nil
}

// createCode creates a new OTP code based on either a time or counter interval.
// The time is used for TOTP and the counter is used for HOTP algorithm.
func (o *OTP) createCode(interval int) (string, error) {
	sec, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(o.Secret)
	if err != nil {
		return "", fmt.Errorf("decode string: %w", err)
	}

	hash := hmac.New(sha1.New, sec)
	if err := binary.Write(hash, binary.BigEndian, int64(interval)); err != nil {
		return "", fmt.Errorf("binary write: %w", err)
	}
	sign := hash.Sum(nil)

	offset := sign[19] & 15
	trunc := binary.BigEndian.Uint32(sign[offset : offset+4])

	return fmt.Sprintf("%0*d", length, (trunc&0x7fffffff)%1000000), nil
}
package otp

import (
	"testing"
	"time"
)

func Test_OTP_CreateURI(t *testing.T) {
	twoFA := &OTP{
		Issuer:  "issuer",
		Account: "account",
		Secret:  "secret",
		Window:  0,
		Counter: 0,
	}

	t.Run("totp uri", func(t *testing.T) {
		uri := twoFA.CreateURI()
		if uri != "otpauth://totp/issuer:account?secret=secret&issuer=issuer" {
			t.Error("expected otpauth://totp/issuer:account?secret=secret&issuer=issuer but got", uri)
		}
	})

	t.Run("hotp uri", func(t *testing.T) {
		twoFA.Counter = 1
		uri := twoFA.CreateURI()
		if uri != "otpauth://hotp/issuer:account?secret=secret&issuer=issuer&counter=1" {
			t.Error("expected otpauth://hotp/issuer:account?secret=secret&issuer=issuer&counter=1 but got", uri)
		}
	})
}

func Test_OTP_CreateHOTPCode(t *testing.T) {
	twoFA := &OTP{
		Issuer:  "issuer",
		Account: "account",
		Window:  0,
		Counter: 1,
	}

	t.Run("error if secret is illegal base32 data", func(t *testing.T) {
		twoFA.Secret = "secret"
		_, err := twoFA.CreateHOTPCode(2)
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "create code: decode string: illegal base32 data at input byte 0" {
			t.Error("expected create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
		}
		if twoFA.Counter != 1 {
			t.Error("counter should have been 1 but got", twoFA.Counter)
		}
	})

	t.Run("successful code creation and counter increase", func(t *testing.T) {
		twoFA.Secret = "GNFE2UCWJRCEOMZSLBHUMVCWKM"
		code, err := twoFA.CreateHOTPCode(2)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if code != "501363" {
			t.Error("expected 501363 but got", code)
		}
		if twoFA.Counter != 2 {
			t.Error("counter should have been 2 but got", twoFA.Counter)
		}
	})
}

func Test_OTP_VerifyCode(t *testing.T) {
	t.Run("error while using invalid code with length", func(t *testing.T) {
		twoFA := &OTP{}

		ok, err := twoFA.VerifyCode("00000") // Not equal to 6
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "invalid length" {
			t.Error("expected invalid length but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("failed verification for hotp code with invalid secret", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "secret",
			Counter: 1,
		}
		ok, err := twoFA.VerifyCode("000000") // Random code
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "verify HOTP: create code: decode string: illegal base32 data at input byte 0" {
			t.Error("expected verify HOTP: create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("failed verification for hotp code", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Counter: 1,
		}
		ok, err := twoFA.VerifyCode("000000") // Random code
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("successful verification for valid hotp code", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Counter: 1,
		}
		ok, err := twoFA.VerifyCode("204727") // Code for current counter
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
	})

	t.Run("failed verification for totp code with invalid secret", func(t *testing.T) {
		twoFA := &OTP{Secret: "secret"}
		ok, err := twoFA.VerifyCode("000000") // Random code
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "verify TOTP: create code: decode string: illegal base32 data at input byte 0" {
			t.Error("expected verify TOTP: create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("failed verification for totp code", func(t *testing.T) {
		twoFA := &OTP{Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM"}
		ok, err := twoFA.VerifyCode("000000") // Random code
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("successful verification for valid totp code", func(t *testing.T) {
		twoFA := &OTP{Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM"}

		code, err := twoFA.createCode(int(time.Now().UTC().Unix() / period)) // Code from current time
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}

		ok, err := twoFA.VerifyCode(code)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
	})
}

func Test_OTP_verifyTOTP(t *testing.T) {
	t.Run("error while decoding illegal secret", func(t *testing.T) {
		twoFA := &OTP{Secret: "secret"}
		ok, err := twoFA.verifyTOTP("000000") // Random code
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "create code: decode string: illegal base32 data at input byte 0" {
			t.Error("expected create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("failed verification for invalid code with resynchronisation disabled", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 0, // resynchronisation disabled
		}
		ok, err := twoFA.verifyTOTP("000000") // Random code
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("failed verification for invalid code with resynchronisation enabled", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 1, // resynchronisation enabled
		}
		ok, err := twoFA.verifyTOTP("000000") // Random code
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("successful verification with resynchronisation disabled and without time drift", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 0, // resynchronisation disabled
		}

		code, err := twoFA.createCode(int(time.Now().UTC().Unix() / period)) // Code from current window (no time drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}

		ok, err := twoFA.verifyTOTP(code)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
	})

	t.Run("successful verification with resynchronisation enabled and with time drift of previous window", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 1, // resynchronisation enabled
		}

		code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) - 1) // Code from previous window (time drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}

		ok, err := twoFA.verifyTOTP(code)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
	})

	t.Run("failed verification with resynchronisation disabled and with time drift of previous window", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 0, // resynchronisation disabled
		}

		code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) - 1) // Code from previous window (time drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}

		ok, err := twoFA.verifyTOTP(code)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("successful verification with resynchronisation enabled and with time drift of next window", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 1, // resynchronisation enabled
		}

		code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) + 1) // Code from next window (time drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}

		ok, err := twoFA.verifyTOTP(code)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
	})

	t.Run("failed verification with resynchronisation disabled and with time drift of next window", func(t *testing.T) {
		twoFA := &OTP{
			Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window: 0, // resynchronisation disabled
		}

		code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) + 1) // Code from next window (time drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}

		ok, err := twoFA.verifyTOTP(code)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})
}

func Test_OTP_verifyHOTP(t *testing.T) {
	t.Run("error while decoding illegal secret", func(t *testing.T) {
		twoFA := &OTP{Secret: "secret"}
		ok, err := twoFA.verifyHOTP("000000") // Random code
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "create code: decode string: illegal base32 data at input byte 0" {
			t.Error("expected create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
	})

	t.Run("failed verification for invalid code with resynchronisation disabled", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window:  0, // resynchronisation disabled
			Counter: 1,
		}
		ok, err := twoFA.verifyHOTP("000000") // Random code
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
		if twoFA.Counter != 2 {
			t.Error("counter should have been 2 but got", twoFA.Counter)
		}
	})

	t.Run("failed verification for invalid code with resynchronisation enabled", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window:  1, // resynchronisation enabled
			Counter: 1,
		}
		ok, err := twoFA.verifyHOTP("000000") // Random code
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
		if twoFA.Counter != 2 {
			t.Error("counter should have been 2 but got", twoFA.Counter)
		}
	})

	t.Run("successful verification with resynchronisation disabled and without counter drift", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window:  0, // resynchronisation disabled
			Counter: 1,
		}
		ok, err := twoFA.verifyHOTP("204727") // Code for current counter
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
		if twoFA.Counter != 2 {
			t.Error("counter should have been 2 but got", twoFA.Counter)
		}
	})

	t.Run("successful verification with resynchronisation enabled and with counter drift", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window:  1, // resynchronisation enabled
			Counter: 1,
		}
		ok, err := twoFA.verifyHOTP("501363") // 501363 belongs to counter 2 (counter drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if !ok {
			t.Error("expected true but got false")
		}
		if twoFA.Counter != 3 {
			t.Error("counter should have been 3 but got", twoFA.Counter)
		}
	})

	t.Run("failed verification with resynchronisation disabled and with counter drift", func(t *testing.T) {
		twoFA := &OTP{
			Secret:  "GNFE2UCWJRCEOMZSLBHUMVCWKM",
			Window:  0, // resynchronisation disabled
			Counter: 1,
		}
		ok, err := twoFA.verifyHOTP("501363") // 501363 belongs to counter 2 (counter drift)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if ok {
			t.Error("expected false but got true")
		}
		if twoFA.Counter != 2 {
			t.Error("counter should have been 2 but got", twoFA.Counter)
		}
	})
}

func Test_OTP_createCode(t *testing.T) {
	t.Run("error while decoding illegal secret", func(t *testing.T) {
		twoFA := &OTP{Secret: "secret"}
		code, err := twoFA.createCode(1)
		if err == nil {
			t.Error("expected an error but got nil")
		}
		if err.Error() != "decode string: illegal base32 data at input byte 0" {
			t.Error("expected decode string: illegal base32 data at input byte 0 but got", err.Error())
		}
		if code != "" {
			t.Error("expected empty code but got", code)
		}
	})

	t.Run("successful code creation", func(t *testing.T) {
		twoFA := &OTP{Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM"}
		code, err := twoFA.createCode(1)
		if err != nil {
			t.Error("an error was not expected but got", err.Error())
		}
		if code != "204727" {
			t.Error("expected 501363 but got", code)
		}
	})
}

测试

$ gotest -v ./...
=== RUN   Test_OTP_CreateURI
=== RUN   Test_OTP_CreateURI/totp_uri
=== RUN   Test_OTP_CreateURI/hotp_uri
--- PASS: Test_OTP_CreateURI (0.00s)
    --- PASS: Test_OTP_CreateURI/totp_uri (0.00s)
    --- PASS: Test_OTP_CreateURI/hotp_uri (0.00s)
=== RUN   Test_OTP_CreateHOTPCode
=== RUN   Test_OTP_CreateHOTPCode/error_if_secret_is_illegal_base32_data
=== RUN   Test_OTP_CreateHOTPCode/successful_code_creation_and_counter_increase
--- PASS: Test_OTP_CreateHOTPCode (0.00s)
    --- PASS: Test_OTP_CreateHOTPCode/error_if_secret_is_illegal_base32_data (0.00s)
    --- PASS: Test_OTP_CreateHOTPCode/successful_code_creation_and_counter_increase (0.00s)
=== RUN   Test_OTP_VerifyCode
=== RUN   Test_OTP_VerifyCode/error_while_using_invalid_code_with_length
=== RUN   Test_OTP_VerifyCode/failed_verification_for_hotp_code_with_invalid_secret
=== RUN   Test_OTP_VerifyCode/failed_verification_for_hotp_code
=== RUN   Test_OTP_VerifyCode/successful_verification_for_valid_hotp_code
=== RUN   Test_OTP_VerifyCode/failed_verification_for_totp_code_with_invalid_secret
=== RUN   Test_OTP_VerifyCode/failed_verification_for_totp_code
=== RUN   Test_OTP_VerifyCode/successful_verification_for_valid_totp_code
--- PASS: Test_OTP_VerifyCode (0.00s)
    --- PASS: Test_OTP_VerifyCode/error_while_using_invalid_code_with_length (0.00s)
    --- PASS: Test_OTP_VerifyCode/failed_verification_for_hotp_code_with_invalid_secret (0.00s)
    --- PASS: Test_OTP_VerifyCode/failed_verification_for_hotp_code (0.00s)
    --- PASS: Test_OTP_VerifyCode/successful_verification_for_valid_hotp_code (0.00s)
    --- PASS: Test_OTP_VerifyCode/failed_verification_for_totp_code_with_invalid_secret (0.00s)
    --- PASS: Test_OTP_VerifyCode/failed_verification_for_totp_code (0.00s)
    --- PASS: Test_OTP_VerifyCode/successful_verification_for_valid_totp_code (0.00s)
=== RUN   Test_OTP_verifyTOTP
=== RUN   Test_OTP_verifyTOTP/error_while_decoding_illegal_secret
=== RUN   Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled
=== RUN   Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled
=== RUN   Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_disabled_and_without_time_drift
=== RUN   Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_previous_window
=== RUN   Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_previous_window
=== RUN   Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_next_window
=== RUN   Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_next_window
--- PASS: Test_OTP_verifyTOTP (0.00s)
    --- PASS: Test_OTP_verifyTOTP/error_while_decoding_illegal_secret (0.00s)
    --- PASS: Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled (0.00s)
    --- PASS: Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled (0.00s)
    --- PASS: Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_disabled_and_without_time_drift (0.00s)
    --- PASS: Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_previous_window (0.00s)
    --- PASS: Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_previous_window (0.00s)
    --- PASS: Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_next_window (0.00s)
    --- PASS: Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_next_window (0.00s)
=== RUN   Test_OTP_verifyHOTP
=== RUN   Test_OTP_verifyHOTP/error_while_decoding_illegal_secret
=== RUN   Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled
=== RUN   Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled
=== RUN   Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_disabled_and_without_counter_drift
=== RUN   Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_enabled_and_with_counter_drift
=== RUN   Test_OTP_verifyHOTP/failed_verification_with_resynchronisation_disabled_and_with_counter_drift
--- PASS: Test_OTP_verifyHOTP (0.00s)
    --- PASS: Test_OTP_verifyHOTP/error_while_decoding_illegal_secret (0.00s)
    --- PASS: Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled (0.00s)
    --- PASS: Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled (0.00s)
    --- PASS: Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_disabled_and_without_counter_drift (0.00s)
    --- PASS: Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_enabled_and_with_counter_drift (0.00s)
    --- PASS: Test_OTP_verifyHOTP/failed_verification_with_resynchronisation_disabled_and_with_counter_drift (0.00s)
=== RUN   Test_OTP_createCode
=== RUN   Test_OTP_createCode/error_while_decoding_illegal_secret
=== RUN   Test_OTP_createCode/successful_code_creation
--- PASS: Test_OTP_createCode (0.00s)
    --- PASS: Test_OTP_createCode/error_while_decoding_illegal_secret (0.00s)
    --- PASS: Test_OTP_createCode/successful_code_creation (0.00s)
=== RUN   Test_NewQR
--- PASS: Test_NewQR (0.00s)
=== RUN   Test_NewSecret
--- PASS: Test_NewSecret (0.00s)
PASS
ok  	github.com/you/otp	0.065s

使用方法

// Create secret
sec, err := otp.NewSecret()
if err != nil {
	log.Fatalln(err)
}

iss := "Inanzzz"
acc := "you@example.com"

// Use TOTP
twoFA := &otp.OTP{
	Issuer:  iss,
	Account: acc,
	Secret:  sec,
	Window:  0, // Without time synchronization (prefer 1 to enable synchronization)
}

// ctr := 1
//
// Use HOTP
// twoFA := &otp.OTP{
// 	Issuer:  iss,
// 	Account: acc,
// 	Secret:  sec,
// 	Window:  0, // Without counter synchronization (prefer 1 to enable synchronization)
// 	Counter: ctr,
// }

// Create HOTP manually
// code, err := twoFA.CreateHOTPCode(ctr)
// if err != nil {
// 	log.Fatalln(err)
// }

// Create and save QR image
qr, err := otp.NewQR(twoFA.CreateURI())
if err != nil {
	log.Fatalln(err)
}
err = ioutil.WriteFile("qr.png", qr, 0600)
if err != nil {
	log.Fatalln(err)
}

code := "891329"

// Verify TOP code
ok, err := twoFA.VerifyCode(code)
if err != nil {
	log.Fatalln(err)
}
if !ok {
	log.Println("INVALID")
} else {
	log.Println("VALID")
}