installed plugin Infinite Uploads version 2.0.8

This commit is contained in:
2025-05-02 12:03:21 +00:00
committed by Gitium
parent 7ca941b591
commit 8fefb19ab4
1179 changed files with 99739 additions and 0 deletions

View File

@ -0,0 +1,102 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\Cbc;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream;
/**
* Legacy abstract encryption client. New workflows should use
* AbstractCryptoClientV2.
*
* @deprecated
* @internal
*/
abstract class AbstractCryptoClient
{
public static $supportedCiphers = ['cbc', 'gcm'];
public static $supportedKeyWraps = [\UglyRobot\Infinite_Uploads\Aws\Crypto\KmsMaterialsProvider::WRAP_ALGORITHM_NAME];
/**
* Returns if the passed cipher name is supported for encryption by the SDK.
*
* @param string $cipherName The name of a cipher to verify is registered.
*
* @return bool If the cipher passed is in our supported list.
*/
public static function isSupportedCipher($cipherName)
{
return in_array($cipherName, self::$supportedCiphers);
}
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @param string $cipherName Name of the cipher being used for encrypting
* or decrypting.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return string
*/
protected abstract function getCipherOpenSslName($cipherName, $keySize);
/**
* Constructs a CipherMethod for the given name, initialized with the other
* data passed for use in encrypting or decrypting.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return CipherMethod
*
* @internal
*/
protected abstract function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
protected abstract function getCipherFromAesName($aesName);
/**
* Dependency to provide an interface for building an encryption stream for
* data given cipher details, metadata, and materials to do so.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
* @param MaterialsProvider $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be added to.
*
* @return AesStreamInterface
*
* @internal
*/
public abstract function encrypt(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream $plaintext, array $cipherOptions, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProvider $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope);
/**
* Dependency to provide an interface for building a decryption stream for
* cipher text given metadata and materials to do so.
*
* @param string $cipherText Plain-text data to be decrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterface $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
* @param array $cipherOptions Additional verification options.
*
* @return AesStreamInterface
*
* @internal
*/
public abstract function decrypt($cipherText, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterface $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope, array $cipherOptions = []);
}

View File

