allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

View File

@@ -0,0 +1,121 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use Aws\Crypto\Cipher\Cbc;
use 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 = [
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
*/
abstract protected 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
*/
abstract protected 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
*/
abstract protected 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
*/
abstract public function encrypt(
Stream $plaintext,
array $cipherOptions,
MaterialsProvider $provider,
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
*/
abstract public function decrypt(
$cipherText,
MaterialsProviderInterface $provider,
MetadataEnvelope $envelope,
array $cipherOptions = []
);
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use GuzzleHttp\Psr7\Stream;
/**
* @internal
*/
abstract class AbstractCryptoClientV2
{
const KEY_COMMITMENT_POLICIES = [
'FORBID_ENCRYPT_ALLOW_DECRYPT'
];
public static $supportedCiphers = ['gcm'];
public static $supportedKeyWraps = [
KmsMaterialsProviderV2::WRAP_ALGORITHM_NAME
];
public static $supportedSecurityProfiles = ['V2', 'V2_AND_LEGACY'];
public static $legacySecurityProfiles = ['V2_AND_LEGACY'];
/**
* Returns if the passed policy name is supported for encryption by the SDK.
*
* @param string $policy The name of a key commitment policy to verify is registered.
*
* @return bool If the key commitment policy passed is in our supported list.
*/
public static function isSupportedKeyCommitmentPolicy(string $policy): bool
{
return in_array($policy, self::KEY_COMMITMENT_POLICIES, strict: true);
}
/**
* 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
*/
abstract protected 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
*/
abstract protected 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
*/
abstract protected 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
*/
abstract public function encrypt(
Stream $plaintext,
array $options,
MaterialsProviderV2 $provider,
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
*/
abstract public function decrypt(
$cipherText,
MaterialsProviderInterfaceV2 $provider,
MetadataEnvelope $envelope,
array $options = []
);
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use GuzzleHttp\Psr7\AppendStream;
use GuzzleHttp\Psr7\Stream;
/**
* @internal
*/
abstract class AbstractCryptoClientV3
{
const SUPPORTED_SECURITY_PROFILES = ['V3', 'V3_AND_LEGACY'];
const LEGACY_SECURITY_PROFILES = ['V3_AND_LEGACY'];
const KEY_COMMITMENT_POLICIES = [
'FORBID_ENCRYPT_ALLOW_DECRYPT',
'REQUIRE_ENCRYPT_ALLOW_DECRYPT',
'REQUIRE_ENCRYPT_REQUIRE_DECRYPT'
];
public static array $supportedCiphers = ['gcm'];
public static array $supportedKeyWraps = [
KmsMaterialsProviderV3::WRAP_ALGORITHM_NAME
];
/**
* Returns if the passed policy name is supported for encryption by the SDK.
*
* @param string $policy The name of a key commitment policy to verify is registered.
*
* @return bool If the key commitment policy passed is in our supported list.
*/
public static function isSupportedKeyCommitmentPolicy(string $policy): bool
{
return in_array($policy, AbstractCryptoClientV3::KEY_COMMITMENT_POLICIES, strict: true);
}
/**
* 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(string $cipherName): bool
{
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
*/
abstract protected 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
*/
abstract protected function buildCipherMethod(
$cipherName,
$iv,
$keySize
);
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param string $aesName
*
* @return string
*
* @internal
*/
abstract protected 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 AlgorithmSuite $algorithmSuite AlgorithmSuite for use in encryption.
* @param array $options Options for use in encryption, including cipher
* options, and encryption context.
* @param MaterialsProviderV3 $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 AppendStream
*
* @internal
*/
abstract public function encrypt(
Stream $plaintext,
AlgorithmSuite $algorithmSuite,
array $options,
MaterialsProviderV3 $provider,
MetadataEnvelope $envelope
): AppendStream;
/**
* 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 string $commitmentPolicy Commitment Policy to use for decrypting objects.
* @param array $options Options used for decryption.
*
* @return AesStreamInterface
*
* @internal
*/
abstract public function decrypt(
string $cipherText,
MaterialsProviderInterfaceV3 $provider,
MetadataEnvelope $envelope,
string $commitmentPolicy,
array $options = []
): AesStreamInterface;
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use \LogicException;
use Psr\Http\Message\StreamInterface;
use Aws\Crypto\Cipher\CipherMethod;
/**
* @internal Represents a stream of data to be decrypted with passed cipher.
*/
class AesDecryptingStream implements 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(
StreamInterface $cipherText,
$key,
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(): ?int
{
$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(): bool
{
return false;
}
public function read($length): string
{
if ($length > strlen($this->buffer)) {
$this->buffer .= $this->decryptBlock(
(int) (
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): void
{
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((int) ($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,151 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use \LogicException;
use Psr\Http\Message\StreamInterface;
use Aws\Crypto\Cipher\CipherMethod;
/**
* @internal Represents a stream of data to be encrypted with a passed cipher.
*/
class AesEncryptingStream implements 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(
StreamInterface $plainText,
$key,
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(): ?int
{
$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(): bool
{
return false;
}
public function read($length): string
{
if ($length > strlen($this->buffer)) {
$this->buffer .= $this->encryptBlock(
(int)
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): void
{
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((int) ($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,104 @@
<?php
namespace Aws\Crypto;
use Aws\Exception\CryptoException;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* @internal Represents a stream of data to be gcm decrypted.
*/
class AesGcmDecryptingStream implements AesStreamInterface
{
use StreamDecoratorTrait;
private $aad;
private $initializationVector;
private $key;
private $keySize;
private $cipherText;
private $tag;
private $tagLength;
/**
* @var StreamInterface
*/
private $stream;
/**
* @param StreamInterface $cipherText
* @param string $key
* @param string $initializationVector
* @param string $tag
* @param string $aad
* @param int $tagLength
* @param int $keySize
*/
public function __construct(
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;
// unsetting the property forces the first access to go through
// __get().
unset($this->stream);
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-gcm";
}
public function getAesName()
{
return 'AES/GCM/NoPadding';
}
public function getCurrentIv()
{
return $this->initializationVector;
}
public function createStream()
{
$result = \openssl_decrypt(
(string)$this->cipherText,
$this->getOpenSslName(),
$this->key,
OPENSSL_RAW_DATA,
$this->initializationVector,
$this->tag,
$this->aad
);
if ($result === false) {
throw new CryptoException('The requested object could not be '
. 'decrypted due to an invalid authentication tag.');
}
return Psr7\Utils::streamFor($result);
}
public function isWritable(): bool
{
return false;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
/**
* @internal Represents a stream of data to be gcm encrypted.
*/
class AesGcmEncryptingStream implements AesStreamInterface, AesStreamInterfaceV2
{
use StreamDecoratorTrait;
private $aad;
private $initializationVector;
private $key;
private $keySize;
private $plaintext;
private $tag = '';
private $tagLength;
/**
* @var StreamInterface
*/
private $stream;
/**
* 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(
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;
// unsetting the property forces the first access to go through
// __get().
unset($this->stream);
}
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()
{
return Psr7\Utils::streamFor(\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(): bool
{
return false;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Aws\Crypto;
use 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,31 @@
<?php
namespace Aws\Crypto;
use 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,26 @@
<?php
namespace Aws\Crypto;
class AlgorithmConstants
{
/**
* The maximum number of 16-byte blocks that can be encrypted with a
* GCM cipher. Note the maximum bit-length of the plaintext is (2^39 - 256),
* which translates to a maximum byte-length of (2^36 - 32), which in turn
* translates to a maximum block-length of (2^32 - 2).
*
* Reference: NIST Special Publication 800-38D.
* @link http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf
*/
public const GCM_MAX_CONTENT_LENGTH_BITS = (1 << 39) - 256;
/**
* The Maximum length of the content that can be encrypted in CBC mode.
*/
public const CBC_MAX_CONTENT_LENGTH_BYTES = 1 << 55;
/**
* The maximum number of bytes that can be securely encrypted per a single key using AES/CTR.
*/
public const CTR_MAX_CONTENT_LENGTH_BYTES = -1;
}

View File

@@ -0,0 +1,227 @@
<?php
namespace Aws\Crypto;
use Aws\S3\Crypto\S3EncryptionClientV3;
enum AlgorithmSuite: int
{
case ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY = 0x0073;
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF = 0x0072;
case ALG_AES_256_CBC_IV16_NO_KDF = 0x0070;
public function getId(): int
{
return $this->value;
}
public function isLegacy(): bool
{
return match ($this) {
self::ALG_AES_256_CBC_IV16_NO_KDF => true,
default => false,
};
}
public function isKeyCommitting(): bool
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => true,
default => false,
};
}
public function getDataKeyAlgorithm(): string
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF,
self::ALG_AES_256_CBC_IV16_NO_KDF => "AES",
};
}
public function getDataKeyLengthBits(): string
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF,
self::ALG_AES_256_CBC_IV16_NO_KDF => "256",
};
}
public function getCipherName(): string
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF => "gcm",
self::ALG_AES_256_CBC_IV16_NO_KDF => "cbc",
};
}
public function getCipherBlockSizeBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF,
self::ALG_AES_256_CBC_IV16_NO_KDF => 128,
};
}
public function getCipherBlockSizeBytes(): int
{
return $this->getCipherBlockSizeBits() / 8;
}
public function getIvLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF => 96,
self::ALG_AES_256_CBC_IV16_NO_KDF => 128,
};
}
public function getIvLengthBytes(): int
{
return $this->getIvLengthBits() / 8;
}
public function getCipherTagLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF => 128,
self::ALG_AES_256_CBC_IV16_NO_KDF => 0,
};
}
public function getCipherTagLengthInBytes(): int
{
return $this->getCipherTagLengthBits() / 8;
}
public function getCipherMaxContentLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF => AlgorithmConstants::GCM_MAX_CONTENT_LENGTH_BITS,
self::ALG_AES_256_CBC_IV16_NO_KDF => AlgorithmConstants::CBC_MAX_CONTENT_LENGTH_BYTES,
};
}
public function getCipherMaxContentLengthBytes(): int
{
return $this->getCipherMaxContentLengthBits() / 8;
}
public function getHashingAlgorithm(): string
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => "sha512",
default => "",
};
}
public function getDerivationInputKeyLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => 256,
default => 0,
};
}
public function getDerivationInputKeyLengthBytes(): int
{
return $this->getDerivationInputKeyLengthBits() / 8;
}
public function getDerivationOutputKeyLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => 256,
default => 0,
};
}
public function getDerivationOutputKeyLengthBytes(): int
{
return $this->getDerivationOutputKeyLengthBits() / 8;
}
public function getCommitmentInputKeyLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => 256,
default => 0,
};
}
public function getCommitmentInputKeyLengthBytes(): int
{
return $this->getCommitmentInputKeyLengthBits() / 8;
}
public function getCommitmentOutputKeyLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => 224,
default => 0,
};
}
public function getCommitmentOutputKeyLengthBytes(): int
{
return $this->getCommitmentOutputKeyLengthBits() / 8;
}
public function getKeyCommitmentSaltLengthBits(): int
{
return match ($this) {
self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY => 224,
default => 0,
};
}
//= ../specification/s3-encryption/client.md#key-commitment
//# The S3EC MUST validate the configured Encryption Algorithm against the provided key commitment policy.
public static function validateCommitmentPolicyOnEncrypt(
array $cipherOptions,
string $keyCommitmentPolicy
): self {
$cipherOptions['Cipher'] = strtolower($cipherOptions['Cipher']);
//= ../specification/s3-encryption/client.md#encryption-algorithm
//# The S3EC MUST validate that the configured encryption algorithm is not legacy.
if (!S3EncryptionClientV3::isSupportedCipher($cipherOptions['Cipher'])) {
//= ../specification/s3-encryption/client.md#encryption-algorithm
//# If the configured encryption algorithm is legacy, then the S3EC MUST throw an exception.
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 (!MaterialsProviderV3::isSupportedKeySize($cipherOptions['KeySize'])) {
throw new \InvalidArgumentException('The cipher "KeySize" requested'
. ' is not supported by AES (256).');
}
//= ../specification/s3-encryption/key-commitment.md#commitment-policy
//# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST NOT encrypt using an algorithm suite which supports key commitment.
if ($keyCommitmentPolicy === 'FORBID_ENCRYPT_ALLOW_DECRYPT') {
return self::ALG_AES_256_GCM_IV12_TAG16_NO_KDF;
} else {
//= ../specification/s3-encryption/key-commitment.md#commitment-policy
//# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment.
//= ../specification/s3-encryption/key-commitment.md#commitment-policy
//# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST only encrypt using an algorithm suite which supports key commitment.
return self::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace 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 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,72 @@
<?php
namespace Aws\Crypto\Cipher;
use 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 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 CryptoException('Unrecognized or unsupported'
. ' AESName for reverse lookup.');
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace 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,181 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LimitStream;
use 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
*/
abstract protected 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
*/
abstract protected 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,
MaterialsProviderInterface $provider,
MetadataEnvelope $envelope,
array $cipherOptions = []
) {
$cipherOptions['Iv'] = base64_decode(
$envelope[MetadataEnvelope::IV_HEADER]
);
$cipherOptions['TagLength'] =
$envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
$cek = $provider->decryptCek(
base64_decode(
$envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER]
),
json_decode(
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
true
)
);
$cipherOptions['KeySize'] = strlen($cek) * 8;
$cipherOptions['Cipher'] = $this->getCipherFromAesName(
$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
);
$decryptionStream = $this->getDecryptingStream(
$cipherText,
$cek,
$cipherOptions
);
unset($cek);
return $decryptionStream;
}
private function getTagFromCiphertextStream(
StreamInterface $cipherText,
$tagLength
) {
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown'
. ' size.');
}
return (string) new LimitStream(
$cipherText,
$tagLength,
$cipherTextSize - $tagLength
);
}
private function getStrippedCiphertextStream(
StreamInterface $cipherText,
$tagLength
) {
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown'
. ' size.');
}
return new 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 = Psr7\Utils::streamFor($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
);
return new AesGcmDecryptingStream(
$this->getStrippedCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
),
$cek,
$cipherOptions['Iv'],
$cipherOptions['Tag'],
$cipherOptions['Aad'] = isset($cipherOptions['Aad'])
? $cipherOptions['Aad']
: '',
$cipherOptions['TagLength'] ?: null,
$cipherOptions['KeySize']
);
default:
$cipherMethod = $this->buildCipherMethod(
$cipherOptions['Cipher'],
$cipherOptions['Iv'],
$cipherOptions['KeySize']
);
return new AesDecryptingStream(
$cipherTextStream,
$cek,
$cipherMethod
);
}
}
}

