Source of “pachyauth.php”.
212 lines, 7.6 KBytes.   Last modified 6:40 pm, 1st September 2015 PDT.
1 <?php // Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- 2 3 //////////////////////////////////////////////////////////////////////////// 4 // // 5 // Pachylet: Andrew's Web Mail Interface, Version 2 // 6 // // 7 // Copyright (c) 2014 // 8 // // 9 // See http://birrell.org/pachylet/help.html#securityDetails // 10 // // 11 // Authentication and encryption assistance // 12 // - the derived key machinery uses the constant "C_pwdDir"; // 13 // - the DK cookie functions use the constants "C_dkCookie" and // 14 // "C_dkUser". 15 // // 16 // The cryptography uses SHA-256, AES-256-CBC, HMAC, and /dev/urandom // 17 // // 18 //////////////////////////////////////////////////////////////////////////// 19 20 21 function getRandomString($n) { 22 // Return a reasonably securely random binary string of given length 23 // 24 $random = fopen("/dev/urandom", "rb"); 25 stream_set_read_buffer($random, $n); 26 $res = fread($random, $n); 27 fclose($random); 28 return $res; 29 } 30 31 function getHashedKey($seed, $key) { 32 // Return a 32-byte secure pseudo-random string, using a keyed hash of 33 // $seed. The client is required to use different seeds or keys for 34 // each separate string that's desired. The seed can be public. 35 // 36 return hash_hmac("sha256", $seed, $key, true); 37 } 38 39 function pbkdf2($hashFn, $pwd, $salt, $rounds, $resBytes) { 40 // Perform the PBKDF2 algorithm, with result in binary, 41 // using hmac-$hashFn as the pseudo-random function. The result string 42 // has length $resBytes. 43 // 44 // See RFC 2898. 45 // 46 $hashFnBytes = strlen(hash($hashFn, "", true)); 47 $blocks = ceil($resBytes / $hashFnBytes); 48 $res = ""; 49 for ($i = 1; $i <= $blocks; $i++) { 50 $state = $salt . pack("N", $i); 51 $sum = ($state = hash_hmac($hashFn, $state, $pwd, true)); 52 for ($j = 1; $j < $rounds; $j++) { 53 $sum ^= ($state = hash_hmac($hashFn, $state, $pwd, true)); 54 } 55 $res .= $sum; 56 } 57 return substr($res, 0, $resBytes); 58 } 59 60 function writePwdFile($user, $prefix, $value) { 61 // Write a value into a given user's file is the pachypwd directory. 62 // Return true on success, false otherwise. 63 // 64 if (strpos($user, "/") !== false) return false; 65 $rc = file_put_contents(C_pwdDir . "/$prefix-$user", $value); 66 return ($rc === false ? false : true); 67 } 68 69 function pachyhash($verb, $user, $dk, $newDk = false) { 70 // Perform an operation via pachyhash, using the given derived key(s). 71 // Returns true on success, false on failure. 72 // 73 $fds = array( 74 0 => array("pipe", "r"), // child's stdin 75 1 => array("pipe", "w"), // child's stdout 76 2 => array("pipe", "w") // child's stderr 77 ); 78 $child = proc_open(C_pwdDir . "/pachyhash $verb " . 79 escapeshellarg($user), $fds, $pipes); 80 if (!is_resource($child)) { 81 return false; 82 } else { 83 fwrite($pipes[0], $dk); 84 if ($newDk) fwrite($pipes[0], "\n$newDk"); 85 fclose($pipes[0]); 86 $output = stream_get_contents($pipes[1]); 87 $errout = stream_get_contents($pipes[2]); 88 fclose($pipes[1]); 89 fclose($pipes[2]); 90 $status = proc_close($child); 91 return ($status == 0 ? true : false); 92 } 93 } 94 95 function getMysqlLoginKey($dk) { 96 // Return the derived MySQL login key ("H2" in the documentation) 97 // 98 return base64_encode(getHashedKey("pachyH2", base64_decode($dk))); 99 } 100 101 function setDerivedKey($user, $dk, $pwd) { 102 // Given the user's new plain-text password, create appropriate PBKDF2 103 // parameters, keep them and the derived hashes in the file system, 104 // and return the new derived key. The result is base-64 encoded. 105 // 106 $saltBytes = 16; // 128 bits 107 $hashFn = "sha256"; // the latest fashion in crypto hash functions 108 $rounds = 100000; // takes about 200 msec on birrell.org 109 $resBytes = 32; // 256 bits, needing one SHA-256 block in PBKDF2 110 $salt = getRandomString($saltBytes); 111 $newDk = base64_encode(pbkdf2($hashFn, $pwd, $salt, $rounds, $resBytes)) ; 112 if (!pachyhash("set", $user, $dk, $newDk)) return false; 113 if (!writePwdFile($user, "salt", 114 "$hashFn:$rounds:$resBytes:" . base64_encode($salt))) return false; 115 if (!writePwdFile($user, "h2", getMysqlLoginKey($newDk))) return false; 116 return $newDk; 117 } 118 119 function getDerivedKey($user, $pwd) { 120 // Given the user's plain-text password, return the derived key 121 // based on previously-computed PBKDF2 parameters. The result is 122 // base-64 encoded. For an invalid user, returns an empty string. 123 // 124 if ($user == "root") return $pwd; 125 if (strpos($user, "/") !== false) return ""; 126 $saltPath = C_pwdDir . "/salt-$user"; 127 if (!is_readable($saltPath)) return ""; 128 $params = file_get_contents($saltPath); 129 $parts = explode(":", $params); 130 if (count($parts) != 4) return ""; 131 return base64_encode(pbkdf2($parts[0], $pwd, base64_decode($parts[3]), 132 $parts[1], $parts[2])); 133 } 134 135 function verifyDerivedKey($user, $dk) { 136 // Verify that $dk matches the recorded H1 in the file system. 137 // On success, returns newly computed MySQL login key; on failure 138 // returns false. 139 // 140 if ($user == "root") return $dk; 141 if (!pachyhash("verify", $user, $dk)) return false; 142 return getMysqlLoginKey($dk); 143 } 144 145 function recordDk($user, $dk, $persist = false) { 146 // Set or erase the DK cookie and matching user name 147 // 148 $dkExp = ($persist ? ($dk ? time() + 7 * 24 * 60 * 60 : 12) : 0); 149 $dkVal = ($dk ? $dk : "gone"); 150 $userVal = ($dk ? $user : ""); 151 $dkPath = preg_replace('#/[^/]*$#', "/", $_SERVER["SCRIPT_NAME"]); 152 setcookie(C_dkCookie, $dkVal, $dkExp, $dkPath, null, true, true); 153 setcookie(C_dkUser, $userVal, $dkExp, $dkPath, null, true, false); 154 } 155 156 function readDk() { 157 // Return the DK cookie, if it exists, or "unset" 158 // 159 return (isset($_COOKIE[C_dkCookie]) ? $_COOKIE[C_dkCookie] : "unset"); 160 } 161 162 function checkReferrer() { 163 // Return true iff the HTTP "referer" is present and correct. 164 // This is to prevent cross-site request forgeries (CSRF). 165 // 166 if (!isset($_SERVER["HTTP_REFERER"])) return false; 167 $ref = preg_replace('#/[^/]*$#', "", $_SERVER["HTTP_REFERER"]); 168 $scr = preg_replace('#/[^/]*$#', "", $_SERVER["SCRIPT_NAME"]); 169 $host = strtolower($_SERVER["SERVER_NAME"]); 170 if (!preg_match('#^https://([^/]*)(.*)$#i', $ref, 171 $matches)) return false; 172 return strtolower($matches[1]) == $host && $matches[2] == $scr; 173 } 174 175 176 // 177 // Encrypt or decrypt content 178 // 179 180 function encryptContent($plain, $key, $iv) { 181 // Encrypt the given plain text using the given key and IV 182 // 183 // Assumes the key is exactly 256 bits, and the IV is exactly 128 bits. 184 // Uses AES-256-CBC with PKCS #7 block padding. 185 // This does not create any integrity check (MAC); the client should. 186 // Returns the cipher text. 187 // 188 return openssl_encrypt($plain, "aes-256-cbc", $key, true, $iv); 189 // openssl enc -aes-256-cbc -in $plain -K bin2hex($key) -iv bin2hex($iv) 190 } 191 192 function decryptContent($cipher, $key, $iv) { 193 // Decrypt the given cipher text using the given key and IV 194 // 195 // Assumes the key is exactly 256 bits, and the IV is exactly 128 bits. 196 // Uses AES-256-CBC with PKCS #7 block padding. 197 // This does not verify any integrity check (MAC); the client should. 198 // Returns the plain text, or false on error. 199 // 200 return openssl_decrypt($cipher, "aes-256-cbc", $key, true, $iv); 201 // openssl enc -d -aes-256-cbc -in $cipher -K bin2hex($key) -iv bin2hex($iv) 202 } 203 204 function macContent($data, $key) { 205 // Return a secured integrity check value for the given data. 206 // 207 // We use HMAC-SHA-256 keyed with $key 208 // 209 return hash_hmac("sha256", $data, $key, true); 210 } 211 212 ?>
End of listing