@ -0,0 +1,98 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream;
/**
* @internal
*/
abstract class AbstractCryptoClientV2
{
public static $supportedCiphers = ['gcm'];
public static $supportedKeyWraps = [\UglyRobot\Infinite_Uploads\Aws\Crypto\KmsMaterialsProviderV2::WRAP_ALGORITHM_NAME];
public static $supportedSecurityProfiles = ['V2', 'V2_AND_LEGACY'];
public static $legacySecurityProfiles = ['V2_AND_LEGACY'];
/**
* Returns if the passed cipher name is supported for encryption by the SDK.
*
* @param string $cipherName The name of a cipher to verify is registered.
*
* @return bool If the cipher passed is in our supported list.
*/
public static function isSupportedCipher($cipherName)
{
return in_array($cipherName, self::$supportedCiphers, true);
}
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-gcm`
*
* @param string $cipherName Name of the cipher being used for encrypting
* or decrypting.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return string
*/
protected abstract function getCipherOpenSslName($cipherName, $keySize);
/**
* Constructs a CipherMethod for the given name, initialized with the other
* data passed for use in encrypting or decrypting.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return CipherMethod
*
* @internal
*/
protected abstract function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
protected abstract function getCipherFromAesName($aesName);
/**
* Dependency to provide an interface for building an encryption stream for
* data given cipher details, metadata, and materials to do so.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param array $options Options for use in encryption.
* @param MaterialsProviderV2 $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be added to.
*
* @return AesStreamInterface
*
* @internal
*/
public abstract function encrypt(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream $plaintext, array $options, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderV2 $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope);
/**
* Dependency to provide an interface for building a decryption stream for
* cipher text given metadata and materials to do so.
*
* @param string $cipherText Plain-text data to be decrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterface $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
* @param array $options Options used for decryption.
*
* @return AesStreamInterface
*
* @internal
*/
public abstract function decrypt($cipherText, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterfaceV2 $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope, array $options = []);
}

View File

@ -0,0 +1,108 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\StreamDecoratorTrait;
use LogicException;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod;
/**
* @internal Represents a stream of data to be decrypted with passed cipher.
*/
class AesDecryptingStream implements \UglyRobot\Infinite_Uploads\Aws\Crypto\AesStreamInterface
{
const BLOCK_SIZE = 16;
// 128 bits
use StreamDecoratorTrait;
/**
* @var string
*/
private $buffer = '';
/**
* @var CipherMethod
*/
private $cipherMethod;
/**
* @var string
*/
private $key;
/**
* @var StreamInterface
*/
private $stream;
/**
* @param StreamInterface $cipherText
* @param string $key
* @param CipherMethod $cipherMethod
*/
public function __construct(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $cipherText, $key, \UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod $cipherMethod)
{
$this->stream = $cipherText;
$this->key = $key;
$this->cipherMethod = clone $cipherMethod;
}
public function getOpenSslName()
{
return $this->cipherMethod->getOpenSslName();
}
public function getAesName()
{
return $this->cipherMethod->getAesName();
}
public function getCurrentIv()
{
return $this->cipherMethod->getCurrentIv();
}
public function getSize()
{
$plainTextSize = $this->stream->getSize();
if ($this->cipherMethod->requiresPadding()) {
// PKCS7 padding requires that between 1 and self::BLOCK_SIZE be
// added to the plaintext to make it an even number of blocks. The
// plaintext is between strlen($cipherText) - self::BLOCK_SIZE and
// strlen($cipherText) - 1
return null;
}
return $plainTextSize;
}
public function isWritable()
{
return false;
}
public function read($length)
{
if ($length > strlen($this->buffer)) {
$this->buffer .= $this->decryptBlock(self::BLOCK_SIZE * ceil(($length - strlen($this->buffer)) / self::BLOCK_SIZE));
}
$data = substr($this->buffer, 0, $length);
$this->buffer = substr($this->buffer, $length);
return $data ? $data : '';
}
public function seek($offset, $whence = SEEK_SET)
{
if ($offset === 0 && $whence === SEEK_SET) {
$this->buffer = '';
$this->cipherMethod->seek(0, SEEK_SET);
$this->stream->seek(0, SEEK_SET);
} else {
throw new \LogicException('AES encryption streams only support being' . ' rewound, not arbitrary seeking.');
}
}
private function decryptBlock($length)
{
if ($this->stream->eof()) {
return '';
}
$cipherText = '';
do {
$cipherText .= $this->stream->read($length - strlen($cipherText));
} while (strlen($cipherText) < $length && !$this->stream->eof());
$options = OPENSSL_RAW_DATA;
if (!$this->stream->eof() && $this->stream->getSize() !== $this->stream->tell()) {
$options |= OPENSSL_ZERO_PADDING;
}
$plaintext = openssl_decrypt($cipherText, $this->cipherMethod->getOpenSslName(), $this->key, $options, $this->cipherMethod->getCurrentIv());
$this->cipherMethod->update($cipherText);
return $plaintext;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\StreamDecoratorTrait;
use LogicException;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod;
/**
* @internal Represents a stream of data to be encrypted with a passed cipher.
*/
class AesEncryptingStream implements \UglyRobot\Infinite_Uploads\Aws\Crypto\AesStreamInterface
{
const BLOCK_SIZE = 16;
// 128 bits
use StreamDecoratorTrait;
/**
* @var string
*/
private $buffer = '';
/**
* @var CipherMethod
*/
private $cipherMethod;
/**
* @var string
*/
private $key;
/**
* @var StreamInterface
*/
private $stream;
/**
* @param StreamInterface $plainText
* @param string $key
* @param CipherMethod $cipherMethod
*/
public function __construct(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $plainText, $key, \UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod $cipherMethod)
{
$this->stream = $plainText;
$this->key = $key;
$this->cipherMethod = clone $cipherMethod;
}
public function getOpenSslName()
{
return $this->cipherMethod->getOpenSslName();
}
public function getAesName()
{
return $this->cipherMethod->getAesName();
}
public function getCurrentIv()
{
return $this->cipherMethod->getCurrentIv();
}
public function getSize()
{
$plainTextSize = $this->stream->getSize();
if ($this->cipherMethod->requiresPadding() && $plainTextSize !== null) {
// PKCS7 padding requires that between 1 and self::BLOCK_SIZE be
// added to the plaintext to make it an even number of blocks.
$padding = self::BLOCK_SIZE - $plainTextSize % self::BLOCK_SIZE;
return $plainTextSize + $padding;
}
return $plainTextSize;
}
public function isWritable()
{
return false;
}
public function read($length)
{
if ($length > strlen($this->buffer)) {
$this->buffer .= $this->encryptBlock(self::BLOCK_SIZE * ceil(($length - strlen($this->buffer)) / self::BLOCK_SIZE));
}
$data = substr($this->buffer, 0, $length);
$this->buffer = substr($this->buffer, $length);
return $data ? $data : '';
}
public function seek($offset, $whence = SEEK_SET)
{
if ($whence === SEEK_CUR) {
$offset = $this->tell() + $offset;
$whence = SEEK_SET;
}
if ($whence === SEEK_SET) {
$this->buffer = '';
$wholeBlockOffset = (int) ($offset / self::BLOCK_SIZE) * self::BLOCK_SIZE;
$this->stream->seek($wholeBlockOffset);
$this->cipherMethod->seek($wholeBlockOffset);
$this->read($offset - $wholeBlockOffset);
} else {
throw new \LogicException('Unrecognized whence.');
}
}
private function encryptBlock($length)
{
if ($this->stream->eof()) {
return '';
}
$plainText = '';
do {
$plainText .= $this->stream->read($length - strlen($plainText));
} while (strlen($plainText) < $length && !$this->stream->eof());
$options = OPENSSL_RAW_DATA;
if (!$this->stream->eof() || $this->stream->getSize() !== $this->stream->tell()) {
$options |= OPENSSL_ZERO_PADDING;
}
$cipherText = openssl_encrypt($plainText, $this->cipherMethod->getOpenSslName(), $this->key, $options, $this->cipherMethod->getCurrentIv());
$this->cipherMethod->update($cipherText);
return $cipherText;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\StreamDecoratorTrait;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\AesGcm;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key;
/**
* @internal Represents a stream of data to be gcm decrypted.
*/
class AesGcmDecryptingStream implements \UglyRobot\Infinite_Uploads\Aws\Crypto\AesStreamInterface
{
use StreamDecoratorTrait;
private $aad;
private $initializationVector;
private $key;
private $keySize;
private $cipherText;
private $tag;
private $tagLength;
/**
* @param StreamInterface $cipherText
* @param string $key
* @param string $initializationVector
* @param string $tag
* @param string $aad
* @param int $tagLength
* @param int $keySize
*/
public function __construct(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $cipherText, $key, $initializationVector, $tag, $aad = '', $tagLength = 128, $keySize = 256)
{
$this->cipherText = $cipherText;
$this->key = $key;
$this->initializationVector = $initializationVector;
$this->tag = $tag;
$this->aad = $aad;
$this->tagLength = $tagLength;
$this->keySize = $keySize;
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-gcm";
}
public function getAesName()
{
return 'AES/GCM/NoPadding';
}
public function getCurrentIv()
{
return $this->initializationVector;
}
public function createStream()
{
if (version_compare(PHP_VERSION, '7.1', '<')) {
return \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\AesGcm::decrypt((string) $this->cipherText, $this->initializationVector, new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key($this->key), $this->aad, $this->tag, $this->keySize));
} else {
$result = \openssl_decrypt((string) $this->cipherText, $this->getOpenSslName(), $this->key, OPENSSL_RAW_DATA, $this->initializationVector, $this->tag, $this->aad);
if ($result === false) {
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException('The requested object could not be' . ' decrypted due to an invalid authentication tag.');
}
return \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for($result);
}
}
public function isWritable()
{
return false;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\AesGcm;
use UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\StreamDecoratorTrait;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* @internal Represents a stream of data to be gcm encrypted.
*/
class AesGcmEncryptingStream implements \UglyRobot\Infinite_Uploads\Aws\Crypto\AesStreamInterface, \UglyRobot\Infinite_Uploads\Aws\Crypto\AesStreamInterfaceV2
{
use StreamDecoratorTrait;
private $aad;
private $initializationVector;
private $key;
private $keySize;
private $plaintext;
private $tag = '';
private $tagLength;
/**
* Same as non-static 'getAesName' method, allowing calls in a static
* context.
*
* @return string
*/
public static function getStaticAesName()
{
return 'AES/GCM/NoPadding';
}
/**
* @param StreamInterface $plaintext
* @param string $key
* @param string $initializationVector
* @param string $aad
* @param int $tagLength
* @param int $keySize
*/
public function __construct(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $plaintext, $key, $initializationVector, $aad = '', $tagLength = 16, $keySize = 256)
{
$this->plaintext = $plaintext;
$this->key = $key;
$this->initializationVector = $initializationVector;
$this->aad = $aad;
$this->tagLength = $tagLength;
$this->keySize = $keySize;
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-gcm";
}
/**
* Same as static method and retained for backwards compatibility
*
* @return string
*/
public function getAesName()
{
return self::getStaticAesName();
}
public function getCurrentIv()
{
return $this->initializationVector;
}
public function createStream()
{
if (version_compare(PHP_VERSION, '7.1', '<')) {
return \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\AesGcm::encrypt((string) $this->plaintext, $this->initializationVector, new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key($this->key), $this->aad, $this->tag, $this->keySize));
} else {
return \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for(\openssl_encrypt((string) $this->plaintext, $this->getOpenSslName(), $this->key, OPENSSL_RAW_DATA, $this->initializationVector, $this->tag, $this->aad, $this->tagLength));
}
}
/**
* @return string
*/
public function getTag()
{
return $this->tag;
}
public function isWritable()
{
return false;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
interface AesStreamInterface extends StreamInterface
{
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @return string
*/
public function getOpenSslName();
/**
* Returns an AES recognizable name, such as 'AES/GCM/NoPadding'.
*
* @return string
*/
public function getAesName();
/**
* Returns the IV that should be used to initialize the next block in
* encrypt or decrypt.
*
* @return string
*/
public function getCurrentIv();
}

View File

@ -0,0 +1,29 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
interface AesStreamInterfaceV2 extends StreamInterface
{
/**
* Returns an AES recognizable name, such as 'AES/GCM/NoPadding'. V2
* interface is accessible from a static context.
*
* @return string
*/
public static function getStaticAesName();
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @return string
*/
public function getOpenSslName();
/**
* Returns the IV that should be used to initialize the next block in
* encrypt or decrypt.
*
* @return string
*/
public function getCurrentIv();
}

View File

@ -0,0 +1,76 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher;
use InvalidArgumentException;
use LogicException;
/**
* An implementation of the CBC cipher for use with an AesEncryptingStream or
* AesDecrypting stream.
*
* This cipher method is deprecated and in maintenance mode - no new updates will be
* released. Please see https://docs.aws.amazon.com/general/latest/gr/aws_sdk_cryptography.html
* for more information.
*
* @deprecated
*/
class Cbc implements \UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\CipherMethod
{
const BLOCK_SIZE = 16;
/**
* @var string
*/
private $baseIv;
/**
* @var string
*/
private $iv;
/**
* @var int
*/
private $keySize;
/**
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @throws InvalidArgumentException Thrown if the passed iv does not match
* the iv length required by the cipher.
*/
public function __construct($iv, $keySize = 256)
{
$this->baseIv = $this->iv = $iv;
$this->keySize = $keySize;
if (strlen($iv) !== openssl_cipher_iv_length($this->getOpenSslName())) {
throw new \InvalidArgumentException('Invalid initialization vector');
}
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-cbc";
}
public function getAesName()
{
return 'AES/CBC/PKCS5Padding';
}
public function getCurrentIv()
{
return $this->iv;
}
public function requiresPadding()
{
return true;
}
public function seek($offset, $whence = SEEK_SET)
{
if ($offset === 0 && $whence === SEEK_SET) {
$this->iv = $this->baseIv;
} else {
throw new \LogicException('CBC initialization only support being' . ' rewound, not arbitrary seeking.');
}
}
public function update($cipherTextBlock)
{
$this->iv = substr($cipherTextBlock, self::BLOCK_SIZE * -1);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher;
use UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException;
trait CipherBuilderTrait
{
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @param string $cipherName Name of the cipher being used for encrypting
* or decrypting.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return string
*/
protected function getCipherOpenSslName($cipherName, $keySize)
{
return "aes-{$keySize}-{$cipherName}";
}
/**
* Constructs a CipherMethod for the given name, initialized with the other
* data passed for use in encrypting or decrypting.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return CipherMethod
*
* @internal
*/
protected function buildCipherMethod($cipherName, $iv, $keySize)
{
switch ($cipherName) {
case 'cbc':
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher\Cbc($iv, $keySize);
default:
return null;
}
}
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
protected function getCipherFromAesName($aesName)
{
switch ($aesName) {
case 'AES/GCM/NoPadding':
return 'gcm';
case 'AES/CBC/PKCS5Padding':
return 'cbc';
default:
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException('Unrecognized or unsupported' . ' AESName for reverse lookup.');
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Cipher;
interface CipherMethod
{
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @return string
*/
public function getOpenSslName();
/**
* Returns an AES recognizable name, such as 'AES/GCM/NoPadding'.
*
* @return string
*/
public function getAesName();
/**
* Returns the IV that should be used to initialize the next block in
* encrypt or decrypt.
*
* @return string
*/
public function getCurrentIv();
/**
* Indicates whether the cipher method used with this IV requires padding
* the final block to make sure the plaintext is evenly divisible by the
* block size.
*
* @return boolean
*/
public function requiresPadding();
/**
* Adjust the return of this::getCurrentIv to reflect a seek performed on
* the encryption stream using this IV object.
*
* @param int $offset
* @param int $whence
*
* @throws LogicException Thrown if the requested seek is not supported by
* this IV implementation. For example, a CBC IV
* only supports a full rewind ($offset === 0 &&
* $whence === SEEK_SET)
*/
public function seek($offset, $whence = SEEK_SET);
/**
* Take account of the last cipher text block to adjust the return of
* this::getCurrentIv
*
* @param string $cipherTextBlock
*/
public function update($cipherTextBlock);
}

View File

@ -0,0 +1,109 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\LimitStream;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
trait DecryptionTrait
{
/**
* Dependency to reverse lookup the openssl_* cipher name from the AESName
* in the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
protected abstract function getCipherFromAesName($aesName);
/**
* Dependency to generate a CipherMethod from a set of inputs for loading
* in to an AesDecryptingStream.
*
* @param string $cipherName Name of the cipher to generate for decrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return Cipher\CipherMethod
*
* @internal
*/
protected abstract function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Builds an AesStreamInterface using cipher options loaded from the
* MetadataEnvelope and MaterialsProvider. Can decrypt data from both the
* legacy and V2 encryption client workflows.
*
* @param string $cipherText Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterface $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
* @param array $cipherOptions Additional verification options.
*
* @return AesStreamInterface
*
* @throws \InvalidArgumentException Thrown when a value in $cipherOptions
* is not valid.
*
* @internal
*/
public function decrypt($cipherText, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterface $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope, array $cipherOptions = [])
{
$cipherOptions['Iv'] = base64_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::IV_HEADER]);
$cipherOptions['TagLength'] = $envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
$cek = $provider->decryptCek(base64_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_KEY_V2_HEADER]), json_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], true));
$cipherOptions['KeySize'] = strlen($cek) * 8;
$cipherOptions['Cipher'] = $this->getCipherFromAesName($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]);
$decryptionStream = $this->getDecryptingStream($cipherText, $cek, $cipherOptions);
unset($cek);
return $decryptionStream;
}
private function getTagFromCiphertextStream(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $cipherText, $tagLength)
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown' . ' size.');
}
return (string) new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\LimitStream($cipherText, $tagLength, $cipherTextSize - $tagLength);
}
private function getStrippedCiphertextStream(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $cipherText, $tagLength)
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown' . ' size.');
}
return new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\LimitStream($cipherText, $cipherTextSize - $tagLength, 0);
}
/**
* Generates a stream that wraps the cipher text with the proper cipher and
* uses the content encryption key (CEK) to decrypt the data when read.
*
* @param string $cipherText Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param string $cek A content encryption key for use by the stream for
* encrypting the plaintext data.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
*
* @return AesStreamInterface
*
* @internal
*/
protected function getDecryptingStream($cipherText, $cek, $cipherOptions)
{
$cipherTextStream = \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream($cipherTextStream, $cipherOptions['TagLength']);
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\AesGcmDecryptingStream($this->getStrippedCiphertextStream($cipherTextStream, $cipherOptions['TagLength']), $cek, $cipherOptions['Iv'], $cipherOptions['Tag'], $cipherOptions['Aad'] = isset($cipherOptions['Aad']) ? $cipherOptions['Aad'] : null, $cipherOptions['TagLength'] ?: null, $cipherOptions['KeySize']);
default:
$cipherMethod = $this->buildCipherMethod($cipherOptions['Cipher'], $cipherOptions['Iv'], $cipherOptions['KeySize']);
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\AesDecryptingStream($cipherTextStream, $cek, $cipherMethod);
}
}
}

View File

@ -0,0 +1,138 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\LimitStream;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
trait DecryptionTraitV2
{
/**
* Dependency to reverse lookup the openssl_* cipher name from the AESName
* in the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
protected abstract function getCipherFromAesName($aesName);
/**
* Dependency to generate a CipherMethod from a set of inputs for loading
* in to an AesDecryptingStream.
*
* @param string $cipherName Name of the cipher to generate for decrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return Cipher\CipherMethod
*
* @internal
*/
protected abstract function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Builds an AesStreamInterface using cipher options loaded from the
* MetadataEnvelope and MaterialsProvider. Can decrypt data from both the
* legacy and V2 encryption client workflows.
*
* @param string $cipherText Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterfaceV2 $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
* @param array $options Options used for decryption.
*
* @return AesStreamInterface
*
* @throws \InvalidArgumentException Thrown when a value in $cipherOptions
* is not valid.
*
* @internal
*/
public function decrypt($cipherText, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterfaceV2 $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope, array $options = [])
{
$options['@CipherOptions'] = !empty($options['@CipherOptions']) ? $options['@CipherOptions'] : [];
$options['@CipherOptions']['Iv'] = base64_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::IV_HEADER]);
$options['@CipherOptions']['TagLength'] = $envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
$cek = $provider->decryptCek(base64_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_KEY_V2_HEADER]), json_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], true), $options);
$options['@CipherOptions']['KeySize'] = strlen($cek) * 8;
$options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]);
$this->validateOptionsAndEnvelope($options, $envelope);
$decryptionStream = $this->getDecryptingStream($cipherText, $cek, $options['@CipherOptions']);
unset($cek);
return $decryptionStream;
}
private function getTagFromCiphertextStream(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $cipherText, $tagLength)
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown' . ' size.');
}
return (string) new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\LimitStream($cipherText, $tagLength, $cipherTextSize - $tagLength);
}
private function getStrippedCiphertextStream(\UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface $cipherText, $tagLength)
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown' . ' size.');
}
return new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\LimitStream($cipherText, $cipherTextSize - $tagLength, 0);
}
private function validateOptionsAndEnvelope($options, $envelope)
{
$allowedCiphers = AbstractCryptoClientV2::$supportedCiphers;
$allowedKeywraps = AbstractCryptoClientV2::$supportedKeyWraps;
if ($options['@SecurityProfile'] == 'V2_AND_LEGACY') {
$allowedCiphers = array_unique(array_merge($allowedCiphers, AbstractCryptoClient::$supportedCiphers));
$allowedKeywraps = array_unique(array_merge($allowedKeywraps, AbstractCryptoClient::$supportedKeyWraps));
}
$v1SchemaException = new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException("The requested object is encrypted" . " with V1 encryption schemas that have been disabled by" . " client configuration @SecurityProfile=V2. Retry with" . " V2_AND_LEGACY enabled or reencrypt the object.");
if (!in_array($options['@CipherOptions']['Cipher'], $allowedCiphers)) {
if (in_array($options['@CipherOptions']['Cipher'], AbstractCryptoClient::$supportedCiphers)) {
throw $v1SchemaException;
}
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException("The requested object is encrypted with" . " the cipher '{$options['@CipherOptions']['Cipher']}', which is not" . " supported for decryption with the selected security profile." . " This profile allows decryption with: " . implode(", ", $allowedCiphers));
}
if (!in_array($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], $allowedKeywraps)) {
if (in_array($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], AbstractCryptoClient::$supportedKeyWraps)) {
throw $v1SchemaException;
}
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException("The requested object is encrypted with" . " the keywrap schema '{$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER]}'," . " which is not supported for decryption with the current security" . " profile.");
}
$matdesc = json_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], true);
if (isset($matdesc['aws:x-amz-cek-alg']) && $envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] !== $matdesc['aws:x-amz-cek-alg']) {
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException("There is a mismatch in specified content" . " encryption algrithm between the materials description value" . " and the metadata envelope value: {$matdesc['aws:x-amz-cek-alg']}" . " vs. {$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]}.");
}
}
/**
* Generates a stream that wraps the cipher text with the proper cipher and
* uses the content encryption key (CEK) to decrypt the data when read.
*
* @param string $cipherText Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param string $cek A content encryption key for use by the stream for
* encrypting the plaintext data.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
*
* @return AesStreamInterface
*
* @internal
*/
protected function getDecryptingStream($cipherText, $cek, $cipherOptions)
{
$cipherTextStream = \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream($cipherTextStream, $cipherOptions['TagLength']);
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\AesGcmDecryptingStream($this->getStrippedCiphertextStream($cipherTextStream, $cipherOptions['TagLength']), $cek, $cipherOptions['Iv'], $cipherOptions['Tag'], $cipherOptions['Aad'] = isset($cipherOptions['Aad']) ? $cipherOptions['Aad'] : null, $cipherOptions['TagLength'] ?: null, $cipherOptions['KeySize']);
default:
$cipherMethod = $this->buildCipherMethod($cipherOptions['Cipher'], $cipherOptions['Iv'], $cipherOptions['KeySize']);
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\AesDecryptingStream($cipherTextStream, $cek, $cipherMethod);
}
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\AppendStream;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream;
trait EncryptionTrait
{
private static $allowedOptions = ['Cipher' => true, 'KeySize' => true, 'Aad' => true];
/**
* Dependency to generate a CipherMethod from a set of inputs for loading
* in to an AesEncryptingStream.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return Cipher\CipherMethod
*
* @internal
*/
protected abstract function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Builds an AesStreamInterface and populates encryption metadata into the
* supplied envelope.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
* @param MaterialsProvider $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be added to.
*
* @return AesStreamInterface
*
* @throws \InvalidArgumentException Thrown when a value in $cipherOptions
* is not valid.
*
* @internal
*/
public function encrypt(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream $plaintext, array $cipherOptions, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProvider $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope)
{
$materialsDescription = $provider->getMaterialsDescription();
$cipherOptions = array_intersect_key($cipherOptions, self::$allowedOptions);
if (empty($cipherOptions['Cipher'])) {
throw new \InvalidArgumentException('An encryption cipher must be' . ' specified in the "cipher_options".');
}
if (!self::isSupportedCipher($cipherOptions['Cipher'])) {
throw new \InvalidArgumentException('The cipher requested is not' . ' supported by the SDK.');
}
if (empty($cipherOptions['KeySize'])) {
$cipherOptions['KeySize'] = 256;
}
if (!is_int($cipherOptions['KeySize'])) {
throw new \InvalidArgumentException('The cipher "KeySize" must be' . ' an integer.');
}
if (!\UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProvider::isSupportedKeySize($cipherOptions['KeySize'])) {
throw new \InvalidArgumentException('The cipher "KeySize" requested' . ' is not supported by AES (128, 192, or 256).');
}
$cipherOptions['Iv'] = $provider->generateIv($this->getCipherOpenSslName($cipherOptions['Cipher'], $cipherOptions['KeySize']));
$cek = $provider->generateCek($cipherOptions['KeySize']);
list($encryptingStream, $aesName) = $this->getEncryptingStream($plaintext, $cek, $cipherOptions);
// Populate envelope data
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_KEY_V2_HEADER] = $provider->encryptCek($cek, $materialsDescription);
unset($cek);
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::IV_HEADER] = base64_encode($cipherOptions['Iv']);
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] = $provider->getWrapAlgorithmName();
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName;
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] = strlen($plaintext);
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] = json_encode($materialsDescription);
if (!empty($cipherOptions['Tag'])) {
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] = strlen($cipherOptions['Tag']) * 8;
}
return $encryptingStream;
}
/**
* Generates a stream that wraps the plaintext with the proper cipher and
* uses the content encryption key (CEK) to encrypt the data when read.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param string $cek A content encryption key for use by the stream for
* encrypting the plaintext data.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
*
* @return [AesStreamInterface, string]
*
* @internal
*/
protected function getEncryptingStream(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream $plaintext, $cek, &$cipherOptions)
{
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['TagLength'] = 16;
$cipherTextStream = new \UglyRobot\Infinite_Uploads\Aws\Crypto\AesGcmEncryptingStream($plaintext, $cek, $cipherOptions['Iv'], $cipherOptions['Aad'] = isset($cipherOptions['Aad']) ? $cipherOptions['Aad'] : null, $cipherOptions['TagLength'], $cipherOptions['KeySize']);
if (!empty($cipherOptions['Aad'])) {
trigger_error("'Aad' has been supplied for content encryption" . " with " . $cipherTextStream->getAesName() . ". The" . " PHP SDK encryption client can decrypt an object" . " encrypted in this way, but other AWS SDKs may not be" . " able to.", E_USER_WARNING);
}
$appendStream = new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\AppendStream([$cipherTextStream->createStream()]);
$cipherOptions['Tag'] = $cipherTextStream->getTag();
$appendStream->addStream(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for($cipherOptions['Tag']));
return [$appendStream, $cipherTextStream->getAesName()];
default:
$cipherMethod = $this->buildCipherMethod($cipherOptions['Cipher'], $cipherOptions['Iv'], $cipherOptions['KeySize']);
$cipherTextStream = new \UglyRobot\Infinite_Uploads\Aws\Crypto\AesEncryptingStream($plaintext, $cek, $cipherMethod);
return [$cipherTextStream, $cipherTextStream->getAesName()];
}
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\AppendStream;
use UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream;
use UglyRobot\Infinite_Uploads\Psr\Http\Message\StreamInterface;
trait EncryptionTraitV2
{
private static $allowedOptions = ['Cipher' => true, 'KeySize' => true, 'Aad' => true];
private static $encryptClasses = ['gcm' => \UglyRobot\Infinite_Uploads\Aws\Crypto\AesGcmEncryptingStream::class];
/**
* Dependency to generate a CipherMethod from a set of inputs for loading
* in to an AesEncryptingStream.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return Cipher\CipherMethod
*
* @internal
*/
protected abstract function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Builds an AesStreamInterface and populates encryption metadata into the
* supplied envelope.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param array $options Options for use in encryption, including cipher
* options, and encryption context.
* @param MaterialsProviderV2 $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be added to.
*
* @return StreamInterface
*
* @throws \InvalidArgumentException Thrown when a value in $options['@CipherOptions']
* is not valid.
*s
* @internal
*/
public function encrypt(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream $plaintext, array $options, \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderV2 $provider, \UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope)
{
$options = array_change_key_case($options);
$cipherOptions = array_intersect_key($options['@cipheroptions'], self::$allowedOptions);
if (empty($cipherOptions['Cipher'])) {
throw new \InvalidArgumentException('An encryption cipher must be' . ' specified in @CipherOptions["Cipher"].');
}
$cipherOptions['Cipher'] = strtolower($cipherOptions['Cipher']);
if (!self::isSupportedCipher($cipherOptions['Cipher'])) {
throw new \InvalidArgumentException('The cipher requested is not' . ' supported by the SDK.');
}
if (empty($cipherOptions['KeySize'])) {
$cipherOptions['KeySize'] = 256;
}
if (!is_int($cipherOptions['KeySize'])) {
throw new \InvalidArgumentException('The cipher "KeySize" must be' . ' an integer.');
}
if (!\UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderV2::isSupportedKeySize($cipherOptions['KeySize'])) {
throw new \InvalidArgumentException('The cipher "KeySize" requested' . ' is not supported by AES (128 or 256).');
}
$cipherOptions['Iv'] = $provider->generateIv($this->getCipherOpenSslName($cipherOptions['Cipher'], $cipherOptions['KeySize']));
$encryptClass = self::$encryptClasses[$cipherOptions['Cipher']];
$aesName = $encryptClass::getStaticAesName();
$materialsDescription = ['aws:x-amz-cek-alg' => $aesName];
$keys = $provider->generateCek($cipherOptions['KeySize'], $materialsDescription, $options);
// Some providers modify materials description based on options
if (isset($keys['UpdatedContext'])) {
$materialsDescription = $keys['UpdatedContext'];
}
$encryptingStream = $this->getEncryptingStream($plaintext, $keys['Plaintext'], $cipherOptions);
// Populate envelope data
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_KEY_V2_HEADER] = $keys['Ciphertext'];
unset($keys);
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::IV_HEADER] = base64_encode($cipherOptions['Iv']);
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] = $provider->getWrapAlgorithmName();
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName;
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] = strlen($plaintext);
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] = json_encode($materialsDescription);
if (!empty($cipherOptions['Tag'])) {
$envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] = strlen($cipherOptions['Tag']) * 8;
}
return $encryptingStream;
}
/**
* Generates a stream that wraps the plaintext with the proper cipher and
* uses the content encryption key (CEK) to encrypt the data when read.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param string $cek A content encryption key for use by the stream for
* encrypting the plaintext data.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
*
* @return [AesStreamInterface, string]
*
* @internal
*/
protected function getEncryptingStream(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Stream $plaintext, $cek, &$cipherOptions)
{
switch ($cipherOptions['Cipher']) {
// Only 'gcm' is supported for encryption currently
case 'gcm':
$cipherOptions['TagLength'] = 16;
$encryptClass = self::$encryptClasses['gcm'];
$cipherTextStream = new $encryptClass($plaintext, $cek, $cipherOptions['Iv'], $cipherOptions['Aad'] = isset($cipherOptions['Aad']) ? $cipherOptions['Aad'] : '', $cipherOptions['TagLength'], $cipherOptions['KeySize']);
if (!empty($cipherOptions['Aad'])) {
trigger_error("'Aad' has been supplied for content encryption" . " with " . $cipherTextStream->getAesName() . ". The" . " PHP SDK encryption client can decrypt an object" . " encrypted in this way, but other AWS SDKs may not be" . " able to.", E_USER_WARNING);
}
$appendStream = new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\AppendStream([$cipherTextStream->createStream()]);
$cipherOptions['Tag'] = $cipherTextStream->getTag();
$appendStream->addStream(\UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\stream_for($cipherOptions['Tag']));
return $appendStream;
}
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Kms\KmsClient;
/**
* Uses KMS to supply materials for encrypting and decrypting data.
*
* Legacy implementation that supports legacy S3EncryptionClient and
* S3EncryptionMultipartUploader, which use an older encryption workflow. Use
* KmsMaterialsProviderV2 with S3EncryptionClientV2 or
* S3EncryptionMultipartUploaderV2 if possible.
*
* @deprecated
*/
class KmsMaterialsProvider extends \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProvider implements \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterface
{
const WRAP_ALGORITHM_NAME = 'kms';
private $kmsClient;
private $kmsKeyId;
/**
* @param KmsClient $kmsClient A KMS Client for use encrypting and
* decrypting keys.
* @param string $kmsKeyId The private KMS key id to be used for encrypting
* and decrypting keys.
*/
public function __construct(\UglyRobot\Infinite_Uploads\Aws\Kms\KmsClient $kmsClient, $kmsKeyId = null)
{
$this->kmsClient = $kmsClient;
$this->kmsKeyId = $kmsKeyId;
}
public function fromDecryptionEnvelope(\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope)
{
if (empty($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER])) {
throw new \RuntimeException('Not able to detect the materials description.');
}
$materialsDescription = json_decode($envelope[\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], true);
if (empty($materialsDescription['kms_cmk_id']) && empty($materialsDescription['aws:x-amz-cek-alg'])) {
throw new \RuntimeException('Not able to detect kms_cmk_id (legacy' . ' implementation) or aws:x-amz-cek-alg (current implementation)' . ' from kms materials description.');
}
return new self($this->kmsClient, isset($materialsDescription['kms_cmk_id']) ? $materialsDescription['kms_cmk_id'] : null);
}
/**
* The KMS key id for use in matching this Provider to its keys,
* consistently with other SDKs as 'kms_cmk_id'.
*
* @return array
*/
public function getMaterialsDescription()
{
return ['kms_cmk_id' => $this->kmsKeyId];
}
public function getWrapAlgorithmName()
{
return self::WRAP_ALGORITHM_NAME;
}
/**
* Takes a content encryption key (CEK) and description to return an encrypted
* key by using KMS' Encrypt API.
*
* @param string $unencryptedCek Key for use in encrypting other data
* that itself needs to be encrypted by the
* Provider.
* @param string $materialDescription Material Description for use in
* encrypting the $cek.
*
* @return string
*/
public function encryptCek($unencryptedCek, $materialDescription)
{
$encryptedDataKey = $this->kmsClient->encrypt(['Plaintext' => $unencryptedCek, 'KeyId' => $this->kmsKeyId, 'EncryptionContext' => $materialDescription]);
return base64_encode($encryptedDataKey['CiphertextBlob']);
}
/**
* Takes an encrypted content encryption key (CEK) and material description
* for use decrypting the key by using KMS' Decrypt API.
*
* @param string $encryptedCek Encrypted key to be decrypted by the Provider
* for use decrypting other data.
* @param string $materialDescription Material Description for use in
* encrypting the $cek.
*
* @return string
*/
public function decryptCek($encryptedCek, $materialDescription)
{
$result = $this->kmsClient->decrypt(['CiphertextBlob' => $encryptedCek, 'EncryptionContext' => $materialDescription]);
return $result['Plaintext'];
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException;
use UglyRobot\Infinite_Uploads\Aws\Kms\KmsClient;
/**
* Uses KMS to supply materials for encrypting and decrypting data. This
* V2 implementation should be used with the V2 encryption clients (i.e.
* S3EncryptionClientV2).
*/
class KmsMaterialsProviderV2 extends \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderV2 implements \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterfaceV2
{
const WRAP_ALGORITHM_NAME = 'kms+context';
private $kmsClient;
private $kmsKeyId;
/**
* @param KmsClient $kmsClient A KMS Client for use encrypting and
* decrypting keys.
* @param string $kmsKeyId The private KMS key id to be used for encrypting
* and decrypting keys.
*/
public function __construct(\UglyRobot\Infinite_Uploads\Aws\Kms\KmsClient $kmsClient, $kmsKeyId = null)
{
$this->kmsClient = $kmsClient;
$this->kmsKeyId = $kmsKeyId;
}
/**
* @inheritDoc
*/
public function getWrapAlgorithmName()
{
return self::WRAP_ALGORITHM_NAME;
}
/**
* @inheritDoc
*/
public function decryptCek($encryptedCek, $materialDescription, $options)
{
$params = ['CiphertextBlob' => $encryptedCek, 'EncryptionContext' => $materialDescription];
if (empty($options['@KmsAllowDecryptWithAnyCmk'])) {
if (empty($this->kmsKeyId)) {
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException('KMS CMK ID was not specified and the' . ' operation is not opted-in to attempting to use any valid' . ' CMK it discovers. Please specify a CMK ID, or explicitly' . ' enable attempts to use any valid KMS CMK with the' . ' @KmsAllowDecryptWithAnyCmk option.');
}
$params['KeyId'] = $this->kmsKeyId;
}
$result = $this->kmsClient->decrypt($params);
return $result['Plaintext'];
}
/**
* @inheritDoc
*/
public function generateCek($keySize, $context, $options)
{
if (empty($this->kmsKeyId)) {
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException('A KMS key id is required for encryption' . ' with KMS keywrap. Use a KmsMaterialsProviderV2 that has been' . ' instantiated with a KMS key id.');
}
$options = array_change_key_case($options);
if (!isset($options['@kmsencryptioncontext']) || !is_array($options['@kmsencryptioncontext'])) {
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException("'@KmsEncryptionContext' is a" . " required argument when using KmsMaterialsProviderV2, and" . " must be an associative array (or empty array).");
}
if (isset($options['@kmsencryptioncontext']['aws:x-amz-cek-alg'])) {
throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoException("Conflict in reserved @KmsEncryptionContext" . " key aws:x-amz-cek-alg. This value is reserved for the S3" . " Encryption Client and cannot be set by the user.");
}
$context = array_merge($options['@kmsencryptioncontext'], $context);
$result = $this->kmsClient->generateDataKey(['KeyId' => $this->kmsKeyId, 'KeySpec' => "AES_{$keySize}", 'EncryptionContext' => $context]);
return ['Plaintext' => $result['Plaintext'], 'Ciphertext' => base64_encode($result['CiphertextBlob']), 'UpdatedContext' => $context];
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
abstract class MaterialsProvider implements \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterface
{
private static $supportedKeySizes = [128 => true, 192 => true, 256 => true];
/**
* Returns if the requested size is supported by AES.
*
* @param int $keySize Size of the requested key in bits.
*
* @return bool
*/
public static function isSupportedKeySize($keySize)
{
return isset(self::$supportedKeySizes[$keySize]);
}
/**
* Performs further initialization of the MaterialsProvider based on the
* data inside the MetadataEnvelope.
*
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
*
* @return MaterialsProvider
*
* @throws \RuntimeException Thrown when there is an empty or improperly
* formed materials description in the envelope.
*
* @internal
*/
public abstract function fromDecryptionEnvelope(\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope);
/**
* Returns the material description for this Provider so it can be verified
* by encryption mechanisms.
*
* @return string
*/
public abstract function getMaterialsDescription();
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
public abstract function getWrapAlgorithmName();
/**
* Takes a content encryption key (CEK) and description to return an
* encrypted key according to the Provider's specifications.
*
* @param string $unencryptedCek Key for use in encrypting other data
* that itself needs to be encrypted by the
* Provider.
* @param string $materialDescription Material Description for use in
* encrypting the $cek.
*
* @return string
*/
public abstract function encryptCek($unencryptedCek, $materialDescription);
/**
* Takes an encrypted content encryption key (CEK) and material description
* for use decrypting the key according to the Provider's specifications.
*
* @param string $encryptedCek Encrypted key to be decrypted by the Provider
* for use decrypting other data.
* @param string $materialDescription Material Description for use in
* encrypting the $cek.
*
* @return string
*/
public abstract function decryptCek($encryptedCek, $materialDescription);
/**
* @param string $keySize Length of a cipher key in bits for generating a
* random content encryption key (CEK).
*
* @return string
*/
public function generateCek($keySize)
{
return openssl_random_pseudo_bytes($keySize / 8);
}
/**
* @param string $openSslName Cipher OpenSSL name to use for generating
* an initialization vector.
*
* @return string
*/
public function generateIv($openSslName)
{
return openssl_random_pseudo_bytes(openssl_cipher_iv_length($openSslName));
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
interface MaterialsProviderInterface
{
/**
* Returns if the requested size is supported by AES.
*
* @param int $keySize Size of the requested key in bits.
*
* @return bool
*/
public static function isSupportedKeySize($keySize);
/**
* Performs further initialization of the MaterialsProvider based on the
* data inside the MetadataEnvelope.
*
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
*
* @internal
*/
public function fromDecryptionEnvelope(\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope);
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
public function getWrapAlgorithmName();
/**
* Takes an encrypted content encryption key (CEK) and material description
* for use decrypting the key according to the Provider's specifications.
*
* @param string $encryptedCek Encrypted key to be decrypted by the Provider
* for use decrypting other data.
* @param string $materialDescription Material Description for use in
* encrypting the $cek.
*
* @return string
*/
public function decryptCek($encryptedCek, $materialDescription);
/**
* @param string $keySize Length of a cipher key in bits for generating a
* random content encryption key (CEK).
*
* @return string
*/
public function generateCek($keySize);
/**
* @param string $openSslName Cipher OpenSSL name to use for generating
* an initialization vector.
*
* @return string
*/
public function generateIv($openSslName);
}

View File

@ -0,0 +1,50 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
interface MaterialsProviderInterfaceV2
{
/**
* Returns if the requested size is supported by AES.
*
* @param int $keySize Size of the requested key in bits.
*
* @return bool
*/
public static function isSupportedKeySize($keySize);
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
public function getWrapAlgorithmName();
/**
* Takes an encrypted content encryption key (CEK) and material description
* for use decrypting the key according to the Provider's specifications.
*
* @param string $encryptedCek Encrypted key to be decrypted by the Provider
* for use decrypting other data.
* @param string $materialDescription Material Description for use in
* decrypting the CEK.
* @param array $options Options for use in decrypting the CEK.
*
* @return string
*/
public function decryptCek($encryptedCek, $materialDescription, $options);
/**
* @param string $keySize Length of a cipher key in bits for generating a
* random content encryption key (CEK).
* @param array $context Context map needed for key encryption
* @param array $options Additional options to be used in CEK generation
*
* @return array
*/
public function generateCek($keySize, $context, $options);
/**
* @param string $openSslName Cipher OpenSSL name to use for generating
* an initialization vector.
*
* @return string
*/
public function generateIv($openSslName);
}

View File

@ -0,0 +1,57 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
abstract class MaterialsProviderV2 implements \UglyRobot\Infinite_Uploads\Aws\Crypto\MaterialsProviderInterfaceV2
{
private static $supportedKeySizes = [128 => true, 256 => true];
/**
* Returns if the requested size is supported by AES.
*
* @param int $keySize Size of the requested key in bits.
*
* @return bool
*/
public static function isSupportedKeySize($keySize)
{
return isset(self::$supportedKeySizes[$keySize]);
}
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
public abstract function getWrapAlgorithmName();
/**
* Takes an encrypted content encryption key (CEK) and material description
* for use decrypting the key according to the Provider's specifications.
*
* @param string $encryptedCek Encrypted key to be decrypted by the Provider
* for use decrypting other data.
* @param string $materialDescription Material Description for use in
* decrypting the CEK.
* @param string $options Options for use in decrypting the CEK.
*
* @return string
*/
public abstract function decryptCek($encryptedCek, $materialDescription, $options);
/**
* @param string $keySize Length of a cipher key in bits for generating a
* random content encryption key (CEK).
* @param array $context Context map needed for key encryption
* @param array $options Additional options to be used in CEK generation
*
* @return array
*/
public abstract function generateCek($keySize, $context, $options);
/**
* @param string $openSslName Cipher OpenSSL name to use for generating
* an initialization vector.
*
* @return string
*/
public function generateIv($openSslName)
{
return openssl_random_pseudo_bytes(openssl_cipher_iv_length($openSslName));
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
use UglyRobot\Infinite_Uploads\Aws\HasDataTrait;
use ArrayAccess;
use IteratorAggregate;
use InvalidArgumentException;
use JsonSerializable;
/**
* Stores encryption metadata for reading and writing.
*
* @internal
*/
class MetadataEnvelope implements \ArrayAccess, \IteratorAggregate, \JsonSerializable
{
use HasDataTrait;
const CONTENT_KEY_V2_HEADER = 'x-amz-key-v2';
const IV_HEADER = 'x-amz-iv';
const MATERIALS_DESCRIPTION_HEADER = 'x-amz-matdesc';
const KEY_WRAP_ALGORITHM_HEADER = 'x-amz-wrap-alg';
const CONTENT_CRYPTO_SCHEME_HEADER = 'x-amz-cek-alg';
const CRYPTO_TAG_LENGTH_HEADER = 'x-amz-tag-len';
const UNENCRYPTED_CONTENT_LENGTH_HEADER = 'x-amz-unencrypted-content-length';
private static $constants = [];
public static function getConstantValues()
{
if (empty(self::$constants)) {
$reflection = new \ReflectionClass(static::class);
foreach (array_values($reflection->getConstants()) as $constant) {
self::$constants[$constant] = true;
}
}
return array_keys(self::$constants);
}
public function offsetSet($name, $value)
{
$constants = self::getConstantValues();
if (is_null($name) || !in_array($name, $constants)) {
throw new \InvalidArgumentException('MetadataEnvelope fields must' . ' must match a predefined offset; use the header constants.');
}
$this->data[$name] = $value;
}
public function jsonSerialize()
{
return $this->data;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto;
interface MetadataStrategyInterface
{
/**
* Places the information in the MetadataEnvelope to the strategy specific
* location. Populates the PutObject arguments with any information
* necessary for loading.
*
* @param MetadataEnvelope $envelope Encryption data to save according to
* the strategy.
* @param array $args Starting arguments for PutObject.
*
* @return array Updated arguments for PutObject.
*/
public function save(\UglyRobot\Infinite_Uploads\Aws\Crypto\MetadataEnvelope $envelope, array $args);
/**
* Generates a MetadataEnvelope according to the specific strategy using the
* passed arguments.
*
* @param array $args Arguments from Command and Result that contains
* S3 Object information, relevant headers, and command
* configuration.
*
* @return MetadataEnvelope
*/
public function load(array $args);
}

View File

@ -0,0 +1,163 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill;
use UglyRobot\Infinite_Uploads\Aws\Exception\CryptoPolyfillException;
use InvalidArgumentException;
use RangeException;
/**
* Class AesGcm
*
* This provides a polyfill for AES-GCM encryption/decryption, with caveats:
*
* 1. Only 96-bit nonces are supported.
* 2. Only 128-bit authentication tags are supported. (i.e. non-truncated)
*
* Supports AES key sizes of 128-bit, 192-bit, and 256-bit.
*
* @package Aws\Crypto\Polyfill
*/
class AesGcm
{
use NeedsTrait;
/** @var Key $aesKey */
private $aesKey;
/** @var int $keySize */
private $keySize;
/** @var int $blockSize */
protected $blockSize = 8192;
/**
* AesGcm constructor.
*
* @param Key $aesKey
* @param int $keySize
* @param int $blockSize
*
* @throws CryptoPolyfillException
* @throws InvalidArgumentException
* @throws RangeException
*/
public function __construct(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key $aesKey, $keySize = 256, $blockSize = 8192)
{
/* Preconditions: */
self::needs(\in_array($keySize, [128, 192, 256], true), "Key size must be 128, 192, or 256 bits; {$keySize} given", \InvalidArgumentException::class);
self::needs(\is_int($blockSize) && $blockSize > 0 && $blockSize <= PHP_INT_MAX, 'Block size must be a positive integer.', \RangeException::class);
self::needs($aesKey->length() << 3 === $keySize, 'Incorrect key size; expected ' . $keySize . ' bits, got ' . ($aesKey->length() << 3) . ' bits.');
$this->aesKey = $aesKey;
$this->keySize = $keySize;
}
/**
* Encryption interface for AES-GCM
*
* @param string $plaintext Message to be encrypted
* @param string $nonce Number to be used ONCE
* @param Key $key AES Key
* @param string $aad Additional authenticated data
* @param string &$tag Reference to variable to hold tag
* @param int $keySize Key size (bits)
* @param int $blockSize Block size (bytes) -- How much memory to buffer
* @return string
* @throws InvalidArgumentException
*/
public static function encrypt($plaintext, $nonce, \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key $key, $aad, &$tag, $keySize = 256, $blockSize = 8192)
{
self::needs(self::strlen($nonce) === 12, 'Nonce must be exactly 12 bytes', \InvalidArgumentException::class);
$encryptor = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\AesGcm($key, $keySize, $blockSize);
list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
$ciphertext = \openssl_encrypt($plaintext, "aes-{$encryptor->keySize}-ctr", $key->get(), OPENSSL_NO_PADDING | OPENSSL_RAW_DATA, $nonce . "\0\0\0\2");
/* Calculate auth tag in a streaming fashion to minimize memory usage: */
$ciphertextLength = self::strlen($ciphertext);
for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
$cBlock = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
$gmac->update($cBlock);
}
$tag = $gmac->finish($aadLength, $ciphertextLength)->toString();
return $ciphertext;
}
/**
* Decryption interface for AES-GCM
*
* @param string $ciphertext Ciphertext to decrypt
* @param string $nonce Number to be used ONCE
* @param Key $key AES key
* @param string $aad Additional authenticated data
* @param string $tag Authentication tag
* @param int $keySize Key size (bits)
* @param int $blockSize Block size (bytes) -- How much memory to buffer
* @return string Plaintext
*
* @throws CryptoPolyfillException
* @throws InvalidArgumentException
*/
public static function decrypt($ciphertext, $nonce, \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key $key, $aad, &$tag, $keySize = 256, $blockSize = 8192)
{
/* Precondition: */
self::needs(self::strlen($nonce) === 12, 'Nonce must be exactly 12 bytes', \InvalidArgumentException::class);
$encryptor = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\AesGcm($key, $keySize, $blockSize);
list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
/* Calculate auth tag in a streaming fashion to minimize memory usage: */
$ciphertextLength = self::strlen($ciphertext);
for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
$cBlock = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
$gmac->update($cBlock);
}
/* Validate auth tag in constant-time: */
$calc = $gmac->finish($aadLength, $ciphertextLength);
$expected = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray($tag);
self::needs($calc->equals($expected), 'Invalid authentication tag');
/* Return plaintext if auth tag check succeeded: */
return \openssl_decrypt($ciphertext, "aes-{$encryptor->keySize}-ctr", $key->get(), OPENSSL_NO_PADDING | OPENSSL_RAW_DATA, $nonce . "\0\0\0\2");
}
/**
* Initialize a Gmac object with the nonce and this object's key.
*
* @param string $nonce Must be exactly 12 bytes long.
* @param string|null $aad
* @return array
*/
protected function gmacInit($nonce, $aad = null)
{
$gmac = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Gmac($this->aesKey, $nonce . "\0\0\0\1", $this->keySize);
$aadBlock = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray($aad);
$aadLength = $aadBlock->count();
$gmac->update($aadBlock);
$gmac->flush();
return [$aadLength, $gmac];
}
/**
* Calculate the length of a string.
*
* Uses the appropriate PHP function without being brittle to
* mbstring.func_overload.
*
* @param string $string
* @return int
*/
protected static function strlen($string)
{
if (\is_callable('\\mb_strlen')) {
return (int) \mb_strlen($string, '8bit');
}
return (int) \strlen($string);
}
/**
* Return a substring of the provided string.
*
* Uses the appropriate PHP function without being brittle to
* mbstring.func_overload.
*
* @param string $string
* @param int $offset
* @param int|null $length
* @return string
*/
protected static function substr($string, $offset = 0, $length = null)
{
if (\is_callable('\\mb_substr')) {
return \mb_substr($string, $offset, $length, '8bit');
} elseif (!\is_null($length)) {
return \substr($string, $offset, $length);
}
return \substr($string, $offset);
}
}

View File

@ -0,0 +1,234 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill;
/**
* Class ByteArray
* @package Aws\Crypto\Polyfill
*/
class ByteArray extends \SplFixedArray
{
use NeedsTrait;
/**
* ByteArray constructor.
*
* @param int|string|int[] $size
* If you pass in an integer, it creates a ByteArray of that size.
* If you pass in a string or array, it converts it to an array of
* integers between 0 and 255.
* @throws \InvalidArgumentException
*/
public function __construct($size = 0)
{
$arr = null;
// Integer? This behaves just like SplFixedArray.
if (\is_array($size)) {
// Array? We need to pass the count to parent::__construct() then populate
$arr = $size;
$size = \count($arr);
} elseif (\is_string($size)) {
// We need to avoid mbstring.func_overload
if (\is_callable('\\mb_str_split')) {
$tmp = \mb_str_split($size, 1, '8bit');
} else {
$tmp = \str_split($size, 1);
}
// Let's convert each character to an 8-bit integer and store in $arr
$arr = [];
if (!empty($tmp)) {
foreach ($tmp as $t) {
if (strlen($t) < 1) {
continue;
}
$arr[] = \unpack('C', $t)[1] & 0xff;
}
}
$size = \count($arr);
} elseif ($size instanceof ByteArray) {
$arr = $size->toArray();
$size = $size->count();
} elseif (!\is_int($size)) {
throw new \InvalidArgumentException('Argument must be an integer, string, or array of integers.');
}
parent::__construct($size);
if (!empty($arr)) {
// Populate this object with values from constructor argument
foreach ($arr as $i => $v) {
$this->offsetSet($i, $v);
}
} else {
// Initialize to zero.
for ($i = 0; $i < $size; ++$i) {
$this->offsetSet($i, 0);
}
}
}
/**
* Encode an integer into a byte array. 32-bit (unsigned), big endian byte order.
*
* @param int $num
* @return self
*/
public static function enc32be($num)
{
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(\pack('N', $num));
}
/**
* @param ByteArray $other
* @return bool
*/
public function equals(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $other)
{
if ($this->count() !== $other->count()) {
return false;
}
$d = 0;
for ($i = $this->count() - 1; $i >= 0; --$i) {
$d |= $this[$i] ^ $other[$i];
}
return $d === 0;
}
/**
* @param ByteArray $array
* @return ByteArray
*/
public function exclusiveOr(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $array)
{
self::needs($this->count() === $array->count(), 'Both ByteArrays must be equal size for exclusiveOr()');
$out = clone $this;
for ($i = 0; $i < $this->count(); ++$i) {
$out[$i] = $array[$i] ^ $out[$i];
}
return $out;
}
/**
* Returns a new ByteArray incremented by 1 (big endian byte order).
*
* @param int $increase
* @return self
*/
public function getIncremented($increase = 1)
{
$clone = clone $this;
$index = $clone->count();
while ($index > 0) {
--$index;
$tmp = $clone[$index] + $increase & PHP_INT_MAX;
$clone[$index] = $tmp & 0xff;
$increase = $tmp >> 8;
}
return $clone;
}
/**
* Sets a value. See SplFixedArray for more.
*
* @param int $index
* @param int $newval
* @return void
*/
public function offsetSet($index, $newval)
{
parent::offsetSet($index, $newval & 0xff);
}
/**
* Return a copy of this ByteArray, bitshifted to the right by 1.
* Used in Gmac.
*
* @return self
*/
public function rshift()
{
$out = clone $this;
for ($j = $this->count() - 1; $j > 0; --$j) {
$out[$j] = ($out[$j - 1] & 1) << 7 | $out[$j] >> 1;
}
$out[0] >>= 1;
return $out;
}
/**
* Constant-time conditional select. This is meant to read like a ternary operator.
*
* $z = ByteArray::select(1, $x, $y); // $z is equal to $x
* $z = ByteArray::select(0, $x, $y); // $z is equal to $y
*
* @param int $select
* @param ByteArray $left
* @param ByteArray $right
* @return ByteArray
*/
public static function select($select, \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $left, \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $right)
{
self::needs($left->count() === $right->count(), 'Both ByteArrays must be equal size for select()');
$rightLength = $right->count();
$out = clone $right;
$mask = -($select & 1) & 0xff;
for ($i = 0; $i < $rightLength; $i++) {
$out[$i] = $out[$i] ^ ($left[$i] ^ $right[$i]) & $mask;
}
return $out;
}
/**
* Overwrite values of this ByteArray based on a separate ByteArray, with
* a given starting offset and length.
*
* See JavaScript's Uint8Array.set() for more information.
*
* @param ByteArray $input
* @param int $offset
* @param int|null $length
* @return self
*/
public function set(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $input, $offset = 0, $length = null)
{
self::needs(is_int($offset) && $offset >= 0, 'Offset must be a positive integer or zero');
if (is_null($length)) {
$length = $input->count();
}
$i = 0;
$j = $offset;
while ($i < $length && $j < $this->count()) {
$this[$j] = $input[$i];
++$i;
++$j;
}
return $this;
}
/**
* Returns a slice of this ByteArray.
*
* @param int $start
* @param null $length
* @return self
*/
public function slice($start = 0, $length = null)
{
return new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(\array_slice($this->toArray(), $start, $length));
}
/**
* Mutates the current state and sets all values to zero.
*
* @return void
*/
public function zeroize()
{
for ($i = $this->count() - 1; $i >= 0; --$i) {
$this->offsetSet($i, 0);
}
}
/**
* Converts the ByteArray to a raw binary string.
*
* @return string
*/
public function toString()
{
$count = $this->count();
if ($count === 0) {
return '';
}
$args = $this->toArray();
\array_unshift($args, \str_repeat('C', $count));
// constant-time, PHP <5.6 equivalent to pack('C*', ...$args);
return \call_user_func_array('\\pack', $args);
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill;
/**
* Class Gmac
*
* @package Aws\Crypto\Polyfill
*/
class Gmac
{
use NeedsTrait;
const BLOCK_SIZE = 16;
/** @var ByteArray $buf */
protected $buf;
/** @var int $bufLength */
protected $bufLength = 0;
/** @var ByteArray $h */
protected $h;
/** @var ByteArray $hf */
protected $hf;
/** @var Key $key */
protected $key;
/** @var ByteArray $x */
protected $x;
/**
* Gmac constructor.
*
* @param Key $aesKey
* @param string $nonce
* @param int $keySize
*/
public function __construct(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\Key $aesKey, $nonce, $keySize = 256)
{
$this->buf = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(16);
$this->h = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(\openssl_encrypt(\str_repeat("\0", 16), "aes-{$keySize}-ecb", $aesKey->get(), OPENSSL_RAW_DATA | OPENSSL_NO_PADDING));
$this->key = $aesKey;
$this->x = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(16);
$this->hf = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(\openssl_encrypt($nonce, "aes-{$keySize}-ecb", $aesKey->get(), OPENSSL_RAW_DATA | OPENSSL_NO_PADDING));
}
/**
* Update the object with some data.
*
* This method mutates this Gmac object.
*
* @param ByteArray $blocks
* @return self
*/
public function update(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $blocks)
{
if ($blocks->count() + $this->bufLength < self::BLOCK_SIZE) {
// Write to internal buffer until we reach enough to write.
$this->buf->set($blocks, $this->bufLength);
$this->bufLength += $blocks->count();
return $this;
}
// Process internal buffer first.
if ($this->bufLength > 0) {
// 0 <= state.buf_len < BLOCK_SIZE is an invariant
$tmp = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(self::BLOCK_SIZE);
$tmp->set($this->buf->slice(0, $this->bufLength));
$remainingBlockLength = self::BLOCK_SIZE - $this->bufLength;
$tmp->set($blocks->slice(0, $remainingBlockLength), $this->bufLength);
$blocks = $blocks->slice($remainingBlockLength);
$this->bufLength = 0;
$this->x = $this->blockMultiply($this->x->exclusiveOr($tmp), $this->h);
}
// Process full blocks.
$numBlocks = $blocks->count() >> 4;
for ($i = 0; $i < $numBlocks; ++$i) {
$tmp = $blocks->slice($i << 4, self::BLOCK_SIZE);
$this->x = $this->blockMultiply($this->x->exclusiveOr($tmp), $this->h);
}
$last = $numBlocks << 4;
// Zero-fill buffer
for ($i = 0; $i < 16; ++$i) {
$this->buf[$i] = 0;
}
// Feed leftover into buffer.
if ($last < $blocks->count()) {
$tmp = $blocks->slice($last);
$this->buf->set($tmp);
$this->bufLength += $blocks->count() - $last;
}
return $this;
}
/**
* Finish processing the authentication tag.
*
* This method mutates this Gmac object (effectively resetting it).
*
* @param int $aadLength
* @param int $ciphertextLength
* @return ByteArray
*/
public function finish($aadLength, $ciphertextLength)
{
$lengthBlock = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(16);
$state = $this->flush();
// AES-GCM expects bit lengths, not byte lengths.
$lengthBlock->set(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray::enc32be($aadLength >> 29), 0);
$lengthBlock->set(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray::enc32be($aadLength << 3), 4);
$lengthBlock->set(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray::enc32be($ciphertextLength >> 29), 8);
$lengthBlock->set(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray::enc32be($ciphertextLength << 3), 12);
$state->update($lengthBlock);
$output = $state->x->exclusiveOr($state->hf);
// Zeroize the internal values as a best-effort.
$state->buf->zeroize();
$state->x->zeroize();
$state->h->zeroize();
$state->hf->zeroize();
return $output;
}
/**
* Get a specific bit from the provided array, at the given index.
*
* [01234567], 8+[01234567], 16+[01234567], ...
*
* @param ByteArray $x
* @param int $i
* @return int
*/
protected function bit(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $x, $i)
{
$byte = $i >> 3;
return $x[$byte] >> (7 - $i & 7) & 1;
}
/**
* Galois Field Multiplication
*
* This function is the critical path that must be constant-time in order to
* avoid timing side-channels against AES-GCM.
*
* The contents of each are always calculated, regardless of the branching
* condition, to prevent another kind of timing leak.
*
* @param ByteArray $x
* @param ByteArray $y
* @return ByteArray
*/
protected function blockMultiply(\UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $x, \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray $y)
{
static $fieldPolynomial = null;
if (!$fieldPolynomial) {
$fieldPolynomial = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray([0xe1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
}
self::needs($x->count() === 16, 'Argument 1 must be a ByteArray of exactly 16 bytes');
self::needs($y->count() === 16, 'Argument 2 must be a ByteArray of exactly 16 bytes');
$v = clone $y;
$z = new \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray(16);
for ($i = 0; $i < 128; ++$i) {
// if ($b) $z = $z->exclusiveOr($v);
$b = $this->bit($x, $i);
$z = \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray::select($b, $z->exclusiveOr($v), $z);
// if ($b) $v = $v->exclusiveOr($fieldPolynomial);
$b = $v[15] & 1;
$v = $v->rshift();
$v = \UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill\ByteArray::select($b, $v->exclusiveOr($fieldPolynomial), $v);
}
return $z;
}
/**
* Finish processing any leftover bytes in the internal buffer.
*
* @return self
*/
public function flush()
{
if ($this->bufLength !== 0) {
$this->x = $this->blockMultiply($this->x->exclusiveOr($this->buf), $this->h);
$this->bufLength = 0;
}
return $this;
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill;
/**
* Class Key
*
* Wraps a string to keep it hidden from stack traces.
*
* @package Aws\Crypto\Polyfill
*/
class Key
{
/**
* @var string $internalString
*/
private $internalString;
/**
* Hide contents of
*
* @return array
*/
public function __debugInfo()
{
return [];
}
/**
* Key constructor.
* @param string $str
*/
public function __construct($str)
{
$this->internalString = $str;
}
/**
* Defense in depth:
*
* PHP 7.2 includes the Sodium cryptography library, which (among other things)
* exposes a function called sodium_memzero() that we can use to zero-fill strings
* to minimize the risk of sensitive cryptographic materials persisting in memory.
*
* If this function is not available, we XOR the string in-place with itself as a
* best-effort attempt.
*/
public function __destruct()
{
if (extension_loaded('sodium') && function_exists('sodium_memzero')) {
try {
\sodium_memzero($this->internalString);
} catch (\SodiumException $ex) {
// This is a best effort, but does not provide the same guarantees as sodium_memzero():
$this->internalString ^= $this->internalString;
}
}
}
/**
* @return string
*/
public function get()
{
return $this->internalString;
}
/**
* @return int
*/
public function length()
{
if (\is_callable('\\mb_strlen')) {
return (int) \mb_strlen($this->internalString, '8bit');
}
return (int) \strlen($this->internalString);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace UglyRobot\Infinite_Uploads\Aws\Crypto\Polyfill;
use UglyRobot\Infinite_Uploads\Aws\Exception\CryptoPolyfillException;
/**
* Trait NeedsTrait
* @package Aws\Crypto\Polyfill
*/
trait NeedsTrait
{
/**
* Preconditions, postconditions, and loop invariants are very
* useful for safe programing. They also document the specifications.
* This function is to help simplify the semantic burden of parsing
* these constructions.
*
* Instead of constructions like
* if (!(GOOD CONDITION)) {
* throw new \Exception('condition not true');
* }
*
* you can write:
* needs(GOOD CONDITION, 'condition not true');
* @param $condition
* @param $errorMessage
* @param null $exceptionClass
*/
public static function needs($condition, $errorMessage, $exceptionClass = null)
{
if (!$condition) {
if (!$exceptionClass) {
$exceptionClass = \UglyRobot\Infinite_Uploads\Aws\Exception\CryptoPolyfillException::class;
}
throw new $exceptionClass($errorMessage);
}
}
}