git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/otc/otc.go (about)

     1  // package otc provides alphanumeric One Time Codes that can be used for email-based 2FA,
     2  // account verification and more.
     3  package otc
     4  
     5  import (
     6  	"crypto/rand"
     7  	"encoding/base32"
     8  	"encoding/base64"
     9  	"errors"
    10  	"strings"
    11  	"unicode"
    12  
    13  	"git.sr.ht/~pingoo/stdx/crypto"
    14  )
    15  
    16  const (
    17  	otcPrefix   = "otc"
    18  	version1    = "v1"
    19  	tokenPrefix = otcPrefix + "." + version1 + "."
    20  )
    21  
    22  var (
    23  	ErrTokenIsNotValid = errors.New("otc: token is not valid")
    24  )
    25  
    26  type Code struct {
    27  	code  string
    28  	token string
    29  }
    30  
    31  type NewCodeOptions struct {
    32  }
    33  
    34  func New(length uint16) (code Code, err error) {
    35  	randomBytes := make([]byte, length)
    36  
    37  	_, err = rand.Read(randomBytes)
    38  	if err != nil {
    39  		return
    40  	}
    41  
    42  	codeText := base32.StdEncoding.EncodeToString(randomBytes)
    43  	// format code (with '-')
    44  
    45  	hash, err := crypto.HashPassword([]byte(codeText), crypto.DefaultHashPasswordParams)
    46  	if err != nil {
    47  		return
    48  	}
    49  
    50  	encodedHash := base64.RawURLEncoding.EncodeToString([]byte(hash))
    51  
    52  	code = Code{
    53  		code:  codeText,
    54  		token: tokenPrefix + encodedHash,
    55  	}
    56  
    57  	return
    58  }
    59  
    60  func (code *Code) Code() string {
    61  	return code.code
    62  }
    63  
    64  // CodeHTML returns the code wrapped in a <span> and with numbers wrapped in <span style="color: red">
    65  func (code *Code) CodeHTML() (ret string) {
    66  	ret = "<span>"
    67  	for _, c := range code.code {
    68  		if unicode.IsLetter(c) || c == '-' {
    69  			ret += string(c)
    70  		} else {
    71  			ret += `<span style="color: red">`
    72  			ret += string(c)
    73  			ret += `</span>`
    74  		}
    75  	}
    76  
    77  	ret += "</span>"
    78  	return
    79  }
    80  
    81  // Token returns a token of the form otc.v[N].[XXXX]
    82  // where [N] is the version number of the token
    83  // and [XXXX] is Base64URL encoded data
    84  // The token should be stored in a database or a similar secure place
    85  // and use it later to verify that a code is valid
    86  func (code *Code) Token() string {
    87  	return code.token
    88  }
    89  
    90  func Verify(code, token string) bool {
    91  	if strings.Count(token, ".") != 2 {
    92  		return false
    93  	}
    94  	if !strings.HasPrefix(token, tokenPrefix) {
    95  		return false
    96  	}
    97  
    98  	encodedHashStart := strings.LastIndexByte(token, '.')
    99  	encodedHash := token[encodedHashStart+1:]
   100  	hash, err := base64.RawURLEncoding.DecodeString(encodedHash)
   101  	if err != nil {
   102  		return false
   103  	}
   104  
   105  	// TODO: cleanup code from '_'
   106  
   107  	return crypto.VerifyPasswordHash([]byte(code), string(hash))
   108  }