/** * Encrypt a message using the Halite encryption protocol * (Encrypt then MAC -- Xsalsa20 then HMAC-SHA-512/256) * * @param string $plaintext * @param EncryptionKey $secretKey * @param boolean $raw Don't hex encode the output? * @return string */ public static function encrypt(string $plaintext, EncryptionKey $secretKey, bool $raw = false) : string { $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); // Generate a nonce and HKDF salt: $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN); // Split our keys according to the HKDF salt: list($eKey, $aKey) = self::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey); \Sodium\memzero($eKey); // Calculate an authentication tag: $auth = self::calculateMAC(Halite::HALITE_VERSION . $salt . $nonce . $xored, $aKey); \Sodium\memzero($aKey); if (!$raw) { return \Sodium\bin2hex(Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth); } return Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth; }
/** * Encrypt a message using the Halite encryption protocol * * @param string $plaintext * @param Key $secretKey * @param boolean $raw Don't hex encode the output? * @return string */ public static function encrypt($plaintext, 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'); } $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf(Config::HKDF_SALT_LEN); list($eKey, $aKey) = self::splitKeys($secretKey, $salt); $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey); $auth = self::calculateMAC(Config::HALITE_VERSION . $salt . $nonce . $xored, $aKey); \Sodium\memzero($eKey); \Sodium\memzero($aKey); if (!$raw) { return \Sodium\bin2hex(Config::HALITE_VERSION . $salt . $nonce . $xored . $auth); } return Config::HALITE_VERSION . $salt . $nonce . $xored . $auth; }
/** * Stream decryption - Do not call directly * * @param resource $input * @param resource $output * @param Key $encKey * @param string $nonce * @param resource $mac (hash context) * @param &array $config * @throws FileAlert\AccessDenied */ private static final function streamDecrypt($input, $output, \ParagonIE\Halite\Contract\CryptoKeyInterface $encKey, $nonce, $mac, array $config, array &$chunk_macs) { // Reset the stream pointer to the beginning of the ciphertext $start = \ftell($input); if (\fseek($input, -1 * $config['MAC_SIZE'], SEEK_END) === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } $cipher_end = \ftell($input) - 1; if (\fseek($input, $start, SEEK_SET) === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } $break = false; while (!$break) { $pos = \ftell($input); if ($pos === false) { throw new CryptoAlert\CannotPerformOperation('Stream error'); } // Read the data from the input buffer if ($pos + $config['BUFFER'] >= $cipher_end) { $break = true; $read = self::readBytes($input, $cipher_end - $pos + 1); } else { $read = self::readBytes($input, $config['BUFFER']); } // Let's reculcualte the MAC of this chunk, then verify it \hash_update($mac, $read); $calcMAC = \hash_copy($mac); if ($calcMAC === false) { throw new CryptoAlert\CannotPerformOperation('An unknown error has occurred'); } $calc = \hash_final($calcMAC, true); if (empty($chunk_macs)) { throw new CryptoAlert\InvalidMessage('Invalid message authentication code'); } elseif (!\hash_equals(\array_shift($chunk_macs), $calc)) { throw new CryptoAlert\InvalidMessage('Invalid message authentication code'); } $decrypted = \Sodium\crypto_stream_xor($read, $nonce, $encKey->get()); $written = \fwrite($output, $decrypted); if ($written === false) { throw new FileAlert\AccessDenied('Could not write to the file'); } \Sodium\increment($nonce); } }
/** * Encrypt a message using the Halite encryption protocol * * @param string $plaintext * @param EncryptionKey $secretKey * @param boolean $raw Don't hex encode the output? * @return string */ public static function encrypt($plaintext, Contract\KeyInterface $secretKey, $raw = false) { if (!$secretKey instanceof EncryptionKey) { throw new CryptoException\InvalidKey('Expected an instance of EncryptionKey'); } $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN); list($eKey, $aKey) = self::splitKeys($secretKey, $salt, $config); $xored = \Sodium\crypto_stream_xor($plaintext, $nonce, $eKey); $auth = self::calculateMAC(Halite::HALITE_VERSION . $salt . $nonce . $xored, $aKey); \Sodium\memzero($eKey); \Sodium\memzero($aKey); if (!$raw) { return \Sodium\bin2hex(Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth); } return Halite::HALITE_VERSION . $salt . $nonce . $xored . $auth; }
/** * Stream decryption - Do not call directly * * @param ReadOnlyFile $input * @param MutableFile $output * @param Key $encKey * @param string $nonce * @param resource $mac (hash context) * @param Config $config * @return bool * @throws CryptoException\AccessDenied * @throws CryptoException\CannotPerformOperation * @throws CryptoException\FileModified * @throws CryptoException\InvalidKey * @throws CryptoException\InvalidMessage */ private static final function streamDecrypt(ReadOnlyFile $input, MutableFile $output, EncryptionKey $encKey, string $nonce, $mac, Config $config, array &$chunk_macs) : bool { $start = $input->getPos(); $cipher_end = $input->getSize() - $config->MAC_SIZE; // Begin the streaming decryption $input->reset($start); while ($input->remainingBytes() > $config->MAC_SIZE) { /** * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ if ($input->getPos() + $config->BUFFER > $cipher_end) { $read = $input->readBytes($cipher_end - $input->getPos()); } else { $read = $input->readBytes($config->BUFFER); } if ($config->USE_BLAKE2B) { \Sodium\crypto_generichash_update($mac, $read); $calcMAC = '' . $mac; $calc = \Sodium\crypto_generichash_final($calcMAC, $config->MAC_SIZE); } else { \hash_update($mac, $read); $calcMAC = \hash_copy($mac); if ($calcMAC === false) { throw new CryptoException\CannotPerformOperation('An unknown error has occurred'); } $calc = \hash_final($calcMAC, true); } if (empty($chunk_macs)) { throw new CryptoException\InvalidMessage('Invalid message authentication code'); } else { $chkmac = \array_shift($chunk_macs); if (!\hash_equals($chkmac, $calc)) { throw new CryptoException\InvalidMessage('Invalid message authentication code'); } } $decrypted = \Sodium\crypto_stream_xor($read, $nonce, $encKey->getRawKeyMaterial()); $output->writeBytes($decrypted); \Sodium\increment($nonce); } \Sodium\memzero($nonce); return true; }
/** * Stream decryption - Do not call directly * * @param ReadOnlyFile $input * @param MutableFile $output * @param Key $encKey * @param string $nonce * @param resource $mac (hash context) * @param Config $config * @throws FileAlert\AccessDenied */ private static final function streamDecrypt(ReadOnlyFile $input, MutableFile $output, KeyInterface $encKey, $nonce, $mac, Config $config, array &$chunk_macs) { if (!$encKey instanceof EncryptionKey) { throw new \ParagonIE\Halite\Alerts\InvalidKey('Argument 3: Expected an instance of EncryptionKey'); } $start = $input->getPos(); $cipher_end = $input->getSize() - $config->MAC_SIZE; // Begin the streaming decryption $input->reset($start); while ($input->remainingBytes() > $config->MAC_SIZE) { if ($input->getPos() + $config->BUFFER > $cipher_end) { $read = $input->readBytes($cipher_end - $input->getPos()); } else { $read = $input->readBytes($config->BUFFER); } \hash_update($mac, $read); $calcMAC = \hash_copy($mac); if ($calcMAC === false) { throw new CryptoException\CannotPerformOperation('An unknown error has occurred'); } $calc = \hash_final($calcMAC, true); if (empty($chunk_macs)) { throw new CryptoException\InvalidMessage('Invalid message authentication code'); } else { $chkmac = \array_shift($chunk_macs); if (!\hash_equals($chkmac, $calc)) { throw new CryptoException\InvalidMessage('Invalid message authentication code'); } } $decrypted = \Sodium\crypto_stream_xor($read, $nonce, $encKey->get()); $output->writeBytes($decrypted); \Sodium\increment($nonce); } \Sodium\memzero($nonce); return true; }
/** * Encrypt a message using the Halite encryption protocol * * (Encrypt then MAC -- xsalsa20 then keyed-Blake2b) * You don't need to worry about chosen-ciphertext attacks. * * @param HiddenString $plaintext * @param EncryptionKey $secretKey * @param mixed $encoding * @return string */ public static function encrypt(HiddenString $plaintext, EncryptionKey $secretKey, $encoding = Halite::ENCODE_BASE64URLSAFE) : string { $config = SymmetricConfig::getConfig(Halite::HALITE_VERSION, 'encrypt'); // Generate a nonce and HKDF salt: $nonce = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_NONCEBYTES); $salt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN); /* Split our key into two keys: One for encryption, the other for authentication. By using separate keys, we can reasonably dismiss likely cross-protocol attacks. This uses salted HKDF to split the keys, which is why we need the salt in the first place. */ list($encKey, $authKey) = self::splitKeys($secretKey, $salt, $config); // Encrypt our message with the encryption key: $encrypted = \Sodium\crypto_stream_xor($plaintext->getString(), $nonce, $encKey); \Sodium\memzero($encKey); // Calculate an authentication tag: $auth = self::calculateMAC(Halite::HALITE_VERSION . $salt . $nonce . $encrypted, $authKey, $config); \Sodium\memzero($authKey); $message = Halite::HALITE_VERSION . $salt . $nonce . $encrypted . $auth; // Wipe every superfluous piece of data from memory \Sodium\memzero($nonce); \Sodium\memzero($salt); \Sodium\memzero($encrypted); \Sodium\memzero($auth); $encoder = Halite::chooseEncoder($encoding); if ($encoder) { return $encoder($message); } return $message; }
/** * Stream decryption - Do not call directly * * @param ReadOnlyFile $input * @param MutableFile $output * @param EncryptionKey $encKey * @param string $nonce * @param string $mac (hash context for BLAKE2b) * @param Config $config * @param array &$chunk_macs * @return bool * @throws FileAccessDenied * @throws CannotPerformOperation * @throws FileModified * @throws InvalidKey * @throws InvalidMessage */ private static final function streamDecrypt(ReadOnlyFile $input, MutableFile $output, EncryptionKey $encKey, string $nonce, string $mac, Config $config, array &$chunk_macs) : bool { $start = $input->getPos(); $cipher_end = $input->getSize() - $config->MAC_SIZE; // Begin the streaming decryption $input->reset($start); while ($input->remainingBytes() > $config->MAC_SIZE) { /** * Would a full BUFFER read put it past the end of the * ciphertext? If so, only return a portion of the file. */ if ($input->getPos() + $config->BUFFER > $cipher_end) { $read = $input->readBytes($cipher_end - $input->getPos()); } else { $read = $input->readBytes($config->BUFFER); } // Version 2+ uses a keyed BLAKE2b hash instead of HMAC \Sodium\crypto_generichash_update($mac, $read); $calcMAC = Util::safeStrcpy($mac); $calc = \Sodium\crypto_generichash_final($calcMAC, $config->MAC_SIZE); if (empty($chunk_macs)) { // Someone attempted to add a chunk at the end. throw new InvalidMessage('Invalid message authentication code'); } else { $chunkMAC = \array_shift($chunk_macs); if (!\hash_equals($chunkMAC, $calc)) { // This chunk was altered after the original MAC was verified throw new InvalidMessage('Invalid message authentication code'); } } // This is where the decryption actually occurs: $decrypted = \Sodium\crypto_stream_xor($read, $nonce, $encKey->getRawKeyMaterial()); $output->writeBytes($decrypted); \Sodium\increment($nonce); } \Sodium\memzero($nonce); return true; }
/** * Message encryption (secret-key) * * @param string|resource $plaintext * @param EncryptionKey $key * * @return string */ public function encryptSymmetric($plaintext, EncryptionKey $key, array $options = []) : string { // Build our header: // [VV][VV]: $message = \chr(Common::VERSION_MAJOR); $message .= \chr(Common::VERSION_MAJOR); // [DD]: $message .= \chr(0x7f & self::DRIVER_ID); // [CC]: $message .= \chr(Common::VERSION_MAJOR ^ Common::VERSION_MINOR ^ 0x7f & self::DRIVER_ID); // Salt: $salt = \random_bytes(\Sodium\CRYPTO_GENERICHASH_KEYBYTES); // Split keys: list($encKey, $authKey) = $this->splitSymmetricKey($key, $salt); $message .= $salt; // HKDF salt // Nonce: $nonce = \random_bytes(\Sodium\CRYPTO_STREAM_NONCEBYTES); $message .= $nonce; // Nonce for the stream cipher // Encrypt: $message .= \Sodium\crypto_stream_xor($plaintext, $nonce, $encKey->getRawBytes()); unset($encKey); // Authenticate: $message .= \Sodium\crypto_auth($message, $authKey->getRawBytes()); unset($authKey); // Return: return $message; }