承接 tuzelko/yii2-encrypted-attribute 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

tuzelko/yii2-encrypted-attribute

最新稳定版本:v1.0.0

Composer 安装命令:

composer require tuzelko/yii2-encrypted-attribute

包简介

Transparent attribute encryption (sodium secretbox) for Yii2 ActiveRecord

README 文档

README

Project Status: Active Tests Latest Version PHP Version Total Downloads License

Transparent at-rest encryption for Yii2 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

  • 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 via the DI container
  • Multiple attributes, configurable suffix — one behavior covers any number of columns

Requirements

  • PHP >= 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

composer require tuzelko/yii2-encrypted-attribute

Quick start

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

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

$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

Property Type Default Description
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)
cipherOptions array [] Per-call cipher options, keyed by the cipher's OPTION_* constants (see Associated data)

Ciphers

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

Cipher Algorithm Requires Key Stored format
SodiumSecretboxCipher XSalsa20-Poly1305 ext-sodium 32 bytes base64(nonce[24] || ciphertext)
XChaCha20Poly1305Cipher XChaCha20-Poly1305 (IETF) ext-sodium 32 bytes base64(nonce[24] || ciphertext)
AesGcmCipher AES-256-GCM ext-openssl 32 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

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):

use tuzelko\yii\encryptedattribute\ciphers\XChaCha20Poly1305Cipher;

[
    'class'      => EncryptedAttributeBehavior::class,
    'keyName'    => 'appCrypto',
    'attributes' => ['api_token'],
    'cipher'     => XChaCha20Poly1305Cipher::class,
    'cipherOptions' => [
        // Closures are resolved per operation with ($owner, $attribute)
        XChaCha20Poly1305Cipher::OPTION_AD =>
            static fn ($owner, string $attribute): string => $owner::tableName() . '.' . $attribute,
    ],
]

The same options must reproduce on decrypt, so bind only to immutable context. Note that encryption happens at assignment time: a generated primary key does not exist yet on a new record, so include the PK in the AD only if your application sets it before assigning the encrypted value (e.g. client-generated UUIDs).

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).

How it works

  • Stored format: base64 blob of nonce and ciphertext (exact layout per cipher, see 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

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 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

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

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

Condition Result
keyName or cipher not configured yii\base\InvalidConfigException on behavior init
Key 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

make test

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

License

MIT — see LICENSE.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固