View File

@@ -0,0 +1,424 @@
<?php
namespace Aws\Crypto;
use Aws\Exception\CryptoException;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LimitStream;
use 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
*/
abstract protected 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
*/
abstract protected 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,
MaterialsProviderInterfaceV2 $provider,
MetadataEnvelope $envelope,
array $options = []
)
{
$commitmentPolicy = $this->getKeyCommitmentPolicy($options);
unset($options['@CommitmentPolicy']);
if (isset($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3])) {
if ($commitmentPolicy !== "FORBID_ENCRYPT_ALLOW_DECRYPT") {
throw new CryptoException(
"The requested item is encrypted"
. " with Key Commitment. The current policy {$commitmentPolicy}"
. " conflicts with the object metadata. Select an appropriate"
. " '@CommitmentPolicy' to decrypt this item."
);
}
$algorithmSuite = AlgorithmSuite::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
$options['@CipherOptions'] = $options['@CipherOptions'] ?? [];
$options['@CipherOptions']['Iv'] = str_repeat("\1", 12);
$options['@CipherOptions']['TagLength'] = $algorithmSuite->getCipherTagLengthInBytes();
$materialDescription = json_decode(
$envelope[MetadataEnvelope::ENCRYPTION_CONTEXT_V3],
true
);
$cek = $provider->decryptCek(
base64_decode($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3]),
$materialDescription,
$options
);
$options['@CipherOptions']['KeySize'] = strlen($cek) * 8;
$options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName(
$this->numericalContenCipherToAesName($envelope)
);
$this->validateOptionsAndEnvelope($options, $envelope);
$messageId = base64_decode($envelope[MetadataEnvelope::MESSAGE_ID_V3]);
$commitmentKey = base64_decode($envelope[MetadataEnvelope::KEY_COMMITMENT_V3]);
if (strlen($messageId) !== ($algorithmSuite->getKeyCommitmentSaltLengthBits()) / 8) {
throw new CryptoException("Invalid MessageId length found in object envelope.");
}
if (strlen($commitmentKey) !== $algorithmSuite->getCommitmentOutputKeyLengthBytes()) {
throw new CryptoException("Invalid Commitment Key length found in object envelope.");
}
$decryptionStream = $this->getCommitingDecryptingStream(
$cipherText,
$cek,
$options['@CipherOptions'],
$messageId,
$commitmentKey,
$algorithmSuite
);
unset($cek);
return $decryptionStream;
} else {
$options['@CipherOptions'] = $options['@CipherOptions'] ?? [];
$options['@CipherOptions']['Iv'] = base64_decode(
$envelope[MetadataEnvelope::IV_HEADER]
);
$options['@CipherOptions']['TagLength'] =
$envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
$cek = $provider->decryptCek(
base64_decode(
$envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER]
),
json_decode(
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
true
),
$options
);
$options['@CipherOptions']['KeySize'] = strlen($cek) * 8;
$options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName(
$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
);
$this->validateOptionsAndEnvelope($options, $envelope);
$decryptionStream = $this->getDecryptingStream(
$cipherText,
$cek,
$options['@CipherOptions']
);
unset($cek);
return $decryptionStream;
}
}
private function buildMaterialDescription(
MetadataEnvelope $envelope
): array
{
switch ($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]) {
case 12:
return ['aws:x-amz-cek-alg' => '115'];
default:
throw new CryptoException(
"Unknown Encrypted Data Key "
. "wrapping algorithm found: "
. "{$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]}"
);
}
}
private function numericalContenCipherToAesName(
MetadataEnvelope $envelope
): string
{
switch ($envelope[MetadataEnvelope::CONTENT_CIPHER_V3]) {
case 115:
return 'AES/GCM/NoPadding';
default:
throw new CryptoException(
"Unknown Encrypted Data Key "
. "wrapping algorithm found: "
. "{$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]}"
);
}
}
private function getTagFromCiphertextStream(
StreamInterface $cipherText,
$tagLength
)
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown'
. ' size.');
}
return (string) new LimitStream(
$cipherText,
$tagLength,
$cipherTextSize - $tagLength
);
}
private function getStrippedCiphertextStream(
StreamInterface $cipherText,
$tagLength
)
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown'
. ' size.');
}
return new LimitStream(
$cipherText,
$cipherTextSize - $tagLength,
0
);
}
private function validateOptionsAndEnvelope($options, $envelope): void
{
$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 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 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 (isset($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3])) {
if ($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3] !== '12') {
throw new CryptoException("The requested object is encrypted with"
. " the keywrap schema '{$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]}',"
. " which is not supported for decryption with the current security"
. " profile.");
}
} else {
if (!in_array($envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], $allowedKeywraps)) {
if (in_array($envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], AbstractCryptoClient::$supportedKeyWraps)) {
throw $v1SchemaException;
}
throw new CryptoException("The requested object is encrypted with"
. " the keywrap schema '{$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER]}',"
. " which is not supported for decryption with the current security"
. " profile.");
}
$matdesc = json_decode(
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
true
);
if (isset($matdesc['aws:x-amz-cek-alg'])
&& ($envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
!== $matdesc['aws:x-amz-cek-alg'])
) {
throw new 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[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 = Psr7\Utils::streamFor($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
);
return new AesGcmDecryptingStream(
$this->getStrippedCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
),
$cek,
$cipherOptions['Iv'],
$cipherOptions['Tag'],
$cipherOptions['Aad'] = $cipherOptions['Aad'] ?? '',
$cipherOptions['TagLength'] ?: null,
$cipherOptions['KeySize']
);
default:
$cipherMethod = $this->buildCipherMethod(
$cipherOptions['Cipher'],
$cipherOptions['Iv'],
$cipherOptions['KeySize']
);
return new AesDecryptingStream(
$cipherTextStream,
$cek,
$cipherMethod
);
}
}
/**
* Generates a stream that wraps the cipher text with the proper cipher and
* uses the content encryption key (CEK) to derive both a derived content encryption key
* and a commitment key 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.
* @param string $messageId a string value used to calculate both a commitment
* key and derived content encryption key
* @param string $commitmentKey a string value to compare with the calculated commitment
* key value, if the values don't match an exception is raised.
*
* @return AesStreamInterface | CryptoException
*
* @internal
*/
protected function getCommitingDecryptingStream(
string $cipherText,
string $cek,
array $cipherOptions,
string $messageId,
string $commitmentKey,
AlgorithmSuite $algorithmSuite
): AesStreamInterface|CryptoException
{
$algorithmSuiteIdAsBytes = pack('n', $algorithmSuite->getId());
$derivedEncryptionKeyInfo = $algorithmSuiteIdAsBytes . "DERIVEKEY";
$commitmentKeyInfo = $algorithmSuiteIdAsBytes . "COMMITKEY";
$derivedEncryptionKey = hash_hkdf(
$algorithmSuite->getHashingAlgorithm(),
$cek,
$algorithmSuite->getDerivationOutputKeyLengthBytes(),
$derivedEncryptionKeyInfo,
$messageId
);
$calculatedCommitmentKey = hash_hkdf(
$algorithmSuite->getHashingAlgorithm(),
$cek,
$algorithmSuite->getCommitmentOutputKeyLengthBytes(),
$commitmentKeyInfo,
$messageId
);
if ($commitmentKey != $calculatedCommitmentKey) {
throw new CryptoException("Calculated commitment key does "
. "not match expected commitment key value ");
}
$cipherTextStream = Psr7\Utils::streamFor($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
);
$cipherOptions['Aad'] = isset($cipherOptions['Aad'])
? $cipherOptions['Aad'] + $algorithmSuiteIdAsBytes
: $algorithmSuiteIdAsBytes;
return new AesGcmDecryptingStream(
$this->getStrippedCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
),
$derivedEncryptionKey,
$cipherOptions['Iv'],
$cipherOptions['Tag'],
$cipherOptions['Aad'],
$cipherOptions['TagLength'] ?: null,
$cipherOptions['KeySize']
);
default:
throw new CryptoException("Unsupported Cipher used for key commitment messages."
. " Found {$cipherOptions["Cipher"]}. Only 'gcm' is supported.");
}
}
}

