Encrypt and decrypt sensitive data with the cipher service
New in version 14.0
See Feature: #108002 - Introduce built-in symmetric encryption/decryption cipher service.
An extension sometimes has to store a value that it needs again in plain text,
for example an API token of a third-party service. The
\TYPO3\ encrypts and decrypts
such a value. It uses the XChaCha20-Poly1305 cipher of the PHP extension
sodium. The cipher authenticates the data as well: the service rejects a
value that somebody changed after the encryption.
Do not use the cipher service for a password of a website user. A password is verified, not decrypted. Use password hashing for it.
Subpages
Derive a key from the TYPO3 encryption key
The cipher service needs a key. The
\TYPO3\ derives that key from the
encryption key of the installation.
Pass a seed to the method
Key.
The seed names the purpose of the key, for example the class that uses it.
Each seed produces a different key.
Warning
The cipher service cannot decrypt the value after somebody changed the encryption key of the installation. Keep a backup of the encryption key.
The factory also creates a key without the encryption key. The method
Key
takes a key of your own, for example from an environment variable. The method
Key
returns a new random key. Store such a key yourself, otherwise the data stays
encrypted forever.
Encrypt and decrypt a value with the cipher service
The method
Cipher
returns a
\TYPO3\ object. Cast that
object to a string to store it. Each call returns a different string, because
the service uses a new random nonce every time.
The method
Cipher
takes the cipher value back. Build it from the stored string with
Cipher.
The method throws a
\TYPO3\
when the key is wrong or when somebody changed the stored value. Catch that
exception.
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use TYPO3\CMS\Core\Crypto\Cipher\CipherDecryptionFailedException;
use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\CipherValue;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;
final readonly class TokenEncryptionService
{
public function __construct(
private CipherService $cipherService,
private KeyFactory $keyFactory,
) {}
public function encryptToken(string $token): string
{
$key = $this->keyFactory->deriveSharedKeyFromEncryptionKey(self::class);
return (string)$this->cipherService->encrypt($token, $key);
}
public function decryptToken(string $storedToken): string
{
$key = $this->keyFactory->deriveSharedKeyFromEncryptionKey(self::class);
try {
$cipherValue = CipherValue::fromSerialized($storedToken);
return $this->cipherService->decrypt($cipherValue, $key);
} catch (CipherDecryptionFailedException $exception) {
throw new \RuntimeException(
'The stored token cannot be decrypted',
1758700800,
$exception,
);
}
}
}
Bind encrypted data to a context with additional authenticated data
Both methods take additional authenticated data as a third argument. The service does not encrypt this data. It includes the data in the integrity check instead. The decryption therefore only succeeds when the caller passes the same data again.
Use this argument to bind a value to the record that it belongs to. The example stores an API token in an account record. It passes the table name and the uid of the record as additional data.
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\Service;
use TYPO3\CMS\Core\Crypto\Cipher\CipherService;
use TYPO3\CMS\Core\Crypto\Cipher\CipherValue;
use TYPO3\CMS\Core\Crypto\Cipher\KeyFactory;
final readonly class AccountTokenEncryptionService
{
private const TABLE = 'tx_myextension_domain_model_account';
public function __construct(
private CipherService $cipherService,
private KeyFactory $keyFactory,
) {}
public function encryptToken(string $token, int $accountUid): string
{
$key = $this->keyFactory->deriveSharedKeyFromEncryptionKey(self::class);
$cipherValue = $this->cipherService->encrypt(
$token,
$key,
$this->buildContext($accountUid),
);
return (string)$cipherValue;
}
public function decryptToken(string $storedToken, int $accountUid): string
{
$key = $this->keyFactory->deriveSharedKeyFromEncryptionKey(self::class);
// A token of another account record throws a
// CipherDecryptionFailedException here
return $this->cipherService->decrypt(
CipherValue::fromSerialized($storedToken),
$key,
$this->buildContext($accountUid),
);
}
private function buildContext(int $accountUid): string
{
return self::TABLE . ':' . $accountUid;
}
}
Somebody who copies the stored token of account 5 into account 7 gains nothing. The service builds the additional data from the uid of account 7 and refuses to decrypt the token.