package userapi import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" ) // Token prefixes. Matrix tokens are opaque to clients, so the prefix is for // our side: it makes a token recognisable in a log or a bug report, it lets // secret scanners match on something, and it means a client that sends its // refresh token where the access token belongs gets a clear rejection // instead of a puzzling one. const ( accessTokenPrefix = "warp_at_" refreshTokenPrefix = "warp_rt_" ) // tokenEntropyBytes is the random part of a token: 256 bits, the same order // as the session cookies of anything else worth attacking. const tokenEntropyBytes = 32 // deviceIDLength is how many characters a generated device ID has. Ten // uppercase letters is the shape clients and users are used to seeing, and // 26^10 is far more than a user will ever have devices. const deviceIDLength = 10 // deviceIDAlphabet is uppercase ASCII only: device IDs are read aloud during // device verification, so digits and letters that look alike are a nuisance. const deviceIDAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" // GenerateAccessToken returns a new access token. func GenerateAccessToken() (string, error) { return generateToken(accessTokenPrefix) } // GenerateRefreshToken returns a new refresh token. func GenerateRefreshToken() (string, error) { return generateToken(refreshTokenPrefix) } func generateToken(prefix string) (string, error) { raw := make([]byte, tokenEntropyBytes) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("generate token: %w", err) } // URL-safe and unpadded: tokens travel in query strings as well as in // the Authorization header. return prefix + base64.RawURLEncoding.EncodeToString(raw), nil } // HashToken returns the stored form of a token: lowercase hex SHA-256. // // The database holds hashes rather than tokens so that a dump cannot be // replayed as a set of live sessions. SHA-256 with no salt or stretching is // the right choice here, unlike for passwords: the input is 256 bits of // uniform randomness, so there is no dictionary to try, and the hash is on // the path of every authenticated request. func HashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // GenerateDeviceID returns a random device ID for a login that did not name // one. func GenerateDeviceID() (string, error) { raw := make([]byte, deviceIDLength) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("generate device ID: %w", err) } out := make([]byte, deviceIDLength) for i, b := range raw { // The alphabet has 26 letters and a byte has 256 values, so the // modulo is very slightly biased towards the first four letters. // For a name whose only job is to be unique among a user's handful // of devices, that is not worth a rejection loop. out[i] = deviceIDAlphabet[int(b)%len(deviceIDAlphabet)] } return string(out), nil } // MaxDeviceIDLength caps a client-supplied device ID. The spec sets no // limit, but the column is a primary key and an unbounded one invites abuse. const MaxDeviceIDLength = 255 // ValidateDeviceID checks a client-supplied device ID. Device IDs are opaque // strings, so the only rules are that it is non-empty, short enough, and // free of control characters that would mangle a log line. func ValidateDeviceID(deviceID string) error { if deviceID == "" { return fmt.Errorf("%w: device_id must not be empty", ErrInvalidDeviceID) } if len(deviceID) > MaxDeviceIDLength { return fmt.Errorf("%w: device_id must be at most %d bytes", ErrInvalidDeviceID, MaxDeviceIDLength) } for _, r := range deviceID { if r < 0x20 || r == 0x7f { return fmt.Errorf("%w: device_id must not contain control characters", ErrInvalidDeviceID) } } return nil }