t2pcorp / libcrypto
A PHP library for cryptographic operations with KMS integration, Caching(local,redis) and Secret manager.
Requires
- php: >=7.4 || >=8.0
- ext-json: *
- ext-openssl: *
- ext-sodium: *
- aws/aws-sdk-php: ^3.337
- predis/predis: ^v2.4.0
README
PHP implementation of the LibCrypto library, converted from the original Golang version. This library provides secure encryption/decryption functionality with support for multiple key versions, caching, and AWS integration.
Features
- AES-256-GCM Encryption: Secure encryption using industry-standard algorithms
- Key Versioning: Support for multiple key versions with automatic rotation
- Multi-level Caching: Local and Redis caching for improved performance
- AWS Integration: Support for AWS Secrets Manager and KMS
- Mock Support: Complete mock implementations for testing and development
- Thread-Safe: Singleton pattern ensures thread safety
Directory Structure
PHP/
├── Cache/
│ ├── LocalCache/ # In-memory caching implementation
│ └── RedisCache/ # Redis-based caching implementation
├── Encryption/
│ └── LibKMS/ # AWS KMS encryption services
├── LibAWS/ # AWS configuration and utilities
├── LibConfig/ # Configuration management
│ └── MockConfig/ # Mock configuration for testing
├── Stores/
│ ├── SecretManager/ # AWS Secrets Manager integration
│ └── MockSecretManager/ # Mock secret store for testing
├── Types/ # Interface definitions and data structures
├── Utils/ # Utility functions and helpers
├── tests/ # Comprehensive test suite
├── examples.php # Usage examples
├── LibCrypto.php # Main library entry point
└── run.sh # Test runner script
Installation
Prerequisites
- PHP 8.0 or higher
- Required PHP extensions:
openssljsonmbstring
- Composer (for dependency management)
Dependencies
Install required dependencies using Composer:
composer install
AWS Dependencies (Optional)
For AWS integration, you'll need:
- AWS SDK for PHP
- Predis (for Redis support)
These are automatically installed via Composer.
Quick Start
Basic Usage with Mock Store
<?php
require_once 'vendor/autoload.php';
use LibCrypto\CryptoManager;
use LibCrypto\Types\CryptoConfig;
use LibCrypto\Stores\MockSecretManager\KeyStore as MockKeyStore;
// Create configuration
$config = new CryptoConfig([
'LIBC_SM_NAME' => 'T2P-LIBC-DEK-poc',
'LIBC_SM_REGION' => 'ap-southeat-7'
]);
// Use mock store for testing
$mockStore = new MockKeyStore($config);
// Get CryptoManager instance
$result = CryptoManager::getCryptoManager($config, $mockStore);
if ($result[1] !== null) {
throw new Exception('Failed to initialize: ' . $result[1]);
}
$cryptoManager = $result[0];
// Encrypt data
$plaintext = "Hello, World!";
$encryptResult = $cryptoManager->encrypt($plaintext);
if ($encryptResult[1] !== null) {
throw new Exception('Encryption failed: ' . $encryptResult[1]);
}
$encrypted = $encryptResult[0];
echo "Encrypted: " . $encrypted . "\n";
// Decrypt data
$decryptResult = $cryptoManager->decrypt($encrypted);
if ($decryptResult[1] !== null) {
throw new Exception('Decryption failed: ' . $decryptResult[1]);
}
$decrypted = $decryptResult[0];
echo "Decrypted: " . $decrypted . "\n";
Usage with AWS Integration
<?php
use LibCrypto\Stores\SecretManager\KeyStore as RealKeyStore;
// Configure with AWS credentials
$config = new CryptoConfig([
'LIBC_SM_NAME' => 'your-secret-name',
'LIBC_SM_ID' => 'your-aws-access-key',
'LIBC_SM_SECRET' => 'your-aws-secret-key',
'LIBC_SM_REGION' => 'ap-southeat-7',
'LIBC_CACHE_REDISHOST' => 'your-redis-host',
'LIBC_CACHE_REDISPORT' => '6379'
]);
$realStore = new RealKeyStore($config);
$result = CryptoManager::getCryptoManager($config, $realStore);
// ... use as above
Configuration
Environment Variables
The library supports configuration via environment variables:
LIBC_SM_NAME: AWS Secrets Manager secret nameLIBC_SM_ID: AWS Access Key IDLIBC_SM_SECRET: AWS Secret Access KeyLIBC_SM_ROLE: AWS IAM Role ARN (optional)LIBC_SM_REGION: AWS RegionLIBC_CACHE_REDISHOST: Redis hostLIBC_CACHE_REDISPORT: Redis portLIBC_CACHE_REDISPASS: Redis passwordLIBC_CACHE_REDISDB: Redis database numberLIBC_CACHE_REDISTLS: Enable Redis TLS (true/false)
Configuration Object
$config = new CryptoConfig([
'LIBC_SM_NAME' => 'my-secret',
'LIBC_SM_REGION' => 'us-west-2',
// ... other options
]);
API Reference
CryptoManager
Main class for encryption/decryption operations.
Methods
getCryptoManager(CryptoConfig $config, StoreInterface $store = null, ConfigInterface $libConfig = null): array- Returns
[CryptoManager instance, error] - Singleton pattern - returns the same instance across calls
- Returns
encrypt(string $plaintext): array- Returns
[encrypted_string, error] - Encrypts plaintext using the active key
- Returns
decrypt(string $encrypted): array- Returns
[decrypted_string, error] - Decrypts encrypted data
- Returns
getCipherMeta(string $encrypted): array- Returns
[CipherMeta object, error] - Gets metadata about encrypted data
- Returns
clearCache(): ?string- Returns error message or null
- Clears all cached keys and forces refresh
CipherMeta
Contains metadata about encrypted data:
class CipherMeta {
public string $version; // Key version used
public string $algorithm; // Encryption algorithm
public bool $isSupport; // Whether format is supported
}
Testing
Run All Tests
./run.sh
Run Specific Tests
# Basic examples
./run.sh 1
# Comprehensive tests
./run.sh 2
# Performance benchmark
./run.sh 3
# Installation check
./run.sh 4
Manual Testing
# Run examples
php examples.php
# Run test suite
php tests/LibCryptoTest.php
Performance
The library includes performance benchmarking:
use LibCrypto\Tests\PerformanceTest;
$perfTest = new PerformanceTest($cryptoManager);
$perfTest->benchmarkEncryption(1000); // Run 1000 cycles
Typical performance on modern hardware:
- Encryption: ~2-5ms per operation
- Decryption: ~2-5ms per operation
- Throughput: ~200-500 operations/second
Error Handling
The library uses a Go-style error handling pattern where methods return arrays:
[result, null]on success[null, error_message]on failure
$result = $cryptoManager->encrypt($data);
if ($result[1] !== null) {
// Handle error
echo "Error: " . $result[1];
} else {
// Use result
$encrypted = $result[0];
}
Caching
Local Cache
In-memory caching for single-process applications:
- Fast access
- No external dependencies
- Lost on process restart
Redis Cache
Distributed caching for multi-process/multi-server applications:
- Persistent across restarts
- Shared between processes
- Requires Redis server
Cache Timeout
- Default timeout: 20-24 hours (randomized)
- Automatic refresh on timeout
- Manual refresh via
clearCache()
Security Considerations
- Key Storage: Keys are stored securely in memory when possible
- Network Security: Use TLS for Redis connections in production
- AWS Security: Use IAM roles instead of hardcoded credentials when possible
- Memory Safety: Sensitive data is cleared from memory when possible
AWS Credential Caching
When using AWS IAM role assumption (LIBC_SM_ROLE), the library automatically caches credentials:
- Automatic Caching: Temporary credentials are cached in memory by AWS SDK
- Credential Duration: 1 hour (3600 seconds)
- Auto-Refresh: Credentials are automatically refreshed before expiration
- Rate Limiting Prevention: Eliminates excessive AssumeRole API calls
- No Manual Management: Caching and refresh are handled transparently by the SDK
This significantly improves performance and prevents AWS API rate limiting when the same role is used repeatedly.
PHP-FPM Deployment Considerations
Singleton Verification
Want to verify the singleton is working correctly?
Run the verification script:
php examples/verify-singleton-worker.php
This demonstrates that the singleton persists across requests and provides 100-1500x performance improvement.
📖 Full Documentation: See SINGLETON-VERIFICATION.md for complete verification guide, performance benchmarks, and troubleshooting.
Understanding PHP-FPM Worker Behavior
When running with PHP-FPM, it's important to understand how the library behaves:
Singleton Pattern Persistence with Configuration Change Detection
- CryptoManager uses a singleton pattern -
getInstance()returns the same instance for the same configuration - PHP-FPM workers persist - Unlike CGI, workers handle multiple requests
- Static variables survive - The singleton instance lives for the worker's lifetime
- Configuration change detection - Automatically detects when AWS credentials or configuration changes between requests
- Automatic re-initialization - Creates a new instance if configuration differs from cached instance
- Impact:
- First request initializes the CryptoManager
- Subsequent requests with the same config reuse the instance (optimal performance)
- Requests with different config get a new instance (supports multi-tenant scenarios)
AWS Client Caching (Optimized)
The library implements two-level caching for AWS clients:
Client Instance Caching (KeyStore level):
- AWS Secrets Manager client is cached as a static variable
- Reused across multiple
getSecretConfig()calls in the same worker - Automatically invalidated if AWS configuration changes
Credential Caching (AWS SDK level):
- AssumeRoleCredentialProvider caches temporary credentials
- Credentials valid for 1 hour
- Auto-refresh before expiration
Benefits in PHP-FPM
- Reduced AWS API Calls: AssumeRole called once per hour per worker (not per request)
- Better Performance: Client creation overhead eliminated for subsequent requests
- Cost Savings: Fewer AWS API calls = lower costs
- Rate Limit Protection: Prevents hitting AWS AssumeRole rate limits
Best Practices
Configure Appropriate Worker Count:
; php-fpm pool configuration pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 5 pm.max_spare_servers = 10 pm.max_requests = 500 ; Recycle workers periodicallyWorker Recycling:
- Set
pm.max_requeststo recycle workers periodically - Prevents memory leaks and refreshes cached clients
- Recommended: 500-1000 requests per worker
- Set
Multi-Tenant Considerations:
- Now Supported: Configuration change detection automatically handles different credentials
- When
getInstance()is called with different credentials, a new instance is created - Previous instance is replaced (one active config per worker at a time)
- Best Practices:
- For high-performance single-tenant: Use environment variables (automatic caching)
- For multi-tenant: Pass different configs to
getInstance()per request (automatic switching) - For strict tenant isolation: Use separate PHP-FPM pools per tenant (most secure)
Health Monitoring:
// Check if CryptoManager is initialized $result = CryptoManager::getInstance(); if ($result[1] !== null) { error_log("CryptoManager initialization failed: " . $result[1]); }Graceful Restarts:
# Reload PHP-FPM gracefully to pick up new credentials systemctl reload php-fpm
Clearing Caches
To clear various caches (for testing or credential rotation):
// Option 1: Clear AWS client cache only (keeps CryptoManager instance)
\LibCrypto\Stores\SecretManager\KeyStore::clearClientCache();
// Option 2: Clear CryptoManager's key cache (forces re-fetch from AWS)
[$cryptoManager, $error] = CryptoManager::getInstance();
if ($error === null) {
$cryptoManager->clearCache();
}
// Option 3: Clear CryptoManager singleton instance (forces full re-initialization)
\LibCrypto\CryptoManager::clearInstance();
// Option 4: Clear everything (complete reset)
\LibCrypto\CryptoManager::clearInstance();
\LibCrypto\Stores\SecretManager\KeyStore::clearClientCache();
When to use each option:
- Option 1: When AWS credentials change but CryptoManager config is the same
- Option 2: When secrets in AWS Secrets Manager are updated
- Option 3: When you want to force CryptoManager re-initialization (e.g., testing)
- Option 4: Complete reset, useful for credential rotation or major config changes
Differences from Go Version
Skipped Components
As requested, the following components were not converted:
limitqueuepackage (timeout functionality)- Timeout/refresh queue functionality in config
PHP-Specific Adaptations
- Error Handling: Go-style
[result, error]pattern - Interfaces: PHP interfaces instead of Go interfaces
- Memory Management: PHP garbage collection instead of manual memory management
- Concurrency: Single-threaded execution model
- Dependencies: Composer instead of Go modules
Troubleshooting
Common Issues
Missing PHP Extensions
# Install required extensions (Ubuntu/Debian) sudo apt-get install php-openssl php-json php-mbstringAWS Credentials Not Found
- Set environment variables
- Configure AWS credentials file
- Use IAM roles on EC2
Redis Connection Failed
- Check Redis server is running
- Verify connection parameters
- Check firewall settings
Memory Issues
- Increase PHP memory limit:
ini_set('memory_limit', '512M'); - Use Redis cache for better memory management
- Increase PHP memory limit:
Debug Mode
Enable debug output:
// Add to your code for debugging
error_reporting(E_ALL);
ini_set('display_errors', 1);
Contributing
- Follow PSR-12 coding standards
- Write tests for new functionality
- Update documentation
- Run the test suite before submitting
License
This PHP conversion maintains the same license as the original Go implementation.
Support
For issues specific to the PHP conversion, please check:
- PHP version compatibility (8.0+)
- Required extensions are installed
- Dependencies are up to date
- Configuration is correct
For general LibCrypto issues, refer to the original Go documentation.
LibCrypto PHP Library
A PHP library for cryptographic operations with KMS integration, Caching (local, Redis), and Secret Manager support.
Installation
You can install this library via Composer:
composer require t2pcorp/libcrypto
Usage
Please see example.php for basic usage.
<?php
require_once __DIR__ . '/vendor/autoload.php';
use LibCrypto\CryptoManager;
try {
// Initialize CryptoManager (assumes environment variables are set for configuration)
[$cryptoManager, $error] = CryptoManager::getCryptoManager();
if ($error !== null) {
throw new \Exception("Failed to initialize CryptoManager: " . $error);
}
$plaintext = "Hello, secure world!";
[$encrypted, $encError] = $cryptoManager->encrypt($plaintext);
if ($encError !== null) {
throw new \Exception("Encryption failed: " . $encError);
}
echo "Encrypted: " . $encrypted . "\n";
[$decrypted, $decError] = $cryptoManager->decrypt($encrypted);
if ($decError !== null) {
throw new \Exception("Decryption failed: " . $decError);
}
echo "Decrypted: " . $decrypted . "\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
Publishing to Packagist
To make this library publicly available via Composer, follow these steps to publish it to Packagist.org:
Ensure your code is in a public Git repository:
- Packagist tracks your Git repository (e.g., on GitHub, GitLab, Bitbucket).
- Make sure your library's code, including the
composer.jsonfile and thesrc/directory, is committed and pushed to a public repository.
Sign up/Log in to Packagist:
- Go to Packagist.org.
- Sign up or log in. Using your GitHub account is often the easiest if your code is hosted there.
Submit Your Package:
- Once logged in, click the "Submit" button.
- In the "Repository URL (Git/Svn/Hg)" field, paste the public URL of your Git repository (e.g.,
https://github.com/your-username/libcrypto.git). - Click "Check". Packagist will attempt to read your
composer.json. - If successful and the package name
t2pcorp/libcryptois available, confirm the submission.
Set up Automatic Updates (Webhook):
- After submission, Packagist will show your package page. It's highly recommended to set up automatic updates.
- Packagist will provide a webhook URL.
- Go to your Git repository settings (e.g., GitHub: Settings -> Webhooks -> Add webhook).
- Paste the Payload URL from Packagist, set Content type to
application/json, and select the "Pushes" or "Push event" trigger.
Versioning with Git Tags:
- Composer relies on Git tags for versioning. To release a new version (e.g.,
v1.0.0):- Commit all changes:
git commit -am "Release v1.0.0" - Create a Git tag:
git tag v1.0.0 - Push the tag:
git push origin v1.0.0(orgit push --tags)
- Commit all changes:
- Packagist will be notified (via webhook) and make the new version available.
- Composer relies on Git tags for versioning. To release a new version (e.g.,
🧩 4. Submit Your Package to Packagist A. On Packagist: Go to: https://packagist.org/packages/submit Paste your GitLab repository URL (e.g.): https://gitlab.com/t2plib/libcrypto.git If it’s public, it should just work.
🔄 5. Set Up Auto-Update (Recommended) To allow Packagist to auto-update when you push a new tag or commit:
Option 1: Use GitLab Webhook Go to your GitLab project → Settings → Webhooks
Add this webhook URL:
perl Copy Edit https://packagist.org/api/update-package?username=YOUR_USERNAME&apiToken=YOUR_API_TOKEN You can get apiToken from your Packagist API page
Set it to trigger on Push events.
This README.md was generated based on the composer.json for t2pcorp/libcrypto.
#if have redis if use same redis host settings in CICD-PIPELINE
export LIBC_CACHE_REDISHOST="localhost"
export LIBC_CACHE_REDISPORT="6379"
export LIBC_CACHE_REDISUSER=
export LIBC_CACHE_REDISPASS=""
export LIBC_CACHE_REDISDB="0"
export LIBC_CACHE_REDISTLS="DISABLED"
export LIBC_CACHE_REDISCERT=
#SET by CICD-Pipeline
export LIBC_SM_NAME=T2P-LIBC-DEK-poc
export LIBC_SM_ID=""
export LIBC_SM_SECRET=""
export LIBC_SM_ROLE=""
export LIBC_SM_REGION="ap-southeast-7"
export AWS_DEFAULT_REGION=ap-southeast-7
export AWS_REGION=ap-southeast-1
export AWS_SECRET_ACCESS_KEY=SSO
export AWS_ACCESS_KEY_ID=SSO
export LOG_LEVEL=debug
Local Wrapping Key (LWK) System
LibCrypto uses a deterministic HKDF-based Local Wrapping Key system for per-instance key management. This enables automatic key rotation on instance restart while maintaining cache validity.
Features
- Deterministic Key Derivation: Uses HKDF-SHA256 to derive per-instance keys from instance metadata
- Multi-Environment Support: Works on EC2, ECS Fargate, Lambda, and local development
- Automatic Rotation: New instances automatically get new keys
- Cache Persistence: Worker restarts don't invalidate cached wrapped DEKs
- Fallback Chain: L1 (local memory) → Redis → AWS Secrets Manager
How It Works
Instance Start
↓
Detect Metadata (EC2/ECS/Lambda/Local)
↓
LWK = HKDF-SHA256(instance_id, region, type)
↓
Try L1 Cache → Try Redis → Fetch from Secret Manager
↓
Wrap DEK with LWK using AES-256-GCM
↓
Cache in L1 (memory) and Redis (24hr TTL)
Instance Metadata Detection
| Environment | Metadata Source | Example ID |
|---|---|---|
| EC2 | IMDSv2 | i-1234567890abcdef0 |
| ECS | Task Metadata Endpoint | task-abc123xyz789 |
| Lambda | Environment Variables | myfunction-latest |
| Local | Hostname + PID | local-macbook-12345 |
Cache Key Format
Per-instance cache keys prevent collision and enable isolation:
LIBC_DK:{type}:{instance_id}:{key_version}
Examples:
- LIBC_DK:ec2:i-1234567890ab:v1
- LIBC_DK:ecs:task-abc123:v1
- LIBC_DK:lambda:myfunction-$:v1
Performance
- LWK Derivation: <1ms average
- L1 Cache Hit: ~0.1ms
- Redis Cache Hit: ~2ms
- Secret Manager: ~150ms (only on cache miss)
Impact: Worker restart with same instance ID results in Redis cache hit instead of Secret Manager fetch (100x faster)
Verification
Run the verification script to test LWK derivation:
php examples/verify-lwk-derivation.php
Expected output:
=== Local Wrapping Key (LWK) Derivation Verification ===
✅ Instance metadata detected successfully
✅ LWK derivation is deterministic
✅ Different instances produce different LWKs
✅ Same instance produces same LWK after restart
✅ All tests passed!
Benchmarking
Run performance benchmarks:
php examples/benchmark-lwk.php
Architecture Documentation
For comprehensive documentation, see LWK-ARCHITECTURE.md:
- Detailed system architecture
- Security considerations
- Performance characteristics
- Troubleshooting guide
- Operational best practices
Migration
The LWK system is enabled by default. No configuration changes needed.
Cache Migration:
# Clear old cache format (one-time)
redis-cli KEYS "LIBC_DATAKEYCACHE*" | xargs redis-cli DEL
# Workers will automatically rebuild cache with new per-instance format
Lambda Optimization
Lambda instances automatically skip Redis to prevent key bloat. Cached wrapped DEKs are stored in local memory only for Lambda execution environments.