Randomizer.cpp   [plain text]

------- Strong random data generation on a Macintosh (pre - OS X) ------
--	GENERAL: We aim to generate unpredictable bits without explicit
	user interaction. A general review of the problem may be found
	in RFC 1750, "Randomness Recommendations for Security", and some
	more discussion, of general and Mac-specific issues has appeared
	in "Using and Creating Cryptographic- Quality Random Numbers" by
	Jon Callas (www.merrymeet.com/jon/usingrandom.html).

	The data and entropy estimates provided below are based on my
	limited experimentation and estimates, rather than by any
	rigorous study, and the entropy estimates tend to be optimistic.
	They should not be considered absolute.

	Some of the information being collected may be correlated in
	subtle ways. That includes mouse positions, timings, and disk
	size measurements. Some obvious correlations will be eliminated
	by the programmer, but other, weaker ones may remain. The
	reliability of the code depends on such correlations being
	poorly understood, both by us and by potential interceptors.

	This package has been planned to be used with OpenSSL, v. 0.9.5.
	It requires the OpenSSL function RAND_add. 

--	OTHER WORK: Some source code and other details have been
	published elsewhere, but I haven't found any to be satisfactory
	for the Mac per se:

	* The Linux random number generator (by Theodore Ts'o, in
	  drivers/char/random.c), is a carefully designed open-source
	  crypto random number package. It collects data from a variety
	  of sources, including mouse, keyboard and other interrupts.
	  One nice feature is that it explicitly estimates the entropy
	  of the data it collects. Some of its features (e.g. interrupt
	  timing) cannot be reliably exported to the Mac without using
	  undocumented APIs.

	* Truerand by Don P. Mitchell and Matt Blaze uses variations
	  between different timing mechanisms on the same system. This
	  has not been tested on the Mac, but requires preemptive
	  multitasking, and is hardware-dependent, and can't be relied
	  on to work well if only one oscillator is present.

	* Cryptlib's RNG for the Mac (RNDMAC.C by Peter Gutmann),
	  gathers a lot of information about the machine and system
	  environment. Unfortunately, much of it is constant from one
	  startup to the next. In other words, the random seed could be
	  the same from one day to the next. Some of the APIs are
	  hardware-dependent, and not all are compatible with Carbon (OS
	  X). Incidentally, the EGD library is based on the UNIX entropy
	  gathering methods in cryptlib, and isn't suitable for MacOS

	* Mozilla (and perhaps earlier versions of Netscape) uses the
	  time of day (in seconds) and an uninitialized local variable
	  to seed the random number generator. The time of day is known
	  to an outside interceptor (to within the accuracy of the
	  system clock). The uninitialized variable could easily be
	  identical between subsequent launches of an application, if it
	  is reached through the same path.

	* OpenSSL provides the function RAND_screen(), by G. van
	  Oosten, which hashes the contents of the screen to generate a
	  seed. This is not useful for an extension or for an
	  application which launches at startup time, since the screen
	  is likely to look identical from one launch to the next. This
	  method is also rather slow.

	* Using variations in disk drive seek times has been proposed
	  (Davis, Ihaka and Fenstermacher, world.std.com/~dtd/;
	  Jakobsson, Shriver, Hillyer and Juels,
	  www.bell-labs.com/user/shriver/random.html). These variations
	  appear to be due to air turbulence inside the disk drive
	  mechanism, and are very strongly unpredictable. Unfortunately
	  this technique is slow, and some implementations of it may be
	  patented (see Shriver's page above.) It of course cannot be
	  used with a RAM disk.

--	TIMING: On the 601 PowerPC the time base register is guaranteed
	to change at least once every 10 addi instructions, i.e. 10
	cycles. On a 60 MHz machine (slowest PowerPC) this translates to
	a resolution of 1/6 usec. Newer machines seem to be using a 10
	cycle resolution as well.
	For 68K Macs, the Microseconds() call may be used. See Develop
	issue 29 on the Apple developer site
	for information on its accuracy and resolution. The code below
	has been tested only on PowerPC based machines.

	The time from machine startup to the launch of an application in
	the startup folder has a variance of about 1.6 msec on a new G4
	machine with a defragmented and optimized disk, most extensions
	off and no icons on the desktop. This can be reasonably taken as
	a lower bound on the variance. Most of this variation is likely
	due to disk seek time variability. The distribution of startup
	times is probably not entirely even or uncorrelated. This needs
	to be investigated, but I am guessing that it not a majpor
	problem. Entropy = log2 (1600/0.166) ~= 13 bits on a 60 MHz
	machine, ~16 bits for a 450 MHz machine.

	User-launched application startup times will have a variance of
	a second or more relative to machine startup time. Entropy >~22

	Machine startup time is available with a 1-second resolution. It
	is predictable to no better a minute or two, in the case of
	people who show up punctually to work at the same time and
	immediately start their computer. Using the scheduled startup
	feature (when available) will cause the machine to start up at
	the same time every day, making the value predictable. Entropy
	>~7 bits, or 0 bits with scheduled startup.

	The time of day is of course known to an outsider and thus has 0
	entropy if the system clock is regularly calibrated.

--	KEY TIMING: A  very fast typist (120 wpm) will have a typical
	inter-key timing interval of 100 msec. We can assume a variance
	of no less than 2 msec -- maybe. Do good typists have a constant
	rhythm, like drummers? Since what we measure is not the
	key-generated interrupt but the time at which the key event was
	taken off the event queue, our resolution is roughly the time
	between process switches, at best 1 tick (17 msec). I  therefore
	consider this technique questionable and not very useful for
	obtaining high entropy data on the Mac.

--	MOUSE POSITION AND TIMING: The high bits of the mouse position
	are far from arbitrary, since the mouse tends to stay in a few
	limited areas of the screen. I am guessing that the position of
	the mouse is arbitrary within a 6 pixel square. Since the mouse
	stays still for long periods of time, it should be sampled only
	after it was moved, to avoid correlated data. This gives an
	entropy of log2(6*6) ~= 5 bits per measurement.

	The time during which the mouse stays still can vary from zero
	to, say, 5 seconds (occasionally longer). If the still time is
	measured by sampling the mouse during null events, and null
	events are received once per tick, its resolution is 1/60th of a
	second, giving an entropy of log2 (60*5) ~= 8 bits per
	measurement. Since the distribution of still times is uneven,
	this estimate is on the high side.

	For simplicity and compatibility across system versions, the
	mouse is to be sampled explicitly (e.g. in the event loop),
	rather than in a time manager task.

--	STARTUP DISK TOTAL FILE SIZE: Varies typically by at least 20k
	from one startup to the next, with 'minimal' computer use. Won't
	vary at all if machine is started again immediately after
	startup (unless virtual memory is on), but any application which
	uses the web and caches information to disk is likely to cause
	this much variation or more. The variation is probably not
	random, but I don't know in what way. File sizes tend to be
	divisible by 4 bytes since file format fields are often
	long-aligned. Entropy > log2 (20000/4) ~= 12 bits.
	gets fragmented this could be anywhere in principle. In a
	perfectly unfragmented volume this will be strongly correlated
	with the total file size on the disk. With more fragmentation
	comes less certainty. I took the variation in this value to be
	1/8 of the total file size on the volume.

--	SYSTEM REQUIREMENTS: The code here requires System 7.0 and above
	(for Gestalt and Microseconds calls). All the calls used are

/*------------------------------ Includes ----------------------------*/

#include "Randomizer.h"

// Mac OS API
#include <Files.h>
#include <Folders.h>
#include <Events.h>
#include <Processes.h>
#include <Gestalt.h>
#include <Resources.h>
#include <LowMem.h>

// Standard C library
#include <stdlib.h>
#include <math.h>

/*---------------------- Function declarations -----------------------*/

// declared in OpenSSL/crypto/rand/rand.h
extern "C" void RAND_add (const void *buf, int num, double entropy);

unsigned long GetPPCTimer (bool is601);	// Make it global if needed
					// elsewhere

/*---------------------------- Constants -----------------------------*/

#define kMouseResolution 6		// Mouse position has to differ
					// from the last one by this
					// much to be entered
#define kMousePositionEntropy 5.16	// log2 (kMouseResolution**2)
#define kTypicalMouseIdleTicks 300.0	// I am guessing that a typical
					// amount of time between mouse
					// moves is 5 seconds
#define kVolumeBytesEntropy 12.0	// about log2 (20000/4),
					// assuming a variation of 20K
					// in total file size and
					// long-aligned file formats.
#define kApplicationUpTimeEntropy 6.0	// Variance > 1 second, uptime
					// in ticks  
#define kSysStartupEntropy 7.0		// Entropy for machine startup
					// time

/*------------------------ Function definitions ----------------------*/

CRandomizer::CRandomizer (void)
	long	result;
	mSupportsLargeVolumes =
		(Gestalt(gestaltFSAttr, &result) == noErr) &&
		((result & (1L << gestaltFSSupports2TBVols)) != 0);
	if (Gestalt (gestaltNativeCPUtype, &result) != noErr)
		mIsPowerPC = false;
		mIs601 = false;
		mIs601 = (result == gestaltCPU601);
		mIsPowerPC = (result >= gestaltCPU601);
	mLastMouse.h = mLastMouse.v = -10;	// First mouse will
						// always be recorded
	mLastPeriodicTicks = TickCount();
	GetTimeBaseResolution ();
	// Add initial entropy
	AddTimeSinceMachineStartup ();
	AddAbsoluteSystemStartupTime ();
	AddStartupVolumeInfo ();
	AddFiller ();

void CRandomizer::PeriodicAction (void)
	AddCurrentMouse ();
	AddNow (0.0);	// Should have a better entropy estimate here
	mLastPeriodicTicks = TickCount();

/*------------------------- Private Methods --------------------------*/

void CRandomizer::AddCurrentMouse (void)
	Point mouseLoc;
	unsigned long lastCheck;	// Ticks since mouse was last
					// sampled

	GetGlobalMouse (&mouseLoc);
	mouseLoc = LMGetMouseLocation();
	if (labs (mLastMouse.h - mouseLoc.h) > kMouseResolution/2 &&
	    labs (mLastMouse.v - mouseLoc.v) > kMouseResolution/2)
		AddBytes (&mouseLoc, sizeof (mouseLoc),
	if (mLastMouse.h == mouseLoc.h && mLastMouse.v == mouseLoc.v)
		mMouseStill ++;
		double entropy;
		// Mouse has moved. Add the number of measurements for
		// which it's been still. If the resolution is too
		// coarse, assume the entropy is 0.

		lastCheck = TickCount() - mLastPeriodicTicks;
		if (lastCheck <= 0)
			lastCheck = 1;
		entropy = log2l
		if (entropy < 0.0)
			entropy = 0.0;
		AddBytes (&mMouseStill, sizeof (mMouseStill), entropy);
		mMouseStill = 0;
	mLastMouse = mouseLoc;

void CRandomizer::AddAbsoluteSystemStartupTime (void)
	unsigned long	now;		// Time in seconds since
					// 1/1/1904
	GetDateTime (&now);
	now -= TickCount() / 60;	// Time in ticks since machine
					// startup
	AddBytes (&now, sizeof (now), kSysStartupEntropy);

void CRandomizer::AddTimeSinceMachineStartup (void)
	AddNow (1.5);			// Uncertainty in app startup
					// time is > 1.5 msec (for
					// automated app startup).

void CRandomizer::AddAppRunningTime (void)
	ProcessSerialNumber PSN;
	ProcessInfoRec		ProcessInfo;
	ProcessInfo.processInfoLength = sizeof (ProcessInfoRec);
	ProcessInfo.processName = nil;
	ProcessInfo.processAppSpec = nil;
	GetCurrentProcess (&PSN);
	GetProcessInformation (&PSN, &ProcessInfo);

	// Now add the amount of time in ticks that the current process
	// has been active

	AddBytes (&ProcessInfo, sizeof (ProcessInfoRec),

void CRandomizer::AddStartupVolumeInfo (void)
	short			vRefNum;
	long			dirID;
	XVolumeParam	pb;
	OSErr			err;
	if (!mSupportsLargeVolumes)
	FindFolder (kOnSystemDisk, kSystemFolderType, kDontCreateFolder,
			&vRefNum, &dirID);
	pb.ioVRefNum = vRefNum;
	pb.ioCompletion = 0;
	pb.ioNamePtr = 0;
	pb.ioVolIndex = 0;
	err = PBXGetVolInfoSync (&pb);
	if (err != noErr)
	// Base the entropy on the amount of space used on the disk and
	// on the next available allocation block. A lot else might be
	// unpredictable, so might as well toss the whole block in. See
	// comments for entropy estimate justifications.

	AddBytes (&pb, sizeof (pb),
		kVolumeBytesEntropy +
		log2l (((pb.ioVTotalBytes.hi - pb.ioVFreeBytes.hi)
				* 4294967296.0D +
			(pb.ioVTotalBytes.lo - pb.ioVFreeBytes.lo))
				/ pb.ioVAlBlkSiz - 3.0));

	On a typical startup CRandomizer will come up with about 60
	bits of good, unpredictable data. Assuming no more input will
	be available, we'll need some more lower-quality data to give
	OpenSSL the 128 bits of entropy it desires. AddFiller adds some
	relatively predictable data into the soup.

void CRandomizer::AddFiller (void)
		ProcessSerialNumber psn;	// Front process serial
						// number
		RGBColor	hiliteRGBValue;	// User-selected
						// highlight color
		long		processCount;	// Number of active
						// processes
		long		cpuSpeed;	// Processor speed
		long		totalMemory;	// Total logical memory
						// (incl. virtual one)
		long		systemVersion;	// OS version
		short		resFile;	// Current resource file
	} data;
	GetNextProcess ((ProcessSerialNumber*) kNoProcess);
	while (GetNextProcess (&data.psn) == noErr)
	GetFrontProcess (&data.psn);
	LMGetHiliteRGB (&data.hiliteRGBValue);
	Gestalt (gestaltProcClkSpeed, &data.cpuSpeed);
	Gestalt (gestaltLogicalRAMSize, &data.totalMemory);
	Gestalt (gestaltSystemVersion, &data.systemVersion);
	data.resFile = CurResFile ();
	// Here we pretend to feed the PRNG completely random data. This
	// is of course false, as much of the above data is predictable
	// by an outsider. At this point we don't have any more
	// randomness to add, but with OpenSSL we must have a 128 bit
	// seed before we can start. We just add what we can, without a
	// real entropy estimate, and hope for the best.

	AddBytes (&data, sizeof(data), 8.0 * sizeof(data));
	AddCurrentMouse ();
	AddNow (1.0);

//-------------------  LOW LEVEL ---------------------

void CRandomizer::AddBytes (void *data, long size, double entropy)
	RAND_add (data, size, entropy * 0.125);	// Convert entropy bits
						// to bytes

void CRandomizer::AddNow (double millisecondUncertainty)
	long time = SysTimer();
	AddBytes (&time, sizeof (time), log2l (millisecondUncertainty *

//----------------- TIMING SUPPORT ------------------

void CRandomizer::GetTimeBaseResolution (void)
#ifdef __powerc
	long speed;
	// gestaltProcClkSpeed available on System 7.5.2 and above
	if (Gestalt (gestaltProcClkSpeed, &speed) != noErr)
		// Only PowerPCs running pre-7.5.2 are 60-80 MHz
		// machines.
		mTimebaseTicksPerMillisec =  6000.0D;
	// Assume 10 cycles per clock update, as in 601 spec. Seems true
	// for later chips as well.
	mTimebaseTicksPerMillisec = speed / 1.0e4D;
	// 68K VIA-based machines (see Develop Magazine no. 29)
	mTimebaseTicksPerMillisec = 783.360D;

unsigned long CRandomizer::SysTimer (void)	// returns the lower 32
						// bit of the chip timer
#ifdef __powerc
	return GetPPCTimer (mIs601);
	UnsignedWide usec;
	Microseconds (&usec);
	return usec.lo;

#ifdef __powerc
// The timebase is available through mfspr on 601, mftb on later chips.
// Motorola recommends that an 601 implementation map mftb to mfspr
// through an exception, but I haven't tested to see if MacOS actually
// does this. We only sample the lower 32 bits of the timer (i.e. a
// few minutes of resolution)

asm unsigned long GetPPCTimer (register bool is601)
	cmplwi	is601, 0	// Check if 601
	bne	_601		// if non-zero goto _601
	mftb  	r3		// Available on 603 and later.
	blr			// return with result in r3
	mfspr r3, spr5  	// Available on 601 only.
				// blr inserted automatically