gotosocial/vendor/github.com/pquerna/otp/hotp/hotp.go
kim b6ff55662e [chore] update dependencies (#4188)
Update dependencies:
- github.com/gin-gonic/gin v1.10.0 -> v1.10.1
- github.com/gin-contrib/sessions v1.10.3 -> v1.10.4
- github.com/jackc/pgx/v5 v5.7.4 -> v5.7.5
- github.com/minio/minio-go/v7 v7.0.91 -> v7.0.92
- github.com/pquerna/otp v1.4.0 -> v1.5.0
- github.com/tdewolff/minify/v2 v2.23.5 -> v2.23.8
- github.com/yuin/goldmark v1.7.11 -> v1.7.12
- go.opentelemetry.io/otel{,/*} v1.35.0 -> v1.36.0
- modernc.org/sqlite v1.37.0 -> v1.37.1

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4188
Reviewed-by: Daenney <daenney@noreply.codeberg.org>
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
2025-05-22 16:27:55 +02:00

237 lines
6.2 KiB
Go

/**
* Copyright 2014 Paul Querna
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package hotp
import (
"github.com/pquerna/otp"
"github.com/pquerna/otp/internal"
"io"
"crypto/hmac"
"crypto/rand"
"crypto/subtle"
"encoding/base32"
"encoding/binary"
"fmt"
"math"
"net/url"
"strings"
)
const debug = false
// Validate a HOTP passcode given a counter and secret.
// This is a shortcut for ValidateCustom, with parameters that
// are compataible with Google-Authenticator.
func Validate(passcode string, counter uint64, secret string) bool {
rv, _ := ValidateCustom(
passcode,
counter,
secret,
ValidateOpts{
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
},
)
return rv
}
// ValidateOpts provides options for ValidateCustom().
type ValidateOpts struct {
// Digits as part of the input. Defaults to 6.
Digits otp.Digits
// Algorithm to use for HMAC. Defaults to SHA1.
Algorithm otp.Algorithm
// Encoder to use for output code.
Encoder otp.Encoder
}
// GenerateCode creates a HOTP passcode given a counter and secret.
// This is a shortcut for GenerateCodeCustom, with parameters that
// are compataible with Google-Authenticator.
func GenerateCode(secret string, counter uint64) (string, error) {
return GenerateCodeCustom(secret, counter, ValidateOpts{
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
}
// GenerateCodeCustom uses a counter and secret value and options struct to
// create a passcode.
func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) {
//Set default value
if opts.Digits == 0 {
opts.Digits = otp.DigitsSix
}
// As noted in issue #10 and #17 this adds support for TOTP secrets that are
// missing their padding.
secret = strings.TrimSpace(secret)
if n := len(secret) % 8; n != 0 {
secret = secret + strings.Repeat("=", 8-n)
}
// As noted in issue #24 Google has started producing base32 in lower case,
// but the StdEncoding (and the RFC), expect a dictionary of only upper case letters.
secret = strings.ToUpper(secret)
secretBytes, err := base32.StdEncoding.DecodeString(secret)
if err != nil {
return "", otp.ErrValidateSecretInvalidBase32
}
buf := make([]byte, 8)
mac := hmac.New(opts.Algorithm.Hash, secretBytes)
binary.BigEndian.PutUint64(buf, counter)
if debug {
fmt.Printf("counter=%v\n", counter)
fmt.Printf("buf=%v\n", buf)
}
mac.Write(buf)
sum := mac.Sum(nil)
// "Dynamic truncation" in RFC 4226
// http://tools.ietf.org/html/rfc4226#section-5.4
offset := sum[len(sum)-1] & 0xf
value := int64(((int(sum[offset]) & 0x7f) << 24) |
((int(sum[offset+1] & 0xff)) << 16) |
((int(sum[offset+2] & 0xff)) << 8) |
(int(sum[offset+3]) & 0xff))
l := opts.Digits.Length()
switch opts.Encoder {
case otp.EncoderDefault:
mod := int32(value % int64(math.Pow10(l)))
if debug {
fmt.Printf("offset=%v\n", offset)
fmt.Printf("value=%v\n", value)
fmt.Printf("mod'ed=%v\n", mod)
}
passcode = opts.Digits.Format(mod)
case otp.EncoderSteam:
// Define the character set used by Steam Guard codes.
alphabet := []byte{
'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
'R', 'T', 'V', 'W', 'X', 'Y',
}
radix := int64(len(alphabet))
for i := 0; i < l; i++ {
digit := value % radix
value /= radix
c := alphabet[digit]
passcode += string(c)
}
}
return
}
// ValidateCustom validates an HOTP with customizable options. Most users should
// use Validate().
func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) {
passcode = strings.TrimSpace(passcode)
if len(passcode) != opts.Digits.Length() {
return false, otp.ErrValidateInputInvalidLength
}
otpstr, err := GenerateCodeCustom(secret, counter, opts)
if err != nil {
return false, err
}
if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 {
return true, nil
}
return false, nil
}
// GenerateOpts provides options for .Generate()
type GenerateOpts struct {
// Name of the issuing Organization/Company.
Issuer string
// Name of the User's Account (eg, email address)
AccountName string
// Size in size of the generated Secret. Defaults to 10 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 otp.Digits
// Algorithm to use for HMAC. Defaults to SHA1.
Algorithm otp.Algorithm
// Reader to use for generating HOTP Key.
Rand io.Reader
}
var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
// Generate creates a new HOTP Key.
func Generate(opts GenerateOpts) (*otp.Key, error) {
// url encode the Issuer/AccountName
if opts.Issuer == "" {
return nil, otp.ErrGenerateMissingIssuer
}
if opts.AccountName == "" {
return nil, otp.ErrGenerateMissingAccountName
}
if opts.SecretSize == 0 {
opts.SecretSize = 10
}
if opts.Digits == 0 {
opts.Digits = otp.DigitsSix
}
if opts.Rand == nil {
opts.Rand = rand.Reader
}
// otpauth://hotp/Example:alice@google.com?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)
_, err := io.ReadFull(opts.Rand, secret)
if err != nil {
return nil, err
}
v.Set("secret", b32NoPadding.EncodeToString(secret))
}
v.Set("issuer", opts.Issuer)
v.Set("algorithm", opts.Algorithm.String())
v.Set("digits", opts.Digits.String())
u := url.URL{
Scheme: "otpauth",
Host: "hotp",
Path: "/" + opts.Issuer + ":" + opts.AccountName,
RawQuery: internal.EncodeQuery(v),
}
return otp.NewKeyFromURL(u.String())
}