<?php
declare(strict_types=1);
namespace ParagonIE\Paserk\Operations\PBKW;
use ParagonIE\ConstantTime\{
Base64UrlSafe,
Binary
};
use ParagonIE\HiddenString\HiddenString;
use ParagonIE\Paserk\Util;
use ParagonIE\Paserk\Operations\{
PBKW,
PBKWInterface
};
use ParagonIE\Paserk\PaserkException;
use ParagonIE\Paseto\KeyInterface;
use ParagonIE\Paseto\Keys\{
AsymmetricSecretKey,
SymmetricKey
};
use ParagonIE\Paseto\Protocol\Version3;
use ParagonIE\Paseto\ProtocolInterface;
use Exception;
use TypeError;
use function
hash,
hash_equals,
hash_hmac,
hash_pbkdf2,
openssl_decrypt,
openssl_encrypt,
pack,
random_bytes,
unpack;
/**
* Class PBKWv3
* @package ParagonIE\Paserk\Operations\PBKW
*/
class PBKWv3 implements PBKWInterface
{
/**
* @return string
*/
public static function localHeader(): string
{
return 'k3.local-pw.';
}
/**
* @return string
*/
public static function secretHeader(): string
{
return 'k3.secret-pw.';
}
/**
* @return ProtocolInterface
*/
public static function getProtocol(): ProtocolInterface
{
return new Version3();
}
/**
* @param KeyInterface $key
* @param HiddenString $password
* @param array $options
* @return string
*
* @throws Exception
* @throws PaserkException
*/
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');
}
// Step 1:
$salt = random_bytes(32);
$iterations = $options['iterations'] ?? 100000;
$iterPack = pack('N', $iterations);
// Step 2:
$preKey = hash_pbkdf2('sha384', $password->getString(), $salt, $iterations, 32, true);
// Step 3:
$Ek = Binary::safeSubstr(
hash(
'sha384',
PBKW::DOMAIN_SEPARATION_ENCRYPT . $preKey,
true
),
0,
32
);
/// @SPEC DETAIL: Must be prefixed with 0xFF
// Step 4:
$Ak = hash('sha384', PBKW::DOMAIN_SEPARATION_AUTH . $preKey, true);
/// @SPEC DETAIL: ^ Must be prefixed with 0xFE
// Step 5:
$nonce = random_bytes(16);
// Step 6:
$edk = openssl_encrypt(
$key->raw(),
'aes-256-ctr',
$Ek,
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING,
$nonce
);
// Step 7:
$tag = hash_hmac(
'sha384',
$header . $salt . $iterPack . $nonce . $edk,
$Ak,
true
);
// Step 8:
return Base64UrlSafe::encodeUnpadded(
$salt .
$iterPack .
$nonce .
$edk .
$tag
);
}
/**
* @param string $header
* @param string $wrapped
* @param HiddenString $password
* @return KeyInterface
*
* @throws Exception
*/
public function unwrapWithPassword(
string $header,
string $wrapped,
HiddenString $password
): KeyInterface {
$decoded = Base64UrlSafe::decode($wrapped);
$decodedLen = Binary::safeStrlen($decoded);
// Split into components
$salt = Binary::safeSubstr($decoded, 0, 32);
$iterPack = Binary::safeSubstr($decoded, 32, 4);
$nonce = Binary::safeSubstr($decoded, 36, 16);
$edk = Binary::safeSubstr($decoded, 52, $decodedLen - 100);
$tag = Binary::safeSubstr($decoded, $decodedLen - 48, 48);
$iterations = unpack('N', $iterPack)[1];
// Step 2:
$preKey = hash_pbkdf2('sha384', $password->getString(), $salt, $iterations, 32, true);
// Step 3:
$Ak = hash('sha384', PBKW::DOMAIN_SEPARATION_AUTH . $preKey, true);
/// @SPEC DETAIL: ^ Must be prefixed with 0xFE
// Step 4:
$t2 = hash_hmac(
'sha384',
$header . $salt . $iterPack . $nonce . $edk,
$Ak,
true
);
// Step 5:
if (!hash_equals($t2, $tag)) {
Util::wipe($t2);
Util::wipe($Ak);
throw new PaserkException('Invalid password or wrapped key');
}
/// @SPEC DETAIL: This check must be constant-time.
// Step 6:
$Ek = Binary::safeSubstr(
hash('sha384', PBKW::DOMAIN_SEPARATION_ENCRYPT . $preKey, true),
0,
32
);
/// @SPEC DETAIL: Must be prefixed with 0xFF
// Step 7:
$ptk = openssl_decrypt(
$edk,
'aes-256-ctr',
$Ek,
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING,
$nonce
);
// 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();
}
}
|