---
title: "Encrypt and decrypt sensitive data with the cipher service"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:encryption@main"
source: "ApiOverview/Encryption/Index.rst"
rendered: "2026-09-26T10:15:30+00:00"
---

# Encrypt and decrypt sensitive data with the cipher service {#encryption}

<!-- TODO: no Markdown rendering for "versionadded" -->

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\CMS\Core\Crypto\Cipher\CipherService` 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](https://docs.typo3.org/permalink/t3coreapi:password-hashing@main) for it.

**Subpages**

-   [HMAC](https://docs.typo3.org/permalink/t3coreapi:sign-a-value-with-an-hmac-of-the-hash-service@main)

## Derive a key from the TYPO3 encryption key {#encryption-key}

The cipher service needs a key. The
`\TYPO3\CMS\Core\Crypto\Cipher\KeyFactory` derives that key from the
[encryption key](https://docs.typo3.org/permalink/t3coreapi:typo3confvars-sys-encryptionkey@main) of the installation.
Pass a seed to the method
`\TYPO3\CMS\Core\Crypto\Cipher\KeyFactory::deriveSharedKeyFromEncryptionKey()`.
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
`\TYPO3\CMS\Core\Crypto\Cipher\KeyFactory::createSharedKeyFromString()`
takes a key of your own, for example from an environment variable. The method
`\TYPO3\CMS\Core\Crypto\Cipher\KeyFactory::generateSharedKey()`
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 {#encryption-encrypt-and-decrypt}

The method `\TYPO3\CMS\Core\Crypto\Cipher\CipherService::encrypt()`
returns a `\TYPO3\CMS\Core\Crypto\Cipher\CipherValue` 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 `\TYPO3\CMS\Core\Crypto\Cipher\CipherService::decrypt()`
takes the cipher value back. Build it from the stored string with
`\TYPO3\CMS\Core\Crypto\Cipher\CipherValue::fromSerialized()`.
The method throws a
`\TYPO3\CMS\Core\Crypto\Cipher\CipherDecryptionFailedException`
when the key is wrong or when somebody changed the stored value. Catch that
exception.

**packages/my_extension/Classes/Service/TokenEncryptionService.php**

```php
<?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 {#encryption-additional-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.

**packages/my_extension/Classes/Service/AccountTokenEncryptionService.php**

```php
<?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.
