<?php
declare(strict_types=1);
namespace ParagonIE\Halite;
use \ParagonIE\Halite\Alerts as CryptoException;
use \ParagonIE\Halite\{
Asymmetric\Crypto as AsymmetricCrypto,
Asymmetric\EncryptionPublicKey,
Asymmetric\EncryptionSecretKey,
Asymmetric\SignaturePublicKey,
Asymmetric\SignatureSecretKey,
Contract\FileInterface,
Contract\KeyInterface,
Contract\StreamInterface,
Stream\MutableFile,
Stream\ReadOnlyFile,
Symmetric\AuthenticationKey,
Symmetric\EncryptionKey
};
use ParagonIE\Halite\Asymmetric\Crypto;
final class File implements FileInterface
{
/**
* Lazy fallthrough method for checksumFile() and checksumResource()
*
* @param string|resource $filepath
* @param AuthenticationKey $key
* @param bool $raw
* @return string
* @throws CryptoException\InvalidType
*/
public static function checksum(
$filepath,
KeyInterface $key = null,
$raw = false
): string {
if (\is_resource($filepath) || \is_string($filepath)) {
return self::checksumStream(
new ReadOnlyFile($filepath),
$key,
$raw
);
}
throw new CryptoException\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Lazy fallthrough method for encryptFile() and encryptResource()
*
* @param string|resource $input
* @param string|resource $output
* @param EncryptionKey $key
* @return int
* @throws CryptoException\InvalidType
*/
public static function encrypt(
$input,
$output,
EncryptionKey $key
): int {
if (
\is_resource($input) ||
\is_resource($output) ||
\is_string($input) ||
\is_string($output)
) {
return self::encryptStream(
new ReadOnlyFile($input),
new MutableFile($output),
$key
);
}
throw new CryptoException\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Lazy fallthrough method for decryptFile() and decryptResource()
*
* @param string|resource $input
* @param string|resource $output
* @param EncryptionKey $key
* @return bool
* @throws CryptoException\InvalidType
*/
public static function decrypt(
$input,
$output,
EncryptionKey $key
): bool {
if (
\is_resource($input) ||
\is_resource($output) ||
\is_string($input) ||
\is_string($output)
) {
try {
$readOnly = new ReadOnlyFile($input);
$mutable = new MutableFile($output);
return self::decryptStream(
$readOnly,
$mutable,
$key
);
} catch (CryptoException\HaliteAlert $ex) {
$readOnly->close();
$mutable->close();
throw $ex;
}
}
throw new CryptoException\InvalidType(
'Strings or file handles expected'
);
}
/**
* Lazy fallthrough method for sealFile() and sealResource()
*
* @param string|resource $input
* @param string|resource $output
* @param EncryptionPublicKey $publickey
* @return int Number of bytes written
* @throws Alerts\InvalidType
*/
public static function seal(
$input,
$output,
EncryptionPublicKey $publickey
): int {
if (
\is_resource($input) ||
\is_resource($output) ||
\is_string($input) ||
\is_string($output)
) {
return self::sealStream(
new ReadOnlyFile($input),
new MutableFile($output),
$publickey
);
}
throw new CryptoException\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Lazy fallthrough method for sealFile() and sealResource()
*
* @param string|resource $input
* @param string|resource $output
* @param EncryptionSecretKey $secretkey
* @return bool TRUE on success
* @throws CryptoException\InvalidType
*/
public static function unseal(
$input,
$output,
EncryptionSecretKey $secretkey
): bool {
if (
\is_resource($input) ||
\is_resource($output) ||
\is_string($input) ||
\is_string($output)
) {
try {
$readOnly = new ReadOnlyFile($input);
$mutable = new MutableFile($output);
return self::unsealStream(
$readOnly,
$mutable,
$secretkey
);
} catch (CryptoException\HaliteAlert $ex) {
$readOnly->close();
$mutable->close();
throw $ex;
}
}
throw new CryptoException\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Lazy fallthrough method for signFile() and signResource()
*
* @param string|resource $filename
* @param SignatureSecretKey $secretkey
* @param bool $raw_binary
* @return string
* @throws Alerts\InvalidType
*/
public static function sign(
$filename,
SignatureSecretKey $secretkey,
bool $raw_binary = false
): string {
if (
\is_resource($filename) ||
\is_string($filename)
) {
return self::signStream(
new ReadOnlyFile($filename),
$secretkey,
$raw_binary
);
}
throw new CryptoException\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Lazy fallthrough method for verifyFile() and verifyResource()
*
* @param string|resource $filename
* @param SignaturePublicKey $publickey
* @param string $signature
* @param bool $raw_binary
*
* @return string
* @throws Alerts\InvalidType
*/
public static function verify(
$filename,
SignaturePublicKey $publickey,
string $signature,
bool $raw_binary = false
): bool {
if (
\is_resource($filename) ||
\is_string($filename)
) {
return self::verifyStream(
new ReadOnlyFile($filename),
$publickey,
$signature,
$raw_binary
);
}
throw new CryptoException\InvalidType(
'Argument 1: Expected a filename or resource'
);
}
/**
* Calculate a checksum (derived from BLAKE2b) of a file, by filename
*
* @param string $filepath The file you'd like to checksum
* @param Key $key An optional key to use in the BLAKE2b hash
* @param bool $raw Set to true if you don't want hex
* @return string
* @throws Alerts\FileAccessDenied
*/
protected static function checksumFile(
string $filepath,
Key $key = null,
bool $raw = false
): string {
if (!is_readable($filepath)) {
throw new CryptoException\FileAccessDenied(
'Could not read the file'
);
}
$fp = \fopen($filepath, 'rb');
if ($fp === false) {
throw new CryptoException\FileAccessDenied(
'Could not read the file'
);
}
try {
$checksum = self::checksumResource($fp, $key, $raw);
} catch (CryptoException\HaliteAlert $e) {
\fclose($fp);
throw $e;
}
\fclose($fp);
return $checksum;
}
/**
* Calculate a checksum (derived from BLAKE2b) of a file handle
*
* @param string $fileHandle The file you'd like to checksum
* @param Key $key An optional key to use in the BLAKE2b hash
* @param bool $raw Set to true if you don't want hex
*
* @return string
* @throws Alerts\InvalidType
*/
protected static function checksumResource(
$fileHandle,
Key $key = null,
bool $raw = false
): string {
// Input validation
if (!\is_resource($fileHandle)) {
throw new CryptoException\InvalidType(
'Expected input handle to be a resource'
);
}
return self::checksumStream(
new ReadOnlyFile($fileHandle),
$key,
$raw
);
}
/**
* Calculate the BLAKE2b checksum of an entire stream
*
* @param StreamInterface $fileStream
* @param Key $key
* @param bool $raw
* @return string
* @throws CryptoException\InvalidKey
*/
protected static function checksumStream(
StreamInterface $fileStream,
Key $key = null,
bool $raw = false
): string {
$config = self::getConfig(
Halite::HALITE_VERSION_FILE,
'checksum'
);
if ($key instanceof AuthenticationKey) {
$state = \Sodium\crypto_generichash_init(
$key->get(),
$config->HASH_LEN
);
} elseif($config->CHECKSUM_PUBKEY && $key instanceof SignaturePublicKey) {
// In version 2, we use the public key as a hash key
$state = \Sodium\crypto_generichash_init(
$key->get(),
$config->HASH_LEN
);
} elseif (isset($key)) {
throw new CryptoException\InvalidKey(
'Argument 2: Expected an instance of AuthenticationKey'
);
} else {
$state = \Sodium\crypto_generichash_init(
'',
$config->HASH_LEN
);
}
$size = $fileStream->getSize();
while ($fileStream->remainingBytes() > 0) {
$read = $fileStream->readBytes(
// Don't go past the file size even if $config->BUFFER is not an even multiple of it:
($fileStream->getPos() + $config->BUFFER) > $size
? ($size - $fileStream->getPos())
: $config->BUFFER
);
\Sodium\crypto_generichash_update($state, $read);
}
if ($raw) {
return \Sodium\crypto_generichash_final($state, $config->HASH_LEN);
}
return \Sodium\bin2hex(
\Sodium\crypto_generichash_final($state, $config->HASH_LEN)
);
}
/**
* Encrypt a file with a symmetric key
*
* @param string $inputFile
* @param string $outputFile
* @param EncryptionKey $key
* @return int
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\HaliteAlert
*/
protected static function encryptFile(
string $inputFile,
string $outputFile,
EncryptionKey $key
): int {
if (!\is_readable($inputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
if (!\is_writable($outputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
$inputHandle = \fopen($inputFile, 'rb');
if ($inputHandle === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$outputHandle = \fopen($outputFile, 'wb');
if ($outputHandle === false) {
\fclose($inputHandle);
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
try {
$result = self::encryptResource(
$inputHandle,
$outputHandle,
$key
);
\fclose($inputHandle);
\fclose($outputHandle);
return $result;
} catch (CryptoException\HaliteAlert $e) {
\fclose($inputHandle);
\fclose($outputHandle);
// Rethrow the exception:
throw $e;
}
}
/**
* Decrypt a file with a symmetric key
*
* @param string $inputFile
* @param string $outputFile
* @param EncryptionKey $key
* @return bool
*
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\HaliteAlert
*/
protected static function decryptFile(
string $inputFile,
string $outputFile,
EncryptionKey $key
): bool {
if (!\is_readable($inputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
if (!\is_writable($outputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
$inputHandle = \fopen($inputFile, 'rb');
if ($inputHandle === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$outputHandle = \fopen($outputFile, 'wb');
if ($outputHandle === false) {
\fclose($inputHandle);
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
try {
$result = self::decryptResource(
$inputHandle,
$outputHandle,
$key
);
\fclose($inputHandle);
\fclose($outputHandle);
return $result;
} catch (CryptoException\HaliteAlert $e) {
\fclose($inputHandle);
\fclose($outputHandle);
// Rethrow the exception:
throw $e;
}
}
/**
* Encrypt a file with a public key
*
* @param string $inputFile
* @param string $outputFile
* @param EncryptionPublicKey $publickey
* @return int
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\HaliteAlert
*/
protected static function sealFile(
string $inputFile,
string $outputFile,
EncryptionPublicKey $publickey
): int {
if (!\is_readable($inputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
if (!\is_writable($outputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
$inputHandle = \fopen($inputFile, 'rb');
if ($inputHandle === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$outputHandle = \fopen($outputFile, 'wb');
if ($outputHandle === false) {
\fclose($inputHandle);
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
try {
$return = self::sealResource(
$inputHandle,
$outputHandle,
$publickey
);
\fclose($inputHandle);
\fclose($outputHandle);
return $return;
} catch (CryptoException\HaliteAlert $e) {
\fclose($inputHandle);
\fclose($outputHandle);
// Rethrow the exception:
throw $e;
}
}
/**
* Decrypt a file with a private key
*
* @param string $inputFile
* @param string $outputFile
* @param EncryptionSecretKey $secretkey
* @return bool
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\HaliteAlert
*/
protected static function unsealFile(
string $inputFile,
string $outputFile,
EncryptionSecretKey $secretkey
): bool {
if (!\is_readable($inputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
if (!\is_writable($outputFile)) {
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
$inputHandle = \fopen($inputFile, 'rb');
if ($inputHandle === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$outputHandle = \fopen($outputFile, 'wb');
if ($outputHandle === false) {
\fclose($inputHandle);
throw new CryptoException\FileAccessDenied(
'Could not write to the file'
);
}
try {
$return = self::unsealResource(
$inputHandle,
$outputHandle,
$secretkey
);
\fclose($inputHandle);
\fclose($outputHandle);
return $return;
} catch (CryptoException\HaliteAlert $e) {
\fclose($inputHandle);
\fclose($outputHandle);
// Rethrow the exception:
throw $e;
}
}
/**
* Signs a file
*
* @param string $filename
* @param SignatureSecretKey $secretkey
* @param bool $raw_binary
* @return string
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\HaliteAlert
*/
protected static function signFile(
string $filename,
SignatureSecretKey $secretkey,
bool $raw_binary = false
): string {
if (!\is_readable($filename)) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$inputHandle = \fopen($filename, 'rb');
if ($inputHandle === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
try {
$return = self::signResource(
$inputHandle,
$secretkey,
$raw_binary
);
\fclose($inputHandle);
return $return;
} catch (CryptoException\HaliteAlert $e) {
\fclose($inputHandle);
// Rethrow the exception:
throw $e;
}
}
/**
* Verifies a file
*
* @param string $filename
* @param SignaturePublicKey $publickey
* @param string $signature
* @return bool
* @throws CryptoException\FileAccessDenied
* @throws CryptoException\HaliteAlert
*/
protected static function verifyFile(
string $filename,
SignaturePublicKey $publickey,
string $signature,
bool $raw_binary = false
): bool {
if (!\is_readable($filename)) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
$inputHandle = \fopen($filename, 'rb');
if ($inputHandle === false) {
throw new CryptoException\FileAccessDenied(
'Could not read from the file'
);
}
try {
$return = self::verifyResource(
$inputHandle,
$publickey,
$signature,
$raw_binary
);
\fclose($inputHandle);
return $return;
} catch (CryptoException\HaliteAlert $e) {
\fclose($inputHandle);
// Rethrow the exception:
throw $e;
}
}
/**
* Encrypt a (file handle)
*
* @param $input
* @param $output
* @param EncryptionKey $key
* @return int
* @throws CryptoException\InvalidType
*/
protected static function encryptResource(
$input,
$output,
EncryptionKey $key
): int {
// Input validation
if (!\is_resource($input)) {
throw new \ParagonIE\Halite\Alerts\InvalidType(
'Expected input handle to be a resource'
);
}
if (!\is_resource($output)) {
throw new \ParagonIE\Halite\Alerts\InvalidType(
'Expected output handle to be a resource'
);
}
return self::encryptStream(
new ReadOnlyFile($input),
new MutableFile($output),
$key
);
}
/**
* Encrypt a (file handle)
*
* @param $input
* @param $output
* @param EncryptionKey $key
*/
protected static function encryptStream(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $key
): int {
$config = self::getConfig(Halite::HALITE_VERSION_FILE, 'encrypt');
// Generate a nonce and HKDF salt
$firstnonce = \Sodium\randombytes_buf($config->NONCE_BYTES);
$hkdfsalt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN);
// Let's split our key
list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
$mac = \hash_init('sha256', HASH_HMAC, $authKey);
// We no longer need $authKey after we set up the hash context
unset($authKey);
// Write the header
$output->writeBytes(Halite::HALITE_VERSION_FILE, Halite::VERSION_TAG_LEN);
$output->writeBytes($firstnonce, \Sodium\CRYPTO_STREAM_NONCEBYTES);
$output->writeBytes($hkdfsalt, $config->HKDF_SALT_LEN);
\hash_update($mac, Halite::HALITE_VERSION_FILE);
\hash_update($mac, $firstnonce);
\hash_update($mac, $hkdfsalt);
return self::streamEncrypt(
$input,
$output,
new EncryptionKey($encKey),
$firstnonce,
$mac,
$config
);
}
/**
* Decrypt a (file handle)
*
* @param $input
* @param $output
* @param EncryptionKey $key
* @return bool
* @throws CryptoException\InvalidType
*/
protected static function decryptResource(
$input,
$output,
EncryptionKey $key
): bool {
// Input validation
if (!\is_resource($input)) {
throw new CryptoException\InvalidType(
'Expected input handle to be a resource'
);
}
if (!\is_resource($output)) {
throw new CryptoException\InvalidType(
'Expected output handle to be a resource'
);
}
return self::decryptStream(
new ReadOnlyFile($input),
new MutableFile($output),
$key
);
}
/**
* Decrypt a (file handle)
*
* @param $input
* @param $output
* @param EncryptionKey $key
* @return bool
*/
protected static function decryptStream(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $key
): bool {
$input->reset(0);
// Parse the header, ensuring we get 4 bytes
$header = $input->readBytes(Halite::VERSION_TAG_LEN);
// Load the config
$config = self::getConfig($header, 'encrypt');
// Let's grab the first nonce and salt
$firstnonce = $input->readBytes($config->NONCE_BYTES);
$hkdfsalt = $input->readBytes($config->HKDF_SALT_LEN);
// Split our keys, begin the HMAC instance
list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
$mac = \hash_init('sha256', HASH_HMAC, $authKey);
\hash_update($mac, $header);
\hash_update($mac, $firstnonce);
\hash_update($mac, $hkdfsalt);
// This will throw an exception if it fails.
$old_macs = self::streamVerify($input, \hash_copy($mac), $config);
$ret = self::streamDecrypt(
$input,
$output,
new EncryptionKey($encKey),
$firstnonce,
$mac,
$config,
$old_macs
);
unset($encKey);
unset($authKey);
unset($firstnonce);
unset($mac);
unset($config);
unset($old_macs);
return $ret;
}
/**
* Seal a (file handle)
*
* @param $input
* @param $output
* @param EncryptionPublicKey $publickey
* @return int
* @throws CryptoException\InvalidType
*/
protected static function sealResource(
$input,
$output,
EncryptionPublicKey $publickey
): int {
// Input validation
if (!\is_resource($input)) {
throw new CryptoException\InvalidType(
'Expected input handle to be a resource'
);
}
if (!\is_resource($output)) {
throw new CryptoException\InvalidType(
'Expected output handle to be a resource'
);
}
return self::sealStream(
new ReadOnlyFile($input),
new MutableFile($output),
$publickey
);
}
/**
* Seal a (file handle)
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionPublicKey $publickey
* @return int
*/
protected static function sealStream(
ReadOnlyFile $input,
MutableFile $output,
EncryptionPublicKey $publickey
): int {
// Generate a new keypair for this encryption
$eph_kp = KeyFactory::generateEncryptionKeyPair();
$eph_secret = $eph_kp->getSecretKey();
$eph_public = $eph_kp->getPublicKey();
unset($eph_kp);
// Calculate the shared secret key
$key = AsymmetricCrypto::getSharedSecret($eph_secret, $publickey, true);
// Destroy the secre tkey after we have the shared secret
unset($eph_secret);
$config = self::getConfig(Halite::HALITE_VERSION_FILE, 'seal');
// Generate a nonce as per crypto_box_seal
$nonce = \Sodium\crypto_generichash(
$eph_public->get().$publickey->get(),
'',
\Sodium\CRYPTO_STREAM_NONCEBYTES
);
// Generate a random HKDF salt
$hkdfsalt = \Sodium\randombytes_buf($config->HKDF_SALT_LEN);
// Split the keys
list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
// We no longer need the original key after we split it
unset($key);
$mac = \hash_init('sha256', HASH_HMAC, $authKey);
// We no longer need to retain this after we've set up the hash context
unset($authKey);
$output->writeBytes(Halite::HALITE_VERSION_FILE, Halite::VERSION_TAG_LEN);
$output->writeBytes($eph_public->get(), \Sodium\CRYPTO_BOX_PUBLICKEYBYTES);
$output->writeBytes($hkdfsalt, $config->HKDF_SALT_LEN);
\hash_update($mac, Halite::HALITE_VERSION_FILE);
\hash_update($mac, $eph_public->get());
\hash_update($mac, $hkdfsalt);
unset($eph_public);
return self::streamEncrypt(
$input,
$output,
new EncryptionKey($encKey),
$nonce,
$mac,
$config
);
}
/**
* Unseal a (file handle)
*
* @param $input
* @param $output
* @param EncryptionSecretKey $secretkey
* @return bool
* @throws CryptoException\InvalidType
*/
protected static function unsealResource(
$input,
$output,
EncryptionSecretKey $secretkey
): bool {
// Input validation
if (!\is_resource($input)) {
throw new CryptoException\InvalidType(
'Expected input handle to be a resource'
);
}
if (!\is_resource($output)) {
throw new CryptoException\InvalidType(
'Expected output handle to be a resource'
);
}
return self::unsealStream(
new ReadOnlyFile($input),
new MutableFile($output),
$secretkey
);
}
/**
* Unseal a (file handle)
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionSecretKey $secretkey
*
* @return bool
* @throws CryptoException\InvalidKey
*/
protected static function unsealStream(
ReadOnlyFile $input,
MutableFile $output,
EncryptionSecretKey $secretkey
): bool {
$secret_key = $secretkey->get();
$public_key = \Sodium\crypto_box_publickey_from_secretkey($secret_key);
// Parse the header, ensuring we get 4 bytes
$header = $input->readBytes(Halite::VERSION_TAG_LEN);
// Load the config
$config = self::getConfig($header, 'seal');
// Let's grab the public key and salt
$eph_public = $input->readBytes($config->PUBLICKEY_BYTES);
$hkdfsalt = $input->readBytes($config->HKDF_SALT_LEN);
$nonce = \Sodium\crypto_generichash(
$eph_public . $public_key,
'',
\Sodium\CRYPTO_STREAM_NONCEBYTES
);
$ephemeral = new EncryptionPublicKey($eph_public);
$key = AsymmetricCrypto::getSharedSecret(
$secretkey,
$ephemeral,
true
);
list ($encKey, $authKey) = self::splitKeys($key, $hkdfsalt, $config);
// We no longer need the original key after we split it
unset($key);
$mac = \hash_init('sha256', HASH_HMAC, $authKey);
\hash_update($mac, $header);
\hash_update($mac, $eph_public);
\hash_update($mac, $hkdfsalt);
// This will throw an exception if it fails.
$old_macs = self::streamVerify($input, \hash_copy($mac), $config);
$ret = self::streamDecrypt(
$input,
$output,
new EncryptionKey($encKey),
$nonce,
$mac,
$config,
$old_macs
);
unset($encKey);
unset($authKey);
unset($nonce);
unset($mac);
unset($config);
unset($old_macs);
return $ret;
}
/**
* Sign the contents of a file
*
* @param $input (file handle)
* @param SignatureSecretKey $secretkey
* @param bool $raw_binary Don't hex encode?
*/
protected static function signResource(
$input,
SignatureSecretKey $secretkey,
bool $raw_binary = false
): string {
return self::signStream(
new ReadOnlyFile($input),
$secretkey,
$raw_binary
);
}
/**
* Sign the contents of a file
*
* @param ReadOnlyFile $input
* @param SignatureSecretKey $secretkey
* @param bool $raw_binary Don't hex encode?
* @return string
* @throws CryptoException\InvalidKey
*/
protected static function signStream(
ReadOnlyFile $input,
SignatureSecretKey $secretkey,
bool $raw_binary = false
): string {
if (!($secretkey instanceof SignatureSecretKey)) {
throw new CryptoException\InvalidKey(
'Argument 1: Expected an instance of SignatureSecretKey'
);
}
$csum = self::checksumStream(
$input,
$secretkey->derivePublicKey(),
true
);
return AsymmetricCrypto::sign($csum, $secretkey, $raw_binary);
}
/**
* Verify the contents of a file
*
* @param $input (file handle)
* @param SignaturePublicKey $publickey
* @param string $signature
* @param bool $raw_binary Don't hex encode?
*
* @return bool
*/
protected static function verifyResource(
$input,
SignaturePublicKey $publickey,
string $signature,
bool $raw_binary = false
): bool {
return self::verifyStream(
new ReadOnlyFile($input),
$publickey,
$signature,
$raw_binary
);
}
/**
* Verify the contents of a file
*
* @param $input (file handle)
* @param SignaturePublicKey $publickey
* @param string $signature
* @param bool $raw_binary Don't hex encode?
*
* @return bool
*/
protected static function verifyStream(
ReadOnlyFile $input,
SignaturePublicKey $publickey,
string $signature,
bool $raw_binary = false
): bool {
$csum = self::checksumStream($input, $publickey, true);
return AsymmetricCrypto::verify(
$csum,
$publickey,
$signature,
$raw_binary
);
}
/**
* Get the configuration
*
* @param string $header
* @param string $mode
* @return Config
* @throws CryptoException\InvalidMessage
*/
protected static function getConfig(
string $header,
string $mode = 'encrypt'
): Config {
if (\ord($header[0]) !== 49 || \ord($header[1]) !== 65) {
throw new CryptoException\InvalidMessage(
'Invalid version tag'
);
}
$major = \ord($header[2]);
$minor = \ord($header[3]);
if ($mode === 'encrypt') {
return new Config(
self::getConfigEncrypt($major, $minor)
);
} elseif ($mode === 'seal') {
return new Config(
self::getConfigSeal($major, $minor)
);
} elseif ($mode === 'checksum') {
return new Config(
self::getConfigChecksum($major, $minor)
);
}
}
/**
* Get the configuration for encrypt operations
*
* @param int $major
* @param int $minor
* @return array
* @throws CryptoException\InvalidMessage
*/
protected static function getConfigEncrypt(int $major, int $minor): array
{
if ($major === 1) {
switch ($minor) {
case 0:
return [
'BUFFER' => 1048576,
'NONCE_BYTES' => \Sodium\CRYPTO_STREAM_NONCEBYTES,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
} elseif ($major === 2) {
switch ($minor) {
case 0:
return [
'BUFFER' => 1048576,
'NONCE_BYTES' => \Sodium\CRYPTO_STREAM_NONCEBYTES,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
}
throw new CryptoException\InvalidMessage(
'Invalid version tag'
);
}
/**
* Get the configuration for seal operations
*
* @param int $major
* @param int $minor
* @return array
* @throws CryptoException\InvalidMessage
*/
protected static function getConfigSeal(int $major, int $minor): array
{
if ($major === 1) {
switch ($minor) {
case 0:
return [
'BUFFER' => 1048576,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'PUBLICKEY_BYTES' => \Sodium\CRYPTO_BOX_PUBLICKEYBYTES,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
} elseif ($major === 2) {
switch ($minor) {
case 0:
return [
'BUFFER' => 1048576,
'HKDF_SALT_LEN' => 32,
'MAC_SIZE' => 32,
'PUBLICKEY_BYTES' => \Sodium\CRYPTO_BOX_PUBLICKEYBYTES,
'HKDF_SBOX' => 'Halite|EncryptionKey',
'HKDF_AUTH' => 'AuthenticationKeyFor_|Halite'
];
}
}
throw new CryptoException\InvalidMessage(
'Invalid version tag'
);
}
/**
* Get the configuration for encrypt operations
*
* @param int $major
* @param int $minor
* @return array
* @throws CryptoException\InvalidMessage
*/
protected static function getConfigChecksum(int $major, int $minor): array
{
if ($major === 1) {
switch ($minor) {
case 0:
return [
'CHECKSUM_PUBKEY' => false,
'BUFFER' => 1048576,
'HASH_LEN' => \Sodium\CRYPTO_GENERICHASH_BYTES_MAX
];
}
} elseif ($major === 2) {
switch ($minor) {
case 0:
return [
'CHECKSUM_PUBKEY' => true,
'BUFFER' => 1048576,
'HASH_LEN' => \Sodium\CRYPTO_GENERICHASH_BYTES_MAX
];
}
}
throw new CryptoException\InvalidMessage(
'Invalid version tag'
);
}
/**
* Split a key using HKDF
*
* @param KeyInterface $master
* @param string $salt
* @param Config $config
* @return string[]
*/
protected static function splitKeys(
KeyInterface $master,
string $salt = '',
Config $config = null
): array {
$binary = $master->get();
return [
Util::hkdfBlake2b(
$binary,
\Sodium\CRYPTO_SECRETBOX_KEYBYTES,
$config->HKDF_SBOX,
$salt
),
Util::hkdfBlake2b(
$binary,
\Sodium\CRYPTO_AUTH_KEYBYTES,
$config->HKDF_AUTH,
$salt
)
];
}
/**
* Stream encryption - Do not call directly
*
* @param ReadOnlyFile $input
* @param MutableFile $output
* @param EncryptionKey $encKey
* @param string $nonce
* @param resource $mac (hash context)
* @param Config $config
* @return int
* @throws CryptoException\AccessDenied
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidKey
*/
final private static function streamEncrypt(
ReadOnlyFile $input,
MutableFile $output,
EncryptionKey $encKey,
string $nonce,
$mac,
Config $config
): int {
$initHash = $input->getHash();
// Begin the streaming decryption
$size = $input->getSize();
while ($input->remainingBytes() > 0) {
$read = $input->readBytes(
($input->getPos() + $config->BUFFER) > $size
? ($size - $input->getPos())
: $config->BUFFER
);
$encrypted = \Sodium\crypto_stream_xor(
$read,
$nonce,
$encKey->get()
);
\hash_update($mac, $encrypted);
$output->writeBytes($encrypted);
\Sodium\increment($nonce);
}
\Sodium\memzero($nonce);
// Check that our input file was not modified before we MAC it
if (!\hash_equals($input->gethash(), $initHash)) {
throw new CryptoException\FileModified(
'Read-only file has been modified since it was opened for reading'
);
}
return $output->writeBytes(
\hash_final($mac, 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
* @return bool
* @throws CryptoException\AccessDenied
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\FileModified
* @throws CryptoException\InvalidKey
* @throws CryptoException\InvalidMessage
*/
final private static 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) {
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;
}
/**
* Recalculate and verify the HMAC of the input file
*
* @param resource $input
* @param resource $mac (hash context)
* @param Config $config
*
* @return array Hashes of various chunks
* @throws CryptoException\CannotPerformOperation
* @throws CryptoException\InvalidMessage
*/
final private static function streamVerify(
ReadOnlyFile $input,
$mac,
Config $config
): array {
$start = $input->getPos();
$cipher_end = $input->getSize() - $config->MAC_SIZE;
$input->reset($cipher_end);
$stored_mac = $input->readBytes($config->MAC_SIZE);
$input->reset($start);
$chunk_macs = [];
$break = false;
while (!$break && $input->getPos() < $cipher_end) {
/**
* 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) {
$break = true;
$read = $input->readBytes($cipher_end - $input->getPos());
} else {
$read = $input->readBytes($config->BUFFER);
}
/**
* We're updating our HMAC and nothing else
*/
\hash_update($mac, $read);
/**
* Store a MAC of each chunk
*/
$chunkMAC = \hash_copy($mac);
if ($chunkMAC === false) {
throw new CryptoException\CannotPerformOperation(
'An unknown error has occurred'
);
}
$chunk_macs []= \hash_final($chunkMAC, true);
}
/**
* We should now have enough data to generate an identical HMAC
*/
$finalHMAC = \hash_final($mac, true);
/**
* Use hash_equals() to be timing-invariant
*/
if (!\hash_equals($finalHMAC, $stored_mac)) {
throw new CryptoException\InvalidMessage(
'Invalid message authentication code'
);
}
$input->reset($start);
return $chunk_macs;
}
}
|