View File

@@ -0,0 +1,530 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use Aws\Exception\CryptoException;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LimitStream;
use PHPUnit\Framework\Constraint\IsEmpty;
use Psr\Http\Message\StreamInterface;
trait DecryptionTraitV3
{
/**
* Dependency to reverse lookup the openssl_* cipher name from the AESName
* in the MetadataEnvelope.
*
* @param string $aesName
*
* @return string
*
* @internal
*/
abstract protected 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
*/
abstract protected 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 V3 encryption client workflows.
*
* @param string $cipherText Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterfaceV3 $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 string $commitmentPolicy Commitment Policy to use for decrypting objects.
* @param array $options Options used for decryption.
*
* @return AesStreamInterface
*
* @throws \InvalidArgumentException Thrown when a value in $cipherOptions
* is not valid.
*
* @internal
*/
public function decrypt(
string $cipherText,
MaterialsProviderInterfaceV3 $provider,
MetadataEnvelope $envelope,
string $commitmentPolicy,
array $options = []
): AesStreamInterface
{
if (isset($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3])) {
$this->checkEnvelopeForExclusiveMapKeys(
$envelope,
MetadataEnvelope::getV2Fields(),
"Expected V3 only fields but found V2 fields in header metadata."
);
// PHP only supports one commiting algorithm suite
$algorithmSuite = AlgorithmSuite::ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY;
$options['@CipherOptions'] = $options['@CipherOptions'] ?? [];
$options['@CipherOptions']['Iv'] = str_repeat("\1", 12);
$options['@CipherOptions']['TagLength'] = $algorithmSuite->getCipherTagLengthInBytes();
//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
//= type=implication
//# - The wrapping algorithm value "12" MUST be translated to kms+context upon retrieval, and vice versa on write.
$materialDescription = $this->buildMaterialDescription($envelope);
$cek = $provider->decryptCek(
base64_decode($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3]),
$materialDescription,
$options
);
$options['@CipherOptions']['KeySize'] = strlen($cek) * 8;
$options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName(
$this->numericalContenCipherToAesName($envelope)
);
$this->validateOptionsAndEnvelope($options, $envelope, $commitmentPolicy);
$messageId = base64_decode($envelope[MetadataEnvelope::MESSAGE_ID_V3]);
$commitmentKey = base64_decode($envelope[MetadataEnvelope::KEY_COMMITMENT_V3]);
if (strlen($messageId) !== ($algorithmSuite->getKeyCommitmentSaltLengthBits()) / 8) {
throw new CryptoException("Invalid MessageId length found in object envelope.");
}
if (strlen($commitmentKey) !== $algorithmSuite->getCommitmentOutputKeyLengthBytes()) {
throw new CryptoException("Invalid Commitment Key length found in object envelope.");
}
$decryptionStream = $this->getCommitingDecryptingStream(
$cipherText,
$cek,
$options['@CipherOptions'],
$messageId,
$commitmentKey,
$algorithmSuite
);
unset($cek);
return $decryptionStream;
} else {
//= ../specification/s3-encryption/key-commitment.md#commitment-policy
//# When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT,
//# the S3EC MUST NOT allow decryption using algorithm suites which do not support key commitment.
if ($commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT") {
//= ../specification/s3-encryption/client.md#key-commitment
//# If the configured Encryption Algorithm is incompatible with
//# the key commitment policy, then it MUST throw an exception.
throw new CryptoException("Message is encrypted with a "
. "non commiting algorithm but commitment policy is set "
. "to {$commitmentPolicy}. Select a valid commitment "
. "policy to decrypt this object. ");
}
$this->checkEnvelopeForExclusiveMapKeys(
$envelope,
MetadataEnvelope::getV3Fields(),
"Expected V2 only fields but found V3 fields in header metadata."
);
$options['@CipherOptions'] = $options['@CipherOptions'] ?? [];
$options['@CipherOptions']['Iv'] = base64_decode(
$envelope[MetadataEnvelope::IV_HEADER]
);
$options['@CipherOptions']['TagLength'] =
$envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] / 8;
$cek = $provider->decryptCek(
base64_decode(
$envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER]
),
json_decode(
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
true
),
$options
);
$options['@CipherOptions']['KeySize'] = strlen($cek) * 8;
$options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName(
$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
);
//= ../specification/s3-encryption/decryption.md#key-commitment
//= type=implication
//# The S3EC MUST validate the algorithm suite used for decryption
//# against the key commitment policy before attempting to decrypt
//# the content ciphertext.
$this->validateOptionsAndEnvelope($options, $envelope, $commitmentPolicy);
$decryptionStream = $this->getNonCommitingDecryptingStream(
$cipherText,
$cek,
$options['@CipherOptions']
);
unset($cek);
return $decryptionStream;
}
}
private function checkEnvelopeForExclusiveMapKeys(
MetadataEnvelope $envelope,
array $exclusiveKeys,
string $errorMessage
): void
{
foreach ($exclusiveKeys as $exclusiveKey) {
//= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
//# If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception.
if (isset($envelope[$exclusiveKey])) {
throw new CryptoException($errorMessage);
}
}
}
private function numericalContenCipherToAesName(
MetadataEnvelope $envelope
): string
{
switch ($envelope[MetadataEnvelope::CONTENT_CIPHER_V3]) {
case 115:
return 'AES/GCM/NoPadding';
default:
throw new CryptoException(
"Unknown Encrypted Data Key "
. "wrapping algorithm found: "
. "{$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]}"
);
}
}
private function buildMaterialDescription(
MetadataEnvelope $envelope
): array
{
switch ($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]) {
case 12:
return json_decode(
$envelope[MetadataEnvelope::ENCRYPTION_CONTEXT_V3],
true
);
default:
throw new CryptoException(
"Unknown Encrypted Data Key "
. "wrapping algorithm found: "
. "{$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]}"
);
}
}
private function getTagFromCiphertextStream(
StreamInterface $cipherText,
$tagLength
): string
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown size');
}
return (string) new LimitStream(
$cipherText,
$tagLength,
$cipherTextSize - $tagLength
);
}
private function getStrippedCiphertextStream(
StreamInterface $cipherText,
$tagLength
): LimitStream
{
$cipherTextSize = $cipherText->getSize();
if ($cipherTextSize == null || $cipherTextSize <= 0) {
throw new \RuntimeException('Cannot decrypt a stream of unknown size');
}
return new LimitStream(
$cipherText,
$cipherTextSize - $tagLength,
0
);
}
private function validateOptionsAndEnvelope(
array $options,
MetadataEnvelope $envelope,
string $commitmentPolicy
): void
{
//= ../specification/s3-encryption/key-commitment.md#commitment-policy
//# When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT,
//# the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
//= ../specification/s3-encryption/key-commitment.md#commitment-policy
//# When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT,
//# the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
$allowedCiphers = AbstractCryptoClientV3::$supportedCiphers;
$allowedKeywraps = AbstractCryptoClientV3::$supportedKeyWraps;
//= ../specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms
//# When enabled, the S3EC MUST be able to decrypt objects encrypted with all
//# supported wrapping algorithms (both legacy and fully supported).
if ($options['@SecurityProfile'] == 'V3_AND_LEGACY') {
$allowedCiphers = array_unique(array_merge(
$allowedCiphers,
AbstractCryptoClient::$supportedCiphers
));
$allowedKeywraps = array_unique(array_merge(
$allowedKeywraps,
AbstractCryptoClient::$supportedKeyWraps
));
}
$v1SchemaException = new CryptoException("The requested object is encrypted"
. " with V1 encryption schemas that have been disabled by"
. " client configuration @SecurityProfile=V3. Retry with"
. " V3_AND_LEGACY enabled or reencrypt the object.");
if (!in_array($options['@CipherOptions']['Cipher'], $allowedCiphers)) {
//= ../specification/s3-encryption/client.md#enable-legacy-wrapping-algorithms
//= type=implication
//# When disabled, the S3EC MUST NOT decrypt objects encrypted using legacy wrapping algorithms;
//# it MUST throw an exception when attempting to decrypt an object encrypted with a legacy wrapping algorithm.
//= ../specification/s3-encryption/decryption.md#legacy-decryption
//# The S3EC MUST NOT decrypt objects encrypted using legacy unauthenticated algorithm suites unless specifically configured to do so.
if (in_array($options['@CipherOptions']['Cipher'], AbstractCryptoClient::$supportedCiphers)) {
//= ../specification/s3-encryption/decryption.md#legacy-decryption
//# If the S3EC is not configured to enable legacy unauthenticated content decryption, the client MUST throw an exception when attempting to decrypt an object encrypted with a legacy unauthenticated algorithm suite.
throw $v1SchemaException;
}
throw new 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 (isset($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3])) {
if ($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3] !== '12') {
throw new CryptoException("The requested object is encrypted with"
. " the keywrap schema '{$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3]}',"
. " which is not supported for decryption with the current security"
. " profile.");
}
} else {
if (!in_array($envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], $allowedKeywraps)) {
if (in_array($envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], AbstractCryptoClient::$supportedKeyWraps)) {
throw $v1SchemaException;
}
throw new CryptoException("The requested object is encrypted with"
. " the keywrap schema '{$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER]}',"
. " which is not supported for decryption with the current security"
. " profile.");
}
$matdesc = json_decode(
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER],
true
);
if (isset($matdesc['aws:x-amz-cek-alg'])
&& ($envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]
!== $matdesc['aws:x-amz-cek-alg'])
) {
throw new 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[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]}.");
}
}
//= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
//= type=implication
//# - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format.
if (!MetadataEnvelope::isV3Envelope($envelope)
&& $commitmentPolicy == "REQUIRE_ENCRYPT_REQUIRE_DECRYPT"
) {
//= ../specification/s3-encryption/decryption.md#key-commitment
//# If the commitment policy requires decryption using a committing algorithm suite
//# and the algorithm suite associated with the object does not support key commitment,
//# then the S3EC MUST throw an exception.
throw new CryptoException("There is a mismatch in specified"
. "commitment policy value {$commitmentPolicy} and"
. "Metadata Envelope found in object.");
}
}
/**
* 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 getNonCommitingDecryptingStream(
string $cipherText,
string $cek,
array $cipherOptions
): AesStreamInterface
{
$cipherTextStream = Psr7\Utils::streamFor($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
);
return new AesGcmDecryptingStream(
$this->getStrippedCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
),
$cek,
$cipherOptions['Iv'],
$cipherOptions['Tag'],
$cipherOptions['Aad'] = isset($cipherOptions['Aad'])
? $cipherOptions['Aad']
: '',
$cipherOptions['TagLength'] ?: null,
$cipherOptions['KeySize']
);
default:
$cipherMethod = $this->buildCipherMethod(
$cipherOptions['Cipher'],
$cipherOptions['Iv'],
$cipherOptions['KeySize']
);
return new AesDecryptingStream(
$cipherTextStream,
$cek,
$cipherMethod
);
}
}
/**
* Generates a stream that wraps the cipher text with the proper cipher and
* uses the content encryption key (CEK) to derive both a derived content encryption key
* and a commitment key 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.
* @param string $messageId a string value used to calculate both a commitment
* key and derived content encryption key
* @param string $commitmentKey a string value to compare with the calculated commitment
* key value, if the values don't match an exception is raised.
*
* @return AesStreamInterface | CryptoException
*
* @internal
*/
protected function getCommitingDecryptingStream(
string $cipherText,
string $cek,
array $cipherOptions,
string $messageId,
string $commitmentKey,
AlgorithmSuite $algorithmSuite
): AesStreamInterface|CryptoException
{
$algorithmSuiteIdAsBytes = pack('n', $algorithmSuite->getId());
$derivedEncryptionKeyInfo = $algorithmSuiteIdAsBytes . "DERIVEKEY";
$commitmentKeyInfo = $algorithmSuiteIdAsBytes . "COMMITKEY";
$calculatedCommitmentKey = hash_hkdf(
$algorithmSuite->getHashingAlgorithm(),
$cek,
$algorithmSuite->getCommitmentOutputKeyLengthBytes(),
$commitmentKeyInfo,
$messageId
);
//= ../specification/s3-encryption/decryption.md#decrypting-with-commitment
//= type=implication
//# When using an algorithm suite which supports key commitment, the client MUST verify that the [derived key commitment](./key-derivation.md#hkdf-operation) contains the same bytes as the stored key commitment retrieved from the stored object's metadata.
if (
//= ../specification/s3-encryption/decryption.md#decrypting-with-commitment
//= type=implication
//# When using an algorithm suite which supports key commitment,
//# the verification of the derived key commitment value MUST be done in constant time.
!hash_equals($commitmentKey, $calculatedCommitmentKey)
) {
//= ../specification/s3-encryption/decryption.md#decrypting-with-commitment
//= type=implication
//# When using an algorithm suite which supports key commitment, the client MUST
//# throw an exception when the derived key commitment value
//# and stored key commitment value do not match.
throw new CryptoException("Calculated commitment key does "
. "not match expected commitment key value ");
}
//= ../specification/s3-encryption/decryption.md#decrypting-with-commitment
//= type=implication
//# When using an algorithm suite which supports key commitment,
//# the client MUST verify the key commitment values match before
//# deriving the [derived encryption key](./key-derivation.md#hkdf-operation).
$derivedEncryptionKey = hash_hkdf(
$algorithmSuite->getHashingAlgorithm(),
$cek,
$algorithmSuite->getDerivationOutputKeyLengthBytes(),
$derivedEncryptionKeyInfo,
$messageId
);
$cipherTextStream = Psr7\Utils::streamFor($cipherText);
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['Tag'] = $this->getTagFromCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
);
$cipherOptions['Aad'] = isset($cipherOptions['Aad'])
? $cipherOptions['Aad'] + $algorithmSuiteIdAsBytes
: $algorithmSuiteIdAsBytes;
return new AesGcmDecryptingStream(
$this->getStrippedCiphertextStream(
$cipherTextStream,
$cipherOptions['TagLength']
),
$derivedEncryptionKey,
$cipherOptions['Iv'],
$cipherOptions['Tag'],
$cipherOptions['Aad'],
$cipherOptions['TagLength'] ?: null,
$cipherOptions['KeySize']
);
default:
throw new CryptoException("Unsupported Cipher used for key commitment messages."
. " Found {$cipherOptions["Cipher"]}. Only 'gcm' is supported.");
}
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\AppendStream;
use 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
*/
abstract protected 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(
Stream $plaintext,
array $cipherOptions,
MaterialsProvider $provider,
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 (!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[MetadataEnvelope::CONTENT_KEY_V2_HEADER] =
$provider->encryptCek(
$cek,
$materialsDescription
);
unset($cek);
$envelope[MetadataEnvelope::IV_HEADER] =
base64_encode($cipherOptions['Iv']);
$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] =
$provider->getWrapAlgorithmName();
$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName;
$envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] =
strlen($plaintext);
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] =
json_encode($materialsDescription);
if (!empty($cipherOptions['Tag'])) {
$envelope[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 array returns an array with two elements as follows: [string, AesStreamInterface]
*
* @internal
*/
protected function getEncryptingStream(
Stream $plaintext,
$cek,
&$cipherOptions
) {
switch ($cipherOptions['Cipher']) {
case 'gcm':
$cipherOptions['TagLength'] = 16;
$cipherTextStream = new AesGcmEncryptingStream(
$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 AppendStream([
$cipherTextStream->createStream()
]);
$cipherOptions['Tag'] = $cipherTextStream->getTag();
$appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag']));
return [$appendStream, $cipherTextStream->getAesName()];
default:
$cipherMethod = $this->buildCipherMethod(
$cipherOptions['Cipher'],
$cipherOptions['Iv'],
$cipherOptions['KeySize']
);
$cipherTextStream = new AesEncryptingStream(
$plaintext,
$cek,
$cipherMethod
);
return [$cipherTextStream, $cipherTextStream->getAesName()];
}
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\AppendStream;
use GuzzleHttp\Psr7\Stream;
use Psr\Http\Message\StreamInterface;
trait EncryptionTraitV2
{
private static $allowedOptions = [
'Cipher' => true,
'KeySize' => true,
'Aad' => true,
];
private static $encryptClasses = [
'gcm' => 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
*/
abstract protected 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(
Stream $plaintext,
array $options,
MaterialsProviderV2 $provider,
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 (!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[MetadataEnvelope::CONTENT_KEY_V2_HEADER] = $keys['Ciphertext'];
unset($keys);
$envelope[MetadataEnvelope::IV_HEADER] =
base64_encode($cipherOptions['Iv']);
$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] =
$provider->getWrapAlgorithmName();
$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName;
$envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] =
(string) strlen($plaintext);
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] =
json_encode($materialsDescription);
if (!empty($cipherOptions['Tag'])) {
$envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] =
(string) (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 array returns an array with two elements as follows: [string, AesStreamInterface]
*
* @internal
*/
protected function getEncryptingStream(
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'] = $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 AppendStream([
$cipherTextStream->createStream()
]);
$cipherOptions['Tag'] = $cipherTextStream->getTag();
$appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag']));
return $appendStream;
}
}
}

View File

@@ -0,0 +1,474 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use Aws\Exception\CryptoException;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\AppendStream;
use GuzzleHttp\Psr7\Stream;
use Psr\Http\Message\StreamInterface;
trait EncryptionTraitV3
{
private static array $allowedOptions = [
'Cipher' => true,
'KeySize' => true,
'Aad' => true,
];
private static array $encryptClasses = [
'gcm' => 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
*/
abstract protected 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 AlgorithmSuite $algorithmSuite Algorithm Suite for use in encryption
* @param array $options Options for use in encryption, including cipher
* options, and encryption context.
* @param MaterialsProviderV3 $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 AppendStream
*
* @throws \InvalidArgumentException Thrown when a value in $options['@CipherOptions']
* is not valid.
*s
* @internal
*/
public function encrypt(
Stream $plaintext,
AlgorithmSuite $algorithmSuite,
array $options,
MaterialsProviderV3 $provider,
MetadataEnvelope $envelope
): AppendStream
{
$options = array_change_key_case($options);
$cipherOptions = array_intersect_key(
$options['@cipheroptions'],
self::$allowedOptions
);
//= ../specification/s3-encryption/encryption.md#content-encryption
//= type=implication
//# The client MUST validate that the length of the plaintext bytes does not exceed
//# the algorithm suite's cipher's maximum content length in bytes.
if (strlen($plaintext) > $algorithmSuite->getCipherMaxContentLengthBytes()) {
throw new \InvalidArgumentException("The contentLength of the object you are attempting"
. " to encrypt exceeds the maximum length allowed for GCM encryption.");
}
$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 (!MaterialsProviderV3::isSupportedKeySize($cipherOptions['KeySize'])) {
throw new \InvalidArgumentException('The cipher "KeySize" requested'
. ' is not supported by AES (256).');
}
$encryptClass = self::$encryptClasses[$algorithmSuite->getCipherName()];
$aesName = $encryptClass::getStaticAesName();
$materialsDescription = $algorithmSuite->isKeyCommitting()
? ['aws:x-amz-cek-alg' => '115']
: ['aws:x-amz-cek-alg' => $aesName];
$keys = $provider->generateCek(
$algorithmSuite->getDataKeyLengthBits(),
$materialsDescription,
$options
);
// Some providers modify materials description based on options
if (isset($keys['UpdatedContext'])) {
$materialsDescription = $keys['UpdatedContext'];
}
if ($algorithmSuite->isKeyCommitting()) {
return $this->encryptCommitingStream(
$plaintext,
$algorithmSuite,
$cipherOptions,
$keys,
$materialsDescription,
$provider,
$envelope
);
} else {
return $this->encryptNonCommitingStream(
$plaintext,
$cipherOptions,
$keys,
$materialsDescription,
$aesName,
$provider,
$envelope
);
}
}
private function encryptNonCommitingStream(
Stream $plaintext,
array &$cipherOptions,
array $keys,
array $materialsDescription,
string $aesName,
MaterialsProviderV3 $provider,
MetadataEnvelope $envelope
): AppendStream
{
//= ../specification/s3-encryption/encryption.md#content-encryption
//# The generated IV or Message ID MUST be set or returned from the
//# encryption process such that it can be included in the content metadata.
//= ../specification/s3-encryption/encryption.md#content-encryption
//# The client MUST generate an IV or Message ID using the length of the IV or Message ID defined in the algorithm suite.
$cipherOptions['Iv'] = $provider->generateIv(
$this->getCipherOpenSslName(
$cipherOptions['Cipher'],
$cipherOptions['KeySize']
)
);
// Some providers modify materials description based on options
if (isset($keys['UpdatedContext'])) {
$materialsDescription = $keys['UpdatedContext'];
}
$encryptingStream = $this->getNonCommittingEncryptingStream(
$plaintext,
$keys['Plaintext'],
$cipherOptions
);
// Populate envelope data
//= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
//# - If the metadata contains "x-amz-iv" and "x-amz-metadata-x-amz-key-v2" then the object MUST be considered as an S3EC-encrypted object using the V2 format.
$envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER] = $keys['Ciphertext'];
unset($keys);
$envelope[MetadataEnvelope::IV_HEADER] =
base64_encode($cipherOptions['Iv']);
$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] =
$provider->getWrapAlgorithmName();
$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName;
$envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] =
(string) strlen($plaintext);
$envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] =
json_encode($materialsDescription);
if (!empty($cipherOptions['Tag'])) {
$envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] =
(string) (strlen($cipherOptions['Tag']) * 8);
}
if (!MetadataEnvelope::isV2Envelope($envelope)) {
throw new CryptoException("Error while writing metadata envelope."
. " Not all required fields were set.");
}
return $encryptingStream;
}
private function encryptCommitingStream(
Stream $plaintext,
AlgorithmSuite $algorithmSuite,
array &$options,
array $keys,
array $materialsDescription,
MaterialsProviderV3 $provider,
MetadataEnvelope $envelope
): AppendStream
{
//= ../specification/s3-encryption/encryption.md#content-encryption
//# The generated IV or Message ID MUST be set or returned from the
//# encryption process such that it can be included in the content metadata.
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# When encrypting or decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY,
//# the IV used in the AES-GCM content encryption/decryption
//# MUST consist entirely of bytes with the value 0x01.
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# The IV's total length MUST match the IV length defined by the algorithm suite.
$options['Iv'] = str_repeat("\1", $algorithmSuite->getIvLengthBytes());
$messageId = $provider->generateIv(
$this->getCipherOpenSslName(
$algorithmSuite->getCipherName(),
//= ../specification/s3-encryption/encryption.md#content-encryption
//# The client MUST generate an IV or Message ID using the length of
//# the IV or Message ID defined in the algorithm suite.
$algorithmSuite->getKeyCommitmentSaltLengthBits()
)
);
// Some providers modify materials description based on options
if (isset($keys['UpdatedContext'])) {
$materialsDescription["aws:x-amz-cek-alg"] = (string) $algorithmSuite->getId();
}
$commitingEncryptingArray = $this->getCommitingEncryptionStream(
$plaintext,
$keys['Plaintext'],
$options,
$messageId,
$algorithmSuite
);
//= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
//= type=implication
//# The derived key commitment value MUST be set or returned from the encryption process
//# such that it can be included in the content metadata.
$commitmentKey = $commitingEncryptingArray[0];
$encryptionStream = $commitingEncryptingArray[1];
// Populate envelope data
$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3] = $keys['Ciphertext'];
unset($keys);
$envelope[MetadataEnvelope::CONTENT_CIPHER_V3] = $algorithmSuite->getId();
// we are able to always set the encryption context in the envelope because PHP
// only supports a KMS Material Provider.
$envelope[MetadataEnvelope::ENCRYPTION_CONTEXT_V3] =
json_encode($materialsDescription);
//= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
//# The Encryption Context value MUST be used for wrapping algorithm `kms+context` or `12`.
$envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3] = '12';
$envelope[MetadataEnvelope::KEY_COMMITMENT_V3] = $commitmentKey;
$envelope[MetadataEnvelope::MESSAGE_ID_V3] = base64_encode($messageId);
if (!MetadataEnvelope::isV3Envelope($envelope)) {
throw new CryptoException("Error while writing metadata envelope."
. " Not all required fields were set.");
}
return $encryptionStream;
}
/**
* 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 AppendStream returns an AppendStream
*
* @internal
*/
protected function getNonCommittingEncryptingStream(
Stream $plaintext,
string $cek,
array &$cipherOptions
): AppendStream
{
// Only 'gcm' is supported for encryption currently
switch ($cipherOptions['Cipher']) {
//= ../specification/s3-encryption/encryption.md#content-encryption
//= type=implication
//# The S3EC MUST use the encryption algorithm configured during [client](./client.md) initialization.
case 'gcm':
$cipherOptions['TagLength'] = 16;
$encryptClass = self::$encryptClasses['gcm'];
//= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-iv12-tag16-no-kdf
//= type=implication
//# The client MUST initialize the cipher, or call an AES-GCM encryption API,
//# with the plaintext data key, the generated IV, and the tag length defined
//# in the Algorithm Suite when encrypting with ALG_AES_256_GCM_IV12_TAG16_NO_KDF.
$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 AppendStream([
$cipherTextStream->createStream()
]);
//= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-iv12-tag16-no-kdf
//= type=implication
//# The client MUST append the GCM auth tag to the ciphertext if the underlying
//# crypto provider does not do so automatically.
$cipherOptions['Tag'] = $cipherTextStream->getTag();
$appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag']));
return $appendStream;
default:
throw new CryptoException("Unsupported Cipher used for key commitment messages."
. " Found {$cipherOptions["Cipher"]}. Only 'gcm' is supported.");
}
}
/**
* 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.
* @param string $messageId salt value used for key extraction step in the key
* derivation process.
* @param AlgorithmSuite $algorithmSuite options used for key commitment
*
* @return array returns an array with two elements as follows: [commitmentKey, AppendStream]
*
* @internal
*/
protected function getCommitingEncryptionStream(
Stream $plaintext,
string $dek,
array &$cipherOptions,
string $messageId,
AlgorithmSuite $algorithmSuite
): array
{
$algorithmSuiteIdAsBytes = pack('n', $algorithmSuite->getId());
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The input info MUST be a concatenation of the algorithm suite ID as bytes followed by the string DERIVEKEY as UTF8 encoded bytes.
$derivedEncryptionKeyInfo = $algorithmSuiteIdAsBytes . "DERIVEKEY";
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The input info MUST be a concatenation of the algorithm suite ID as bytes followed by the string COMMITKEY as UTF8 encoded bytes.
$commitmentKeyInfo = $algorithmSuiteIdAsBytes . "COMMITKEY";
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The length of the input keying material MUST equal the key derivation
//# input length specified by the algorithm suite commit key derivation setting.
if (strlen($dek) !== $algorithmSuite->getDerivationInputKeyLengthBytes()) {
throw new CryptoException("Input Key Material length exceeds "
. "key derivation input length specified by the algorithm suite.");
}
//= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
//= type=implication
//# The client MUST use HKDF to derive the key commitment value and
//# the derived encrypting key as described in [Key Derivation](key-derivation.md).
$cek = hash_hkdf(
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The hash function MUST be specified by the algorithm suite commitment settings.
$algorithmSuite->getHashingAlgorithm(),
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The input keying material MUST be the plaintext data key (PDK) generated by the key provider.
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The DEK input pseudorandom key MUST be the output from the extract step.
$dek,
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The length of the output keying material MUST equal the encryption key length specified by the algorithm suite encryption settings.
$algorithmSuite->getDerivationOutputKeyLengthBytes(),
$derivedEncryptionKeyInfo,
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The salt MUST be the Message ID with the length defined in the algorithm suite.
$messageId
);
$commitmentKey = hash_hkdf(
$algorithmSuite->getHashingAlgorithm(),
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The CK input pseudorandom key MUST be the output from the extract step.
$dek,
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# - The length of the output keying material MUST equal the commit key length specified by the supported algorithm suites.
$algorithmSuite->getCommitmentOutputKeyLengthBytes(),
$commitmentKeyInfo,
$messageId
);
switch ($cipherOptions['Cipher']) {
// Only 'gcm' is supported for encryption currently
case 'gcm':
$cipherOptions['TagLength'] = $algorithmSuite->getCipherTagLengthInBytes();
$encryptClass = self::$encryptClasses[$algorithmSuite->getCipherName()];
if (!empty($cipherOptions['Aad'])) {
trigger_error("'Aad' has been supplied for content encryption"
. " with " . $encryptClass->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_NOTICE);
}
$cipherOptions['Aad'] = isset($cipherOptions['Aad'])
? $cipherOptions['Aad'] + $algorithmSuiteIdAsBytes
: $algorithmSuiteIdAsBytes;
//= ../specification/s3-encryption/key-derivation.md#hkdf-operation
//= type=implication
//# The client MUST initialize the cipher, or call an AES-GCM encryption API,
//# with the derived encryption key, an IV containing only bytes with the value 0x01,
//# and the tag length defined in the Algorithm Suite when encrypting or
//# decrypting with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.
$cipherTextStream = new $encryptClass(
$plaintext,
$cek,
$cipherOptions['Iv'],
$cipherOptions['Aad'],
$cipherOptions['TagLength'],
$cipherOptions['KeySize']
);
$appendStream = new AppendStream([
$cipherTextStream->createStream()
]);
//= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
//= type=implication
//# The client MUST append the GCM auth tag to the ciphertext if the underlying
//# crypto provider does not do so automatically.
$cipherOptions['Tag'] = $cipherTextStream->getTag();
$appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag']));
return [base64_encode($commitmentKey), $appendStream];
default:
throw new CryptoException("Unsupported Cipher used for content encryption");
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Aws\Crypto;
use 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 MaterialsProvider implements 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(
KmsClient $kmsClient,
$kmsKeyId = null
) {
$this->kmsClient = $kmsClient;
$this->kmsKeyId = $kmsKeyId;
}
public function fromDecryptionEnvelope(MetadataEnvelope $envelope)
{
if (empty($envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER])) {
throw new \RuntimeException('Not able to detect the materials description.');
}
$materialsDescription = json_decode(
$envelope[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,100 @@
<?php
namespace Aws\Crypto;
use Aws\Exception\CryptoException;
use 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 MaterialsProviderV2 implements 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(
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 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 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 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 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,159 @@
<?php
namespace Aws\Crypto;
use Aws\Exception\CryptoException;
use 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 KmsMaterialsProviderV3 extends MaterialsProviderV3 implements MaterialsProviderInterfaceV3
{
const WRAP_ALGORITHM_NAME = 'kms+context';
private KmsClient $kmsClient;
private ?string $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(
KmsClient $kmsClient,
?string $kmsKeyId = null
) {
$this->kmsClient = $kmsClient;
$this->kmsKeyId = $kmsKeyId;
}
/**
* @inheritDoc
*/
public function getWrapAlgorithmName(): string
{
return self::WRAP_ALGORITHM_NAME;
}
/**
* @inheritDoc
*/
public function decryptCek(
string $encryptedCek,
array $materialDescription,
array $options
): string
{
$options = array_change_key_case($options);
$encryptionContext = null;
// no encryption context was provided we will use the one in the metadata
if (!isset($options['@kmsencryptioncontext'])) {
$encryptionContext = $materialDescription;
} else {
// encryption context was passed in, we have to make sure it is an array
// and that the reserved keywords were not used
if (!is_array($options['@kmsencryptioncontext'])) {
throw new CryptoException("'When using @KmsMaterialsProviderV3, it"
. " must be an associative array (or empty array).");
}
if (isset($options['@kmsencryptioncontext']['aws:x-amz-cek-alg'])) {
throw new 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.");
}
if (isset($options['@kmsencryptioncontext']['kms_cmk_id'])) {
throw new CryptoException("Conflict in reserved @KmsEncryptionContext"
. " key kms_cmk_id. This value is reserved for the S3"
. " Encryption Client and cannot be set by the user.");
}
//= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context
//= type=implication
//# When decrypting using Kms+Context mode, the KmsKeyring MUST validate the provided (request) encryption context with the stored (materials) encryption context.
// We are validating the encryption context to match S3EC V2 behavior
// Refer to KMSMaterialsHandler in the V2 client for details
$materialsDescriptionContextCopy = $materialDescription;
unset($materialsDescriptionContextCopy["aws:x-amz-cek-alg"]);
unset($materialsDescriptionContextCopy["kms_cmk_id"]);
$requestEncryptionContext = $options['@kmsencryptioncontext'];
//= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context
//= type=implication
//# The stored encryption context with the two reserved keys removed MUST match the provided encryption context.
if ($materialsDescriptionContextCopy !== $requestEncryptionContext) {
//= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context
//= type=implication
//# If the stored encryption context with the two reserved keys removed does not match the provided encryption context, the KmsKeyring MUST throw an exception.
throw new CryptoException("Provided encryption context does not match information retrieved from S3");
}
$encryptionContext = $materialDescription;
}
$params = [
'CiphertextBlob' => $encryptedCek,
'EncryptionContext' => $encryptionContext
];
if (empty($options['@kmsallowdecryptwithanycmk'])) {
if (empty($this->kmsKeyId)) {
throw new 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): array
{
if (empty($this->kmsKeyId)) {
throw new 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 CryptoException("'@KmsEncryptionContext' is a"
. " required argument when using KmsMaterialsProviderV3, and"
. " must be an associative array (or empty array).");
}
if (isset($options['@kmsencryptioncontext']['aws:x-amz-cek-alg'])) {
throw new 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.");
}
if (isset($options['@kmsencryptioncontext']['kms_cmk_id'])) {
throw new CryptoException("Conflict in reserved @KmsEncryptionContext"
. " key kms_cmk_id. 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,105 @@
<?php
namespace Aws\Crypto;
abstract class MaterialsProvider implements 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
*/
abstract public function fromDecryptionEnvelope(MetadataEnvelope $envelope);
/**
* Returns the material description for this Provider so it can be verified
* by encryption mechanisms.
*
* @return string
*/
abstract public function getMaterialsDescription();
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
abstract public 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
*/
abstract public 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
*/
abstract 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)
{
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,61 @@
<?php
namespace 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(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,53 @@
<?php
namespace 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,61 @@
<?php
namespace Aws\Crypto;
interface MaterialsProviderInterfaceV3
{
/**
* 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(int $keySize): bool;
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
public function getWrapAlgorithmName(): string;
/**
* 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 array $materialDescription Material Description for use in
* decrypting the CEK.
* @param AlgorithmSuite $algorithmSuite AlgorithmSuite for use in decrypting the CEK.
*
* @return string
*/
public function decryptCek(
string $encryptedCek,
array $materialDescription,
array $options
): string;
/**
* @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(
string $keySize,
array $context,
array $options
): array;
/**
* @param string $openSslName Cipher OpenSSL name to use for generating
* an initialization vector.
*
* @return string
*/
public function generateIv(string $openSslName): string;
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Aws\Crypto;
abstract class MaterialsProviderV2 implements 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
*/
abstract 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 string $options Options for use in decrypting the CEK.
*
* @return string
*/
abstract 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
*/
abstract 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)
{
return openssl_random_pseudo_bytes(
openssl_cipher_iv_length($openSslName)
);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Aws\Crypto;
use Aws\Exception\CryptoException;
abstract class MaterialsProviderV3 implements MaterialsProviderInterfaceV3
{
private static array $supportedKeySizes = [
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(int $keySize): bool
{
return isset(self::$supportedKeySizes[$keySize]);
}
/**
* Returns the wrap algorithm name for this Provider.
*
* @return string
*/
abstract public function getWrapAlgorithmName(): string;
/**
* 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 array $materialDescription Material Description for use in
* decrypting the CEK.
* @param array $options Options for use in decrypting the CEK.
*
* @return string
*/
abstract public function decryptCek(
string $encryptedCek,
array $materialDescription,
array $options
): string;
/**
* @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
*/
abstract public function generateCek(
string $keySize,
array $context,
array $options
): array;
/**
* @param string $openSslName Cipher OpenSSL name to use for generating
* an initialization vector.
*
* @return string
*/
public function generateIv(string $openSslName): string
{
$iv = null;
$cstrong = null;
if ($openSslName === "aes-96-gcm") {
$iv = openssl_random_pseudo_bytes(12, $cstrong);
} else if ($openSslName === "aes-224-gcm") {
$iv = openssl_random_pseudo_bytes(28, $cstrong);
} else {
$iv = openssl_random_pseudo_bytes(
openssl_cipher_iv_length($openSslName),
$cstrong
);
}
if (!$cstrong) {
throw new CryptoException("No strong cryptographic source available to generate a random IV.");
}
return $iv;
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Aws\Crypto;
use 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;
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# The "x-amz-" prefix denotes that the metadata is owned by an Amazon product
//# and MUST be prepended to all S3EC metadata mapkeys.
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-key-v2" MUST be present for V2 format objects.
const CONTENT_KEY_V2_HEADER = 'x-amz-key-v2';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-3") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_V3" or similar in the implementation code.
const ENCRYPTED_DATA_KEY_V3 = 'x-amz-3';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-iv" MUST be present for V1 format objects.
const IV_HEADER = 'x-amz-iv';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-matdesc" MUST be present for V1 format objects.
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-matdesc" MUST be present for V2 format objects.
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-iv" MUST be present for V2 format objects.
const MATERIALS_DESCRIPTION_HEADER = 'x-amz-matdesc';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-m") SHOULD be represented by a constant named "MAT_DESC_V3" or similar in the implementation code.
const MAT_DESC_V3 = 'x-amz-m';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-wrap-alg" MUST be present for V2 format objects.
const KEY_WRAP_ALGORITHM_HEADER = 'x-amz-wrap-alg';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-w") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_ALGORITHM_V3" or similar in the implementation code.
const ENCRYPTED_DATA_KEY_ALGORITHM_V3 = 'x-amz-w';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-cek-alg" MUST be present for V2 format objects.
const CONTENT_CRYPTO_SCHEME_HEADER = 'x-amz-cek-alg';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-c") SHOULD be represented by a constant named "CONTENT_CIPHER_V3" or similar in the implementation code.
const CONTENT_CIPHER_V3 = 'x-amz-c';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-tag-len" MUST be present for V2 format objects.
const CRYPTO_TAG_LENGTH_HEADER = 'x-amz-tag-len';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V1 format objects.
const UNENCRYPTED_CONTENT_LENGTH_HEADER = 'x-amz-unencrypted-content-length';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-t") SHOULD be represented by a constant named "ENCRYPTION_CONTEXT_V3" or similar in the implementation code.
const ENCRYPTION_CONTEXT_V3 = 'x-amz-t';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-d") SHOULD be represented by a constant named "KEY_COMMITMENT_V3" or similar in the implementation code.
const KEY_COMMITMENT_V3 = 'x-amz-d';
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - This mapkey ("x-amz-i") SHOULD be represented by a constant named "MESSAGE_ID_V3" or similar in the implementation code.
const MESSAGE_ID_V3 = 'x-amz-i';
private static $constants = [];
public static function getConstantValues()
{
if (empty(self::$constants)) {
$reflection = new \ReflectionClass(static::class);
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code.
foreach (array_values($reflection->getConstants()) as $constant) {
self::$constants[$constant] = true;
}
}
return array_keys(self::$constants);
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($name, $value)
{
$constants = self::getConstantValues();
//= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
//# In general, if there is any deviation from the above format, with the exception of additional unrelated mapkeys, then the S3EC SHOULD throw an exception.
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;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->data;
}
public static function isV2Envelope(MetadataEnvelope $envelope): bool
{
if (!isset($envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER])
|| !isset($envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER])
|| !isset($envelope[MetadataEnvelope::IV_HEADER])
|| !isset($envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER])
|| !isset($envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER])
|| !isset($envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER])
) {
return false;
}
return true;
}
public static function isV1Envelope(MetadataEnvelope $envelope): bool
{
if (!isset($envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-matdesc" MUST be present for V1 format objects.
|| !isset($envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-iv" MUST be present for V1 format objects.
|| !isset($envelope[MetadataEnvelope::IV_HEADER])
|| !isset($envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER])
|| !isset($envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//# - The mapkey "x-amz-unencrypted-content-length" SHOULD be present for V1 format objects.
|| !isset($envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER])
) {
return false;
}
return true;
}
public static function isV3Envelope(MetadataEnvelope $envelope): bool
{
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - The mapkey "x-amz-3" MUST be present for V3 format objects.
if (!isset($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_V3])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - The mapkey "x-amz-c" MUST be present for V3 format objects.
|| !isset($envelope[MetadataEnvelope::CONTENT_CIPHER_V3])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - The mapkey "x-amz-d" MUST be present for V3 format objects.
|| !isset($envelope[MetadataEnvelope::KEY_COMMITMENT_V3])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - The mapkey "x-amz-i" MUST be present for V3 format objects.
|| !isset($envelope[MetadataEnvelope::MESSAGE_ID_V3])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - The mapkey "x-amz-w" MUST be present for V3 format objects.
|| !isset($envelope[MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3])
//= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
//= type=implication
//# - The mapkey "x-amz-t" SHOULD be present for V3 format objects that use KMS Encryption Context.
|| !isset($envelope[MetadataEnvelope::ENCRYPTION_CONTEXT_V3])
) {
return false;
}
return true;
}
public static function getV2Fields(): array
{
return [
MetadataEnvelope::CONTENT_KEY_V2_HEADER,
MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER,
MetadataEnvelope::IV_HEADER,
MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER,
MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER,
MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER
];
}
public static function getV3Fields(): array
{
return [
MetadataEnvelope::ENCRYPTED_DATA_KEY_V3,
MetadataEnvelope::CONTENT_CIPHER_V3,
MetadataEnvelope::KEY_COMMITMENT_V3,
MetadataEnvelope::MESSAGE_ID_V3,
MetadataEnvelope::ENCRYPTED_DATA_KEY_ALGORITHM_V3,
MetadataEnvelope::ENCRYPTION_CONTEXT_V3
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace 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(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);
}