<?php
declare(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());
}
}
|