Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ciphersweet Encrypt Laravel Package

philetaylor/ciphersweet-encrypt

A Laravel package for integrating CipherSweet field-level encryption, letting you store sensitive database columns as encrypted values while keeping them searchable through blind indexes. It provides Eloquent-friendly configuration and casts for encrypting and decrypting attributes transparently.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Doctrine DBAL custom type that wraps paragonie/ciphersweet: a column mapped to the type is encrypted on write and decrypted on read, and you get blind indexes as a companion mechanism so you can still do exact-match lookups on encrypted data. It is not an Eloquent feature — that distinction drives most of what follows.

Minimal steps

  1. composer require philetaylor/ciphersweet-encrypt. Note the Packagist name does not match the repo name (doctrine-ciphersweet); don't guess.
  2. Read the README, then src/ — specifically the type class's getName() (the string you register under), convertToDatabaseValue(), convertToPHPValue(), and how/where it constructs or receives the CipherSweet engine. That's the whole API surface; it's a small package.
  3. Read CipherSweet's own README for CipherSweet, StringProvider, Field, and BlindIndex. You cannot use this package without understanding those.
  4. Build the engine and register the type before any DB connection is used:
use Doctrine\DBAL\Types\Type;
use ParagonIE\CipherSweet\CipherSweet;
use ParagonIE\CipherSweet\Backend\FIPSCrypto;      // verify backend name for your CipherSweet major
use ParagonIE\CipherSweet\KeyProvider\StringProvider;

$engine = new CipherSweet(
    new StringProvider(base64_decode(config('ciphersweet.key')))
);

Type::addType('ciphersweet', \PhilETaylor\CipherSweetEncrypt\...\Type::class);
// then set the engine/field factory on the type instance, per the README

In Laravel put this in a service provider's register() (not boot()), and guard it with if (!Type::hasType('ciphersweet')) so Octane/queue workers re-running the provider don't double-register.

First use case: a users.ssn column. Map it to the type, add a plain users.ssn_index string column, and on save compute the blind index so WHERE ssn_index = ? finds the row without decrypting anything. That single vertical slice exercises the type, the index, and the key config — get it working end-to-end before touching a second table.

Realistically in a Laravel app: the DBAL type only fires on Doctrine ORM / DBAL hydration paths. If you're on plain Eloquent, skip the package and write a ~40-line custom cast around CipherSweet directly. Use this package if you run laravel-doctrine/orm, or a Doctrine app with a Laravel console/queue layer.

Implementation Patterns

Entity mapping (Doctrine ORM)

/** @ORM\Column(type="ciphersweet", nullable=true) */
private ?string $ssn = null;

/** @ORM\Column(type="string", length=255) */
private ?string $ssnIndex = null;

Encrypted columns must be TEXT/LONGTEXT. Ciphertext plus nonce plus base64 expansion is roughly 2–3× the plaintext plus fixed overhead; varchar(255) silently truncates and you lose data permanently.

Write path: ciphertext + blind index in one transaction

$db->transactional(function () use ($db, $engine, $user) {
    $db->update('users', [
        'ssn'       => $user->ssn,                                        // type encrypts
        'ssn_index' => $engine->getBlindIndex('users.ssn', $user->ssn),  // field name per CipherSweet docs
    ], ['id' => $user->id]);
});

Every write path must set both columns: ORM, seeders, imports, INSERT ... SELECT, raw admin SQL. A stale or missing blind index is the #1 production bug with this approach and is invisible until a lookup misses.

Lookup path

$index = $engine->getBlindIndex('users.ssn', $submittedSsn);
$user  = $db->fetchAssociative('SELECT * FROM users WHERE ssn_index = ?', [$index]);

Never WHERE ssn = ?. CipherSweet is non-deterministic; the same plaintext produces different ciphertext each time. That's the point, and it also means no ORDER BY, no LIKE, no ranges, no GROUP BY on the encrypted column. If you need those, denormalize a separate derived column or a search table.

Multi-tenant keys

Use CipherSweet's MultiTenantProvider and construct/select the engine per tenant, not per request. In a queue or CLI context, resolve the tenant explicitly — there's no ambient tenant in a worker, and if the provider silently falls back to a default key you get rows encrypted under the wrong tenant that can never be read again. Assert the tenant is set.

Version and key separation

Blind indexes and encryption keys are separate secrets in CipherSweet. Keep both in env/KMS, both base64 in a config file, neither in git. For rotation, CipherSweet ships key-rotation helpers — verify the API against the major version pinned here (see Gotchas) and back up before running one against production.

Testing

Pin a fixed test key in phpunit.xml/.env.testing. Then:

  • assert the raw column via DB::selectOne('select ssn from users ...')->ssn is not the plaintext;
  • assert two rows with identical plaintext have different ssn but the same ssn_index (catches "the type never registered" and "the index is salted per-row");
  • assert WHERE ssn_index = ? round-trips.

That trio catches nearly every misconfiguration before it reaches a real database.

Gotchas and Tips

  • DBAL version skew is the big one. The last release is 2020-09-21, which puts it in the CipherSweet v2/v3, DBAL 2.x era. Laravel ships doctrine/dbal ^3 (and 4.x in newer versions) for schema introspection, and both the type-registration API and CipherSweet's backends changed across majors — FIPSCrypto in particular was reworked in later CipherSweet versions. Pin doctrine/dbal and paragonie/ciphersweet to the majors the README was written against, or fork and port.
  • Eloquent silently bypasses it. DB::table('users')->insert(...) and Model::create() go through PDO, not DBAL types, so ciphertext is inserted literally and decrypted on read as garbage (or a TypeError). If most of your app is Eloquent, the DBAL type is the wrong tool — use a cast.
  • Register before the connection is created. Unknown column type "ciphersweet" at boot, or worse, at Schema:: time, means you registered too late or in boot().
  • Nulls. Check convertToPHPValue()'s null handling before adding nullable: true; a null round-tripping as "" will quietly corrupt your blind-index comparisons.
  • Don't SELECT * for bulk exports. Decryption is per-value work with a symmetric cipher; a 100k-row report that used to be cheap can become minutes.
  • Debug with hex. bin2hex($rawColumn) is the fastest way to confirm ciphertext at rest, and comparing two rows' hex is the fastest way to confirm non-determinism.
  • Field names must match. CipherSweet's Field and blind-index names are string identifiers that must line up with what the type constructs internally, or you compute an index over a different field and lookups silently return zero rows. Verify against the type's source, not the README, when they disagree.
  • Key loss = data loss. No recovery path exists. Document the key location in the same runbook as your database backups, and test a restore.
  • Adoption signals are weak: 1 star, 0 dependents, one maintainer, single 2020 release. Treat it as reference implementation rather than infrastructure. Read the ~200 lines, understand the mapping, then either fork it into your app or lift the pattern into a custom Eloquent cast — you'll be maintaining it either way when the CipherSweet major ticks over.
  • Extension points worth knowing: subclass the type to override getName() and inject your engine via a singleton rather than static config; and wrap reads with a decryption-failure catch so a key mismatch surfaces as a loud, logged error instead of a corrupt string flowing into your domain layer.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console