* fixed NT services not working. app must now call lcoreinit() at some point before...
[lcore.git] / lcorernd.pas
diff --git a/lcorernd.pas b/lcorernd.pas
new file mode 100644 (file)
index 0000000..006f6ce
--- /dev/null
@@ -0,0 +1,427 @@
+{ Copyright (C) 2005 Bas Steendijk and Peter Green\r
+  For conditions of distribution and use, see copyright notice in zlib_license.txt\r
+  which is included in the package\r
+  ----------------------------------------------------------------------------- }\r
+\r
+unit lcorernd;\r
+\r
+interface\r
+\r
+{$include lcoreconfig.inc}\r
+\r
+{\r
+written by Bas Steendijk (beware)\r
+\r
+the aim of this unit is to provide randomness in a consistent way, using OS specific methods for seeding\r
+\r
+this unit uses MD5 for performance and code size, but it is made so it is easy to use a different hash,\r
+as long as it is atleat 128 bits, and a multiple of the "word size" (32 bits)\r
+\r
+goals:\r
+\r
+- for the code to be:\r
+ - relatively simple and small\r
+ - reasonably fast\r
+\r
+- for the numbers to be\r
+ - random: pass diehard and similar tests\r
+ - unique: generate UUID's\r
+ - secure: difficult for a remote attacker to guess the internal state, even\r
+   when given some output\r
+\r
+typical intended uses:\r
+ - anything that needs random numbers without extreme demands on security or\r
+   speed should be able to use this\r
+ - seeding other (faster) RNG's\r
+ - generation of passwords, UUID's, cookies, and session keys\r
+ - randomizing protocol fields to protect against spoofing attacks\r
+ - randomness for games\r
+\r
+this is not intended to be directly used for:\r
+- high securirity purposes (generating RSA root keys etc)\r
+- needing random numbers at very high rates (disk wiping, some simulations, etc)\r
+\r
+performance:\r
+- 24 MB/s on 2.2 GHz athlon64 core on windows XP 32 bits\r
+- 6.4 MB/s on 1 GHz p3 on linux\r
+\r
+exe size:\r
+- fpc 2.2, linux: fastmd5: 12 kb; lcorernd: 6 kb.\r
+- delphi 6: fastmd5: 3 kb; lcorernd: 2 kb\r
+\r
+reasoning behind the security of this RNG:\r
+\r
+- seeding:\r
+1: i assume that any attacker has no local access to the machine. if one gained\r
+  this, then there are more seriousness weaknesses to consider.\r
+2: i attempt to use enough seeding to be difficult to guess.\r
+  on windows: GUID, various readouts of hi res timestamps, heap stats, cursor\r
+  position\r
+  on *nix: i assume /dev/(u)random output is secure and difficult to guess. if\r
+  it misses, i use /dev/wtmp, which typically has as lot of entropy in it. i also use hi res timestamps.\r
+3: on a state compromise, one can easily get up to the hash size worth of previous output, beyond that one has\r
+  to invert the hash operation.\r
+\r
+- mixing/expansion: a secure hash random walk on a buffer with a constant secret and a changing exposed part,\r
+  the big secret part serves to make it difficult for an attacker to predict next and previous output.\r
+  the secret part is changed during a reseed.\r
+\r
+\r
+                                       OS randomness\r
+                                             v\r
+                              <wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww>\r
+ ____________________________  ________________________________________________\r
+[            pool            ][                    seed                        ]\r
+[hashsize][hashsize][hashsize]\r
+          <rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr>\r
+                bighash()             seeding\r
+                   v\r
+          <wwwwwwwwwwwwwwwwww>\r
+<rrrrrrrrrrrrrrrrrrrrrrrrrrrr>\r
+  hash()                            random walk\r
+    v\r
+<wwwwwwww>\r
+[ output ][      secret      ]\r
+\r
+\r
+this needs testing on platforms other than i386\r
+\r
+\r
+these routines are called by everything else in lcore, and if the app coder desires, by the app.\r
+because one may want to use their own random number source, the PRNG here can be excluded from linking,\r
+and the routines here can be hooked.\r
+}\r
+\r
+{$include uint32.inc}\r
+\r
+{return a dword with 32 random bits}\r
+type\r
+  wordtype=uint32;\r
+\r
+var\r
+  randomdword:function:wordtype;\r
+\r
+{fill a buffer with random bytes}\r
+procedure fillrandom(var buf;length:integer);\r
+\r
+{generate an integer of 0 <= N < i}\r
+function randominteger(i:longint):longint;\r
+\r
+{generate an integer with the lowest b bits being random}\r
+function randombits(b:integer):longint;\r
+\r
+{generate a version 4 random uuid}\r
+function generate_uuid:string;\r
+\r
+{$ifndef nolcorernd}\r
+\r
+{call this to mix seeding into the pool. is normally done automatically and does not have to be called\r
+but can be done if one desires more security, for example for key generation}\r
+procedure seedpool;\r
+\r
+{get some raw OS specific randomness. the output is not mixed so it should not be used directly as random numbers}\r
+function collect_seeding(var output;const bufsize:integer):integer;\r
+\r
+function internalrandomdword:wordtype;\r
+\r
+var\r
+  reseedinterval:integer=64;\r
+{$endif}\r
+\r
+implementation\r
+\r
+{$ifndef nolcorernd}\r
+uses\r
+  {$ifdef win32}windows,activex,types,{$endif}\r
+  {$ifdef unix}baseunix,unix,unixutil,{$endif}\r
+  fastmd5,sysutils;\r
+\r
+{$ifdef unix}{$include unixstuff.inc}{$endif}\r
+\r
+type\r
+  {hashtype must be array of bytes}\r
+  hashtype=tmd5;\r
+\r
+const\r
+  wordsizeshift=2;\r
+  wordsize=1 shl wordsizeshift;\r
+\r
+  {$if (wordsize <> sizeof(wordtype))}'wordsizeshift must be setcorrectly'{$ifend}\r
+\r
+  hashsize=sizeof(hashtype);\r
+  halfhashsize=hashsize div 2;\r
+  hashdwords=hashsize div wordsize;\r
+  pooldwords=3*hashdwords;\r
+  seeddwords=32;\r
+  hashpasssize=48; {this number has to be small enough that hashing this size uses only one block transform}\r
+\r
+var\r
+  {the seed part of this buffer must be atleast as big as the OS seed (windows: 104 bytes, unix: 36 bytes)}\r
+  pool:array[0..(pooldwords+seeddwords-1)] of wordtype;\r
+  reseedcountdown:integer;\r
+\r
+{$ifdef win32}\r
+function collect_seeding(var output;const bufsize:integer):integer;\r
+var\r
+  l:packed record\r
+    guid:array[0..3] of longint;\r
+    qpcbuf:array[0..1] of longint;\r
+    rdtscbuf:array[0..1] of longint;\r
+    systemtimebuf:array[0..3] of longint;\r
+    pid:longint;\r
+    tid:longint;\r
+    cursor:tpoint;\r
+    hs:theapstatus;\r
+  end absolute output;\r
+  rdtsc_0,rdtsc_1:integer;\r
+begin\r
+  result := 0;\r
+  if (bufsize < sizeof(l)) then exit;\r
+  result := sizeof(l);\r
+  {PID}\r
+  l.pid := GetCurrentProcessId;\r
+  l.tid := GetCurrentThreadId;\r
+\r
+  {COCREATEGUID}\r
+  cocreateguid(tguid(l.guid));\r
+\r
+  {QUERYPERFORMANCECOUNTER}\r
+  queryperformancecounter(tlargeinteger(l.qpcbuf));\r
+\r
+  {RDTSC}\r
+  {$ifdef cpu386}\r
+  asm\r
+    db $0F; db $31\r
+    mov rdtsc_0,eax\r
+    mov rdtsc_1,edx\r
+  end;\r
+  l.rdtscbuf[0] := rdtsc_0;\r
+  l.rdtscbuf[1] := rdtsc_1;\r
+  {$endif}\r
+  {GETSYSTEMTIME}\r
+  getsystemtime(tsystemtime(l.systemtimebuf));\r
+\r
+  {cursor position}\r
+  getcursorpos(l.cursor);\r
+\r
+  l.hs := getheapstatus;\r
+end;\r
+{$endif}\r
+\r
+{$ifdef unix}\r
+\r
+var\r
+  wtmpinited:boolean;\r
+  wtmpcached:hashtype;\r
+\r
+procedure wtmphash;\r
+var\r
+  f:file;\r
+  buf:array[0..4095] of byte;\r
+  numread:integer;\r
+  state:tmd5state;\r
+begin\r
+  if wtmpinited then exit;\r
+\r
+  assignfile(f,'/var/log/wtmp');\r
+  filemode := 0;\r
+  {$i-}reset(f,1);{$i+}\r
+  if (ioresult <> 0) then exit;\r
+  md5init(state);\r
+  while not eof(f) do begin\r
+    blockread(f,buf,sizeof(buf),numread);\r
+    md5process(state,buf,numread);\r
+  end;\r
+  closefile(f);\r
+  md5finish(state,wtmpcached);\r
+  wtmpinited := true;\r
+end;\r
+\r
+\r
+function collect_seeding(var output;const bufsize:integer):integer;\r
+var\r
+  f:file;\r
+  a:integer;\r
+  l:packed record\r
+    devrnd:array[0..3] of integer;\r
+    rdtscbuf:array[0..1] of integer;\r
+    tv:ttimeval;\r
+    pid:integer;\r
+  end absolute output;\r
+  rdtsc_0,rdtsc_1:integer;\r
+\r
+begin\r
+  result := 0;\r
+  if (bufsize < sizeof(l)) then exit;\r
+  result := sizeof(l);\r
+\r
+  {/DEV/URANDOM}\r
+  a := 1;\r
+  assignfile(f,'/dev/urandom');\r
+  filemode := 0;\r
+  {$i-}reset(f,1);{$i+}\r
+  a := ioresult;\r
+  if (a <> 0) then begin\r
+    assignfile(f,'/dev/random');\r
+    {$i-}reset(f,1);{$i+}\r
+    a := ioresult;\r
+  end;\r
+  if (a = 0) then begin\r
+    blockread(f,l.devrnd,sizeof(l.devrnd));\r
+    closefile(f);\r
+  end else begin\r
+    {the OS we are on has no /dev/random or /dev/urandom, get a hash from /var/log/wtmp}\r
+    wtmphash;\r
+    move(wtmpcached,l.devrnd,sizeof(l.devrnd));\r
+  end;\r
+  {get more randomness in case there's no /dev/random}\r
+  {$ifdef cpu386}{$ASMMODE intel}\r
+  asm\r
+    db $0F; db $31\r
+    mov rdtsc_0,eax\r
+    mov rdtsc_1,edx\r
+  end;\r
+  l.rdtscbuf[0] := rdtsc_0;\r
+  l.rdtscbuf[1] := rdtsc_1;\r
+  {$endif}\r
+\r
+  gettimeofday(l.tv);\r
+  l.pid := getpid;\r
+end;\r
+{$endif}\r
+\r
+{this produces a hash which is twice the native hash size (32 bytes for MD5)}\r
+procedure bighash(const input;len:integer;var output);\r
+var\r
+  inarr:array[0..65535] of byte absolute input;\r
+  outarr:array[0..65535] of byte absolute output;\r
+\r
+  h1,h2,h3,h4:hashtype;\r
+  a:integer;\r
+begin\r
+  a := len div 2;\r
+  {first hash round}\r
+  getmd5(inarr[0],a,h1);\r
+  getmd5(inarr[a],len-a,h2);\r
+\r
+  move(h1[0],h3[0],halfhashsize);\r
+  move(h2[0],h3[halfhashsize],halfhashsize);\r
+  move(h1[halfhashsize],h4[0],halfhashsize);\r
+  move(h2[halfhashsize],h4[halfhashsize],halfhashsize);\r
+\r
+  getmd5(h3,hashsize,outarr[0]);\r
+  getmd5(h4,hashsize,outarr[hashsize]);\r
+end;\r
+\r
+procedure seedpool;\r
+var\r
+  a:integer;\r
+begin\r
+  a := collect_seeding(pool[pooldwords],seeddwords*wordsize);\r
+  if (a = 0) then halt;\r
+  bighash(pool[hashdwords],(2*hashsize)+a,pool[hashdwords]);\r
+  getmd5(pool[0],hashpasssize,pool[0]);\r
+end;\r
+\r
+function internalrandomdword;\r
+begin\r
+  if (reseedcountdown <= 0) then begin\r
+    seedpool;\r
+    reseedcountdown := reseedinterval * hashdwords;\r
+  end else if ((reseedcountdown mod hashdwords) = 0) then begin;\r
+    getmd5(pool[0],hashpasssize,pool[0]);\r
+  end;\r
+  dec(reseedcountdown);\r
+\r
+  result := pool[reseedcountdown mod hashdwords];\r
+end;\r
+{$endif}\r
+\r
+procedure fillrandom(var buf;length:integer);\r
+var\r
+  a,b:integer;\r
+  buf_:array[0..16383] of uint32 absolute buf;\r
+\r
+begin\r
+  b := 0;\r
+  for a := (length shr wordsizeshift)-1 downto 0 do begin\r
+    buf_[b] := randomdword;\r
+    inc(b);\r
+  end;\r
+  length := length and (wordsize-1);\r
+  if length <> 0 then begin\r
+    a := randomdword;\r
+    move(a,buf_[b],length);\r
+  end;\r
+end;\r
+\r
+const\r
+  wordsizebits=32;\r
+\r
+function randombits(b:integer):longint;\r
+begin\r
+  result := randomdword;\r
+  result := result and (-1 shr (wordsizebits-b));\r
+  if (b = 0) then result := 0;\r
+end;\r
+\r
+function randominteger(i:longint):longint;\r
+var\r
+  a,b:integer;\r
+  j:integer;\r
+begin\r
+  //bitscounter := bitscounter + numofbitsininteger(i);\r
+  if (i = 0) then begin\r
+    result := 0;\r
+    exit;\r
+  end;\r
+  {find number of bits needed}\r
+  j := i-1;\r
+  if (j < 0) then begin\r
+    result := randombits(wordsizebits);\r
+    exit\r
+  end else if (j >= (1 shl (wordsizebits-2))) then begin\r
+    b := wordsizebits-1\r
+  end else begin\r
+    b := -1;\r
+    for a := 0 to (wordsizebits-2) do begin\r
+      if j < 1 shl a then begin\r
+        b := a;\r
+        break;\r
+      end;\r
+    end;\r
+  end;\r
+  repeat\r
+    result := randombits(b);\r
+  until result < i;\r
+end;\r
+\r
+const\r
+  ch:array[0..15] of char='0123456789abcdef';\r
+\r
+function generate_uuid:string;\r
+var\r
+  buf:array[0..7] of word;\r
+function inttohex(w:word):string;\r
+begin\r
+  result := ch[w shr 12] + ch[(w shr 8) and $f] + ch[(w shr 4) and $f] + ch[w and $f];\r
+end;\r
+begin\r
+  fillrandom(buf,sizeof(buf));\r
+\r
+  {uuid version 4}\r
+  buf[3] := (buf[3] and $fff) or $4000;\r
+\r
+  {uuid version 4}\r
+  buf[4] := (buf[4] and $3fff) or $8000;\r
+\r
+  result := inttohex(buf[0]) + inttohex(buf[1]) + '-' + inttohex(buf[2]) +'-'+ inttohex(buf[3]) + '-' + inttohex(buf[4])\r
+  + '-' + inttohex(buf[5]) + inttohex(buf[6]) + inttohex(buf[7]);\r
+end;\r
+\r
+{$ifndef nolcorernd}\r
+initialization randomdword := @internalrandomdword;\r
+{$endif}\r
+\r
+end.\r
+\r