在这个例子中,我们将在Golang中创建一个一次性密码(OTP)库。它将利用基于时间的一次性密码(TOTP)和基于HMAC的一次性密码(HOTP)版本。这两个版本都尊重其IETF标准,即RFC 6238和RFC 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")
}