Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Added
- ✨ New: Cryptographically secure random number generator option
- Library parameter `randstr-secure-random?` to enable secure random mode
- CLI option `-s/--secure` to use cryptographically secure random
- Environment variable `RANDSTR_SECURE` to enable secure random from environment

## 0.2.0 (2026-01-09)

### Added
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ racket -l randstr/cli "[a-z]{5}"
(randstr "[a-z]{5}") ; Generate a random 5-letter lowercase string
(randstr "(abc|def)+") ; Generate a string with repeated "abc" or "def"
(randstr* "[0-9]{3}" 10) ; Generate 10 random 3-digit numbers

;; Use cryptographically secure random number generator
(parameterize ([randstr-secure-random? #t])
(randstr "[A-Za-z0-9]{32}")) ; Generate a secure random string
```

### As a Command-Line Tool
Expand All @@ -39,11 +43,13 @@ racket -l randstr/cli "[a-z]{5}"
randstr "[a-z]{5}" # Generate one random string
randstr -n 10 "[0-9]{3}" # Generate 10 random 3-digit numbers
randstr -m 20 "a+" # '+' and '*' max repetition cap
randstr -s "[A-Z]{10}" # Use cryptographically secure random
```

You can also set an environment variable:
You can also set environment variables:

- `RANDSTR_MAX_REPEAT`: positive integer; default cap for `*` and `+` (CLI only).
- `RANDSTR_SECURE`: set to `1`, `true`, `yes`, or `on` to enable cryptographically secure random (CLI only).

## Pattern Syntax

Expand Down Expand Up @@ -130,6 +136,22 @@ Capture generated content and reuse it later in the pattern:
(randstr "(?<a>[A-Z]{2})(?<b>\\d{2})-\\k<a>\\k<b>") ; => "XY42-XY42"
```

### Cryptographically Secure Random

For security-sensitive applications (e.g., generating tokens, passwords, or secrets), enable cryptographically secure random number generation:

```racket
;; Using parameterize (recommended for scoped usage)
(parameterize ([randstr-secure-random? #t])
(randstr "[A-Za-z0-9]{32}")) ; => Secure random 32-character string

;; Or set globally
(randstr-secure-random? #t)
(randstr "[0-9]{6}") ; => Secure random 6-digit code
```

**Note**: Cryptographically secure random uses `crypto-random-bytes` from Racket, which is backed by the operating system's cryptographic random number generator (e.g., `/dev/urandom` on Unix systems). This is slower than the default pseudo-random number generator but provides unpredictable output suitable for security purposes.

## Examples

```racket
Expand Down
11 changes: 6 additions & 5 deletions randstr/char-classes.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
racket/string
racket/list
racket/random
racket/set)
racket/set
"config.rkt")

(provide
(contract-out
Expand Down Expand Up @@ -57,12 +58,12 @@
;; Generate a random character
(define (random-character)
(let ([chars "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"])
(string-ref chars (random (string-length chars)))))
(string-ref chars (randstr-random (string-length chars)))))

;; Generate a random word character (alphanumeric + underscore)
(define (random-word-char)
(let ([chars "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"])
(string-ref chars (random (string-length chars)))))
(string-ref chars (randstr-random (string-length chars)))))

;; Generate a random whitespace character
(define (random-whitespace-char)
Expand All @@ -86,11 +87,11 @@

;; Get a random element from a list
(define (random-ref lst)
(list-ref lst (random (length lst))))
(list-ref lst (randstr-random (length lst))))

;; Get a random element from a vector
(define (vector-random-ref vec)
(vector-ref vec (random (vector-length vec))))
(vector-ref vec (randstr-random (vector-length vec))))

