diff --git a/CHANGELOG.md b/CHANGELOG.md index f81bc5a..b5d584f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 87f2dee..820d03d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -130,6 +136,22 @@ Capture generated content and reuse it later in the pattern: (randstr "(?[A-Z]{2})(?\\d{2})-\\k\\k") ; => "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 diff --git a/randstr/char-classes.rkt b/randstr/char-classes.rkt index b983803..52b9768 100644 --- a/randstr/char-classes.rkt +++ b/randstr/char-classes.rkt @@ -4,7 +4,8 @@ racket/string racket/list racket/random - racket/set) + racket/set + "config.rkt") (provide (contract-out @@ -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) @@ -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) diff --git a/randstr/cli/main.rkt b/randstr/cli/main.rkt index a4ba05e..ee21543 100644 --- a/randstr/cli/main.rkt +++ b/randstr/cli/main.rkt @@ -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)) @@ -17,6 +18,13 @@ (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] Generate random strings based on regex-like patterns. @@ -24,6 +32,7 @@ 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 ") @@ -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" @@ -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) @@ -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)))]) diff --git a/randstr/config.rkt b/randstr/config.rkt index 076e580..ffccfad 100644 --- a/randstr/config.rkt +++ b/randstr/config.rkt @@ -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) (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)))]) + (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))) diff --git a/randstr/generator.rkt b/randstr/generator.rkt index 15b13b7..895ce18 100644 --- a/randstr/generator.rkt +++ b/randstr/generator.rkt @@ -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)] @@ -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)] @@ -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)]) @@ -129,7 +129,7 @@ (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)]) @@ -137,7 +137,7 @@ ;; 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 @@ -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) @@ -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)))]) diff --git a/randstr/main.rkt b/randstr/main.rkt index 9be83cc..858e647 100644 --- a/randstr/main.rkt +++ b/randstr/main.rkt @@ -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 diff --git a/randstr/scribblings/randstr.scrbl b/randstr/scribblings/randstr.scrbl index fc54d5f..aa6b709 100644 --- a/randstr/scribblings/randstr.scrbl +++ b/randstr/scribblings/randstr.scrbl @@ -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} @@ -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: diff --git a/randstr/tests/test.rkt b/randstr/tests/test.rkt index 32b70fd..9e39a55 100644 --- a/randstr/tests/test.rkt +++ b/randstr/tests/test.rkt @@ -103,4 +103,25 @@ ;; Named group with quantifier on backreference (check-true (string? (randstr "(?[A-Z]{2})\\k{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")