SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Security.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.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Exception;
  17. use InvalidArgumentException;
  18.  
  19. use Kohana;
  20.  
  21. use Sprout\Exceptions\SignatureInvalidException;
  22.  
  23.  
  24. /**
  25.  * Functions for implementing security, including secure random numbers
  26.  */
  27. class Security
  28. {
  29.  
  30. /**
  31.   * Returns a binary string of random bytes
  32.   *
  33.   * @param int $length
  34.   * @return string Binary string
  35.   */
  36. public static function randBytes($length)
  37. {
  38. $length = (int) $length;
  39. if ($length < 8) {
  40. throw new InvalidArgumentException('Insufficient length; min is 8 bytes');
  41. }
  42.  
  43. if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
  44. return random_bytes($length);
  45. }
  46.  
  47. if (function_exists('openssl_random_pseudo_bytes')) {
  48. $strong = false;
  49. $rand = openssl_random_pseudo_bytes($length, $strong);
  50. if ($strong) {
  51. return $rand;
  52. }
  53. }
  54.  
  55. if (function_exists('mcrypt_create_iv')) {
  56. return mcrypt_create_iv($length, MCRYPT_DEV_RANDOM);
  57. }
  58.  
  59. throw new Exception('A secure random implementation is not available');
  60. }
  61.  
  62.  
  63. /**
  64.   * Return a single random byte
  65.   *
  66.   * @return string Binary string; one byte
  67.   */
  68. public static function randByte()
  69. {
  70. static $buffer = [];
  71. if (count($buffer) === 0) {
  72. $buffer = str_split(self::randBytes(256));
  73. }
  74. return array_pop($buffer);
  75. }
  76.  
  77.  
  78. /**
  79.   * Returns a string of random characters
  80.   *
  81.   * @param int $length
  82.   * @return string
  83.   */
  84. public static function randStr($length = 16, $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890')
  85. {
  86. $num_chars = strlen($chars) * 1.0;
  87. $mask = 256 - (256 % $num_chars);
  88.  
  89. $result = '';
  90. do {
  91. $val = self::randByte();
  92. if (ord($val) >= $mask) {
  93. continue;
  94. }
  95. $result .= $chars[ord($val) % $num_chars];
  96. } while (strlen($result) < $length);
  97.  
  98. return $result;
  99. }
  100.  
  101.  
  102. /**
  103.   * Constant-time string comparison
  104.   *
  105.   * @param string $known_string The known hash
  106.   * @param string $user_string The user supplied hash to check
  107.   * @return bool True if the strings match, false if they don't
  108.   */
  109. public static function compareStrings($known_string, $user_string)
  110. {
  111. if (function_exists('hash_equals')) {
  112. return hash_equals($known_string, $user_string);
  113. } else {
  114. $ret = strlen($known_string) ^ strlen($user_string);
  115. $ret |= array_sum(unpack("C*", $known_string ^ $user_string));
  116. return !$ret;
  117. }
  118. }
  119.  
  120.  
  121. /**
  122.   * Return the server key
  123.   *
  124.   * @throws InvalidArgumentException Config option is not set
  125.   * @throws InvalidArgumentException Test-server only value used in production
  126.   * @return string Unqiue key for this site
  127.   */
  128. protected static function getServerKey()
  129. {
  130. $server_key = Kohana::config('database.server_key');
  131. if (empty($server_key)) {
  132. throw new InvalidArgumentException('Config "database.server_key" not set');
  133. }
  134. if (IN_PRODUCTION and $server_key === 'NOT SECURE') {
  135. throw new InvalidArgumentException('Config "database.server_key" set to test-server only value');
  136. }
  137. return $server_key;
  138. }
  139.  
  140.  
  141. /**
  142.   * Generate a signature from a given set of fields, using the server key
  143.   *
  144.   * For a given set of fields, the signature will always be the same value.
  145.   * Returned signatures are always URL and HTML safe
  146.   *
  147.   * @example
  148.   * // In method which creates link to resource/download
  149.   * $sig = Security::serverKeySign(['id' => $id]);
  150.   * $file_url = "resource/download?id={$id}&sig={$sig}";
  151.   *
  152.   * @param array $fields Key-value fields making up the data to sign
  153.   * @return string Signature of the data, always url safe
  154.   */
  155. public static function serverKeySign(array $fields)
  156. {
  157. sort($fields);
  158. $data = http_build_query($fields);
  159. $data = strtolower($data);
  160.  
  161. $key = self::getServerKey();
  162.  
  163. return hash_hmac('sha1', $data, $key);
  164. }
  165.  
  166.  
  167. /**
  168.   * Verify a signature which was generated by {@see Security::serverKeySign}
  169.   *
  170.   * @example
  171.   * // In resource::download method
  172.   * Security::serverKeyVerify(['id' => $id], $_GET['sig']);
  173.   *
  174.   * @param array $fields Key-value fields making up the data to verify
  175.   * @param string $signature Incoming signature to check
  176.   * @throws SignatureInvalidException A non-string value was specified for the signature
  177.   * @throws SignatureInvalidException If the signature is not valid
  178.   * @return void
  179.   */
  180. public static function serverKeyVerify(array $fields, $signature)
  181. {
  182. if (!is_string($signature)) {
  183. throw new SignatureInvalidException('Signature not valid');
  184. }
  185. $expected = self::serverKeySign($fields);
  186. $sig_valid = self::compareStrings($expected, $signature);
  187. if (!$sig_valid) {
  188. throw new SignatureInvalidException('Signature not valid');
  189. }
  190. }
  191.  
  192.  
  193. /**
  194.   * Check the given password meets complexity requirements
  195.   *
  196.   * @param string $str String to check
  197.   * @param int $length Minimum length in bytes
  198.   * @param int $classes Minumum number of "character classes", so 2 would accept 'passWORD' but not 'password'
  199.   * @param bool $bad_list SHould the password be checked against the "bad list" of most common passwords
  200.   * @return array Errors, may be an empty array
  201.   */
  202. public static function passwordComplexity($str, $length, $classes, $bad_list)
  203. {
  204. $errs = [];
  205.  
  206. if (strlen($str) < $length) {
  207. $errs[] = "Too short, minimum length {$length} characters";
  208. }
  209.  
  210. if ($classes > 1) {
  211. $num = 0;
  212. if (preg_match('/[a-z]/', $str)) $num += 1;
  213. if (preg_match('/[A-Z]/', $str)) $num += 1;
  214. if (preg_match('/[0-9]/', $str)) $num += 1;
  215. if (preg_match('/[^a-zA-Z0-9]/', $str)) $num += 1;
  216. if ($num < $classes) {
  217. $errs[] = "Need {$classes} character types (lowercase, uppercase, numbers, symbols)";
  218. }
  219. }
  220.  
  221. if ($bad_list) {
  222. $bad_passwords = file(APPPATH . 'config/bad_passwords.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  223. foreach ($bad_passwords as $bad_pass) {
  224. // Ignore licence at start of file
  225. if ($bad_pass[0] == '/') {
  226. continue;
  227. }
  228.  
  229. if (strcmp($bad_pass, $str) == 0) {
  230. $errs[] = 'Matches a very common password';
  231. break;
  232. }
  233. }
  234. }
  235.  
  236. return $errs;
  237. }
  238.  
  239. }
  240.