;; Generate list of alphanumeric characters
(define (alphanumeric-chars)
Expand Down
18 changes: 18 additions & 0 deletions randstr/cli/main.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
(define pattern (make-parameter ""))
(define count (make-parameter 1))
(define max-repeat (make-parameter #f))
(define secure-random (make-parameter #f))

(define (env->positive-integer name)
(define v (getenv name))
Expand All @@ -17,13 +18,21 @@
(define n (string->number v))
(and (exact-integer? n) (positive? n) n)]))

(define (env->boolean name)
(define v (getenv name))
(cond
[(or (not v) (string=? v "")) #f]
[else
(member (string-downcase v) '("1" "true" "yes" "on"))]))

(define help-text
"Usage: randstr [options] <pattern>
Generate random strings based on regex-like patterns.

Options:
-n, --count N Generate N strings (default: 1)
-m, --max-repeat N Maximum repetition for * and + (default: env RANDSTR_MAX_REPEAT or 5)
-s, --secure Use cryptographically secure random number generator (default: env RANDSTR_SECURE or false)
-h, --help Show this help message
")

Expand All @@ -36,6 +45,10 @@ Options:
(define env-max (env->positive-integer "RANDSTR_MAX_REPEAT"))
(when env-max
(randstr-max-repeat env-max))

;; Apply env default for secure random (if present)
(when (env->boolean "RANDSTR_SECURE")
(secure-random #t))

(command-line
#:program "randstr"
Expand All @@ -44,6 +57,8 @@ Options:
(count (string->number N))]
[("-m" "--max-repeat") N "Maximum repetition for * and +"
(max-repeat (string->number N))]
[("-s" "--secure") "Use cryptographically secure random number generator"
(secure-random #t)]
;;; [("-h" "--help") "Show help"
;;; (show-help)]
#:args (pattern-arg)
Expand All @@ -52,6 +67,9 @@ Options:
(when (max-repeat)
(randstr-max-repeat (max-repeat)))

(when (secure-random)
(randstr-secure-random? #t))

(if (= (count) 1)
(displayln (randstr (pattern)))
(for ([str (in-list (randstr* (pattern) (count)))])
Expand Down
71 changes: 69 additions & 2 deletions randstr/config.rkt
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
#lang racket/base

(require racket/contract)
(require racket/contract
racket/random)

(provide
(contract-out
;; Maximum repetition for '*' and '+' quantifiers.
;; '*' picks an integer in [0, max], '+' picks an integer in [1, max].
[randstr-max-repeat (parameter/c exact-positive-integer?)]))
[randstr-max-repeat (parameter/c exact-positive-integer?)]
;; When #t, use cryptographically secure random number generation.
;; Default is #f (use standard PRNG for better performance).
[randstr-secure-random? (parameter/c boolean?)]
;; Generate a random non-negative integer less than n.
;; Uses crypto-random-bytes when randstr-secure-random? is #t.
[randstr-random (exact-positive-integer? . -> . exact-nonnegative-integer?)]
;; Generate a random floating point number in [0, 1).
;; Uses crypto-random-bytes when randstr-secure-random? is #t.
[randstr-random-real (-> (and/c real? (>=/c 0) (</c 1)))]))

(define randstr-max-repeat
(make-parameter
Expand All @@ -15,3 +25,60 @@
(cond
[(and (exact-integer? v) (positive? v)) v]
[else (raise-argument-error 'randstr-max-repeat "exact-positive-integer?" v)]))))

;; Parameter to enable cryptographically secure random number generation
(define randstr-secure-random?
(make-parameter #f))

;; Constants for byte manipulation
(define 2^64 (expt 2 64)) ; Used for converting 8 bytes to [0, 1) float
(define BITS-PER-BYTE 8) ; Number of bits per byte
(define VALUES-PER-BYTE 256) ; Number of distinct values per byte (2^8)
(define MIN-BYTES 4) ; Minimum bytes for reasonable randomness (32 bits)

;; Calculate the number of bytes needed to represent values up to n
;; Uses ceiling to ensure we have enough bytes for the full range
(define (bytes-needed-for n)
(max MIN-BYTES (inexact->exact (ceiling (/ (+ 1 (integer-length n)) BITS-PER-BYTE)))))

;; Generate a random integer in [0, n) using crypto-random-bytes with rejection sampling
;; This ensures unbiased results by rejecting values that would cause modulo bias
(define (crypto-random-integer n)
(let* ([byte-count (bytes-needed-for n)]
;; Maximum value that can be represented with byte-count bytes
[max-val (expt VALUES-PER-BYTE byte-count)]
;; Largest multiple of n that fits in max-val
;; We reject values >= limit to avoid modulo bias
[limit (* n (quotient max-val n))])
(let loop ()
(let* ([bytes (crypto-random-bytes byte-count)]
;; Convert bytes to integer in big-endian order (most significant byte first)
[val (for/fold ([acc 0]) ([i (in-range byte-count)])
(+ (bytes-ref bytes i) (* acc VALUES-PER-BYTE)))])
Comment on lines +56 to +57
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The byte-to-integer conversion accumulates bytes in little-endian order (least significant byte first), but the comment doesn't clarify this. The order doesn't affect randomness, but for code clarity and potential debugging, consider adding a comment about the byte order being used, or using a more explicit loop structure that makes the order clear.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in commit 64b605a. Added comment clarifying big-endian byte order in the conversion.

(if (< val limit)
(modulo val n)
;; Reject and retry to avoid bias
(loop))))))

;; Generate a random floating-point number in [0, 1) using crypto-random-bytes
(define (crypto-random-real)
;; Use 8 bytes (64 bits) for good precision
(let* ([bytes (crypto-random-bytes BITS-PER-BYTE)]
[val (for/fold ([acc 0]) ([i (in-range BITS-PER-BYTE)])
(+ (bytes-ref bytes i) (* acc VALUES-PER-BYTE)))])
;; Divide by 2^64 to get a value in [0, 1)
(/ val 2^64)))

;; Generate a random non-negative integer less than n.
;; Uses cryptographically secure random when randstr-secure-random? is #t.
(define (randstr-random n)
(if (randstr-secure-random?)
(crypto-random-integer n)
(random n)))

;; Generate a random floating point number in [0, 1).
;; Uses cryptographically secure random when randstr-secure-random? is #t.
(define (randstr-random-real)
(if (randstr-secure-random?)
(crypto-random-real)
(random)))
14 changes: 7 additions & 7 deletions randstr/generator.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
;; For order 3: average of 3 samples, variance = original/3, etc.
(define (normal-sample mean order)
(let ([samples (for/list ([i (in-range order)])
(random))])
(randstr-random-real))])
;; Sum of uniform samples centered around 0.5
;; Scale to have mean 0, then scale to target mean
(let* ([sum (apply + samples)]
Expand All @@ -80,7 +80,7 @@
;; Uses the same central limit theorem approach but maps to the specified range
(define (normal-range-sample min-val max-val order)
(let ([samples (for/list ([i (in-range order)])
(random))])
(randstr-random-real))])
;; Average of samples gives us a value centered around 0.5
(let* ([sum (apply + samples)]
[avg (/ sum order)]
Expand Down Expand Up @@ -120,7 +120,7 @@
(make-list quantifier char-or-func))]
[(and quantifier (eq? quantifier 'star)) ; *
(let* ([max-repeat (randstr-max-repeat)]
[count (random (+ max-repeat 1))])
[count (randstr-random (+ max-repeat 1))])
(if (procedure? char-or-func)
;; If char-or-func is a function, call it count times to get count different characters
(for/list ([i (in-range count)])
Expand All @@ -129,15 +129,15 @@
(make-list count char-or-func)))]
[(and quantifier (eq? quantifier 'plus)) ; +
(let* ([max-repeat (randstr-max-repeat)]
[count (+ 1 (random (max 1 max-repeat)))])
[count (+ 1 (randstr-random (max 1 max-repeat)))])
(if (procedure? char-or-func)
;; If char-or-func is a function, call it count times to get count different characters
(for/list ([i (in-range count)])
(char-or-func))
;; If char-or-func is a character, repeat it count times
(make-list count char-or-func)))]
[(and quantifier (eq? quantifier 'optional)) ; ?
(if (zero? (random 2))
(if (zero? (randstr-random 2))
'() ; empty list means don't add anything
(if (procedure? char-or-func)
;; If char-or-func is a function, call it once to get a character
Expand Down Expand Up @@ -210,7 +210,7 @@
(let ([chars (cc:unicode-property-chars property)])
(if (null? chars)
#\? ; Default character if no chars
(list-ref chars (random (length chars))))))])
(list-ref chars (randstr-random (length chars))))))])
(let ([chars (apply-quantifier char-func (token-quantifier token))])
(loop (cdr tokens) (append (reverse chars) result) env)))]
[(group)
Expand All @@ -227,7 +227,7 @@
;; If there are alternatives, we need to handle quantifiers properly
;; For each repetition, we should randomly select an alternative
(let ([char-func (lambda ()
(let* ([selected-alternative (list-ref alternatives (random (length alternatives)))]
(let* ([selected-alternative (list-ref alternatives (randstr-random (length alternatives)))]
[sub-tokens (tokenize-pattern selected-alternative)])
(let-values ([(sub-string _) (generate-from-tokens-with-env sub-tokens env)])
sub-string)))])
Expand Down
1 change: 1 addition & 0 deletions randstr/main.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
[randstr* (string? exact-positive-integer? . -> . (listof string?))]
[parse-and-generate (string? . -> . string?)]
[randstr-max-repeat (parameter/c exact-positive-integer?)]
[randstr-secure-random? (parameter/c boolean?)]
[tokenize-pattern (string? . -> . (listof (struct/c token any/c any/c any/c)))]
[parse-character-class (list? . -> . (values vector? list?))]
[parse-quantifier (list? . -> . (values
Expand Down
21 changes: 21 additions & 0 deletions randstr/scribblings/randstr.scrbl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A library for generating random strings based on regex-like patterns.
@item{Named groups and backreferences for pattern reuse}
@item{Command-line interface for quick generation}
@item{Fair distribution: duplicate characters in classes are deduplicated}
@item{Cryptographically secure random number generation option}
]

@section{Functions}
Expand All @@ -44,6 +45,26 @@ A library for generating random strings based on regex-like patterns.
]
}

@defparam[randstr-secure-random? secure? boolean?]{
When set to @racket[#t], all random number generation uses cryptographically secure
random numbers (via @racket[crypto-random-bytes]). Default is @racket[#f].

Use this for security-sensitive applications like generating tokens, passwords, or secrets.

@racketblock[
;; Using parameterize for scoped secure mode
(parameterize ([randstr-secure-random? #t])
(randstr "[A-Za-z0-9]{32}")) ; => Secure random 32-character string

;; Or set globally
(randstr-secure-random? #t)
(randstr "[0-9]{6}") ; => Secure random 6-digit code
]

@bold{Note}: Cryptographically secure random is slower than the default pseudo-random
number generator, but provides unpredictable output suitable for security purposes.
}

@section{Pattern Syntax}

The following pattern syntax is supported:
Expand Down
21 changes: 21 additions & 0 deletions randstr/tests/test.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,25 @@
;; Named group with quantifier on backreference
(check-true (string? (randstr "(?<prefix>[A-Z]{2})\\k<prefix>{2}")))

;; Tests for cryptographically secure random mode
;; Default should be non-secure
(check-false (randstr-secure-random?))

;; Test that secure mode works with parameterize
(parameterize ([randstr-secure-random? #t])
(check-true (randstr-secure-random?))
(check-true (string? (randstr "[a-z]{10}")))
(check-true (string? (randstr "\\w{5}")))
(check-true (string? (randstr "[[:alpha:]]{8}")))
(check-true (string? (randstr "(abc|def){3}"))))

;; Verify it returns to normal mode after parameterize
(check-false (randstr-secure-random?))

;; Test that randstr* works in secure mode
(parameterize ([randstr-secure-random? #t])
(let ([results (randstr* "[0-9]{6}" 5)])
(check-equal? (length results) 5)
(check-true (andmap string? results))))

(printf "All tests passed!\n")