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 onmcrypt
andCIVICRM_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
andCryptoToken
. 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 onCryptoRegistry
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.