2006. április 24., hétfő

Cryptographic Random Numbers


Problem/Question/Abstract:

The time comes in many security related applications to generate random numbers. If done poorly, the entire application will be compromised (think netscape's CSS!). This article demonstrates a TRUE random number generator implemented in 3 layers.

Answer:

Simply calling Randomize and using the Random() procedure is a severe security flaw in application seeking to pretect data with random numbers. A random number generator gets is 'randomness' from entropy. Borlands Random() procedure uses a 32bit seed as entropy, and that seed is generated by the Randomize procedure which gets its entropy the system time and date which are very probabilistic and can be tested for quickly.

To generate random numbers that cannot be differentiated from pure chaos is a VERY difficult task on a computer, mainly because you rely on internal states that are often too predictable. The idea is to gather entropy from the least predictable states of the system and dillute that entropy inside a much larger pool. The pool I refer to is the internal state of the random number generator.

WHAT IT IS

There are important properties that have to be respected when generating random numbers. More specificaly, random numbers intended for encryption. The properties that implicated in this random number gerenartors design are strongly based on Bruce Schneier's Yarrow (www.counterpane.com).

The first property is to ensure there is always anought entropy in the pool before outputing random numbers so that the pool never enters a weakened state where the next random numbers that are output have predictible information.

The next property comes in handy if you're going to be using the generator to make session keys that will change multiple times during a chat session. It is important that one compromised key will not reveal any of the previous keys nor any of the next keys that will be used. To do this we need to eliminate the mathematical relationship between the random numbers that are output and the state of the pool.

The third desired property implies that enven if the entropy gathered from your sources is of poor quality (fairly predictable) the pool must not suffer for the low entropy and the output random numbers must not show any evidence of this.

I have tested this unit extensively. The final and most crucial test centered around the third property. To make an extreme case, I started the pool with nothing but zeros in it and generated ~12MB (100,000,000 bits). I used the DieHard battery of tests (http://stat.fsu.edu/~geo/diehard.html) and it passed all 15 with flying colors... without collecting any entropy. With this I am satisfied of the random number generator's performance and submit it to you to use as a secure alternative to what is commonly seen in programs.


HOW TO IT WORKS

two entropy gatherers are created:

a thread that tracks mouse movement at random intervals taking 4bits of entropy from the mouse position and state of the system's high-resolution timer.
a latency calculator that gets 4bits of entropy from the high-resolution timer when called by the main app (this is used by calling   TKeyGenerator.AddLatency on the OnKeyDown event of an edit box, to count harddrive latency, or irq latency)

When either of the entropy gatherers has accumulated 32bits, it sends it to the entropy pool.

The entroyp pool takes in entropy 32bits at a time and uses it to fill an entropy buffer of 256bits, when the buffer is full, a primary reseed is executed.

The primary reseed updates the primary pool (a Hash Context: internal state of a hash function) with the entropy and XORs it with the pool's seed (this seed is used similarly the way randomize generates randseed). After every primary reseed, the seed (with now 256bits of entropy) is ready to be used to output random numbers if the calling application so desires it, but it will continue to reseed and gather entropy regardless regardless of that. After 8 primary reseeds have taken place, a secondary reseed is executed.

The secondary reseed updates the secondary pool with the contents of the primary pool and then flushes the contents of the primary pool into a state with no entropy. The secondary pool is persistant in that it is never flushed and will carry entropy bits from various reseeds. A completly new seed is generated from the secondary reseed (where as the primary on modifies it with entropy). This secondary reseed prevents backtracking properties (gessing previous states of the pool) and ensures there is entropy in the pool even under conditions where new entropy is of poor quality.

When the calling application needs to generate a key it calls SafeGetKey which ensures that no more than 8 sets of 256bits of random numbers can be generated from a single reseed. To do this a key reserve counter is incremented every primary reseed, and cannot exceed 8. When a you generate a set of random numbers the key reserve is decremented and the function will return fasle if the key reserve is at 0. NOTE: an application can ignore the key reserve and call ForceGetKey. This is very risky practice and I seriously discourage you from using this function.

The random output created by GetKey is a generated with the entropy pool's seed. The seed is used as an encryption key and then permuted (with an expansion-compression). The new seed is used as data to be encrypted. It is encrypted with the previous seed and expanded-compressed in 64 rounds. These rounds ensure that it is impossible to determine the state of the seed, the primary pool, the secondary pool or the entorpy buffer; in turn, preventing anyone from finding the previous or next outputs.

NOTES

Assign a variable of type TKeyGenerator and call it's .Create. This will start the process. When you are done, call .Destropy.

You can use .KeyCount to find out the state of the key reserve (how many GetKey calls can be made before the next reseed). I strongly condone raising the value of MAX_KEY_RESERVE.

You can manipulate the speed at which entropy is gathered from the mouse by setting the MOUSE_INTERVAL constant (in milli-seconds). A value lower than 10 is unrecommended.

No error checking is done to ensure there is a high-frequency counter on the system, this should be verified by the calling application. If there is no such counter, the random number generator will work but will output non-random numbers.

The application must provide 32 BYTES of memory space in a variable to pass to the GetKey functions. No error checking is done here.

You may change KEY_BUILD_ROUNDS to any value greater than or equal to 32, but larger than 64 is quite useless.

IMPORTANT NOTE

The source bellow is part of a library in progress cummulating 3 years of my research. If you want to use it in a program, a little bit of credit would be nice.

NSCrypt.pas contains a pseudo random number generator that comes from an unknown source, and these implementations of Haval and Rijndael are [severe] modifications of David Barton's haval.pas and rijndael.pas found in the DCPcrypt 1.3 component suite.

Download NSCrypt.txt and rename it NSCrypt.pas (silly webhost) I've included the cryptographic functions sepperatly because they total 1300 lines together. My actual website is www.drmungkee.com

//you'll have to deal with the documentation above.

unit NoiseSpunge;

interface

uses Windows, Classes, Controls, NSCrypt;

const
  SEED_SIZE = 8;
  PRIMARY_RESEED = SEED_SIZE;
  SECONDARY_RESEED = SEED_SIZE;

  //parameters
  MAX_KEY_RESERVE = 8;
  KEY_BUILD_ROUNDS = 64;
  MOUSE_INTERVAL = 10;

type
  Key256 = array[0..SEED_SIZE - 1] of longword;

  TNoiseSpungeAddEntropy = procedure(Block: longword) of object;
  TNoiseSpungeProcedure = procedure of object;

  TMouseCollector = class(TThread)
  protected
    PCtx: Prng_CTX;
    x, y: integer;
    Block: longword;
    BitsGathered: longword;
    Interval, Frequency, ThisTime, LastTime: TLargeInteger;
    SendMouseEntropy: TNoiseSpungeAddEntropy;
  public
    constructor Create;
    procedure SyncSendMouseEntropy;
    procedure Execute; override;
  end;

  TLatencyCollector = class
  protected
    Block: longword;
    BitsGathered: longword;
    Time: TLargeInteger;
    SendLatencyEntropy: TNoiseSpungeAddEntropy;
  public
    constructor Create;
    procedure MeasureLatency;
  end;

  TEntropyPool = class
  protected
    Seed: Key256;
    EntropyBuffer: Key256;
    PrimaryPool: Haval_CTX;
    SecondaryPool: Haval_CTX;
    PrimaryReseedCount: byte;
    EntropyCount: byte;
    KeyReserve: byte;
    procedure PermuteSeed;
    procedure PrimaryReseed;
    procedure SecondaryReseed;
    procedure AddEntropy(Block: longword);
  public
    constructor Create;
  end;

  TKeyGenerator = class
  protected
    EntropyPool: TEntropyPool;
    MouseCollector: TMouseCollector;
    LatencyCollector: TLatencyCollector;
  public
    AddLatency: TNoiseSpungeProcedure;
    constructor Create;
    destructor Destroy; override;
    function KeyCount: byte;
    function SafeGetKey(var Key): boolean;
    procedure ForcedGetKey(var Key);
  end;

implementation

constructor TMouseCollector.Create;
begin
  inherited Create(true);
  Randomize;
  PrngInit(@PCtx, RandSeed);
  FreeOnTerminate := true;
  Priority := tpNormal;
  Resume;
end;

procedure TMouseCollector.SyncSendMouseEntropy;
begin
  SendMouseEntropy(Block);
end;

procedure TMouseCollector.execute;
var
  NilHandle: pointer;
  Idled: boolean;
begin
  NilHandle := nil;
  BitsGathered := 0;
  Idled := false;
  QueryPerformanceFrequency(Frequency);
  repeat
    if Idled = false then
    begin
      MsgWaitForMultipleObjects(0, NilHandle, false, MOUSE_INTERVAL, 0);
      Idled := true;
    end;
    QueryPerformanceCounter(ThisTime);
    if ThisTime - LastTime > Interval then
    begin
      if (x <> mouse.cursorpos.x) and (y <> mouse.cursorpos.y) then
      begin
        x := mouse.cursorpos.x;
        y := mouse.cursorpos.y;
        Inc(Block, (((x and 15) xor (y and 15)) xor (ThisTime and 15)) shl
          BitsGathered);
        Inc(BitsGathered, 4);
        if BitsGathered = 32 then
        begin
          PrngInit(@PCtx, Block);
          Synchronize(SyncSendMouseEntropy);
          Block := 0;
          BitsGathered := 0;
        end;
        Interval := ((((Prng(@PCtx) mod MOUSE_INTERVAL) div 2) + MOUSE_INTERVAL)
          * Frequency) div 1000;
        QueryPerformanceCounter(LastTime);
        Idled := false;
      end
      else
      begin
        QueryPerformanceCounter(LastTime);
        Idled := false;
      end;
    end;
  until Terminated = true;
end;

constructor TLatencyCollector.Create;
begin
  inherited Create;
  Block := 0;
  BitsGathered := 0;
end;

procedure TLatencyCollector.MeasureLatency;
begin
  QueryPerformanceCounter(Time);
  Inc(Block, (Time and 15) shl BitsGathered);
  Inc(BitsGathered, 4);
  if BitsGathered = 32 then
  begin
    SendLatencyEntropy(Block);
    Block := 0;
    BitsGathered := 0;
  end;
end;

constructor TEntropyPool.Create;
begin
  inherited Create;
  HavalInit(@PrimaryPool);
  HavalInit(@SecondaryPool);
  FillChar(Seed, SizeOf(Seed), 0);
  EntropyCount := 0;
  PrimaryReseedCount := 0;
  KeyReserve := 0;
end;

procedure TEntropyPool.PermuteSeed;
var
  TempBuffer: array[0..1] of Key256;
  PCtx: Prng_CTX;
  HCtx: Haval_CTX;
  i: byte;
begin
  for i := 0 to SEED_SIZE - 1 do
  begin
    PrngInit(@PCtx, Seed[i]);
    TempBuffer[0, i] := Prng(@PCtx);
    TempBuffer[1, i] := Prng(@PCtx);
  end;
  HavalInit(@HCtx);
  HavalUpdate(@HCtx, TempBuffer, SizeOf(TempBuffer));
  HavalOutput(@HCtx, Seed);
end;

procedure TEntropyPool.PrimaryReseed;
var
  TempSeed: Key256;
  i: byte;
begin
  HavalUpdate(@PrimaryPool, EntropyBuffer, SizeOf(EntropyBuffer));
  if PrimaryReseedCount
  begin
    HavalOutput(@PrimaryPool, TempSeed);
    for i := 0 to SEED_SIZE - 1 do
      Seed[i] := Seed[i] xor TempSeed[i];
    Inc(PrimaryReseedCount);
  end
else
  SecondaryReseed;
  FillChar(EntropyBuffer, SizeOf(EntropyBuffer), 0);
  if KeyReserve EntropyCount := 0;
end;

procedure TEntropyPool.SecondaryReseed;
begin
  HavalOutput(@PrimaryPool, Seed);
  HavalUpdate(@SecondaryPool, Seed, SizeOf(Seed));
  HavalOutput(@SecondaryPool, Seed);
  PermuteSeed;
  HavalInit(@PrimaryPool);
  PrimaryReseedCount := 0;
end;

procedure TEntropyPool.AddEntropy(Block: longword);
begin
  Move(Block, pointer(longword(@EntropyBuffer) + (EntropyCount * SizeOf(Block)))^,
    SizeOf(Block));
  Inc(EntropyCount, 1);
  if EntropyCount = PRIMARY_RESEED then
    PrimaryReseed;
end;

constructor TKeyGenerator.Create;
begin
  inherited Create;
  EntropyPool := TEntropyPool.Create;
  MouseCollector := TMouseCollector.Create;
  MouseCollector.SendMouseEntropy := EntropyPool.AddEntropy;
  LatencyCollector := TLatencyCollector.Create;
  LatencyCollector.SendLatencyEntropy := EntropyPool.AddEntropy;
  AddLatency := LatencyCollector.MeasureLatency;
end;

destructor TKeyGenerator.Destroy;
begin
  MouseCollector.terminate;
  LatencyCollector.destroy;
  EntropyPool.destroy;
  inherited Destroy;
end;

function TKeyGenerator.KeyCount: byte;
begin
  Result := EntropyPool.KeyReserve;
end;

function TKeyGenerator.SafeGetKey(var Key): boolean;
var
  TempSeed: Key256;
  TempBuffer: array[0..1] of Key256;
  RCtx: Rijndael_CTX;
  PCtx: Prng_CTX;
  HCtx: Haval_CTX;
  i, j: byte;
begin
  if EntropyPool.KeyReserve = 0 then
  begin
    Exit;
    Result := false;
  end
  else
    Result := true;
  Move(EntropyPool.Seed, TempSeed, SizeOf(TempSeed));
  EntropyPool.PermuteSeed;
  RijndaelInit(@RCtx, EntropyPool.Seed);
  for i := 0 to KEY_BUILD_ROUNDS - 1 do
  begin
    RijndaelEncrypt(@RCtx, TempSeed[0]);
    RijndaelEncrypt(@RCtx, TempSeed[4]);
    for j := 0 to SEED_SIZE - 1 do
    begin
      PrngInit(@pctx, TempSeed[j]);
      TempBuffer[0, j] := Prng(@PCtx);
      TempBuffer[1, j] := Prng(@PCtx);
    end;
    HavalInit(@HCtx);
    HavalUpdate(@HCtx, TempBuffer, SizeOf(TempBuffer));
    HavalOutput(@HCtx, TempSeed);
  end;
  Move(TempSeed, Key, SizeOf(TempSeed));
  Dec(EntropyPool.KeyReserve, 1);
end;

procedure TKeyGenerator.ForcedGetKey(var Key);
var
  TempSeed: Key256;
  TempBuffer: array[0..1] of Key256;
  RCtx: Rijndael_CTX;
  PCtx: Prng_CTX;
  HCtx: Haval_CTX;
  i, j: byte;
begin
  Move(EntropyPool.Seed, TempSeed, SizeOf(TempSeed));
  EntropyPool.PermuteSeed;
  RijndaelInit(@RCtx, EntropyPool.Seed);
  for i := 0 to KEY_BUILD_ROUNDS - 1 do
  begin
    RijndaelEncrypt(@RCtx, TempSeed[0]);
    RijndaelEncrypt(@RCtx, TempSeed[4]);
    for j := 0 to SEED_SIZE - 1 do
    begin
      PrngInit(@pctx, TempSeed[j]);
      TempBuffer[0, j] := Prng(@PCtx);
      TempBuffer[1, j] := Prng(@PCtx);
    end;
    HavalInit(@HCtx);
    HavalUpdate(@HCtx, TempBuffer, SizeOf(TempBuffer));
    HavalOutput(@HCtx, TempSeed);
  end;
  Move(TempSeed, Key, SizeOf(TempSeed));
  if EntropyPool.KeyReserve < 0 then
    Dec(EntropyPool.KeyReserve, 1);
end;

end.

Nincsenek megjegyzések:

Megjegyzés küldése