Skip to content

Commit

Permalink
otp (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
xwi88 authored Oct 21, 2022
1 parent 58d7b28 commit 0fa50d0
Show file tree
Hide file tree
Showing 8 changed files with 645 additions and 7 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [x] [ip](ip) parse, match, convert, info.
- [x] [json](json) support multi json packages.
- [x] [number](number) round, bytes convert.
- [x] [otp](otp) `TOTP`, `HOTP`.
- [x] [random](random) rand, random.
- [x] [uuid](uuid) requestID, go.uuid, ksuid, xid.
- [x] [xlo](xlo) some utils ref *lo*, more pls use [lo](https://github.com/samber/lo) directly.
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ require (
github.com/bytedance/sonic v1.5.0
github.com/goccy/go-json v0.9.11
github.com/json-iterator/go v1.1.12
github.com/pquerna/otp v1.3.0
github.com/rs/xid v1.4.0
github.com/samber/lo v1.33.0
github.com/satori/go.uuid v1.2.0
github.com/segmentio/ksuid v1.0.4
github.com/smartystreets/goconvey v1.7.2
)

require (
github.com/bits-and-blooms/bitset v1.3.3 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/samber/lo v1.33.0 // indirect
github.com/smartystreets/assertions v1.13.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
Expand Down
16 changes: 11 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/bits-and-blooms/bitset v1.3.3 h1:R1XWiopGiXf66xygsiLpzLo67xEYvMkHw3w+rCOSAwg=
github.com/bits-and-blooms/bitset v1.3.3/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.5.0 h1:XWdTi8bwPgxIML+eNV1IwNuTROK6EUrQ65ey8yd6fRQ=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 h1:1sDoSuDPWzhkdzNVxCxtIaKiAe96ESVPv8coGwc1gZ4=
Expand All @@ -21,12 +22,16 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk=
Expand All @@ -42,8 +47,9 @@ github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hg
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
Expand All @@ -55,8 +61,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
60 changes: 60 additions & 0 deletions otp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# OTP: One-Time Password

>[Time-based one-time password](https://en.wikipedia.org/wiki/Time-based_one-time_password#Client_implementations)
Referenced the following packages:
- [pyotp](https://github.com/pyauth/pyotp)
- [otp](https://github.com/pquerna/otp)
- [Key-Uri-Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)

## Formula

`OTP(K, C) = Truncate(HMAC-SHA-1(K, C))`

>`HMAC` supports the following algorithms:
> - SHA1 (Default)
> - SHA256
> - SHA512
> - MD5
## Why

`kit4go/otp` is a Go library for generating and verifying one-time passwords. It can be used to implement two-factor (2FA)
or multi-factor (MFA) authentication methods in web applications and in other systems that require users to log in.

It enables you to easily add TOTPs to your own application, increasing your user's security against mass-password breaches and malware.

## Features

- Generating QR Code images for easy user enrollment.
- Time-based One-time Password Algorithm (TOTP) (RFC 6238): Time based OTP, _the most commonly used method_.
- HMAC-based One-time Password Algorithm (HOTP) (RFC 4226): Counter based OTP, which TOTP is based upon.
- Generation and Validation of codes for either algorithm.

## Shall Know

- OTPs involve a shared secret, stored both on the device(client) and the server
- OTPs can be generated on a device without internet connectivity
- OTPs should always be used as a second factor of authentication (if your device is lost, you account is still secured with a password)
- Microsoft Authenticator, Google Authenticator and other OTP client apps allow you to store multiple OTP secrets and provision those using a QR Code

## Usage

- secret key
- `RandomSecret(length int) string` generates a random secret, b32NoPadding.
- `VerifySecret(secret string) bool` verifies the secret is base32.
- otp url
- `GenerateURLHOTP(opts KeyOpts) string` generates the hotp url
- `GenerateURLTOTP(opts KeyOpts) string` generates the totp url
- **code totp** _most commonly used_
- `Code(secret string) string` generates the totp code
- `CodeCustom(secret string, t time.Time) string` generates the totp code with time
- `TOTPCode(secret string) (code string)` generates the totp code
- `TOTPCodeCustom(secret string, t time.Time, opts *Opts) string` generates the totp code with time and opts
- `VerifyTOTP(passcode string, secret string) bool` verifies the code of totp
- `VerifyTOTPCustom(passcode string, secret string, t time.Time, opts *Opts) bool` verifies the code of totp with opts
- code hotp
- `HOTPCode(secret string, counter uint64) string` generates the hotp code
- `HOTPCodeCustom(secret string, counter uint64, opts *Opts) string` generates the hotp code with the opts
- `VerifyHOTP(passcode string, counter uint64, secret string) bool` verifies the code of hotp
- `VerifyHOTPCustom(passcode string, counter uint64, secret string, opts *Opts) bool` verifies the code of hotp with opts
183 changes: 183 additions & 0 deletions otp/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package otp

import (
"crypto/rand"
"encoding/base32"
"errors"
"io"
"net/url"
"strconv"
"strings"

xtp "github.com/pquerna/otp"
"github.com/pquerna/otp/hotp"
"github.com/pquerna/otp/totp"
)

const (
// AlgorithmSHA1 should be used for compatibility with Google Authenticator.
//
// See https://github.com/pquerna/otp/issues/55 for additional details.
AlgorithmSHA1 = xtp.AlgorithmSHA1
AlgorithmSHA256 = xtp.AlgorithmSHA256
AlgorithmSHA512 = xtp.AlgorithmSHA512
AlgorithmMD5 = xtp.AlgorithmMD5
)

var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)

// KeyOpts provides options for Generate(). The default values
// are compatible with Google-Authenticator.
//
// Required: Issuer, AccountName, htop also need counter.
type KeyOpts struct {
// Name of the issuing Organization/Company.
Issuer string
// Name of the User's Account (eg, email address)
AccountName string
// Number of seconds a TOTP hash is valid for. Defaults to 30 seconds.
Period uint
// Size in size of the generated Secret. Defaults to 20 bytes.
SecretSize uint
// Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty.
Secret []byte
// Digits to request. Defaults to 6.
Digits xtp.Digits
// Algorithm to use for HMAC. Defaults to SHA1.
Algorithm xtp.Algorithm
// Reader to use for generating TOTP Key.
Rand io.Reader
// Counter for HOTP. if type is hotp: The counter parameter is required when provisioning a key for use with HOTP. It will set the initial counter value.
Counter uint64
}

// RandomSecret generates a random secret of given length (number of bytes) without padding,
// if rand.Read failed returns empty string.
func RandomSecret(length int) (secret string) {
secretB := make([]byte, length)
gen, err := rand.Read(secretB)
if err != nil || gen != length {
return secret
}
secret = b32NoPadding.EncodeToString(secretB)
return
}

// VerifySecret verifies the secret is valid, support padding or NoPadding format.
func VerifySecret(secret string) bool {
secret = strings.TrimSpace(secret)
if n := len(secret) % 8; n != 0 {
secret = secret + strings.Repeat("=", 8-n)
}
_, err := base32.StdEncoding.DecodeString(secret)
return err == nil
}

// GenerateURLHOTP returns the HOTP URL as a string.
func GenerateURLHOTP(opts KeyOpts) (url string) {
if key, err := simpleURL(opts, "hotp"); err == nil {
key.Type()
url = key.URL()
}
return
}

// GenerateURLTOTP returns the TOTP URL as a string.
func GenerateURLTOTP(opts KeyOpts) (url string) {
if key, err := simpleURL(opts, "totp"); err == nil {
url = key.URL()
}
return
}

// KeyFromTOTPOpts creates a new TOTP Key.
func KeyFromTOTPOpts(opts KeyOpts) (*xtp.Key, error) {
return totp.Generate(totp.GenerateOpts{
Issuer: opts.Issuer,
AccountName: opts.AccountName,
Period: opts.Period,
Secret: opts.Secret,
SecretSize: opts.SecretSize,
Digits: opts.Digits,
Algorithm: opts.Algorithm,
Rand: opts.Rand,
})
}

// KeyFromHOTPOpts creates a new HOTP Key.
func KeyFromHOTPOpts(opts KeyOpts) (*xtp.Key, error) {
return hotp.Generate(hotp.GenerateOpts{
Issuer: opts.Issuer,
AccountName: opts.AccountName,
Secret: opts.Secret,
SecretSize: opts.SecretSize,
Digits: opts.Digits,
Algorithm: opts.Algorithm,
Rand: opts.Rand,
})
}

// KeyFromURL creates a new Key from an TOTP or HOTP url.
//
// The URL format is documented here:
//
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
func KeyFromURL(url string) (*xtp.Key, error) {
if len(url) == 0 {
return nil, errors.New("empty URL")
}
return xtp.NewKeyFromURL(url)
}

func simpleURL(opts KeyOpts, otpType string) (*xtp.Key, error) {
// url encode the Issuer/AccountName
if opts.Issuer == "" {
return nil, xtp.ErrGenerateMissingIssuer
}

if opts.AccountName == "" {
return nil, xtp.ErrGenerateMissingAccountName
}

if opts.SecretSize == 0 {
opts.SecretSize = 10
}

if opts.Rand == nil {
opts.Rand = rand.Reader
}

// otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example

v := url.Values{}
if len(opts.Secret) != 0 {
v.Set("secret", b32NoPadding.EncodeToString(opts.Secret))
} else {
secret := make([]byte, opts.SecretSize)
_, _ = opts.Rand.Read(secret)
v.Set("secret", b32NoPadding.EncodeToString(secret))
}

v.Set("issuer", opts.Issuer)
if opts.Digits == 0 {
opts.Digits = xtp.DigitsSix
} else {
v.Set("digits", opts.Digits.String())
}
if opts.Algorithm != xtp.AlgorithmSHA1 {
v.Set("algorithm", opts.Algorithm.String())
}

if otpType == "hotp" {
v.Set("counter", strconv.FormatUint(opts.Counter, 10))
}

u := url.URL{
Scheme: "otpauth",
Host: otpType,
Path: "/" + opts.Issuer + ":" + opts.AccountName,
RawQuery: v.Encode(),
}

return xtp.NewKeyFromURL(u.String())
}
Loading

0 comments on commit 0fa50d0

Please sign in to comment.