| 
<?phpdeclare(strict_types=1);
 namespace ParagonIE\Paserk\Operations\PBKW;
 
 use ParagonIE\ConstantTime\{
 Base64UrlSafe,
 Binary
 };
 use ParagonIE\HiddenString\HiddenString;
 use ParagonIE\Paserk\Operations\{
 PBKW,
 PBKWInterface
 };
 use ParagonIE\Paserk\PaserkException;
 use ParagonIE\Paseto\KeyInterface;
 use ParagonIE\Paseto\Keys\{
 AsymmetricSecretKey,
 SymmetricKey
 };
 use ParagonIE\Paseto\Protocol\Version4;
 use ParagonIE\Paseto\ProtocolInterface;
 use Exception;
 use SodiumException;
 use TypeError;
 use function
 hash_equals,
 sodium_crypto_generichash,
 sodium_crypto_pwhash,
 sodium_crypto_stream_xchacha20_xor,
 pack,
 random_bytes,
 unpack;
 
 /**
 * Class PBKWv4
 * @package ParagonIE\Paserk\Operations\PBKW
 */
 class PBKWv4 implements PBKWInterface
 {
 /**
 * @return string
 */
 public static function localHeader(): string
 {
 return 'k4.local-pw.';
 }
 
 /**
 * @return string
 */
 public static function secretHeader(): string
 {
 return 'k4.secret-pw.';
 }
 
 
 /**
 * @return ProtocolInterface
 */
 public static function getProtocol(): ProtocolInterface
 {
 return new Version4();
 }
 /**
 * @param KeyInterface $key
 * @param HiddenString $password
 * @param array $options
 * @return string
 *
 * @throws Exception
 * @throws PaserkException
 * @throws SodiumException
 */
 public function wrapWithPassword(
 KeyInterface $key,
 HiddenString $password,
 array $options = []
 ): string {
 if ($key instanceof SymmetricKey) {
 $header = static::localHeader();
 } elseif ($key instanceof AsymmetricSecretKey) {
 $header = static::secretHeader();
 } else {
 throw new PaserkException('Invalid key type');
 }
 
 $ops = $options['opslimit'] ?? SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE;
 $mem = $options['memlimit'] ?? SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE;
 $memPack = pack('J', $mem);
 $opsPack = pack('N', $ops);
 $paraPack = "\x00\x00\x00\x01"; // We can't set this in PHP
 
 // Step 1:
 $salt = random_bytes(16);
 
 // Step 2:
 $preKey = sodium_crypto_pwhash(
 32,
 $password->getString(),
 $salt,
 $ops,
 $mem,
 SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
 );
 
 // Step 3:
 $Ek = sodium_crypto_generichash(PBKW::DOMAIN_SEPARATION_ENCRYPT . $preKey);
 /// @SPEC DETAIL:                ^ Must be prefixed with 0xFF for encryption
 
 // Step 4:
 $Ak = sodium_crypto_generichash(PBKW::DOMAIN_SEPARATION_AUTH . $preKey);
 /// @SPEC DETAIL:                ^ Must be prefixed with 0xFE for authentication
 
 // Step 5:
 $nonce = random_bytes(24);
 
 // Step 6:
 $edk = sodium_crypto_stream_xchacha20_xor(
 $key->raw(),
 $nonce,
 $Ek
 );
 
 // Step 7:
 $tag = sodium_crypto_generichash(
 $header . $salt . $memPack . $opsPack . $paraPack . $nonce . $edk,
 $Ak
 );
 
 // Step 8:
 return Base64UrlSafe::encodeUnpadded(
 $salt . $memPack . $opsPack . $paraPack . $nonce . $edk . $tag
 );
 }
 
 /**
 * @param string $header
 * @param string $wrapped
 * @param HiddenString $password
 * @return KeyInterface
 *
 * @throws Exception
 * @throws PaserkException
 * @throws SodiumException
 * @throws TypeError
 */
 public function unwrapWithPassword(
 string $header,
 string $wrapped,
 HiddenString $password
 ): KeyInterface {
 $decoded = Base64UrlSafe::decode($wrapped);
 $decodedLen = Binary::safeStrlen($decoded);
 
 $salt = Binary::safeSubstr($decoded, 0, 16);
 $memPack = Binary::safeSubstr($decoded, 16, 8);
 $opsPack = Binary::safeSubstr($decoded, 24, 4);
 $paraPack = Binary::safeSubstr($decoded, 28, 4);
 $nonce = Binary::safeSubstr($decoded, 32, 24);
 $edk = Binary::safeSubstr($decoded, 56, $decodedLen - 88);
 $tag = Binary::safeSubstr($decoded, $decodedLen - 32, 32);
 $mem = unpack('J', $memPack)[1];
 $ops = unpack('N', $opsPack)[1];
 // Parallelism is not used in PHP, but we still store it as p=1
 if (!hash_equals($paraPack, "\x00\x00\x00\x01")) {
 // Fail fast if an invalid parameter is provided
 throw new PaserkException("Parallelism > 1 is not supported in PHP");
 }
 
 // Step 2:
 $preKey = sodium_crypto_pwhash(
 32,
 $password->getString(),
 $salt,
 $ops,
 $mem,
 SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
 );
 
 // Step 3:
 $Ak = sodium_crypto_generichash(PBKW::DOMAIN_SEPARATION_AUTH . $preKey);
 /// @SPEC DETAIL:                ^ Must be prefixed with 0xFE for authentication
 
 // Step 4:
 $t2 = sodium_crypto_generichash(
 $header . $salt . $memPack . $opsPack . $paraPack . $nonce . $edk,
 $Ak
 );
 
 // Step 5:
 if (!hash_equals($t2, $tag)) {
 throw new PaserkException('Invalid password or wrapped key');
 }
 /// @SPEC DETAIL: This check must be constant-time.
 
 // Step 6:
 $Ek = sodium_crypto_generichash(PBKW::DOMAIN_SEPARATION_ENCRYPT . $preKey);
 /// @SPEC DETAIL:                ^ Must be prefixed with 0xFF for encryption
 
 // Step 7:
 $ptk = sodium_crypto_stream_xchacha20_xor(
 $edk,
 $nonce,
 $Ek
 );
 
 // Step 8:
 if (hash_equals($header, static::localHeader())) {
 return new SymmetricKey($ptk, static::getProtocol());
 }
 if (hash_equals($header, static::secretHeader())) {
 return new AsymmetricSecretKey($ptk, static::getProtocol());
 }
 throw new TypeError();
 }
 }
 
 |