| 
<?phpdeclare(strict_types=1);
 namespace ParagonIE\Paserk\Operations\PKE;
 
 use ParagonIE\ConstantTime\{
 Base64UrlSafe,
 Binary
 };
 use Exception;
 use ParagonIE\Paserk\Operations\Key\{
 SealingPublicKey,
 SealingSecretKey
 };
 use ParagonIE\Paserk\Operations\{
 PKE,
 PKEInterface
 };
 use ParagonIE\Paserk\PaserkException;
 use ParagonIE\Paserk\Util;
 use ParagonIE\Paseto\{
 Keys\SymmetricKey,
 Protocol\Version4,
 ProtocolInterface
 };
 use SodiumException;
 use function
 hash_equals,
 sodium_crypto_box_keypair,
 sodium_crypto_box_publickey,
 sodium_crypto_box_secretkey,
 sodium_crypto_generichash,
 sodium_crypto_scalarmult,
 sodium_crypto_sign_ed25519_sk_to_curve25519,
 sodium_crypto_sign_ed25519_pk_to_curve25519,
 sodium_crypto_stream_xchacha20_xor;
 
 
 /**
 * Class PKEv4
 * @package ParagonIE\Paserk\Operations\PKE
 */
 class PKEv4 implements PKEInterface
 {
 use PKETrait;
 
 /**
 * @return string
 */
 public static function header(): string
 {
 return 'k4.seal.';
 }
 
 /**
 * @return ProtocolInterface
 */
 public static function getProtocol(): ProtocolInterface
 {
 return new Version4();
 }
 
 /**
 * @link https://github.com/paseto-standard/paserk/blob/master/operations/PKE.md#v2v4-encryption
 *
 * @param SymmetricKey $ptk
 * @param SealingPublicKey $pk
 * @return string
 *
 * @throws PaserkException
 * @throws SodiumException
 */
 public function seal(SymmetricKey $ptk, SealingPublicKey $pk): string
 {
 $header = static::header();
 $this->assertKeyVersion($pk);
 
 // Step 1:
 $xpk = sodium_crypto_sign_ed25519_pk_to_curve25519($pk->raw());
 
 // Step 2:
 $eph_kp = sodium_crypto_box_keypair();
 $eph_sk = sodium_crypto_box_secretkey($eph_kp);
 $eph_pk = sodium_crypto_box_publickey($eph_kp);
 
 // Step 3:
 $xk = sodium_crypto_scalarmult($eph_sk, $xpk);
 
 // Step 4:
 $Ek = sodium_crypto_generichash(
 PKE::DOMAIN_SEPARATION_ENCRYPT . $header . $xk . $eph_pk . $xpk
 );
 /// @SPEC DETAIL: Prefix is 0x01 for encryption keys
 $Ak = sodium_crypto_generichash(
 PKE::DOMAIN_SEPARATION_AUTH . $header . $xk . $eph_pk . $xpk
 );
 /// @SPEC DETAIL: Prefix is 0x02 for authentication keys
 $nonce = sodium_crypto_generichash($eph_pk . $xpk, '', 24);
 
 $edk = sodium_crypto_stream_xchacha20_xor(
 $ptk->raw(),
 $nonce,
 $Ek
 );
 $tag = sodium_crypto_generichash($header . $eph_pk . $edk, $Ak);
 /// @SPEC DETAIL: h || epk || edk, in that order
 Util::wipe($Ek);
 Util::wipe($nonce);
 Util::wipe($xk);
 Util::wipe($Ak);
 return Base64UrlSafe::encodeUnpadded($tag . $eph_pk . $edk);
 }
 
 /**
 * @link https://github.com/paseto-standard/paserk/blob/master/operations/PKE.md#v2v4-decryption
 *
 * @param string $header
 * @param string $encoded
 * @param SealingSecretKey $sk
 * @return SymmetricKey
 *
 * @throws PaserkException
 * @throws SodiumException
 */
 public function unseal(string $header, string $encoded, SealingSecretKey $sk): SymmetricKey
 {
 $bin = Base64UrlSafe::decode($encoded);
 $tag = Binary::safeSubstr($bin, 0, 32);
 $eph_pk = Binary::safeSubstr($bin, 32, 32);
 $edk = Binary::safeSubstr($bin, 64, 32);
 
 // Step 1:
 if (!hash_equals($header, static::header())) {
 throw new PaserkException('Header mismatch');
 }
 $this->assertKeyVersion($sk);
 
 // Step 2:
 try {
 $xsk = sodium_crypto_sign_ed25519_sk_to_curve25519($sk->raw());
 $xpk = sodium_crypto_sign_ed25519_pk_to_curve25519($sk->getPublicKey()->raw());
 } catch (Exception $ex) {
 throw new PaserkException("Cryptographic error", 0, $ex);
 }
 
 // Step 3:
 $xk = sodium_crypto_scalarmult($xsk, $eph_pk);
 
 // Step 4:
 $Ak = sodium_crypto_generichash(
 PKE::DOMAIN_SEPARATION_AUTH . $header . $xk . $eph_pk . $xpk
 );
 /// @SPEC DETAIL: Prefix is 0x02 for authentication keys
 
 // Step 5:
 $t2 = sodium_crypto_generichash($header . $eph_pk . $edk, $Ak);
 /// @SPEC DETAIL: h || epk || edk
 
 // Step 6:
 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 7:
 $Ek = sodium_crypto_generichash(
 PKE::DOMAIN_SEPARATION_ENCRYPT . $header . $xk . $eph_pk . $xpk
 );
 /// @SPEC DETAIL: Prefix is 0x01 for encryption keys
 
 // Step 8:
 $nonce = sodium_crypto_generichash($eph_pk . $xpk, '', 24);
 
 // Step 9:
 $ptk = sodium_crypto_stream_xchacha20_xor(
 $edk,
 $nonce,
 $Ek
 );
 Util::wipe($Ek);
 Util::wipe($nonce);
 Util::wipe($xk);
 Util::wipe($xsk);
 Util::wipe($Ak);
 
 // Step 10:
 return new SymmetricKey($ptk, $sk->getProtocol());
 }
 }
 
 |