package token import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" ) // Token derives one Reader's userscript credential from the deployment // secret, the Reader's Discord id and their token epoch. // // The credential is deterministic rather than stored random because the // server must be able to rebuild the install URL after a restart while the // database holds only hashes: a random token with no plaintext copy anywhere // would be unreconstructible, and keeping plaintext in memory would break // every install link on restart. HMAC output is high-entropy, indistinguishable // from random to anyone without the secret, and changes whenever the epoch // does — which is what rotation is. The stored form is Hash of this value, // so a database leak yields nothing but hashes of unguessable strings. func Token(key []byte, discordID string, epoch int64) string { mac := hmac.New(sha256.New, key) // The separator is unambiguous: discord ids are decimal snowflakes and // epochs are plain integers, so no two (id, epoch) pairs can collide. mac.Write([]byte(discordID)) mac.Write([]byte{0}) mac.Write([]byte(strconv.FormatInt(epoch, 10))) return hex.EncodeToString(mac.Sum(nil)) } // Hash is the SHA-256 of a credential — the only form that ever touches the // database (readers.token_sha256). SHA-256 rather than a password hash is // deliberate: these are unguessable values with nothing to brute-force, so a // slow hash would only add per-request cost. func Hash(cred string) [32]byte { return sha256.Sum256([]byte(cred)) }