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