# Building a Reliable Encryption Utility in PHP Using Libsodium

Encryption is essential when dealing with sensitive data like API keys, personal information, or internal tokens. PHP comes bundled with [libsodium](https://libsodium.org/) — a powerful cryptographic library designed for secure-by-default encryption. To simplify working with it, I chose [Halite](https://github.com/paragonie/halite), a high-level wrapper around libsodium that handles key management, encryption, and decryption using object-oriented interfaces.

### Why This Matters

Many developers focus solely on implementing encryption. But in production systems, you also need to answer:

* Can I trust the data will decrypt reliably every time?
    
* What happens when a key is missing or invalid?
    
* How can I scale this system across environments like local, cloud, or containerized setups?
    
* Is my encryption system testable without hitting actual key stores?
    

In this guide, I’ll walk you through how I built an extensible encryption utility using libsodium and Halite that I use in production to safely store sensitive values like deal notes, contact details, and API tokens.

## Why Libsodium?

Libsodium is a cryptographic library that’s built with one purpose: get security right, without the guesswork. It offers:

* Modern, authenticated encryption (you can’t accidentally decrypt tampered data)
    
* Key generation and memory safety (including secure string wrappers)
    
* Defaults that follow best practices so I don’t have to pick cipher modes or manage IVs
    

Since PHP 7.2, libsodium is bundled into PHP, so I didn’t need to install any extra extensions.

### Generating Keys with Halite

Libsodium provides the raw functions, but Halite simplifies key generation through its factory interface. This uses libsodium’s `sodium_crypto_secretbox_keygen()` under the hood.

To centraliz[e key](https://github.com/paragonie/halite) generation in my project, I wrapped this in a dedicated class:

```php
class HaliteKeyGenerator
{
    public function generateKey(): EncryptionKey
    {
        return KeyFactory::generateEncryptionKey();
    }
}
```

This isolates how keys are created. It also makes unit testing easier because I can inject this generator and mock it if needed.

### Defining a KeyStore Interface

Keys had to be stored either in the local filesystem (for development) or AWS Secrets Manager (for production). To support both without coupling my logic, I created a simple interface.

Here’s the interface:

```php
interface KeyStore
{
    public function getKey(string $keyName): EncryptionKey;

    public function saveKey(string $keyName, EncryptionKey $key): void;
}
```

And a simple implementation for the local filesystem:

```php
class LocalKeyStore implements KeyStore
{
    public function getKey(string $keyName): EncryptionKey
    {
        $file = __DIR__ . \"/keys/{$keyName}.key\";

        if (!file_exists($file)) {
            throw new KeyNotFoundException(\"Key not found: {$keyName}\");
        }

        return KeyFactory::loadEncryptionKey($file);
    }

    public function saveKey(string $keyName, EncryptionKey $key): void
    {
        $file = __DIR__ . \"/keys/{$keyName}.key\";
        KeyFactory::save($key, $file);
    }
}
```

## The CryptoService

At the core is the CryptoService class — this is the part of the system that other developers or services will use. It handles encryption and decryption, and delegates key management to the KeyStore. If the key for `contact_notes` doesn’t exist, it’s generated automatically and stored for future use.

```php
class CryptoService
{
    public function __construct(
        protected KeyStore $store,
        protected HaliteKeyGenerator $generator
    ) {}

    public function encrypt(string $plaintext, string $keyName): string
    {
        try {
            $key = $this->store->getKey($keyName);
        } catch (KeyNotFoundException) {
            $key = $this->generator->generateKey();
            $this->store->saveKey($keyName, $key);
        }

        return base64_encode(Crypto::encrypt(new HiddenString($plaintext), $key));
    }

    public function decrypt(string $cipher, string $keyName): string
    {
        $key = $this->store->getKey($keyName);
        $decoded = base64_decode($cipher, true);

        if ($decoded === false) {
            throw new DecryptionFailedException(\"Invalid base64 ciphertext.\");
        }

        return Crypto::decrypt($decoded, $key)->getString();
    }
}
```

> In an Ideal situation I would rather recommend you throw an exception if a key does not exist and ensure to create the keys upfront.

Usage

```php
$keyStore = new LocalKeyStore(); 
$generator = new HaliteKeyGenerator();
$crypto = new CryptoService($keyStore, $generator); 

$encrypted = $crypto->encrypt('my-secret-value', 'contact_notes'); 
$decrypted = $crypto->decrypt($encrypted, 'contact_notes');
```

## Errors Are Explicit

* If a key is missing and cannot be generated: `KeyNotFoundException`
    
* If the ciphertext is tampered or invalid: `DecryptionFailedException`
    

This makes the service safe by default and predictable in error states.

## Caching for Performance

Each KeyStore implementation caches keys internally for the request lifecycle:

```php
// Inside LocalKeyStore:
private array $cache = [];

public function getKey(string $keyName): EncryptionKey
{
    if (isset($this->cache[$keyName])) {
        return $this->cache[$keyName];
    }

    // Load and cache...
}
```

This is especially helpful in high-performance systems or frameworks that handle hundreds of requests per minute.

## Summary

This architecture balances security, flexibility, and clarity. It’s easy to plug in different key storage options, easy to test each part in isolation, and resistant to common failure modes in encryption systems.

This service now:

* Leverages libsodium (via Halite) for secure, authenticated encryption
    
* Separates concerns across storage, encryption, and key generation
    
* Handles errors clearly and consistently
    
* Can be deployed in both local and cloud environments
    

When building a PHP backend that needs to store sensitive information, I recommend starting with libsodium. It’s simple, safe, and built into PHP. This pattern has worked well in my own production systems and helps keep things secure without over-complicating the implementation.

If you're curious about the full implementation (including key stores and Halite integration), or you’re building something similar and want feedback, feel free to reach out.
