SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/tests/securityHelperTest.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13. use Sprout\Helpers\Security;
  14.  
  15.  
  16. /**
  17. * Test suite
  18. **/
  19. class securityHelperTest extends PHPUnit_Framework_TestCase
  20. {
  21.  
  22. public function testRandBytes()
  23. {
  24. $bytes = Security::randBytes(16);
  25. $this->assertTrue(strlen($bytes) === 16, 'Return value length');
  26. }
  27.  
  28. public function testRandByte()
  29. {
  30. $byte = Security::randByte();
  31. $this->assertTrue(strlen($byte) === 1, 'Return value length');
  32. }
  33.  
  34. public function testRandStr()
  35. {
  36. $string = Security::randStr(16);
  37. $this->assertTrue(strlen($string) === 16, 'Return value length');
  38. }
  39.  
  40.  
  41. /**
  42.   * Data for testing random distributions
  43.   */
  44. public function dataRandDistribution()
  45. {
  46. return [
  47. [
  48. 'Sprout\Helpers\Security::randBytes',
  49. [4096 * 512],
  50. 256
  51. ],
  52. [
  53. 'Sprout\Helpers\Security::randStr',
  54. [4096 * 512, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'],
  55. 26
  56. ],
  57. [
  58. 'Sprout\Helpers\Security::randStr',
  59. [4096 * 512, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'],
  60. 52
  61. ],
  62. [
  63. 'Sprout\Helpers\Security::randStr',
  64. [4096 * 512, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'],
  65. 62
  66. ],
  67. ];
  68. }
  69.  
  70. /**
  71.   * @dataProvider dataRandDistribution
  72.   *
  73.   * @param callable $func Function to generate random strings
  74.   * @param array $args Function arguments
  75.   * @param int $num_unique Expected number of unique values returned from $func
  76.   */
  77. public function testRandDistribution($func, array $args, $num_unique)
  78. {
  79. $bytes = call_user_func_array($func, $args);
  80. $bytes = str_split($bytes);
  81.  
  82. $dists = [];
  83. foreach ($bytes as $b) {
  84. $b = ord($b);
  85. if (isset($dists[$b])) {
  86. $dists[$b]++;
  87. } else {
  88. $dists[$b] = 1;
  89. }
  90. }
  91. $this->assertCount($num_unique, $dists);
  92.  
  93. $avg = count($bytes) / $num_unique;
  94. $thresh = $avg * 0.1;
  95. foreach ($dists as $b => $count) {
  96. $diff = abs($count - $avg);
  97. $this->assertLessThan($thresh, $diff, "Byte {$b} count {$count} expected {$avg} (+/- {$thresh})");
  98. }
  99. }
  100.  
  101.  
  102. public function testCompareStrings()
  103. {
  104. $this->assertTrue(Security::compareStrings('aaa', 'aaa'));
  105. $this->assertFalse(Security::compareStrings('aaa', 'bbb'));
  106. }
  107.  
  108. public function testCompareStringsTimingSafe()
  109. {
  110. if (getenv('TRAVIS')) {
  111. $this->markTestSkipped('Timing not stable in Travis CI');
  112. }
  113.  
  114. $xxx = str_repeat('x', 1024 * 32);
  115. $yyy = str_repeat('x', 1024 * 32 - 1) . 'y';
  116. $zzz = 'z' . str_repeat('x', 1024 * 32 - 1);
  117. $matches = [0.0, 0.0, 0.0];
  118.  
  119. // When using hash_equals its much faster than the fallback
  120. // and this makes the timing unstable so more iterations are required
  121. if (function_exists('hash_equals')) {
  122. $iter = 5000;
  123. } else {
  124. $iter = 500;
  125. }
  126.  
  127. // Test one - both strings matching
  128. for ($i = 0; $i < $iter; ++$i) {
  129. $start = microtime(true);
  130. Security::compareStrings($xxx, $xxx);
  131. $matches[0] += (microtime(true) - $start) * 1000;
  132. }
  133.  
  134. // Test two - matching except last character
  135. for ($i = 0; $i < $iter; ++$i) {
  136. $start = microtime(true);
  137. Security::compareStrings($xxx, $yyy);
  138. $matches[1] += (microtime(true) - $start) * 1000;
  139. }
  140.  
  141. // Test three - matching except first character
  142. for ($i = 0; $i < $iter; ++$i) {
  143. $start = microtime(true);
  144. Security::compareStrings($xxx, $zzz);
  145. $matches[2] += (microtime(true) - $start) * 1000;
  146. }
  147.  
  148. // Calculate the average time across all three tests
  149. $average = array_sum($matches) / count($matches);
  150.  
  151. // Compare each test against the average, as a percentage
  152. // Require to be within 10% or better
  153. foreach ($matches as $idx => $val) {
  154. $diff = abs($val - $average);
  155. $perc = $diff / $average * 100.0;
  156. $this->assertLessThan(10, $perc);
  157. }
  158. }
  159.  
  160.  
  161. public function dataPasswordComplexityLength()
  162. {
  163. return [
  164. ['abcdefg', 8, 'Too short, minimum length 8 characters'],
  165. ['abcdefgh', 8, null],
  166. ['abcdefghi', 8, null],
  167. ['abcdefghi', 10, 'Too short, minimum length 10 characters'],
  168. ['abcdefghij', 10, null],
  169. ['abcdefghijk', 10, null],
  170. ];
  171. }
  172.  
  173. /**
  174.   * @dataProvider dataPasswordComplexityLength
  175.   */
  176. public function testPasswordComplexityLength($string, $length, $errmsg)
  177. {
  178. $errs = Security::passwordComplexity($string, $length, 0, false);
  179. $this->assertEquals($errmsg?[$errmsg]:[], $errs);
  180. }
  181.  
  182.  
  183. public function dataPasswordComplexityClasses()
  184. {
  185. return [
  186. ['password', 1, null],
  187. ['password', 2, 'Need 2 character types (lowercase, uppercase, numbers, symbols)'],
  188. ['password', 3, 'Need 3 character types (lowercase, uppercase, numbers, symbols)'],
  189. ['password', 4, 'Need 4 character types (lowercase, uppercase, numbers, symbols)'],
  190. ['passWORD', 1, null],
  191. ['passWORD', 2, null],
  192. ['passWORD', 3, 'Need 3 character types (lowercase, uppercase, numbers, symbols)'],
  193. ['passWORD', 4, 'Need 4 character types (lowercase, uppercase, numbers, symbols)'],
  194. ['passW0RD', 1, null],
  195. ['passW0RD', 2, null],
  196. ['passW0RD', 3, null],
  197. ['passW0RD', 4, 'Need 4 character types (lowercase, uppercase, numbers, symbols)'],
  198. ['pa!sW0RD', 1, null],
  199. ['pa!sW0RD', 2, null],
  200. ['pa!sW0RD', 3, null],
  201. ['pa!sW0RD', 4, null],
  202. ];
  203. }
  204.  
  205. /**
  206.   * @dataProvider dataPasswordComplexityClasses
  207.   */
  208. public function testPasswordComplexityClasses($string, $classes, $errmsg)
  209. {
  210. $errs = Security::passwordComplexity($string, 0, $classes, false);
  211. $this->assertEquals($errmsg?[$errmsg]:[], $errs);
  212. }
  213.  
  214.  
  215. public function dataPasswordComplexityBadlist()
  216. {
  217. return [
  218. ['password', 'Matches a very common password'],
  219. ['dsbfb83s', null],
  220. ];
  221. }
  222.  
  223. /**
  224.   * @dataProvider dataPasswordComplexityBadlist
  225.   */
  226. public function testPasswordComplexityBadlist($string, $errmsg)
  227. {
  228. $errs = Security::passwordComplexity($string, 0, 0, true);
  229. $this->assertEquals($errmsg?[$errmsg]:[], $errs);
  230. }
  231.  
  232. }
  233.