Encryption

Overview

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

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

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 when null.
  • $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()

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). Use generateKey() 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()

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 from encrypt().
  • $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()

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

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 from encryptWithPassword().
  • $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()

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

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 from hashPassword().
// 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()

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

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

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

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)];

Examples

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.');
}