| 
<?phpdeclare(strict_types=1);
 namespace ParagonIE\Paserk\Operations\PKE;
 
 use ParagonIE\ConstantTime\{
 Base64UrlSafe,
 Binary,
 Hex
 };
 use ParagonIE\EasyECC\{
 EasyECC,
 ECDSA\PublicKey,
 ECDSA\SecretKey
 };
 use ParagonIE\Paseto\{
 ProtocolInterface,
 Keys\SymmetricKey,
 Protocol\Version3
 };
 use ParagonIE\Paserk\Operations\Key\{
 SealingPublicKey,
 SealingSecretKey
 };
 use ParagonIE\Paserk\Operations\{
 PKE,
 PKEInterface
 };
 use ParagonIE\Paserk\PaserkException;
 use ParagonIE\Paserk\Util;
 use Exception;
 use TypeError;
 use function
 hash,
 hash_equals,
 hash_hmac,
 openssl_decrypt,
 openssl_encrypt;
 
 /**
 * Class PKEv3
 * @package ParagonIE\Paserk\Operations\PKE
 */
 class PKEv3 implements PKEInterface
 {
 use PKETrait;
 
 /**
 * @return string
 */
 public static function header(): string
 {
 return 'k3.seal.';
 }
 
 /**
 * @return ProtocolInterface
 */
 public static function getProtocol(): ProtocolInterface
 {
 return new Version3();
 }
 
 /**
 * @link https://github.com/paseto-standard/paserk/blob/master/operations/PKE.md#v3-encryption
 *
 * @param SymmetricKey $ptk
 * @param SealingPublicKey $pk
 * @return string
 *
 * @throws Exception
 */
 public function seal(SymmetricKey $ptk, SealingPublicKey $pk): string
 {
 $header = self::header();
 $easyECC = new EasyECC('P384');
 
 // Step 1:
 $this->assertKeyVersion($pk);
 $eph_sk = SecretKey::generate('P384');
 /** @var PublicKey $eph_pk */
 $eph_pk = $eph_sk->getPublicKey();
 $seal_pk = PublicKey::importPem($pk->raw());
 
 $pk_compressed = Hex::decode($seal_pk->toString());
 $eph_pk_compressed = Hex::decode($eph_pk->toString());
 
 // Step 2:
 $xk = $easyECC->scalarmult($eph_sk, $seal_pk);
 
 // Step 3:
 $tmp = hash(
 'sha384',
 PKE::DOMAIN_SEPARATION_ENCRYPT . $header . $xk . $eph_pk_compressed . $pk_compressed,
 true
 );
 /// @SPEC DETAIL: Prefix must be 0x01 for encryption keys
 $Ek = Binary::safeSubstr($tmp, 0, 32);
 $nonce = Binary::safeSubstr($tmp, 32, 16);
 
 // Step 4:
 $Ak = hash(
 'sha384',
 PKE::DOMAIN_SEPARATION_AUTH . $header . $xk . $eph_pk_compressed . $pk_compressed,
 true
 );
 /// @SPEC DETAIL: Prefix must be 0x02 for authentication keys
 
 // Step 5:
 $edk = openssl_encrypt(
 $ptk->raw(),
 'aes-256-ctr',
 $Ek,
 OPENSSL_RAW_DATA | OPENSSL_NO_PADDING,
 $nonce
 );
 
 // Step 6:
 $tag = hash_hmac(
 'sha384',
 $header . $eph_pk_compressed . $edk,
 $Ak,
 true
 );
 /// @SPEC DETAIL: h || epk || edk
 
 Util::wipe($tmp);
 Util::wipe($Ek);
 Util::wipe($nonce);
 Util::wipe($xk);
 Util::wipe($Ak);
 
 // Step 7:
 return Base64UrlSafe::encodeUnpadded($tag . $eph_pk_compressed . $edk);
 }
 
 /**
 * @link https://github.com/paseto-standard/paserk/blob/master/operations/PKE.md#v3-decryption
 *
 * @param string $header
 * @param string $encoded
 * @param SealingSecretKey $sk
 * @return SymmetricKey
 *
 * @throws PaserkException
 * @throws Exception
 */
 public function unseal(string $header, string $encoded, SealingSecretKey $sk): SymmetricKey
 {
 if (!hash_equals($header, self::header())) {
 throw new PaserkException('Header mismatch');
 }
 $this->assertKeyVersion($sk);
 
 $bin = Base64UrlSafe::decode($encoded);
 $tag = Binary::safeSubstr($bin, 0, 48);
 $eph_pk_compressed = Binary::safeSubstr($bin, 48, 49);
 $edk = Binary::safeSubstr($bin, 97);
 
 // Step 1:
 $easyECC = new EasyECC('P384');
 $seal_sk = SecretKey::importPem($sk->raw());
 $eph_pk = PublicKey::fromString(Hex::encode($eph_pk_compressed), 'P384');
 
 $xk = $easyECC->scalarmult($seal_sk, $eph_pk);
 
 /** @var PublicKey $pk_obj */
 $pk_obj = $seal_sk->getPublicKey();
 if (!($pk_obj instanceof PublicKey)) {
 throw new TypeError("An unexpected type violation occurred");
 }
 $pk_compressed = Hex::decode($pk_obj->toString());
 
 // Step 2:
 $Ak = hash(
 'sha384',
 PKE::DOMAIN_SEPARATION_AUTH . $header . $xk . $eph_pk_compressed . $pk_compressed,
 true
 );
 /// @SPEC DETAIL: Prefix must be 0x02 for authentication keys
 
 // Step 3:
 $t2 = hash_hmac(
 'sha384',
 $header . $eph_pk_compressed . $edk,
 $Ak,
 true
 );
 /// @SPEC DETAIL: h || epk || edk
 
 // Step 4:
 if (!hash_equals($t2, $tag)) {
 Util::wipe($t2);
 Util::wipe($Ak);
 throw new PaserkException('Invalid auth tag');
 }
 /// @SPEC DETAIL: This must be a constant-time compare.
 
 // Step 5:
 $tmp = hash(
 'sha384',
 PKE::DOMAIN_SEPARATION_ENCRYPT . $header . $xk . $eph_pk_compressed . $pk_compressed,
 true
 );
 /// @SPEC DETAIL: Prefix must be 0x01 for encryption keys
 $Ek = Binary::safeSubstr($tmp, 0, 32);
 $nonce = Binary::safeSubstr($tmp, 32, 16);
 
 // Step 6:
 $ptk = openssl_decrypt(
 $edk,
 'aes-256-ctr',
 $Ek,
 OPENSSL_NO_PADDING | OPENSSL_RAW_DATA,
 $nonce
 );
 
 Util::wipe($tmp);
 Util::wipe($Ek);
 Util::wipe($nonce);
 Util::wipe($xk);
 Util::wipe($Ak);
 Util::wipe($t2);
 
 // Step 7:
 return new SymmetricKey($ptk, new Version3());
 }
 }
 
 |