package secure import ( "crypto/rand" "encoding/base64" "fmt" "strings" ) const pkceCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" // RandomBytes returns securely generated random bytes of given length. func RandomBytes(length int) ([]byte, error) { if length <= 0 { return nil, fmt.Errorf("length must be positive") } b := make([]byte, length) if _, err := rand.Read(b); err != nil { return nil, err } return b, nil } // RandomString returns a base64url encoded random string of approximately the requested length. func RandomString(length int) (string, error) { if length <= 0 { return "", fmt.Errorf("length must be positive") } // Generate ceil(length * 6/8) bytes to have enough entropy for base64 url encoding. byteLen := (length*6 + 7) / 8 bytes, err := RandomBytes(byteLen) if err != nil { return "", err } s := base64.RawURLEncoding.EncodeToString(bytes) if len(s) > length { return s[:length], nil } // If encoded string shorter, pad using charset if len(s) < length { sb := strings.Builder{} sb.WriteString(s) extraNeeded := length - len(s) more, err := randomFromCharset(extraNeeded) if err != nil { return "", err } sb.WriteString(more) return sb.String(), nil } return s, nil } // PKCECodeVerifier generates a random string compliant with RFC 7636. func PKCECodeVerifier(length int) (string, error) { if length < 43 { length = 43 } if length > 128 { length = 128 } return randomFromCharset(length) } // randomFromCharset returns a random string of given length using pkceCharset. func randomFromCharset(length int) (string, error) { if length <= 0 { return "", fmt.Errorf("length must be positive") } bytes, err := RandomBytes(length) if err != nil { return "", err } out := make([]byte, length) for i, b := range bytes { out[i] = pkceCharset[int(b)%len(pkceCharset)] } return string(out), nil } // Base64URLEncode encodes the input bytes using base64 URL encoding without padding. func Base64URLEncode(data []byte) string { return base64.RawURLEncoding.EncodeToString(data) }