The Encryption class provides cryptographic utilities for your application — symmetric encryption, password-based encryption, secure password hashing, and random value generation. All encryption uses AES-256-GCM, an authenticated encryption mode that provides both confidentiality and tamper detection in a single operation. No separate HMAC step is needed.
- Class:
JiFramework\Core\Security\Encryption - Access:
$app->encryption
Features at a glance:
- Key-based encryption —
encrypt(),decrypt() - Password-based encryption —
encryptWithPassword(),decryptWithPassword() - Key management —
generateKey(),generateKeyFromPassword() - Password hashing —
hashPassword(),verifyPassword(),needsRehash() - Cryptographically secure random —
randomBytes(),randomString(),randomInt() - Fresh random nonce on every encrypt call — no IV reuse possible
- Authentication built into GCM — tampered data is always rejected
$app = new App();
// Generate a key once — store it in your .env file
$key = $app->encryption->generateKey();
// Encrypt
$ciphertext = $app->encryption->encrypt('Sensitive data', $key);
// Decrypt
$plaintext = $app->encryption->decrypt($ciphertext, $key);
// "Sensitive data"
generateKey(): string
Generates a cryptographically secure random 256-bit encryption key. Returns a 64-character hexadecimal string. Run this once per environment and store the result in an environment variable or secrets manager — never in source code or the database.
// Run once to generate your app key
$key = $app->encryption->generateKey();
// "c462d811ac9c5d35c0ea224b54a0ee430c47db4a05c3ee2ae19e12b85b27d7fc"
// Store it in your .env file:
// APP_ENCRYPTION_KEY=c462d811ac9c5d35...
// Load it at runtime
$key = getenv('APP_ENCRYPTION_KEY');
Each call returns a unique key. Never reuse a key across environments. If a key is ever exposed, generate a new one and re-encrypt any affected data.
generateKeyFromPassword(string $password, ?string $salt = null, int $iterations = 100000): array
Derives a deterministic 256-bit encryption key from a password using PBKDF2-SHA256. The same password and salt always produce the same key. Returns an associative array with key and salt, both as hexadecimal strings.
$password— (string) A password of any length.$salt— (string|null, optional) A salt in hexadecimal. A fresh random 128-bit salt is generated whennull.$iterations— (int, optional) PBKDF2 iteration count. Higher = slower = more brute-force resistant. Default: 100,000.
Always store the returned salt alongside any encrypted data so the key can be re-derived for decryption later.
// Derive a new key (random salt generated automatically)
$result = $app->encryption->generateKeyFromPassword('my-passphrase');
// $result['key'] → "df1d7ead96812f9d..." (64 hex chars)
// $result['salt'] → "f24a3151a0b294df..." (32 hex chars)
$encrypted = $app->encryption->encrypt($data, $result['key']);
// Store $encrypted and $result['salt'] together
// Re-derive the same key later using the stored salt
$result2 = $app->encryption->generateKeyFromPassword('my-passphrase', $result['salt']);
$decrypted = $app->encryption->decrypt($encrypted, $result2['key']);
Note: encryptWithPassword() handles salt management automatically. Use generateKeyFromPassword() only when you need direct access to the derived key.
encrypt(string $plaintext, string $key): string
Encrypts data using AES-256-GCM. A fresh random nonce is generated on every call, so encrypting the same plaintext twice always produces a different ciphertext. Authentication is built in — any tampering with the output will cause decrypt() to return false.
$plaintext— (string) The data to encrypt. Any length, including empty string.$key— (string) A 64-character hex string (32 bytes / 256 bits). UsegenerateKey()to produce one.
Returns a base64-encoded string containing the nonce, authentication tag, and ciphertext. Throws \InvalidArgumentException if the key is not a valid 64-character hex string, and \RuntimeException if the underlying OpenSSL call fails.
$key = getenv('APP_ENCRYPTION_KEY');
// Encrypt a value
$ciphertext = $app->encryption->encrypt('Hello, World!', $key);
// "iIIWPc2TqtEfDaMraUwjrn7bzKxLPamz..."
// Encrypting the same value twice produces different ciphertext (random nonce)
$ct1 = $app->encryption->encrypt('same data', $key);
$ct2 = $app->encryption->encrypt('same data', $key);
// $ct1 !== $ct2 — both decrypt to "same data"
Output format (decoded from base64): nonce[12 bytes] + tag[16 bytes] + ciphertext[n bytes]. Total overhead is 28 bytes above plaintext length.
decrypt(string $ciphertext, string $key): string|false
Decrypts a ciphertext produced by encrypt(). Returns the original plaintext on success, or false when decryption or authentication fails. Always check the return value before using the result.
$ciphertext— (string) The base64-encoded ciphertext fromencrypt().$key— (string) The same 64-character hex key used to encrypt.
Returns false when:
- The key is wrong
- The ciphertext has been tampered with (GCM authentication tag mismatch)
- The input is not valid base64 or is too short to be a valid ciphertext
$key = getenv('APP_ENCRYPTION_KEY');
$plaintext = $app->encryption->decrypt($ciphertext, $key);
if ($plaintext === false) {
// Decryption failed — wrong key, corrupted, or tampered data
$app->abort(400, 'Could not decrypt the data.');
}
// Safe to use $plaintext here
echo $plaintext;
Throws \InvalidArgumentException if $key is not a valid 64-character hex string.
encryptWithPassword(string $plaintext, string $password, int $iterations = 100000): string
Encrypts data using a human-supplied password. The password is stretched into a 256-bit key using PBKDF2-SHA256 with a fresh random salt. The salt is embedded in the output automatically — no separate salt storage needed.
$plaintext— (string) The data to encrypt.$password— (string) A password of any length.$iterations— (int, optional) PBKDF2 iteration count. Higher = slower = more brute-force resistant. Default: 100,000.
Returns a base64-encoded string. Each call produces unique output even for identical inputs (random salt and random nonce). Throws \RuntimeException if the underlying OpenSSL call fails.
$passphrase = $_POST['passphrase'];
// Encrypt a document with the user's passphrase
$encrypted = $app->encryption->encryptWithPassword($fileContents, $passphrase);
// Store $encrypted — the salt is embedded, nothing else to save
Output format (decoded from base64): salt[16 bytes] + nonce[12 bytes] + tag[16 bytes] + ciphertext[n bytes]. Total overhead is 44 bytes above plaintext length.
When to use this vs encrypt(): use encryptWithPassword() when a human supplies the passphrase (user-driven encryption). Use encrypt() when your app manages the key (app-level encryption). Applying PBKDF2 to an already-secure random key wastes CPU for no benefit.
decryptWithPassword(string $ciphertext, string $password, int $iterations = 100000): string|false
Decrypts a ciphertext produced by encryptWithPassword(). Extracts the embedded salt, re-derives the key from the password, and decrypts. Returns the original plaintext on success, or false on any failure.
$ciphertext— (string) The base64-encoded ciphertext fromencryptWithPassword().$password— (string) The same password used to encrypt.$iterations— (int, optional) Must match the value used during encryption. Default: 100,000.
Returns false when the password is wrong, the ciphertext is tampered, the data is too short, or the input is not valid base64.
$decrypted = $app->encryption->decryptWithPassword($encrypted, $_POST['passphrase']);
if ($decrypted === false) {
// Wrong passphrase or corrupted data
$app->abort(400, 'Incorrect passphrase or corrupted file.');
}
// Safe to use $decrypted here
hashPassword(string $password): string
Hashes a password for secure storage using bcrypt (PASSWORD_DEFAULT). A random salt is generated automatically on every call, so hashing the same password twice produces a different hash. The returned string is safe to store directly in the database.
$password— (string) The plaintext password to hash.
Never store plaintext passwords. Never encrypt passwords — always hash them. Encryption is reversible; hashing is not.
// On registration
$hash = $app->encryption->hashPassword($_POST['password']);
// "$2y$10$wvb9Dc8xCxbsCEG7DM1bJO..."
// Store $hash in the database — not the plaintext password
$app->db->table('users')->insert([
'email' => $_POST['email'],
'password_hash' => $hash,
]);
verifyPassword(string $password, string $hash): bool
Verifies a plaintext password against a stored bcrypt hash. Uses a constant-time comparison internally to prevent timing attacks. Returns true when the password matches the hash, false otherwise.
$password— (string) The plaintext password to verify.$hash— (string) The stored bcrypt hash fromhashPassword().
// On login
$user = $app->db->table('users')->where('email', $email)->first();
if (!$user || !$app->encryption->verifyPassword($_POST['password'], $user['password_hash'])) {
$app->abort(401, 'Invalid email or password.');
}
// Login successful
needsRehash(string $hash): bool
Returns true when a stored password hash was created with an outdated algorithm or a lower bcrypt cost factor than the current default. Call this after every successful login and silently upgrade the hash if needed. This future-proofs your password storage — when PHP raises the default bcrypt cost, existing users are upgraded transparently on their next login without any manual intervention.
$hash— (string) The stored password hash to inspect.
// After a successful login, check if the hash should be upgraded
if ($app->encryption->needsRehash($user['password_hash'])) {
$newHash = $app->encryption->hashPassword($_POST['password']);
$app->db->table('users')
->where('id', $user['id'])
->update(['password_hash' => $newHash]);
}
randomBytes(int $length): string
Generates $length cryptographically secure random bytes and returns them as a hexadecimal string. The returned string is $length × 2 characters long. Safe for generating API keys, CSRF tokens, password reset tokens, and any value that must be unpredictable.
$length— (int) Number of random bytes to generate.
// 32-byte (256-bit) token — returns a 64-char hex string
$token = $app->encryption->randomBytes(32);
// "82a7a0d334d3621b4bd97d6cd795a0a9e3b71c2f..."
// Typical use: password reset token
$resetToken = $app->encryption->randomBytes(32);
$app->db->table('password_resets')->insert([
'email' => $email,
'token' => $resetToken,
'expires_at' => date('Y-m-d H:i:s', strtotime('+1 hour')),
]);
randomString(int $length): string
Generates a cryptographically secure random alphanumeric string of exactly $length characters. Uses only URL-safe characters (A–Z, a–z, 0–9) — no special characters that require URL encoding. Suitable for invite codes, temporary passwords, readable tokens, and slugs.
$length— (int) Number of characters to generate.
// Short invite code
$code = $app->encryption->randomString(8);
// "wTyuZXDh"
// Longer session token (URL-safe)
$token = $app->encryption->randomString(32);
// "wTyuZXDhfqyO4ihY5Q6GoqMNbRtLpKcE"
// Temporary password
$tempPass = $app->encryption->randomString(12);
randomInt(int $min, int $max): int
Generates a cryptographically secure random integer between $min and $max (both inclusive). Use this instead of rand() or mt_rand() for any security-sensitive integer generation — those functions are not cryptographically secure.
$min— (int) Minimum value (inclusive).$max— (int) Maximum value (inclusive).
// 6-digit OTP
$otp = '';
for ($i = 0; $i < 6; $i++) {
$otp .= $app->encryption->randomInt(0, 9);
}
// "482910"
// Random number in a range
$n = $app->encryption->randomInt(1, 100);
// Pick a random array element securely
$items = ['Alice', 'Bob', 'Carol'];
$winner = $items[$app->encryption->randomInt(0, count($items) - 1)];
Encrypting sensitive database columns
// Store APP_ENCRYPTION_KEY in your .env — generate once with generateKey()
$key = getenv('APP_ENCRYPTION_KEY');
// Save — encrypt before storing
$app->db->table('users')->insert([
'name' => $name,
'email' => $email,
'ssn' => $app->encryption->encrypt($ssn, $key),
]);
// Read — decrypt after fetching
$user = $app->db->table('users')->where('id', $id)->first();
$ssn = $app->encryption->decrypt($user['ssn'], $key);
if ($ssn === false) {
// Key mismatch or corrupted value
}
Full registration and login flow with password hashing
// Registration
$hash = $app->encryption->hashPassword($_POST['password']);
$app->db->table('users')->insert([
'email' => $_POST['email'],
'password_hash' => $hash,
]);
// Login
$user = $app->db->table('users')->where('email', $_POST['email'])->first();
if (!$user || !$app->encryption->verifyPassword($_POST['password'], $user['password_hash'])) {
$app->abort(401, 'Invalid credentials.');
}
// Silently upgrade hash if needed
if ($app->encryption->needsRehash($user['password_hash'])) {
$app->db->table('users')
->where('id', $user['id'])
->update(['password_hash' => $app->encryption->hashPassword($_POST['password'])]);
}
Password reset with a secure token
// Generate and store a reset token
$token = $app->encryption->randomBytes(32);
$app->db->table('password_resets')->insert([
'email' => $email,
'token' => $token,
'expires_at' => date('Y-m-d H:i:s', strtotime('+1 hour')),
]);
// Email the link: https://example.com/reset?token=$token
// On reset form submit — verify token then update password
$row = $app->db->table('password_resets')
->where('token', $_GET['token'])
->where('expires_at', '>', date('Y-m-d H:i:s'))
->first();
if (!$row) {
$app->abort(400, 'Invalid or expired reset link.');
}
$app->db->table('users')
->where('email', $row['email'])
->update(['password_hash' => $app->encryption->hashPassword($_POST['password'])]);
$app->db->table('password_resets')->where('token', $_GET['token'])->delete();
User-driven file encryption with a passphrase
// Encrypt
$encrypted = $app->encryption->encryptWithPassword(
file_get_contents($uploadedFile),
$_POST['passphrase']
);
file_put_contents($storagePath, $encrypted);
// Decrypt
$contents = file_get_contents($storagePath);
$decrypted = $app->encryption->decryptWithPassword($contents, $_POST['passphrase']);
if ($decrypted === false) {
$app->abort(400, 'Incorrect passphrase or corrupted file.');
}