PHPackages                             tuzelko/yii2-encrypted-attribute - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. tuzelko/yii2-encrypted-attribute

ActiveYii2-extension[Security](/categories/security)

tuzelko/yii2-encrypted-attribute
================================

Transparent attribute encryption (sodium secretbox) for Yii2 ActiveRecord

v1.0.0(1mo ago)090↓60%MITPHPPHP &gt;=8.0CI passing

Since Jun 10Pushed 1mo agoCompare

[ Source](https://github.com/TuzelKO/yii2-encrypted-attribute)[ Packagist](https://packagist.org/packages/tuzelko/yii2-encrypted-attribute)[ RSS](/packages/tuzelko-yii2-encrypted-attribute/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (3)Versions (2)Used By (0)

Yii2 Encrypted Attribute extension
==================================

[](#yii2-encrypted-attribute-extension)

[![Project Status: Active](https://camo.githubusercontent.com/39c688bf243eeb6d3bfc529dcf3cb27443613deb696c8fa9f49bccf1e63e3bef/68747470733a2f2f7777772e7265706f7374617475732e6f72672f6261646765732f6c61746573742f6163746976652e737667)](https://www.repostatus.org/#active)[![Tests](https://github.com/TuzelKO/yii2-encrypted-attribute/actions/workflows/tests.yml/badge.svg)](https://github.com/TuzelKO/yii2-encrypted-attribute/actions/workflows/tests.yml)[![Latest Version](https://camo.githubusercontent.com/b6b7cd946cbcaec48c9c568b17273a75beff8e0f6bf366aa1789abcd0e139983/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f74757a656c6b6f2f796969322d656e637279707465642d617474726962757465)](https://packagist.org/packages/tuzelko/yii2-encrypted-attribute)[![PHP Version](https://camo.githubusercontent.com/65cd2e11b4e26c7294a09e24e9a417cbcdf0efa955f621368c7ecc438a095708/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f74757a656c6b6f2f796969322d656e637279707465642d6174747269627574652f706870)](https://packagist.org/packages/tuzelko/yii2-encrypted-attribute)[![Total Downloads](https://camo.githubusercontent.com/e3cad0a2ed6bbb4221fa27979a0f9c2f795a9e259e564e6c4816b10e784ce901/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f74757a656c6b6f2f796969322d656e637279707465642d617474726962757465)](https://packagist.org/packages/tuzelko/yii2-encrypted-attribute)[![License](https://camo.githubusercontent.com/11533d1cdde64bd059f0ea2a476e491ebf347083709d161eeba3970e84946a8f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f54757a656c4b4f2f796969322d656e637279707465642d617474726962757465)](https://github.com/TuzelKO/yii2-encrypted-attribute/blob/main/LICENSE)

Transparent at-rest encryption for [Yii2](https://www.yiiframework.com/) `ActiveRecord` attributes with pluggable ciphers (libsodium secretbox or AES-256-GCM via openssl, the choice is always explicit).

The physical attribute always holds ciphertext; plain values are accessed through a virtual `{attribute}_decrypted` sibling. The model can therefore be serialized to cache, logged, or dumped without ever exposing plaintext — and the encryption key is never stored on the model or the behavior.

Features
--------

[](#features)

- **Virtual accessors** — `$model->secret_decrypted = 'raw'` encrypts; reading it decrypts; the `secret` column only ever sees ciphertext
- **Authenticated encryption only** — tampered ciphertext fails loudly, never decrypts to garbage
- **Pluggable ciphers** — XSalsa20-Poly1305 secretbox or AES-256-GCM, always an explicit choice; custom algorithms via one interface
- **Random nonce per write** — equal plaintexts produce different ciphertexts
- **Cache-safe** — serialized models carry neither plaintext nor key material
- **Key management included** — keys are resolved by name from [tuzelko/yii2-key-storage](https://github.com/TuzelKO/yii2-key-storage) via the DI container
- **Multiple attributes, configurable suffix** — one behavior covers any number of columns

Requirements
------------

[](#requirements)

- PHP &gt;= 8.0
- ext-sodium (`SodiumSecretboxCipher`) **or** ext-openssl (`AesGcmCipher`) — checked lazily, only the extension of the cipher you actually use is needed
- yiisoft/yii2 ~2.0
- tuzelko/yii2-key-storage ^1.0 (installed automatically)

Installation
------------

[](#installation)

```
composer require tuzelko/yii2-encrypted-attribute
```

Quick start
-----------

[](#quick-start)

### 1. Register a key provider in the DI container

[](#1-register-a-key-provider-in-the-di-container)

```
// config/main.php
use tuzelko\yii\keystorage\KeyProviderInterface;
use tuzelko\yii\keystorage\KeyStorage;
use tuzelko\yii\keystorage\types\SodiumSecretboxKey;

'container' => [
    'singletons' => [
        KeyProviderInterface::class => static fn () => new KeyStorage([
            'keys' => [
                'appCrypto' => [
                    'base64' => getenv('APP_CRYPTO_KEY'), // 32 bytes, base64-encoded
                    'type'   => SodiumSecretboxKey::class,
                ],
            ],
        ]),
    ],
],
```

Generate a key:

```
php -r "echo base64_encode(random_bytes(32)), PHP_EOL;"
```

### 2. Attach the behavior

[](#2-attach-the-behavior)

The encrypted column must be a `TEXT`/`VARCHAR` (the stored value is base64).

```
use tuzelko\yii\encryptedattribute\ciphers\SodiumSecretboxCipher;
use tuzelko\yii\encryptedattribute\EncryptedAttributeBehavior;
use yii\db\ActiveRecord;

class Integration extends ActiveRecord
{
    public function behaviors(): array
    {
        return [
            [
                'class'      => EncryptedAttributeBehavior::class,
                'keyName'    => 'appCrypto',
                'attributes' => ['api_token', 'api_secret'],
                'cipher'     => SodiumSecretboxCipher::class,
            ],
        ];
    }
}
```

### 3. Use the virtual accessors

[](#3-use-the-virtual-accessors)

```
$integration = new Integration();
$integration->api_token_decrypted = $rawToken;   // encrypted on assignment
$integration->save();

echo $integration->api_token_decrypted;          // decrypted on read
echo $integration->api_token;                    // base64(nonce || ciphertext) — safe to log

$integration->api_token_decrypted = null;        // clears the column
```

Configuration
-------------

[](#configuration)

PropertyTypeDefaultDescription`attributes``array``[]`Physical column names to virtualize`keyName``string`— *(required)*Key name resolved through `KeyProviderInterface::getRaw()``suffix``string``'_decrypted'`Suffix of the virtual accessor`cipher``CipherInterface|class-string`— *(required)*Encryption algorithm (see [Ciphers](#ciphers))`cipherOptions``array``[]`Static per-call cipher options, keyed by the cipher's `OPTION_*` constants (see [Associated data](#associated-data-and-cipher-options))`cipherOptionMethods``array``[]`Dynamic per-call cipher options: option key =&gt; owner method name, called with the attribute name per operationCiphers
-------

[](#ciphers)

There is intentionally **no default cipher** — the algorithm a model uses must be visible in its configuration.

CipherAlgorithmRequiresKeyStored format`SodiumSecretboxCipher`XSalsa20-Poly1305ext-sodium32 bytes`base64(nonce[24] || ciphertext)``XChaCha20Poly1305Cipher`XChaCha20-Poly1305 (IETF)ext-sodium32 bytes`base64(nonce[24] || ciphertext)``AesGcmCipher`AES-256-GCMext-openssl32 bytes`base64(nonce[12] || tag[16] || ciphertext)`All three are authenticated (AEAD) and use a fresh random nonce per write; nonce sizes are collision-safe for random generation. Algorithms with short nonces unsafe for random use (ChaCha20-Poly1305 IETF) or without built-in authentication (AES-CBC) are intentionally not shipped.

```
use tuzelko\yii\encryptedattribute\ciphers\AesGcmCipher;

[
    'class'      => EncryptedAttributeBehavior::class,
    'keyName'    => 'appCrypto',
    'attributes' => ['api_token'],
    'cipher'     => AesGcmCipher::class,
]
```

The extension check happens lazily, when the cipher is first used — environments without ext-sodium can install the package and use `AesGcmCipher` freely.

A custom algorithm is a class implementing `CipherInterface` (`encrypt`/`decrypt`, authenticated encryption with a fresh random nonce per call). Extend `AbstractCipher` to get strict option validation for free.

Associated data and cipher options
----------------------------------

[](#associated-data-and-cipher-options)

Each cipher declares the per-call options it understands as its own `OPTION_*` class constants; unknown options are rejected with an exception, never silently ignored. The AEAD ciphers (`XChaCha20Poly1305Cipher`, `AesGcmCipher`) support **associated data** — authenticated but unencrypted context the ciphertext is cryptographically bound to. `SodiumSecretboxCipher` predates the AEAD interface and supports no options.

Binding a value to its column prevents transplanting ciphertext between columns or tables (an attacker with DB write access cannot swap one valid encrypted value for another).

Options come in two flavors: static literals in `cipherOptions`, and dynamic values in `cipherOptionMethods` — owner method names (validator-style), called on every operation with the physical attribute name:

```
use tuzelko\yii\encryptedattribute\ciphers\XChaCha20Poly1305Cipher;

public function behaviors(): array
{
    return [
        [
            'class'      => EncryptedAttributeBehavior::class,
            'keyName'    => 'appCrypto',
            'attributes' => ['api_token'],
            'cipher'     => XChaCha20Poly1305Cipher::class,
            'cipherOptionMethods' => [
                XChaCha20Poly1305Cipher::OPTION_AD => 'encryptionContext',
            ],
        ],
    ];
}

public function encryptionContext(string $attribute): string
{
    return self::tableName() . '.' . $attribute;
}
```

**Closures are deliberately not supported** (rejected at init): behaviors are serialized together with their owner, so a Closure in the configuration would make the model uncacheable — `serialize($model)` throws. Method names are plain strings and keep the owner fully serializable.

The same options must reproduce on decrypt, so derive them only from **immutable** context:

- Encryption happens at assignment time: a generated primary key does not exist yet on a new record. Include the PK in the AD only if your application sets it before assigning the encrypted value (e.g. client-generated UUIDs).
- AD cannot be retrofitted: values encrypted without AD decrypt only without AD (and vice versa). Adding or changing AD on a column with existing data requires re-encrypting the rows — the same procedure as [key rotation](#key-rotation). Decide on AD before the first real data lands in the column.

> **Note:** the stored value does not identify the cipher that produced it. The cipher choice is deliberately explicit configuration, never auto-detected from the environment — otherwise the same model could silently encrypt differently on different hosts. Switching ciphers requires re-encrypting existing rows (same procedure as [key rotation](#key-rotation)).

How it works
------------

[](#how-it-works)

- **Stored format**: base64 blob of nonce and ciphertext (exact layout per cipher, see [Ciphers](#ciphers)); the nonce is generated fresh for every assignment.
- **Key resolution is lazy**: the key is fetched from the container-registered `KeyProviderInterface` at encrypt/decrypt time, never kept on the behavior or the owner. A model serialized into Redis/Valkey (or any cache) contains only ciphertext.
- **Authentication**: secretbox is authenticated encryption — any modification of the stored value raises a `RuntimeException` (`authentication tag mismatch`) instead of returning corrupted plaintext.

Why the key cannot be passed directly
-------------------------------------

[](#why-the-key-cannot-be-passed-directly)

The behavior deliberately accepts only a key **name** — there is no `key` property and never will be. This is not an inconvenience, it is the security model:

In Yii2, behaviors are serialized together with their owner. If the raw key (or anything that memoizes it) lived on the behavior, then every `serialize($model)` — caching an AR model in Redis/Valkey, storing it in a session, queueing it in a job payload — would write the encryption key right next to the ciphertext it protects. One `KEYS *` away from a full decrypt.

With the key-storage indirection the serialized model carries only the key *name* (a meaningless string), while the actual bytes live in the [tuzelko/yii2-key-storage](https://github.com/TuzelKO/yii2-key-storage) component inside the DI container and are resolved at encrypt/decrypt time. The test suite asserts this invariant: a serialized model contains neither plaintext nor key material.

Searching and indexing
----------------------

[](#searching-and-indexing)

Ciphertexts are randomized, so encrypted columns **cannot be searched, compared, or indexed** by plaintext. Keep values that must be queryable out of `attributes`, or store a separate deterministic digest (e.g. HMAC) alongside for lookups.

Key rotation
------------

[](#key-rotation)

The behavior intentionally has no multi-key fallback. To rotate a key: add the new key under a new name, re-encrypt existing rows (`read with old keyName → write with new keyName`), then drop the old key. Doing this in a console migration keeps the window where both keys exist explicit and short.

Error handling
--------------

[](#error-handling)

ConditionResult`keyName` or `cipher` not configured`yii\base\InvalidConfigException` on behavior initKey unknown / malformed / wrong length`tuzelko\yii\keystorage\InvalidKeyException`Tampered or corrupted ciphertext, wrong key or associated data`RuntimeException` (authentication tag mismatch)Unknown / unsupported / invalid cipher option`InvalidArgumentException`Running tests
-------------

[](#running-tests)

```
make test
```

Tests run inside Docker (PHP 8.3 + SQLite) with no local setup required.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

46d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/38176435?v=4)[Eugene Frost](/maintainers/TuzelKO)[@TuzelKO](https://github.com/TuzelKO)

---

Top Contributors

[![TuzelKO](https://avatars.githubusercontent.com/u/38176435?v=4)](https://github.com/TuzelKO "TuzelKO (1 commits)")

---

Tags

securityencryptionyii2extensionBehavioractiverecordsodium

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/tuzelko-yii2-encrypted-attribute/health.svg)

```
[![Health](https://phpackages.com/badges/tuzelko-yii2-encrypted-attribute/health.svg)](https://phpackages.com/packages/tuzelko-yii2-encrypted-attribute)
```

###  Alternatives

[craftcms/cms

Craft CMS

3.6k3.6M3.2k](/packages/craftcms-cms)[nickcv/yii2-encrypter

Openssl Encrypter for Yii2

19680.1k1](/packages/nickcv-yii2-encrypter)[skeeks/cms

SkeekS CMS — control panel and tools based on php framework Yii2

13725.8k63](/packages/skeeks-cms)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
