Skip to content

Commit bc7da53

Browse files
gwleclercclaude
andcommitted
fix(types): remove modulo bias from NewID
Reducing crypto/rand bytes with % over a 62-char alphabet favored the first 8 characters (256 % 62 = 8). Reject bytes >= 248 (the largest multiple of 62 that fits in a byte) so every character is equally likely. IDs stay opaque and the persisted format is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent acfaa00 commit bc7da53

1 file changed

Lines changed: 25 additions & 14 deletions

File tree

server/types/id.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,36 @@ package types
22

33
import "crypto/rand"
44

5-
// idAlphabet is a URL-safe base62 alphabet. idLength is chosen to keep IDs short (like the
6-
// previous shortid) while staying collision-resistant: 62^12 ≈ 3.2e21 possibilities.
5+
// idAlphabet is a URL-safe base62 alphabet. idLength keeps IDs short while staying
6+
// collision-resistant: 62^12 ≈ 3.2e21 possibilities.
77
const (
88
idAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
99
idLength = 12
1010
)
1111

12-
// NewID returns a short, random, URL-safe identifier. It replaces the unmaintained
13-
// teris-io/shortid with a zero-dependency crypto/rand generator, keeping IDs short (unlike a
14-
// UUID). IDs are opaque, so the exact alphabet/length is not part of the persisted format.
12+
// NewID returns a short, random, URL-safe identifier using crypto/rand with no external
13+
// dependency. IDs are opaque, so the alphabet/length are not part of the persisted format.
1514
func NewID() string {
16-
b := make([]byte, idLength)
17-
if _, err := rand.Read(b); err != nil {
18-
// crypto/rand.Read does not fail on supported platforms; mirror the previous
19-
// shortid.MustGenerate behavior of not returning an error.
20-
panic(err)
15+
// Largest multiple of the alphabet size that fits in a byte (62*4 = 248). Rejecting bytes
16+
// >= limit removes the modulo bias so every character is equally likely.
17+
const limit = 256 - (256 % len(idAlphabet))
18+
id := make([]byte, 0, idLength)
19+
buf := make([]byte, idLength)
20+
for len(id) < idLength {
21+
if _, err := rand.Read(buf); err != nil {
22+
// crypto/rand.Read does not fail on supported platforms; not returning an error
23+
// keeps NewID callable from initializers.
24+
panic(err)
25+
}
26+
for _, b := range buf {
27+
if int(b) >= limit {
28+
continue
29+
}
30+
id = append(id, idAlphabet[int(b)%len(idAlphabet)])
31+
if len(id) == idLength {
32+
break
33+
}
34+
}
2135
}
22-
for i := range b {
23-
b[i] = idAlphabet[int(b[i])%len(idAlphabet)]
24-
}
25-
return string(b)
36+
return string(id)
2637
}

0 commit comments

Comments
 (0)