Skip to content

CiviCRM Crypto Framework

CiviCRM's cryptography framework is useful for:

  • Storing selected data in encrypted format. For example, if one must store a password or authentication token, then you may encrypt it. In the event that confidiatility is compromised (e.g. SQL injection or exposed SQL backup), an attacker credential can escalate access to other services.

  • Generating signatures. For example, if one integrates with an external system, then you may need to create an authentication token for it. Use cryptographic methods to sign and/or verify the token.

History

  • CiviCRM v3.1 introduced a helper class, CRM_Utils_Crypt. This helper built on mcrypt and CIVICRM_SITE_KEY to provide a basic encryption/decryption mechanism. This was used to encrypt the SMTP password and may have been used by some extensions. It faced challenges in the maintenance of crypto libraries, selection of crypto parameters, and rotation of keys. This helper is now deprecated.

  • CiviCRM v5.34 introduced the current cryptographic components, CryptoRegistry and CryptoToken. This allows storing encrypted credentials. It supports multiple keys, multiple cipher suites, and a more robust data lifecycle. (Note: Some variants of v5.33 may also include a partial backport.)

  • CiviCRM v5.35 introduced CryptoJwt, which builds on CryptoRegistry and provides mechanism to sign and verify JSON Web Tokens.

Configuration

Every site must be configured with unique secret keys. By convention, most keys are configured in civicrm.settings.php. For further information, see System Administrator Guide: Secret keys.

For this developer guide, we will consider how to work the runtime APIs.

Services

The cryptography features are built on a few services:

Service Description
CryptoRegistry Tracks the list of available keys/secrets and ciphers. Also known as Civi::service('crypto.registry').
CryptoToken Performs encryption and decryption using the CTK data-format. This format is suited for storing encrypted values in Civi's MySQL database. Also known as Civi::service('crypto.token).
CryptoJwt Provides digitally signed messages based on JSON Web Tokens. Also known as Civi::service('crypto.jwt').

There is more fine-grained documentation for each service in its respective docblocks. Let's take a look at how these can be used together.

Examples

Encrypt a credential

Suppose you are integrating with an external sytem, Frobulator. To access the Frobulator API, one must present a credential (e.g. username/password, API key, or access token). For simplicity, the Civi-Frobulator extension might store this as a setting:

// Store the Frobulator API key
Civi::settings()->set('frobulatorApiKey', 't0ps3cr37');

// Read the Frobulator API key
$apiKey = Civi::settings()->get('frobulatorApiKey');

However, if there is a breach of database confidentiality (e.g. SQL injection or exposed SQL backup), the API key would be exposed - and then all Frobulator data is at risk!

Remedy this with the CryptoToken service. It provides encrypt() and decrypt() methods:

// Store the Frobulator API key
$encrypted = Civi::service('crypto.token')->encrypt('t0ps3cr37', 'CRED');
Civi::settings()->set('frobulatorApiKey', $encrypted);

// Read the Frobulator API key
$encrypted = Civi::settings()->get('frobulatorApiKey');
$apiKey = Civi::service('crypto.token')->decrypt($encrypted, ['plain', 'CRED']);
What does $encrypted look like?

Encrypted values are strings in ^CTK? format:

^CTK?k=<keyId>&t=<cipherText>

The first character is a control character (chr(2)) which signifies that this value requires extra processing.

The next four characters (CTK?) indicate that this is a cryptographic token with URL-style parameters.

The parameter k (key ID) identifies the active encryption key.

The parameter t is the ciphertext (Base64-URL encoded).

What does CRED mean?

This determines how the data will be encrypted.

In practice, this means that the first value in CIVICRM_CRED_KEYS will be used as the encryption key. It is the responsibility of the system administrator to manage CIVICRM_CRED_KEYS. They should typically provide one key -- but they may provide multiple keys (eg for continuity with old data). They may even give no key (in which case CRED has a fallback for plain-text mode).

Formally, encrypt(...'CRED') will use the CryptoRegistry to lookup any keys with 'tags' => ['CRED'] -- and pick the preferred key.

What if I have existing deployments without encryption?

In this example, we read the value with decrypt(..., ['plain', 'CRED']). This means that it will still read the old, plain-text values.

Similarly, there may be some deployments with a settings-override ($civicrm_settings['domain']['frobulatorApiKey'] = '...';). These values may still be expressed as plain-text.

Finally, it is possible that the sysadmin will change the key in the future. You should listen to hook_civicrm_cryptoRotateKey, eg

function example_civicrm_cryptoRotateKey($tag, $log) {
  if ($tag !== 'CRED') return;

  $new = Civi::service('crypto.token')->rekey(Civi::settings()->get('frobulatorApiKey'));
  if ($new !== NULL) {
    Civi::settings()->set('frobulatorApiKey', $new);
    $log->info("Updated frobulatorApiKey");
  }
}

Register additional keys

A developer may wish to define their own keys, tags, and/or cipher-suites. This can be done using hook_civicrm_crypto to update the crypto registry.

For example, suppose you wanted to encrypt session data for each user with a different key. You could register some key(s) dynamically and tag them as SESSION:

// Register some keys. Tag them as 'SESSION'.
function hook_civicrm_crypto($registry) {
  $registry->addSymmetricKey([
    'key' => '12345678901234567890123456789012',
    'suite' => 'aes-cbc-hs',
    'tags' => ['SESSION'],
    'weight' => -1, // Preferred for new encrypting new values
  ]);
  $registry->addSymmetricKey([
    'key' => 'abcdefghijklmnopqrstuvwxyz123456',
    'suite' => 'aes-cbc-hs',
    'tags' => ['SESSION'],
    'weight' => 10, // Available for decrypting old values
  ]);
}

// Encrypt/decrypt data using one of the 'SESSION' keys.
$encrypted = Civi::service('crypto.token')->encrypt('my-session-data', 'SESSION');
$decrypted = Civi::service('crypto.token')->decrypt($encrypted, 'SESSION');
What is weight for?

The weight determines the priority of the keys to use, lower weighted keys are used first as per the documentation for hook_civicrm_crypto.

Upgrade from CRM_Utils_Crypt

If you are upgrading from an existing implementation, in your extension you should create an upgrade step similar to the CiviCRM upgrade step to re-encrypt SMTP passwords.

Binary data and canBeStored()

It is important to determine as per the canbeStored function in that upgrade step if you still have binary data after running CRM_Utils_Crypt::decrypt as binary data cannot be stored readily in varchar columns.