package util import ( "crypto/rand" "errors" ) const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // rejectionThreshold — граница равномерности при отображении байта в алфавит. // // В алфавите 62 символа, а байт принимает 256 значений. 256 = 4×62 + 8, // поэтому при прямом `charset[b % 62]` первые ВОСЕМЬ символов алфавита // («a»…«h») выпадают пятью значениями байта, а остальные 54 — четырьмя. То // есть распределение смещено примерно на четверть в пользу начала алфавита. // // Байты со значением 248 и выше отбрасываются: оставшиеся 248 = 4×62 делятся // на длину алфавита нацело, и остаток становится равномерным. Цена — около 3% // отброшенных байтов, то есть один лишний вызов rand.Read на несколько сотен // символов. // // Практическая потеря энтропии у прежнего варианта была невелика, но это // ЕДИНСТВЕННЫЙ генератор секретов продукта: через него проходят JWT_SECRET, // PEER_SECRET_KEY, PEER_SECRET_ENCRYPTION_KEY, секрет trafficStats API, // секреты и auth_id пиров. Смещённое распределение в такой позиции чинится // пятью строками и не обсуждается на каждом аудите заново. const rejectionThreshold = 256 - (256 % len(charset)) // RandomString возвращает строку заданной длины из букв и цифр, равномерно // распределённых по алфавиту. func RandomString(length int) (string, error) { if length <= 0 { return "", errors.New("random string length must be positive") } out := make([]byte, 0, length) buf := make([]byte, length) for len(out) < length { if _, err := rand.Read(buf); err != nil { return "", err } for _, b := range buf { if int(b) >= rejectionThreshold { continue } out = append(out, charset[int(b)%len(charset)]) if len(out) == length { break } } } return string(out), nil }