Sorcerer's IsleCode cfPassphrase / diff

1db9287 Add test scripts.

 test/BcryptTest.cfc (new) | 104 +++++++++++++++
 test/Pbkdf2Test.cfc (new) |  54 ++++++++
 test/ScryptTest.cfc (new) |  66 ++++++++++
 test/TestBase.cfc (new)   | 115 +++++++++++++++++
 test/UsageTest.cfc (new)  | 135 ++++++++++++++++++++
 test/runtests.cfm (new)   |  17 +++
 6 files changed, 491 insertions(+)
diff --git a/test/BcryptTest.cfc b/test/BcryptTest.cfc
new file mode 100644
index 0000000..7bbc126
--- /dev/null
+++ b/test/BcryptTest.cfc
@@ -0,0 +1,104 @@
+<cfcomponent extends="TestBase">
+<cfscript>
+
+	function init( IncludeSlowTests )
+	{
+		super.init( argumentcollection=arguments );
+
+		this.Name = 'bcrypt';
+
+		var TestData =
+			[ [ "", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s." ]
+			, [ "", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye" ]
+			, [ "", "$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW" ]
+			, [ "", "$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO" ]
+			, [ "a", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe" ]
+			, [ "a", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V." ]
+			, [ "a", "$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u" ]
+			, [ "a", "$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS" ]
+			, [ "abc", "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i" ]
+			, [ "abc", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm" ]
+			, [ "abc", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi" ]
+			, [ "abc", "$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q" ]
+			, [ "abcdefghijklmnopqrstuvwxyz", "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC" ]
+			, [ "abcdefghijklmnopqrstuvwxyz", "$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz." ]
+			, [ "abcdefghijklmnopqrstuvwxyz", "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq" ]
+			, [ "abcdefghijklmnopqrstuvwxyz", "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG" ]
+			, [ "~!@##$%^&*()      ~!@##$%^&*()PNBFRD", "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO" ]
+			, [ "~!@##$%^&*()      ~!@##$%^&*()PNBFRD", "$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW" ]
+			, [ "~!@##$%^&*()      ~!@##$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS" ]
+			, [ "~!@##$%^&*()      ~!@##$%^&*()PNBFRD", "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC" ]
+			];
+
+		super.start();
+
+		this.test_info( TestData );
+
+		if ( IncludeSlowTests )
+			this.test_basic( TestData );
+		else
+			skip( ArrayLen(TestData)*2 );
+
+		this.test_nonascii();
+
+		return super.end();
+	}
+
+
+	function test_info( TestData )
+	{
+		var info1 = PassphraseInfo('$2a$10$9zXu55aPNya8ek17DwCXZ.X6kExa5cK5bpGmyTBrqD1dg76rkWz4y');
+		assertEqual(info1.Algorithm,'BCrypt');
+		assertEqual(info1.Status,'Supported');
+		assertEqual(info1.Version,'2a');
+		assertEqual(info1.Rounds,10);
+		assertEqual(info1.Salt,'9zXu55aPNya8ek17');
+		assertEqual(info1.Hash,'DwCXZ.X6kExa5cK5bpGmyTBrqD1dg76rkWz4y');
+		assertEqual(StructCount(info1),6);
+
+		var info = PassphraseInfo(Arguments.TestData[1][2]);
+		assertEqual(info.Algorithm,'BCrypt');
+		assertEqual(info.Status,'Supported');
+		assertEqual(info.Version,'2a');
+		assertEqual(info.Rounds,6);
+		assertEqual(info.Salt,'DCq7YPn5Rq63x1La');
+		assertEqual(info.Hash,'d4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.');
+
+		info = PassphraseInfo(Arguments.TestData[20][2]);
+		assertEqual(info.Algorithm,'BCrypt');
+		assertEqual(info.Status,'Supported');
+		assertEqual(info.Version,'2a');
+		assertEqual(info.Rounds,12);
+		assertEqual(info.Salt,'WApznUOJfkEGSmYR');
+		assertEqual(info.Hash,'fnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC');
+	}
+
+
+	function test_basic( TestData )
+	{
+		for ( var i=1 ; i<=ArrayLen(Arguments.TestData) ; ++i )
+		{
+			assertTrue( PassphraseCheck(Arguments.TestData[i][1],Arguments.TestData[i][2]) );
+			assertFalse( PassphraseCheck(Arguments.TestData[i][1],Arguments.TestData[((i + 4) % ArrayLen(Arguments.TestData))+1][2]) );
+		}
+	}
+
+
+	function test_nonascii()
+	{
+		var TestData =
+			[ RepeatString(chr(2605),8)
+			, "????????"
+			];
+
+		var h1 = PassphraseHash(TestData[1],'bcrypt',{rounds:6});
+		var h2 = PassphraseHash(TestData[2],'bcrypt',{rounds:6});
+		assertTrue(PassphraseCheck(TestData[1],h1));
+		assertFalse(PassphraseCheck(TestData[1],h2));
+		assertTrue(PassphraseCheck(TestData[2],h2));
+		assertFalse(PassphraseCheck(TestData[2],h1));
+	}
+
+
+</cfscript>
+</cfcomponent>
\ No newline at end of file
diff --git a/test/Pbkdf2Test.cfc b/test/Pbkdf2Test.cfc
new file mode 100644
index 0000000..c8f9462
--- /dev/null
+++ b/test/Pbkdf2Test.cfc
@@ -0,0 +1,54 @@
+<cfcomponent extends="TestBase">
+<cfscript>
+
+	function init( IncludeSlowTests )
+	{
+		super.init( argumentcollection=arguments );
+
+		this.Name = 'pbkdf2';
+
+		super.start();
+
+		this.test_info();
+
+		if ( IncludeSlowTests )
+			this.test_basic();
+		else
+			skip( 30 );
+
+		return super.end();
+	}
+
+
+	function test_info( TestData )
+	{
+		var info2 = PassphraseInfo('180:5fabd8b160f5f9225ec5569ce2f02d5a2e29a29e0b280614:d11a602ecc7830280b25cdd29b539e9e0f8438a0a43e6637');
+		assertEqual(info2.Algorithm,'PBKDF2');
+		assertEqual(info2.Status,'Supported');
+		assertEqual(info2.Iterations,180);
+		assertEqual(info2.Salt,'5fabd8b160f5f9225ec5569ce2f02d5a2e29a29e0b280614');
+		assertEqual(info2.Hash,'d11a602ecc7830280b25cdd29b539e9e0f8438a0a43e6637');
+		assertEqual(StructCount(info2),5);
+	}
+
+
+	function test_basic()
+	{
+		for ( var i=0 ; i<10 ; ++i )
+		{
+			var password = ""&i;
+			var hash = PassphraseHash(password,'pbkdf2');
+			var secondHash = PassphraseHash(password,'pbkdf2');
+
+			assertNotEqual( hash , secondHash , "Two hashes are equal." );
+
+			var wrongPassword = ""&(i+1);
+			assertFalse( PassphraseCheck(wrongPassword,hash) , "Wrong password accepted." );
+
+			assertTrue( PassphraseCheck(password,hash) , "Good password not accepted." );
+		}
+	}
+
+
+</cfscript>
+</cfcomponent>
\ No newline at end of file
diff --git a/test/ScryptTest.cfc b/test/ScryptTest.cfc
new file mode 100644
index 0000000..af0cb6e
--- /dev/null
+++ b/test/ScryptTest.cfc
@@ -0,0 +1,66 @@
+<cfcomponent extends="TestBase">
+<cfscript>
+
+	function init( IncludeSlowTests )
+	{
+		super.init( argumentcollection=arguments );
+
+		this.Name = 'scrypt';
+
+		super.start();
+
+		this.test_info();
+
+		if ( IncludeSlowTests )
+			this.test_basic();
+		else
+			skip( 30 );
+
+		return super.end();
+	}
+
+
+	function test_info( TestData )
+	{
+		var info3 = PassphraseInfo('$s0$501ff$5JQALASFiKGKdfY9Z0GYMA==$8c4aMGktdRlLIeY+erIA62fNtgb2OxJrjyhw+XeWHk4=');
+		assertEqual(info3.Algorithm,'SCrypt');
+		assertEqual(info3.Status,'Supported');
+		assertEqual(info3.Version,'0');
+		assertEqual(info3.CpuCost,32);
+		assertEqual(info3.MemoryCost,1);
+		assertEqual(info3.Parallelization,255);
+		assertEqual(info3.Salt,'5JQALASFiKGKdfY9Z0GYMA==');
+		assertEqual(info3.Hash,'8c4aMGktdRlLIeY+erIA62fNtgb2OxJrjyhw+XeWHk4=');
+		assertEqual(StructCount(info3),8);
+	}
+
+
+	function test_basic()
+	{
+		var Passwd = "secret";
+		var N = 16384;
+		var r = 8;
+		var p = 1;
+
+		var hashed = PassphraseHash(Passwd,'scrypt',{CpuCost:N,MemoryCost:r,Parallelization:p});
+
+		var parts = hashed.split('\$');
+
+		assertEqual(len(parts),5);
+		assertEqual(parts[1],"");
+		assertEqual(parts[2],"s0");
+		assertEqual(len(BinaryDecode(parts[4],'base64')),16);
+		assertEqual(len(BinaryDecode(parts[5],'base64')),32);
+
+		var params = InputBaseN(parts[3],16);
+		assertEqual( N , 2^( BitAnd(BitShRn(params,16),65535)) );
+		assertEqual( r , BitAnd(BitShRn(params,8),255) );
+		assertEqual( p , BitAnd(BitShRn(params,0),255) );
+
+		assertTrue(PassphraseCheck(passwd,hashed));
+		assertFalse(PassphraseCheck("s3cr3t",hashed));
+	}
+
+
+</cfscript>
+</cfcomponent>
\ No newline at end of file
diff --git a/test/TestBase.cfc b/test/TestBase.cfc
new file mode 100644
index 0000000..26d3079
--- /dev/null
+++ b/test/TestBase.cfc
@@ -0,0 +1,115 @@
+<cfcomponent>
+<cfscript>
+
+	function init()
+	{
+		this.Results = [];
+		this.Messages = [];
+
+		variables.Status =
+			{ Pass : "."
+			, Fail : "!"
+			, Skip : "-"
+			};
+	}
+
+
+	function start()
+	{
+		this.StartTime = getTickCount();
+	}
+
+
+	function end()
+	{
+		return '<br/>#this.Name#: #ArrayToList(this.Results,'')#'
+			& '<br/>time: #getTickCount()-this.StartTime#ms'
+			& '<pre>#ArrayToList(this.Messages,'<br/>')#</pre>'
+			;
+	}
+
+
+	function assertEqual(a,b,Msg='')
+	{
+		if ( len(Arguments.Msg) )
+			return assertTrue( a eq b , Arguments.Msg );
+
+		return assertTrue( a eq b , 'Expected [#a#] to be same as [#b#]' );
+	}
+
+
+	function assertNotEqual(a,b,Msg='')
+	{
+		if ( len(Arguments.Msg) )
+			return assertFalse( a eq b , Arguments.Msg );
+
+		return assertFalse( a eq b , 'Expected [#a#] to differ from [#b#]' );
+	}
+
+
+	function assertTrue(a,Msg='')
+	{
+		if ( not isBoolean(a) )
+			return fail('Expected boolean, received [#a#]');
+
+		if ( not a )
+		{
+			if ( len(Arguments.Msg) )
+				return fail(Arguments.Msg);
+
+			return fail('Expected True received [#a#]');
+		}
+
+		return pass();
+	}
+
+
+	function assertFalse(a,Msg='')
+	{
+		if ( not isBoolean(a) )
+			return fail('Expected boolean, received [#a#]');
+
+		if ( a )
+		{
+			if ( len(Arguments.Msg) )
+				return fail(Arguments.Msg);
+
+			return fail('Expected False received [#a#]');
+		}
+
+		return pass();
+	}
+
+
+	function pass()
+	{
+		ArrayAppend(this.Results,Status.Pass);
+		return true;
+	}
+
+
+	function fail(Msg)
+	{
+		if ( len(Arguments.Msg) )
+			logMessage(Msg);
+
+		ArrayAppend(this.Results,Status.Fail);
+		return false;
+	}
+
+
+	function skip(n=1)
+	{
+		ArrayAppend(this.Results,RepeatString(Status.Skip,n));
+		logMessage('Skipped #n# assertions.');
+	}
+
+
+	function logMessage(Msg)
+	{
+		ArrayAppend(this.Messages,Msg);
+	}
+
+
+</cfscript>
+</cfcomponent>
\ No newline at end of file
diff --git a/test/UsageTest.cfc b/test/UsageTest.cfc
new file mode 100644
index 0000000..cf40e83
--- /dev/null
+++ b/test/UsageTest.cfc
@@ -0,0 +1,135 @@
+<cfcomponent extends="TestBase">
+<cfscript>
+
+	function init()
+	{
+		super.init( argumentcollection=arguments );
+
+		this.Name = 'usage';
+
+		super.start();
+
+		this.test_info();
+		this.test_hash();
+		this.test_check();
+
+		return super.end();
+	}
+
+
+	function test_info()
+	{
+		try
+		{
+			var info = PassphraseInfo('$8$unknown-hash');
+			assertFalse(1,"Expected exception to be thrown");
+		}
+		catch( any e )
+		{
+			assertEqual(e.Message,"Unknown Algorithm Signature");
+		}
+
+		var info = PassphraseInfo('$2a$10$9zXu55aPNya8ek17DwCXZ.X6kExa5cK5bpGmyTBrqD1dg76rkWz4y');
+		assertEqual(info.Algorithm,'BCrypt');
+		assertEqual(info.Status,'Supported');
+
+		var info = PassphraseInfo('180:5fabd8b160f5f9225ec5569ce2f02d5a2e29a29e0b280614:d11a602ecc7830280b25cdd29b539e9e0f8438a0a43e6637');
+		assertEqual(info.Algorithm,'PBKDF2');
+		assertEqual(info.Status,'Supported');
+
+		var info = PassphraseInfo('$s0$501ff$5JQALASFiKGKdfY9Z0GYMA==$8c4aMGktdRlLIeY+erIA62fNtgb2OxJrjyhw+XeWHk4=');
+		assertEqual(info.Algorithm,'SCrypt');
+		assertEqual(info.Status,'Supported');
+
+		var info = PassphraseInfo('$1$etNnh7FA$OlM7eljE/B7F1J4XYNnk81');
+		assertEqual(info.Algorithm,'md5crypt');
+		assertEqual(info.Status,'Obsolete');
+		assertEqual(info.Salt,'etNnh7FA');
+		assertEqual(info.Hash,'OlM7eljE/B7F1J4XYNnk81');
+
+		var info = PassphraseInfo('$3$$8846f7eaee8fb117ad06bdd830b7586c');
+		assertEqual(info.Algorithm,'NT-Hash');
+		assertEqual(info.Status,'Obsolete');
+		assertEqual(info.Hash,'8846f7eaee8fb117ad06bdd830b7586c');
+
+		var info = PassphraseInfo('$5$9ks3nNEqv31FX.F$gdEoLFsCRsn/WRN3wxUnzfeZLoooVlzeF4WjLomTRFD');
+		assertEqual(info.Algorithm,'SHA-2');
+		assertEqual(info.Version,'256');
+		assertEqual(info.Status,'Unsupported');
+		assertEqual(info.Rounds,'5000');
+		assertEqual(info.Salt,'9ks3nNEqv31FX.F');
+		assertEqual(info.Hash,'gdEoLFsCRsn/WRN3wxUnzfeZLoooVlzeF4WjLomTRFD');
+
+		var info = PassphraseInfo('$6$qoE2letU$wWPRl.PVczjzeMVgjiA8LLy2nOyZbf7Amj3qLIL978o18gbMySdKZ7uepq9tmMQXxyTIrS12Pln.2Q/6Xscao0');
+		assertEqual(info.Algorithm,'SHA-2');
+		assertEqual(info.Version,'512');
+		assertEqual(info.Status,'Unsupported');
+		assertEqual(info.Rounds,'5000');
+		assertEqual(info.Salt,'qoE2letU');
+		assertEqual(info.Hash,'wWPRl.PVczjzeMVgjiA8LLy2nOyZbf7Amj3qLIL978o18gbMySdKZ7uepq9tmMQXxyTIrS12Pln.2Q/6Xscao0');
+
+		var info = PassphraseInfo('$md5,rounds=5000$GUBv0xjJ$mSwgIswdjlTY0YxV7HBVm0');
+		assertEqual(info.Algorithm,'SunMD5');
+		assertEqual(info.Status,'Obsolete');
+		assertEqual(info.Rounds,'5000');
+		assertEqual(info.Salt,'GUBv0xjJ');
+		assertEqual(info.Hash,'mSwgIswdjlTY0YxV7HBVm0');
+
+	}
+
+
+	function test_hash()
+	{
+		var r = RandRange(4,6);
+		var hash = PassphraseHash('x','bcrypt',{rounds:r});
+		var info = PassphraseInfo(hash);
+		assertEqual(info.Algorithm,'BCrypt');
+		assertEqual(info.Rounds,r);
+		assertNotEqual(hash,PassphraseHash('x','bcrypt',{rounds:r}));
+
+		var i = RandRange(1000,2000);
+		var hash = PassphraseHash('x','pbkdf2',{iterations:i});
+		var info = PassphraseInfo(hash);
+		assertEqual(info.Algorithm,'PBKDF2');
+		assertEqual(info.Iterations,i);
+		assertNotEqual(hash,PassphraseHash('x','pbkdf2',{iterations:i}));
+
+		var c = 2^RandRange(1,3);
+		var m = RandRange(1,3);
+		var hash = PassphraseHash('x','scrypt',{CpuCost:c,MemoryCost:m});
+		var info = PassphraseInfo(hash);
+		assertEqual(info.Algorithm,'SCrypt');
+		assertEqual(info.CpuCost,c);
+		assertEqual(info.MemoryCost,m);
+		assertNotEqual(hash,PassphraseHash('x','scrypt',{CpuCost:c,MemoryCost:m}));
+	}
+
+
+	function test_check()
+	{
+		var hash = PassphraseHash('x','bcrypt',{rounds:4});
+		assertTrue(PassphraseCheck('x',hash));
+		assertFalse(PassphraseCheck('x ',hash));
+
+		var hash = PassphraseHash('x','pbkdf2',{iterations:1000});
+		assertTrue(PassphraseCheck('x',hash));
+		assertFalse(PassphraseCheck('x ',hash));
+
+		var hash = PassphraseHash('x','scrypt',{CpuCost:2,MemoryCost:2});
+		assertTrue(PassphraseCheck('x',hash));
+		assertFalse(PassphraseCheck('x ',hash));
+
+		try
+		{
+			PassphraseCheck(hash,'');
+			assertFalse(1,"Expected exception to be thrown");
+		}
+		catch( any e )
+		{
+			assertEqual(e.Message,"Unknown Algorithm Signature");
+		}
+	}
+
+
+</cfscript>
+</cfcomponent>
\ No newline at end of file
diff --git a/test/runtests.cfm b/test/runtests.cfm
new file mode 100644
index 0000000..681c6fe
--- /dev/null
+++ b/test/runtests.cfm
@@ -0,0 +1,17 @@
+<cfsetting showdebugoutput=false enablecfoutputonly=true />
+<cfcontent type="text/html"><cfoutput><!doctype html>
+	<meta charset=utf-8 />
+	<style>html{font-family:monospace;}</style>
+	<title>cfPassphrase Tests</title>
+	<b>Testing...</b>
+
+	<cfset IncludeSlowTests = StructKeyExists(Url,'RunAll') />
+
+	<cfset s = getTickCount() />
+	<li>#createObject('UsageTest').init( IncludeSlowTests )#
+	<li>#createObject('BcryptTest').init( IncludeSlowTests )#
+	<li>#createObject('Pbkdf2Test').init( IncludeSlowTests )#
+	<li>#createObject('ScryptTest').init( IncludeSlowTests )#
+	<p><b>Total Time: ~#round((getTickCount()-s)/1000)# seconds</b>
+
+</html></cfoutput>
\ No newline at end of file