randtok.ss
#lang scheme/base
;;; @Package     randtok
;;; @Subtitle    Random Token String Generation in Scheme
;;; @HomePage    http://www.neilvandyke.org/randtok-scheme/
;;; @Author      Neil Van Dyke
;;; @Version     0.1
;;; @Date        2009-06-07
;;; @PLaneT      neil/randtok:1:0

;; $Id: randtok.ss,v 1.17 2009/06/07 18:45:15 neilpair Exp $

;;; @legal
;;; Copyright @copyright{} 2009 Neil Van Dyke.  This program is Free Software;
;;; you can redistribute it and/or modify it under the terms of the GNU Lesser
;;; General Public License as published by the Free Software Foundation; either
;;; version 3 of the License (LGPL 3), or (at your option) any later version.
;;; This program is distributed in the hope that it will be useful, but without
;;; any warranty; without even the implied warranty of merchantability or
;;; fitness for a particular purpose.  See
;;; @indicateurl{http://www.gnu.org/licenses/} for details.  For other licenses
;;; and consulting, please contact the author.
;;; @end legal

;;; @section Introduction

;;; The @b{randtok} library is intended for generating random strings such as
;;; for secure authentication tokens for Web site session cookies.
;;;
;;; @b{randtok} makes fairly efficient use of bits in a scarce random byte
;;; source like @code{/dev/random}.  For example, an 8-character token
;;; consisting of only the lower-case Latin letters consumes only 5 random
;;; bytes, rather than 8.  In a security sensitive application, efficient use
;;; of the source might be important when using effectively random data rather
;;; than a possibly predictable pseudorandom number generator (PRNG).
;;;
;;; Note that this code has not been tested heavily.

(define (%make-variable-bit-reader in (max-bits 8))
  ;; Note: max-bits is currently ignored.
  (or (and (integer? max-bits)
           (<= 1 max-bits 8))
      (raise-type-error 'make-variable-bit-reader
                        "integer 1..8"
                        1
                        max-bits))
  (let ((sema        (make-semaphore 1))
        (last-byte   #f)
        (unused-bits 0))
    (let ((<bit-reader>
           (lambda (num-bits)
             (dynamic-wind
               (lambda ()
                 (semaphore-wait sema))
               (lambda ()
                 (cond
                  ((not (integer? num-bits))
                   (raise-type-error '<bit-reader>
                                     "integer 0..8"
                                     0
                                     num-bits))
                  ((zero? num-bits) 0)
                  (else
                   (let ((diff (- unused-bits num-bits)))
                     (cond
                      ((>= diff 0)
                       (begin0 (bitwise-bit-field last-byte diff unused-bits)
                         (set! unused-bits diff)))
                      ((> num-bits 8)
                       (raise-type-error '<bit-reader>
                                         "integer 0..8"
                                         0
                                         num-bits))
                      (else
                       (let-values
                           (((num1 num-bits)
                             (if (zero? unused-bits)
                                 (values 0 num-bits)
                                 (let ((abs-diff (abs diff)))
                                   (values (arithmetic-shift (bitwise-bit-field
                                                              last-byte
                                                              0
                                                              unused-bits)
                                                             abs-diff)
                                           abs-diff)))))
                         (+ num1
                            (if (= num-bits 8)
                                (read-byte in)
                                (begin
                                  (set! last-byte (read-byte in))
                                  (set! unused-bits (- 8 num-bits))
                                  (bitwise-bit-field last-byte
                                                     unused-bits
                                                     8)))))))))))
               (lambda () (semaphore-post sema))))))
      <bit-reader>)))

(define (%make-fixed-bit-reader in num-bits)
  ;; TODO: We could special-case this for when num-bits is 8.
  (if (and (integer? num-bits)
           (<= 1 num-bits 8))
      (let* ((variable-bit-reader (%make-variable-bit-reader in num-bits))
             (<fixed-bit-reader>  (lambda ()
                                    (variable-bit-reader num-bits))))
        <fixed-bit-reader>)
      (raise-type-error 'make-fixed-bit-reader
                        "integer 1..8"
                        1
                        in
                        num-bits)))

(define %log2
  (let ((log-of-2 (log 2)))
    (lambda (x)
      (/ (log x) log-of-2))))

;;; @defproc make-random-token-string-maker port str len
;;;
;;; Returns a procedure that generats random token strings of length @var{len},
;;; using characters from string @var{str}, with random numbers drawn from
;;; input port @var{port}.
;;;
;;; @lisp
;;; (define random-in (open-input-file "/dev/random"))
;;;
;;; (define f (make-random-token-string-maker
;;;            random-in "0123456789abcdef" 10))
;;;
;;; f   @result{} <procedure:<random-token-string-maker>>
;;; (f) @result{} "7dd73b9ec0"
;;; (f) @result{} "1f7bc42210"
;;;
;;; (close-input-port random-in)
;;; @end lisp
;;;
;;; This is useful for using a single open file of @code{/dev/random} to
;;; generate multiple random token strings throughout the execution of a
;;; program.  For example:
;;;
;;; @lisp
;;; (call-with-input-file "/dev/random"
;;;   (lambda (random-in)
;;;     (file-stream-buffer-mode random-in 'none)
;;;     (let ((randtok (make-random-token-string-maker
;;;                     random-in "ABCD" 10)))
;;;       ...
;;;       (printf "Here is a random token: ~S\n" (randtok))
;;;       ...
;;;       (printf "Here is another random token: ~S\n" (randtok))
;;;       ...)))
;;; @end lisp
;;;
;;; Note that you may wish to disable buffering of the input port, as is done
;;; above.

(define (make-random-token-string-maker port str len)
  (or (and (integer? len)
           (> len 0))
      (raise-type-error 'make-random-token-string-maker
                        "positive integer"
                        2
                        port
                        str
                        len))
  (let* ((str-len   (string-length str))
         (num-bits  (inexact->exact (ceiling (%log2 str-len))))
         (read-bits (%make-fixed-bit-reader port num-bits)))
    (let ((<random-token-string-maker>
           (lambda ()
             (let loop ((i      len)
                        (result '()))
               (if (zero? i)
                   (apply string result)
                   (let ((bits (read-bits)))
                     (if (< bits str-len)
                         (loop (- i 1)
                               (cons (string-ref str bits) result))
                         (loop i result))))))))
      <random-token-string-maker>)))

;;; @defparam current-random-byte-file
;;;
;;; This parameter is a filename for a source of random bytes.  It is used by
;;; procedure @code{random-token-string}.  The default value is
;;; @code{"/dev/random"}.

(define current-random-byte-file (make-parameter "/dev/random"))

;;; @defproc random-token-string [ len ]
;;;
;;; This is a convenient procedure for generating alphanumeric random token
;;; strings, using upper-case and lower-case Latin letters and Arabic digits.
;;; The length defaults to 8.  For example:
;;;
;;; @lisp
;;; (random-token-string)    @result{} "SW6gu2gw"
;;; (random-token-string)    @result{} "9RjdZxyj"
;;; (random-token-string 20) @result{} "thCOmSte6OXWByxtn0G5"
;;; @end lisp
;;;
;;; Note that reusing the output of @code{make-random-token-string-maker}
;;; will be more efficient in some cases.

(define (random-token-string (len 8))
  (call-with-input-file (current-random-byte-file)
    (lambda (in)
      (file-stream-buffer-mode in 'none)
      ((make-random-token-string-maker
        in
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
        len)))))

;;; @unnumberedsec History

;;; @table @asis
;;;
;;; @item Version 0.1 -- 2009-06-07 -- PLaneT @code{(1 0)}
;;; First release.
;;;
;;; @end table

(provide
 current-random-byte-file
 make-random-token-string-maker
 random-token-string)