/** * 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); } }
/** * Decrypt a message using the Halite encryption protocol * * @param string $ciphertext * @param Key $secretKey * @param boolean $raw Don't hex decode the input? */ public static function decrypt($ciphertext, Contract\CryptoKeyInterface $secretKey, $raw = false) { if ($secretKey->isAsymmetricKey()) { throw new CryptoAlert\InvalidKey('Expected a symmetric key, not an asymmetric key'); } if (!$secretKey->isEncryptionKey()) { throw new CryptoAlert\InvalidKey('Encryption key expected'); } if (!$raw) { // We were given hex data: $ciphertext = \Sodium\hex2bin($ciphertext); } $length = CryptoUtil::safeStrlen($ciphertext); // The first 4 bytes are reserved for the version size $version = CryptoUtil::safeSubstr($ciphertext, 0, Config::VERSION_TAG_LEN); // The HKDF is used for key splitting $salt = CryptoUtil::safeSubstr($ciphertext, Config::VERSION_TAG_LEN, Config::HKDF_SALT_LEN); // This is the nonce (we authenticated it): $nonce = CryptoUtil::safeSubstr($ciphertext, Config::VERSION_TAG_LEN + Config::HKDF_SALT_LEN, \Sodium\CRYPTO_STREAM_NONCEBYTES); // This is the crypto_stream_xor()ed ciphertext $xored = CryptoUtil::safeSubstr($ciphertext, Config::VERSION_TAG_LEN + Config::HKDF_SALT_LEN + \Sodium\CRYPTO_STREAM_NONCEBYTES, $length - (Config::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); // Split our keys list($eKey, $aKey) = self::splitKeys($secretKey, $salt); // Check the MAC first if (!self::verifyMAC($auth, $version . $salt . $nonce . $xored, $aKey)) { throw new CryptoAlert\InvalidMessage('Invalid message authenticaiton code'); } // Down the road, do whatever logic around $version here, in case we // need to upgrade our protocol. // Add version logic above $plaintext = \Sodium\crypto_stream_xor($xored, $nonce, $eKey); if ($plaintext === false) { throw new CryptoAlert\InvalidMessage('Invalid message authenticaiton code'); } return $plaintext; }
/** * 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; }
/** * 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]; }