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

     1  package token
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"git.sr.ht/~pingoo/stdx/base32"
     9  	"git.sr.ht/~pingoo/stdx/crypto"
    10  	"git.sr.ht/~pingoo/stdx/guid"
    11  	"github.com/zeebo/blake3"
    12  )
    13  
    14  const (
    15  	SecretSize = crypto.KeySize256
    16  	HashSize   = crypto.KeySize256
    17  )
    18  
    19  var (
    20  	ErrTokenIsNotValid = errors.New("token is not valid")
    21  	ErrDataIsTooLong   = errors.New("data is too long")
    22  )
    23  
    24  type Token struct {
    25  	id     guid.GUID
    26  	secret []byte
    27  	hash   []byte
    28  	str    string
    29  	prefix string
    30  }
    31  
    32  func New(prefix string) (token Token, err error) {
    33  	secret, err := newSecret()
    34  	if err != nil {
    35  		return
    36  	}
    37  
    38  	return newToken(prefix, guid.NewRandom(), secret), nil
    39  }
    40  
    41  func NewWithID(prefix string, id guid.GUID) (token Token, err error) {
    42  	secret, err := newSecret()
    43  	if err != nil {
    44  		return
    45  	}
    46  
    47  	return newToken(prefix, id, secret), nil
    48  }
    49  
    50  // func NewWithSecret(secret []byte) (token Token, err error) {
    51  // 	return new("", secret)
    52  // }
    53  
    54  // func NewWithPrefix(prefix string) (token Token, err error) {
    55  // 	secret, err := newSecret()
    56  // 	if err != nil {
    57  // 		return
    58  // 	}
    59  // 	return newToken(prefix, secret)
    60  // }
    61  
    62  func newSecret() (secret []byte, err error) {
    63  	secret, err = crypto.RandBytes(SecretSize)
    64  	if err != nil {
    65  		err = fmt.Errorf("token: Generating secret: %w", err)
    66  		return
    67  	}
    68  	return
    69  }
    70  
    71  func newToken(prefix string, id guid.GUID, secret []byte) (token Token) {
    72  	idBytes, _ := id.MarshalBinary()
    73  
    74  	hash := generateHash(idBytes, secret)
    75  
    76  	data := append(idBytes, secret...)
    77  	str := base32.EncodeToString(data)
    78  	str = prefix + str
    79  
    80  	token = Token{
    81  		id,
    82  		secret,
    83  		hash,
    84  		str,
    85  		prefix,
    86  	}
    87  	return
    88  }
    89  
    90  func (token *Token) String() string {
    91  	return token.str
    92  }
    93  
    94  func (token *Token) ID() guid.GUID {
    95  	return token.id
    96  }
    97  
    98  func (token *Token) Secret() []byte {
    99  	return token.secret
   100  }
   101  
   102  func (token *Token) Hash() []byte {
   103  	return token.hash
   104  }
   105  
   106  func Parse(prefix, input string) (token Token, err error) {
   107  	var tokenBytes []byte
   108  
   109  	token.str = input
   110  
   111  	if prefix != "" {
   112  		if !strings.HasPrefix(input, prefix) {
   113  			err = ErrTokenIsNotValid
   114  			return
   115  		}
   116  		input = strings.TrimPrefix(input, prefix)
   117  		token.prefix = prefix
   118  	}
   119  
   120  	tokenBytes, err = base32.DecodeString(input)
   121  	if err != nil {
   122  		err = ErrTokenIsNotValid
   123  		return
   124  	}
   125  
   126  	if len(tokenBytes) != guid.Size+SecretSize {
   127  		err = ErrTokenIsNotValid
   128  		return
   129  	}
   130  
   131  	tokenIDBytes := tokenBytes[:guid.Size]
   132  	token.secret = tokenBytes[guid.Size:]
   133  
   134  	token.id, err = guid.FromBytes(tokenIDBytes)
   135  	if err != nil {
   136  		err = ErrTokenIsNotValid
   137  		return
   138  	}
   139  
   140  	token.hash = generateHash(tokenIDBytes, token.secret)
   141  
   142  	return
   143  }
   144  
   145  // FromIdAndHash creates a new token from an ID and a Hash
   146  // it means that the token needs to be refreshed with `Refresh` before being able to use it
   147  // as we din't have the secret, and thus cannot convert it to a valid string
   148  func FromIdAndHash(prefix string, id guid.GUID, hash []byte) (token Token, err error) {
   149  	if len(hash) != HashSize {
   150  		err = ErrTokenIsNotValid
   151  		return
   152  	}
   153  
   154  	token = Token{
   155  		id:     id,
   156  		secret: nil,
   157  		hash:   hash,
   158  		str:    "",
   159  		prefix: prefix,
   160  	}
   161  
   162  	return
   163  }
   164  
   165  func (token *Token) Verify(hash []byte) (err error) {
   166  	// in case we need to update hash size later
   167  	// if len(hash) == OldHashSize {
   168  	// token.hash = crypto.DeriveKeyFromKey(secret, idBytes, OldHashSize)
   169  	// ..
   170  	// }
   171  
   172  	if !crypto.ConstantTimeCompare(hash, token.hash) {
   173  		err = ErrTokenIsNotValid
   174  	}
   175  	return
   176  }
   177  
   178  func (token *Token) Refresh() (err error) {
   179  	idBytes, _ := token.id.MarshalBinary()
   180  	token.secret, err = newSecret()
   181  	if err != nil {
   182  		return
   183  	}
   184  
   185  	token.hash = generateHash(idBytes, token.secret)
   186  
   187  	data := append(idBytes, token.secret...)
   188  	str := base32.EncodeToString(data)
   189  	token.str = token.prefix + str
   190  
   191  	return
   192  }
   193  
   194  func generateHash(tokenID, secret []byte) (hash []byte) {
   195  	hasher := blake3.New()
   196  	hasher.Write(tokenID)
   197  	hasher.Write(secret)
   198  	hash = hasher.Sum(nil)
   199  	return
   200  }