You've already forked pocketbase
mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-11-25 07:34:10 +02:00
180 lines
4.5 KiB
Go
180 lines
4.5 KiB
Go
package security
|
|
|
|
import (
|
|
cryptoRand "crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"regexp/syntax"
|
|
"strings"
|
|
)
|
|
|
|
const defaultMaxRepeat = 6
|
|
|
|
var anyCharNotNLPairs = []rune{'A', 'Z', 'a', 'z', '0', '9'}
|
|
|
|
// RandomStringByRegex generates a random string matching the regex pattern.
|
|
// If optFlags is not set, fallbacks to [syntax.Perl].
|
|
//
|
|
// NB! While the source of the randomness comes from [crypto/rand] this method
|
|
// is not recommended to be used on its own in critical secure contexts because
|
|
// the generated length could vary too much on the used pattern and may not be
|
|
// as secure as simply calling [security.RandomString].
|
|
// If you still insist on using it for such purposes, consider at least
|
|
// a large enough minimum length for the generated string, e.g. `[a-z0-9]{30}`.
|
|
//
|
|
// This function is inspired by github.com/pipe01/revregexp, github.com/lucasjones/reggen and other similar packages.
|
|
func RandomStringByRegex(pattern string, optFlags ...syntax.Flags) (string, error) {
|
|
var flags syntax.Flags
|
|
if len(optFlags) == 0 {
|
|
flags = syntax.Perl
|
|
} else {
|
|
for _, f := range optFlags {
|
|
flags |= f
|
|
}
|
|
}
|
|
|
|
r, err := syntax.Parse(pattern, flags)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var sb = new(strings.Builder)
|
|
|
|
err = writeRandomStringByRegex(r, sb)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
func writeRandomStringByRegex(r *syntax.Regexp, sb *strings.Builder) error {
|
|
// https://pkg.go.dev/regexp/syntax#Op
|
|
switch r.Op {
|
|
case syntax.OpCharClass:
|
|
c, err := randomRuneFromPairs(r.Rune)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = sb.WriteRune(c)
|
|
return err
|
|
case syntax.OpAnyChar, syntax.OpAnyCharNotNL:
|
|
c, err := randomRuneFromPairs(anyCharNotNLPairs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = sb.WriteRune(c)
|
|
return err
|
|
case syntax.OpAlternate:
|
|
idx, err := randomNumber(len(r.Sub))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeRandomStringByRegex(r.Sub[idx], sb)
|
|
case syntax.OpConcat:
|
|
var err error
|
|
for _, sub := range r.Sub {
|
|
err = writeRandomStringByRegex(sub, sb)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
return err
|
|
case syntax.OpRepeat:
|
|
return repeatRandomStringByRegex(r.Sub[0], sb, r.Min, r.Max)
|
|
case syntax.OpQuest:
|
|
return repeatRandomStringByRegex(r.Sub[0], sb, 0, 1)
|
|
case syntax.OpPlus:
|
|
return repeatRandomStringByRegex(r.Sub[0], sb, 1, -1)
|
|
case syntax.OpStar:
|
|
return repeatRandomStringByRegex(r.Sub[0], sb, 0, -1)
|
|
case syntax.OpCapture:
|
|
return writeRandomStringByRegex(r.Sub[0], sb)
|
|
case syntax.OpLiteral:
|
|
_, err := sb.WriteString(string(r.Rune))
|
|
return err
|
|
default:
|
|
return fmt.Errorf("unsupported pattern operator %d", r.Op)
|
|
}
|
|
}
|
|
|
|
func repeatRandomStringByRegex(r *syntax.Regexp, sb *strings.Builder, min int, max int) error {
|
|
if max < 0 {
|
|
max = defaultMaxRepeat
|
|
}
|
|
|
|
if max < min {
|
|
max = min
|
|
}
|
|
|
|
n := min
|
|
if max != min {
|
|
randRange, err := randomNumber(max - min)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n += randRange
|
|
}
|
|
|
|
var err error
|
|
for i := 0; i < n; i++ {
|
|
err = writeRandomStringByRegex(r, sb)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func randomRuneFromPairs(pairs []rune) (rune, error) {
|
|
if len(pairs)%2 != 0 {
|
|
return 0, fmt.Errorf("invalid pairs slice: odd number of elements")
|
|
}
|
|
|
|
// Pre-calculate the cumulative size of all ranges to make the selection process more efficient.
|
|
cumulativeSizes := make([]int, len(pairs)/2)
|
|
totalRunes := 0
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
start, end := pairs[i], pairs[i+1]
|
|
if start > end {
|
|
return 0, fmt.Errorf("invalid range: start '%c' > end '%c'", start, end)
|
|
}
|
|
totalRunes += int(end - start + 1)
|
|
cumulativeSizes[i/2] = totalRunes
|
|
}
|
|
|
|
if totalRunes == 0 {
|
|
return 0, errors.New("no runes to choose from")
|
|
}
|
|
|
|
// Select a random number in the range of total runes.
|
|
runeNumber, err := randomNumber(totalRunes)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to generate random number: %w", err)
|
|
}
|
|
|
|
// Find which range the selected number falls into using the pre-calculated cumulative sizes.
|
|
for i, size := range cumulativeSizes {
|
|
if runeNumber < size {
|
|
startRune := pairs[i*2]
|
|
previousSize := 0
|
|
if i > 0 {
|
|
previousSize = cumulativeSizes[i-1]
|
|
}
|
|
return startRune + rune(runeNumber-previousSize), nil
|
|
}
|
|
}
|
|
|
|
// This part should be unreachable if the logic is correct.
|
|
// It indicates a bug in this function or in randomNumber.
|
|
panic("unreachable: failed to find a rune")
|
|
}
|
|
|
|
func randomNumber(maxSoft int) (int, error) {
|
|
randRange, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(maxSoft)))
|
|
|
|
return int(randRange.Int64()), err
|
|
}
|