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 }