sessionless-1.x-dev/src/SessionlessEncryptAndSign.php
src/SessionlessEncryptAndSign.php
<?php
namespace Drupal\sessionless;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\sessionless\KeyStorage\KeyStorageInterface;
use Drupal\sessionless\Serialization\JsonSafeCompressingObjectAwareSerializationInterface;
use Drupal\sessionless\Utility\CacheTool;
use Drupal\sessionless\Utility\UnauthenticatedFooterParser;
use ParagonIE\Paserk\Operations\Key\SealingSecretKey;
use ParagonIE\Paserk\PaserkException;
use ParagonIE\Paserk\Types\Seal;
use ParagonIE\Paseto\Builder;
use ParagonIE\Paseto\Exception\PasetoException;
use ParagonIE\Paseto\Keys\SymmetricKey;
use ParagonIE\Paseto\Parser;
/**
* Sessionless encryption service.
*/
final class SessionlessEncryptAndSign implements SessionlessInterface {
public function __construct(
protected KeyStorageInterface $keyStorage,
protected JsonSafeCompressingObjectAwareSerializationInterface $serializer,
protected CacheBackendInterface $cache,
) {}
public function encode(mixed $data): string {
$sealingSecretKey = $this->keyStorage->getSealingSecretKey();
// @todo Add key change cache tag instead.
$cid = serialize([__METHOD__, $sealingSecretKey, $data]);
return CacheTool::getOrCompute($this->cache, $cid,
fn() => $this->doEncode($sealingSecretKey, $data));
}
private function doEncode(SealingSecretKey $sealingSecretKey, mixed $data): string {
$sealer = new Seal($sealingSecretKey->getPublicKey());
$sessionKey = SymmetricKey::generate();
$sealedSessionKey = $sealer->encode($sessionKey);
$dataToken = Builder::getLocal($sessionKey)
->set('data', $this->serializer->encode($data))
->withFooterArray(['wpk' => $sealedSessionKey]);
return $dataToken->toString();
}
public function decode(string $token): mixed {
$sealingSecretKey = $this->keyStorage->getSealingSecretKey();
$cid = serialize([__METHOD__, $sealingSecretKey, $token]);
return CacheTool::getOrCompute($this->cache, $cid,
fn() => $this->doDecode($sealingSecretKey, $token));
}
private function doDecode(SealingSecretKey $sealingSecretKey, string $token): mixed {
// Get sealed session key from raw footer data. This is safe because the
// session key is sealed with our secret private key.
$footerArray = (new UnauthenticatedFooterParser($token))->getFooterArray();
$sealedSessionKey = $footerArray['wpk'] ?? NULL;
if (!$sealedSessionKey) {
return NULL;
}
$sealer = new Seal($sealingSecretKey->getPublicKey(), $sealingSecretKey);
try {
$sessionKey = $sealer->decode($sealedSessionKey);
}
catch (PaserkException) {
return NULL;
}
if (!$sessionKey instanceof SymmetricKey) {
return NULL;
}
try {
$parser = Parser::getLocal($sessionKey);
$jsonToken = $parser->parse($token);
$serialized = $jsonToken->get('data');
// Unserializing is no attack vector, as at this point the cryptographic
// data signature is validated, and whatever classes are unserialized, it
// is us, that put it in there.
$data = $this->serializer->decode($serialized);
}
catch (PasetoException) {
$data = NULL;
}
return $data;
}
}
