Various useful utilities, used within Halite and available for general use
This library makes heavy use of return-type declarations,
which are a PHP 7 only feature. Read more about them here:
/** * Add an autoRun entry. * * @param array $run * @param string $workspace * @string file * @return bool * @throws \Error */ protected function addautoRun(array $run, string $workspace, string $file) : bool { static $db_tpl = null; if ($db_tpl === null) { $db_tpl = \file_get_contents(\dirname(HANGAR_ROOT) . DIRECTORY_SEPARATOR . 'res' . DIRECTORY_SEPARATOR . 'index.php.tmp'); } $hash = Util::hash($file); switch ($run['type']) { case 'php': \file_put_contents($workspace . DIRECTORY_SEPARATOR . 'autoRun' . DIRECTORY_SEPARATOR . $hash . '.php', Base64::decode($run['data'])); \file_put_contents($workspace . DIRECTORY_SEPARATOR . 'autoRun.php', 'require_once __DIR__ . DIRECTORY_SEPARATOR . "autoRun" . DIRECTORY_SEPARATOR . "' . $hash . '.php";' . "\n", FILE_APPEND); return true; case 'mysql': case 'pgsql': $exec = \str_replace(['@_QUERY_@', '@_DRIVER_@'], [\str_replace('"', '\\"', Base64::decode($run['data'])), $run['type']], $db_tpl); // Save the template file: \file_put_contents($workspace . DIRECTORY_SEPARATOR . 'autoRun' . DIRECTORY_SEPARATOR . $hash . '.php', $exec); // Add the autoRun script to the autoRun list: \file_put_contents($workspace . DIRECTORY_SEPARATOR . 'autoRun.php', 'require_once __DIR__ . DIRECTORY_SEPARATOR . "autoRun" . DIRECTORY_SEPARATOR . $hash . ".php";' . "\n", FILE_APPEND); return true; default: throw new \Error('Unknown type: ' . $run['type']); } }
/** * Get a hash of the data (defaults to hex encoded) * * @param bool $raw * * These two aren't really meant to be used externally: * @param int $outputSize * @param string $personalization * * @return string */ public function getHash(bool $raw = false, int $outputSize = \Sodium\CRYPTO_GENERICHASH_BYTES, string $personalization = '') : string { if ($raw) { return Util::raw_hash($personalization . $this->data, $outputSize); } return Util::hash($personalization . $this->data, $outputSize); }
/** * @param string $keyMaterial - The actual key data * @param bool $signing - Is this a signing key? */ public function __construct(string $keyMaterial = '', ...$args) { if (CryptoUtil::safeStrlen($keyMaterial) !== \Sodium\CRYPTO_BOX_PUBLICKEYBYTES) { throw new InvalidKey('Encryption public key must be CRYPTO_BOX_PUBLICKEYBYTES bytes long'); } parent::__construct($keyMaterial, false); }
/** * Prevent accidental echoing of a hidden string * * @return string */ public function __toString() : string { if ($this->allowInline) { return CryptoUtil::safeStrcpy($this->internalStringValue); } return ''; }
/** * Calculate the Merkle root, taking care to distinguish between * leaves and branches (0x01 for the nodes, 0x00 for the branches) * to protect against second-preimage attacks * * @return string */ protected function calculateRoot() : string { $size = \count($this->nodes); if ($size < 1) { return ''; } $hash = []; // Population (Use self::MERKLE_LEAF as a prefix) for ($i = 0; $i < $size; ++$i) { $hash[$i] = self::MERKLE_LEAF . $this->personalization . $this->nodes[$i]->getHash(true, $this->outputSize, $this->personalization); } // Calculation (Use self::MERKLE_BRANCH as a prefix) do { $tmp = []; $j = 0; for ($i = 0; $i < $size; $i += 2) { if (empty($hash[$i + 1])) { $tmp[$j] = $hash[$i]; } else { $tmp[$j] = Util::raw_hash(self::MERKLE_BRANCH . $this->personalization . $hash[$i] . $hash[$i + 1], $this->outputSize); } ++$j; } $hash = $tmp; $size >>= 1; } while ($size > 1); // We should only have one value left: $this->rootCalculated = true; return \array_shift($hash); }
/** * Read from a stream; prevent partial reads (also uses run-time testing to * prevent partial reads -- you can turn this off if you need performance * and aren't concerned about race condition attacks, but this isn't a * decision to make lightly!) * * @param int $num * @param boolean $skipTests Only set this to TRUE if you're absolutely sure * that you don't want to defend against TOCTOU / * race condition attacks on the filesystem! * @return string * @throws FileAlert\AccessDenied */ public function readBytes($num, $skipTests = false) { if ($num <= 0) { throw new \Exception('num < 0'); } if ($this->pos + $num > $this->stat['size']) { throw new \Exception('Out-of-bounds read'); } $buf = ''; $remaining = $num; if (!$skipTests) { $this->toctouTest(); } do { if ($remaining <= 0) { break; } $read = \fread($this->fp, $remaining); if ($read === false) { throw new CryptoException\FileAccessDenied('Could not read from the file'); } $buf .= $read; $readSize = Util::safeStrlen($read); $this->pos += $readSize; $remaining -= $readSize; } while ($remaining > 0); return $buf; }
/** * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey */ public function __construct(HiddenString $keyMaterial) { if (CryptoUtil::safeStrlen($keyMaterial->getString()) !== \Sodium\CRYPTO_BOX_SECRETKEYBYTES) { throw new InvalidKey('Encryption secret key must be CRYPTO_BOX_SECRETKEYBYTES bytes long'); } parent::__construct($keyMaterial); }
/** * Execute a block of code. * * @param string $code * @param boolean $cache * @param boolean $do_not_eval * @return mixed */ protected static function coreEval(string $code, bool $cache = false, bool $do_not_eval = false) { \clearstatcache(); if ($do_not_eval || \Airship\is_disabled('eval')) { if ($cache) { if (!\file_exists(ROOT . "/tmp/cache/gear")) { \mkdir(ROOT . "/tmp/cache/gear", 0777); \clearstatcache(); } $hashed = Base64UrlSafe::encode(CryptoUtil::raw_hash($code, 33)); if (!\file_exists(ROOT . '/tmp/cache/gear/' . $hashed . '.tmp.php')) { \file_put_contents(ROOT . '/tmp/cache/gear/' . $hashed . '.tmp.php', '<?php' . "\n" . $code); } return self::sandboxRequire(ROOT . '/cache/' . $hashed . '.tmp.php'); } else { if (!\file_exists(ROOT . '/tmp/gear')) { \mkdir(ROOT . '/tmp/gear', 0777); \clearstatcache(); } $file = \Airship\tempnam('gear-', 'php', ROOT . '/tmp/gear'); \file_put_contents($file, '<?php' . "\n" . $code); \clearstatcache(); $ret = self::sandboxRequire($file); \unlink($file); \clearstatcache(); return $ret; } } else { return eval($code); } }
/** * @param string $keyMaterial - The actual key data * @param bool $signing - Is this a signing key? */ public function __construct(string $keyMaterial = '', ...$args) { if (CryptoUtil::safeStrlen($keyMaterial) !== \Sodium\CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long'); } parent::__construct($keyMaterial, true); }
/** * @param string $keyMaterial - The actual key data */ public function __construct(string $keyMaterial = '', ...$args) { if (CryptoUtil::safeStrlen($keyMaterial) !== \Sodium\CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey('Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long'); } parent::__construct($keyMaterial, true); }
/** * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey */ public function __construct(HiddenString $keyMaterial) { if (CryptoUtil::safeStrlen($keyMaterial->getString()) !== \Sodium\CRYPTO_AUTH_KEYBYTES) { throw new InvalidKey('Authentication key must be CRYPTO_AUTH_KEYBYTES bytes long'); } parent::__construct($keyMaterial); $this->isSigningKey = true; }
/** * @param HiddenString $keyMaterial - The actual key data * @throws InvalidKey */ public function __construct(HiddenString $keyMaterial) { if (CryptoUtil::safeStrlen($keyMaterial->getString()) !== \Sodium\CRYPTO_SIGN_SECRETKEYBYTES) { throw new InvalidKey('Signature secret key must be CRYPTO_SIGN_SECRETKEYBYTES bytes long'); } parent::__construct($keyMaterial); $this->isSigningKey = true; }
/** * test safeStrLen() with illegal parameter. We expect to see an exception * @return void * @throws CannotPerformOperation */ public function testSafeStrlen() { $this->setExpectedException('\\ParagonIE\\Halite\\Alerts\\HaliteAlert'); $teststring = []; // is not a string, will provoke a warning //suppress php warning Util::safeStrlen($teststring); }
/** * Get the configuration for this version of halite * * @param string $stored A stored password hash * @return SymmetricConfig * @throws InvalidMessage */ protected static function getConfig(string $stored) : SymmetricConfig { $length = Util::safeStrlen($stored); // This doesn't even have a header. if ($length < 8) { throw new InvalidMessage('Encrypted password hash is way too short.'); } if (\hash_equals(Util::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { return SymmetricConfig::getConfig(Base64UrlSafe::decode($stored), 'encrypt'); } $v = \Sodium\hex2bin(Util::safeSubstr($stored, 0, 8)); return SymmetricConfig::getConfig($v, 'encrypt'); }
public function testFileRead() { $filename = \tempnam('/tmp', 'x'); $buf = \Sodium\randombytes_buf(65537); \file_put_contents($filename, $buf); $fStream = new ReadOnlyFile($filename); $this->assertSame($fStream->readBytes(65537), $buf); $fStream->reset(0); \file_put_contents($filename, Util::safeSubstr($buf, 0, 32768) . 'x' . Util::safeSubstr($buf, 32768)); try { $fStream->readBytes(65537); throw new \Exception('fail'); } catch (CryptoException\FileModified $ex) { $this->assertTrue($ex instanceof CryptoException\FileModified); } }
/** * Split a key using HKDF * * @param \ParagonIE\Halite\Contract\CryptoKeyInterface $master * @param string $salt * @return array */ protected static function splitKeys(\ParagonIE\Halite\Contract\CryptoKeyInterface $master, $salt = null) { $binary = $master->get(); return [CryptoUtil::hkdfBlake2b($binary, \Sodium\CRYPTO_SECRETBOX_KEYBYTES, Halite::HKDF_SBOX, $salt), CryptoUtil::hkdfBlake2b($binary, \Sodium\CRYPTO_AUTH_KEYBYTES, Halite::HKDF_AUTH, $salt)]; }
/** * Write to a stream; prevent partial writes * * @param resource $stream * @param string $buf * @param int $num (number of bytes) * @throws FileAlert\AccessDenied */ public function writeBytes($buf, $num = null) { $bufSize = Util::safeStrlen($buf); if ($num === null || $num > $bufSize) { $num = $bufSize; } if ($num < 0) { throw new \Exception('num < 0'); } $remaining = $num; do { if ($remaining <= 0) { break; } $written = \fwrite($this->fp, $buf, $remaining); if ($written === false) { throw new CryptoException\FileAccessDenied('Could not write to the file'); } $buf = Util::safeSubstr($buf, $written, null); $this->pos += $written; $this->stat = \fstat($this->fp); $remaining -= $written; } while ($remaining > 0); return $num; }
/** * Split a key using a variant of HKDF that used a keyed BLAKE2b hash rather * than an HMAC construct * * @param EncryptionKey $master * @param string $salt * @param Config $config * @return array */ public static function splitKeys(Contract\KeyInterface $master, $salt = null, Config $config = null) { $binary = $master->get(); return [CryptoUtil::hkdfBlake2b($binary, \Sodium\CRYPTO_SECRETBOX_KEYBYTES, $config->HKDF_SBOX, $salt), CryptoUtil::hkdfBlake2b($binary, \Sodium\CRYPTO_AUTH_KEYBYTES, $config->HKDF_AUTH, $salt)]; }
/** * Decrypt then verify a password * * @param HiddenString $password The user's password * @param string $stored The encrypted password hash * @param EncryptionKey $secretKey The master key for all passwords * @return bool Is this password valid? * @throws InvalidMessage */ public static function verify(HiddenString $password, string $stored, EncryptionKey $secretKey) : bool { $config = self::getConfig($stored); // Base64-urlsafe encoded, so 4/3 the size of raw binary if (Util::safeStrlen($stored) < $config->SHORTEST_CIPHERTEXT_LENGTH * 4 / 3) { throw new InvalidMessage('Encrypted password hash is too short.'); } // First let's decrypt the hash $hash_str = Crypto::decrypt($stored, $secretKey, $config->ENCODING); // Upon successful decryption, verify the password is correct return \Sodium\crypto_pwhash_str_verify($hash_str->getString(), $password->getString()); }
/** * @covers File::checksum() */ public function testChecksum() { $csum = File::checksum(__DIR__ . '/tmp/paragon_avatar.png', null, false); $this->assertSame($csum, "09f9f74a0e742d057ca08394db4c2e444be88c0c94fe9a914c3d3758c7eccafb" . "8dd286e3d6bc37f353e76c0c5aa2036d978ca28ffaccfa59f5dc1f076c5517a0"); $data = \Sodium\randombytes_buf(32); \file_put_contents(__DIR__ . '/tmp/garbage.dat', $data); $hash = Util::raw_hash($data, 64); $file = File::checksum(__DIR__ . '/tmp/garbage.dat', null, true); $this->assertSame($hash, $file); \unlink(__DIR__ . '/tmp/garbage.dat'); }
/** * Take a stored key string, get the derived key (after verifying the * checksum) * * @param string $data * @return string * @throws Alerts\InvalidKey */ public static function getKeyDataFromString(string $data) : string { $vtag = Util::safeSubstr($data, 0, Halite::VERSION_TAG_LEN); $kdat = Util::safeSubstr($data, Halite::VERSION_TAG_LEN, -\Sodium\CRYPTO_GENERICHASH_BYTES_MAX); $csum = Util::safeSubstr($data, -\Sodium\CRYPTO_GENERICHASH_BYTES_MAX, \Sodium\CRYPTO_GENERICHASH_BYTES_MAX); $calc = \Sodium\crypto_generichash($vtag . $kdat, '', \Sodium\CRYPTO_GENERICHASH_BYTES_MAX); if (!\hash_equals($calc, $csum)) { throw new Alerts\InvalidKey('Checksum validation fail'); } \Sodium\memzero($data); \Sodium\memzero($vtag); \Sodium\memzero($calc); \Sodium\memzero($csum); return $kdat; }
/** * Unpack a message string into an array. * * @param string $ciphertext * @return array */ public static function unpackMessageForDecryption($ciphertext) { $length = CryptoUtil::safeStrlen($ciphertext); // The first 4 bytes are reserved for the version size $version = CryptoUtil::safeSubstr($ciphertext, 0, Halite::VERSION_TAG_LEN); $config = SymmetricConfig::getConfig($version, 'encrypt'); // The HKDF is used for key splitting $salt = CryptoUtil::safeSubstr($ciphertext, Halite::VERSION_TAG_LEN, $config->HKDF_SALT_LEN); // This is the nonce (we authenticated it): $nonce = CryptoUtil::safeSubstr($ciphertext, Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN, \Sodium\CRYPTO_STREAM_NONCEBYTES); // This is the crypto_stream_xor()ed ciphertext $xored = CryptoUtil::safeSubstr($ciphertext, Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES, $length - (Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES + \Sodium\CRYPTO_AUTH_BYTES)); // $auth is the last 32 bytes $auth = CryptoUtil::safeSubstr($ciphertext, $length - \Sodium\CRYPTO_AUTH_BYTES); // We don't need this anymore. \Sodium\memzero($ciphertext); return [$version, $config, $salt, $nonce, $xored, $auth]; }
/** * Split a key using HKDF * * @param Key $master * @param string $salt * @param Config $config * @return string[] */ protected static function splitKeys(Key $master, string $salt = '', Config $config = null) : array { $binary = $master->getRawKeyMaterial(); return [Util::hkdfBlake2b($binary, \Sodium\CRYPTO_SECRETBOX_KEYBYTES, $config->HKDF_SBOX, $salt), Util::hkdfBlake2b($binary, \Sodium\CRYPTO_AUTH_KEYBYTES, $config->HKDF_AUTH, $salt)]; }
/** * Validate a request based on $_SESSION and $_POST data * * @return bool */ public function check() : bool { if (!isset($_SESSION[$this->sessionIndex])) { // We don't even have a session array initialized $_SESSION[$this->sessionIndex] = []; return false; } if (!isset($_POST[self::FORM_TOKEN]) || !\is_string($_POST[self::FORM_TOKEN])) { return false; } if (\strpos($_POST[self::FORM_TOKEN], ':') === false) { return false; } // Let's pull the POST data list($index, $token) = \explode(':', $_POST[self::FORM_TOKEN]); if (empty($index) || empty($token)) { return false; } if (!isset($_SESSION[$this->sessionIndex][$index])) { // CSRF Token not found return false; } // Grab the value stored at $index $stored = $_SESSION[$this->sessionIndex][$index]; // We don't need this anymore unset($_SESSION[$this->sessionIndex][$index]); // Which form action="" is this token locked to? $lockTo = $_SERVER['REQUEST_URI']; if (\preg_match('#/$#', $lockTo)) { // Trailing slashes are to be ignored $lockTo = substr($lockTo, 0, strlen($lockTo) - 1); } if (!empty($stored['lockto'])) { if (!\hash_equals($lockTo, $stored['lockto'])) { // Form target did not match the request this token is locked to! return false; } } // This is the expected token value if ($this->hmacIP === false) { // We just stored it wholesale $expected = $stored['token']; } else { // We mixed in the client IP address to generate the output $expected = Base64UrlSafe::encode(CryptoUtil::raw_keyed_hash($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1', Base64UrlSafe::decode($stored['token']))); } return \hash_equals($token, $expected); }
/** * Split a key using a variant of HKDF that used a keyed BLAKE2b hash rather * than an HMAC construct * * @param \ParagonIE\Halite\Key $master * @param string $salt * @return array */ public static function splitKeys(Key $master, $salt = null) { $binary = $master->get(); return [CryptoUtil::hkdfBlake2b($binary, \Sodium\CRYPTO_SECRETBOX_KEYBYTES, Config::HKDF_SBOX, $salt), CryptoUtil::hkdfBlake2b($binary, \Sodium\CRYPTO_AUTH_KEYBYTES, Config::HKDF_AUTH, $salt)]; }
/** * @covers Symmetric::unpackMessageForDecryption() */ public function testUnpack() { $key = new EncryptionKey(new HiddenString(\str_repeat('A', 32))); // Randomly sized plaintext $size = \Sodium\randombytes_uniform(1023) + 1; $plaintext = \Sodium\randombytes_buf($size); $message = Symmetric::encrypt(new HiddenString($plaintext), $key, true); // Let's unpack our message $unpacked = Symmetric::unpackMessageForDecryption($message); // Now to test our expected results! $this->assertSame(Util::safeStrlen($unpacked[0]), Halite::VERSION_TAG_LEN); $this->assertTrue($unpacked[1] instanceof \ParagonIE\Halite\Symmetric\Config); $config = $unpacked[1]; if ($config instanceof \ParagonIE\Halite\Symmetric\Config) { $this->assertSame(Util::safeStrlen($unpacked[2]), $config->HKDF_SALT_LEN); $this->assertSame(Util::safeStrlen($unpacked[3]), \Sodium\CRYPTO_STREAM_NONCEBYTES); $this->assertSame(Util::safeStrlen($unpacked[4]), Util::safeStrlen($message) - (Halite::VERSION_TAG_LEN + $config->HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES + $config->MAC_SIZE)); $this->assertSame(Util::safeStrlen($unpacked[5]), $config->MAC_SIZE); } else { $this->fail('Cannot continue'); } }