Oihana PHP

OpenSSLFileEncryption

File-level symmetric encryption powered by OpenSSL.

Security properties of this class

What it guarantees for files written by encrypt() (format V2):

  1. Confidentiality — AES-256-GCM. Without the passphrase, the plaintext is computationally infeasible to recover.
  2. Integrity / authenticity — every V2 file carries a 16-byte GCM authentication tag. Any tampering with the ciphertext makes decrypt() throw RuntimeException instead of returning corrupted plaintext.
  3. Brute-force resistance on the passphrase — the encryption key is derived via Argon2id (if ext-sodium is loaded) or PBKDF2-SHA256 with 600 000 iterations. A per-file random salt prevents rainbow tables.
  4. Per-file uniqueness — a fresh random salt (16 B) + IV (12 B) is generated for each call to encrypt(). Encrypting the same plaintext twice with the same passphrase produces two unrelated ciphertexts.

What it does NOT guarantee:

  • Forward secrecy: there is no per-session key. Anyone with the passphrase can decrypt every past file encrypted with that passphrase.
  • Key revocation: changing the passphrase does not invalidate old files. You must re-encrypt them with the new passphrase.
  • Protection from a compromised endpoint: if an attacker can read PHP memory while the passphrase is in use, they can recover it. The __destruct() cleanup is best-effort.
  • Protection from the user choosing a weak passphrase: KDF only slows brute-force; it does not prevent it. Use long, random passphrases for sensitive data.

Backward compatibility (legacy format)

Files written by oihana/php-files ≤ 1.0 use the legacy V1 format: a raw IV followed by AES-CBC ciphertext, with the passphrase used directly (zero-padded) as the key, no MAC. Those files have no integrity protection, but they remain readable: decrypt() auto-detects the absence of the V2 magic header and falls back to the legacy code path.

encrypt() always produces V2. To migrate a legacy file, simply call decrypt() (reads V1) then encrypt() (writes V2).

Tags
example
use oihana\files\openssl\OpenSSLFileEncryption;

$crypto = new OpenSSLFileEncryption('my-secret-passphrase');

// Always produces a V2 file ('OPHE\x02…').
$encryptedPath = $crypto->encrypt('/path/to/file.txt');

// Reads V2 or legacy V1 transparently.
$decryptedPath = $crypto->decrypt($encryptedPath);
author

Marc Alcaraz (ekameleon)

since
1.0.0

Table of Contents

Properties

$ivLength  : int
The IV length of the legacy cipher (used for V1 backward-compat).
$cipher  : string
The cipher method used to decrypt legacy V1 files.
$maxInputBytes  : int|null
Optional cap on the size of the input files for {@see encrypt()} / {@see decrypt()}.
$passphrase  : string
The passphrase used for encryption and decryption.

Methods

__construct()  : mixed
Constructor.
__destruct()  : mixed
Destructor.
decrypt()  : string
Decrypts a previously encrypted file.
encrypt()  : string
Encrypts a file in V2 format (AES-256-GCM + KDF + magic header).
hasEncryptedFileSize()  : bool
Checks if a file is large enough to *possibly* be a legacy V1 encrypted file.
isEncryptedFile()  : bool
Heuristically checks whether a file appears to be encrypted by this class.
assertWithinMaxInputBytes()  : void
Rejects an input file whose size exceeds the configured `$maxInputBytes` cap.
decryptLegacy()  : string
Decrypts a legacy V1 payload (no magic, no MAC, raw passphrase as key).
decryptV2()  : string
Decrypts a V2 payload: reads the header, derives the key, validates the tag.
isV2Payload()  : bool
Inspects the first bytes of an encrypted payload to determine whether it is a V2 file (carries magic + version + KDF).

Properties

$ivLength

The IV length of the legacy cipher (used for V1 backward-compat).

public int $ivLength
Hooks
public int get

$maxInputBytes

Optional cap on the size of the input files for {@see encrypt()} / {@see decrypt()}.

private int|null $maxInputBytes

null means no limit.

Methods

__construct()

Constructor.

public __construct(string $passphrase[, string $cipher = EncryptionFormat::LEGACY_CIPHER ][, int|null $maxInputBytes = null ]) : mixed
Parameters
$passphrase : string

Secret used to derive the encryption key. Must be non-empty.

$cipher : string = EncryptionFormat::LEGACY_CIPHER

Cipher used to decrypt legacy V1 files. New V2 files always use EncryptionFormat::DEFAULT_CIPHER. Default aes-256-cbc matches the historical behaviour of this class.

$maxInputBytes : int|null = null

Optional cap on the size of the files read by encrypt() and decrypt(). When set, a file whose size exceeds this value is rejected before being read into memory (RuntimeException). Default null (no limit — historical behaviour). Useful as a defensive guard against OOM when processing untrusted inputs.

Tags
throws
InvalidArgumentException

If the passphrase is empty or the cipher is unsupported by OpenSSL.

__destruct()

Destructor.

public __destruct() : mixed

Best-effort wiping of the passphrase from memory. Uses sodium_memzero() when available (true in-place wipe), falling back to a string overwrite (which only releases the current reference — older copies created by PHP's copy-on-write may persist in RAM).

For strongly-secured deployments, prefer wiping the passphrase at the application level (e.g. immediately after encrypt() / decrypt()) rather than relying on this destructor.

decrypt()

Decrypts a previously encrypted file.

public decrypt(string $inputFile[, string|null $outputFile = null ]) : string

Auto-detects the format by inspecting the first EncryptionFormat::HEADER_LENGTH bytes:

  • If the file begins with 'OPHE\x02' followed by a known KDF byte, it is treated as V2: the salt, IV, ciphertext and authentication tag are extracted; the tag is verified by openssl_decrypt() before the plaintext is returned.
  • Otherwise the file is treated as legacy V1: the first ivLength bytes are the IV, the rest is the AES-CBC ciphertext, and the passphrase is used directly as the key.

Either path produces the same exception type on failure: RuntimeException with the same generic message (no oracle on "wrong passphrase" vs "tampered ciphertext").

Parameters
$inputFile : string

Path to the encrypted file.

$outputFile : string|null = null

Optional output path. If null, strips .enc from $inputFile.

Tags
throws
FileException

If the input file is invalid.

RuntimeException

On read/write/decryption failure (including tampering, wrong passphrase).

Return values
string

Path to the decrypted output file.

encrypt()

Encrypts a file in V2 format (AES-256-GCM + KDF + magic header).

public encrypt(string $inputFile[, string|null $outputFile = null ]) : string

Steps:

  1. Generate a 16-byte salt and a 12-byte IV with random_bytes().
  2. Derive a 32-byte key from the passphrase + salt via the best available KDF (Argon2id if ext-sodium is loaded, PBKDF2 otherwise).
  3. Encrypt with AES-256-GCM, obtaining a 16-byte authentication tag.
  4. Write MAGIC | VERSION | KDF | salt | IV | ciphertext | tag to the output.
Parameters
$inputFile : string

Path to the plaintext input file.

$outputFile : string|null = null

Optional output path. If null, appends .enc to $inputFile.

Tags
throws
FileException

If the input file is invalid.

DirectoryException

If the output directory is not writable.

RuntimeException

On read/write/encryption failure.

Return values
string

Path to the encrypted output file.

hasEncryptedFileSize()

Checks if a file is large enough to *possibly* be a legacy V1 encrypted file.

public hasEncryptedFileSize(string $filePath) : bool

Verifies that the file has at least enough bytes to contain a legacy IV. This is a cheap size-only check; it does NOT validate the V2 magic or confirm anything about the content. Use isEncryptedFile() for a heuristic content check.

Parameters
$filePath : string

Path to the file to check.

Return values
bool

True if the file exists and is at least ivLength bytes long.

isEncryptedFile()

Heuristically checks whether a file appears to be encrypted by this class.

public isEncryptedFile(string $filePath) : bool

For files written by encrypt() (V2), simply checks for the magic header.

For legacy V1 files (no magic), falls back to the historical heuristic:

  • file at least ivLength bytes long;
  • IV not entirely zero;
  • IV not dominated by printable characters (which would indicate plaintext).

This method is best-effort. Some plaintext binary files (e.g. arbitrary compressed data) may pass the legacy heuristic. Always treat its result as a hint, never as a guarantee that decryption will succeed.

Parameters
$filePath : string

Path to the file to check.

Return values
bool

True if the file likely contains encrypted content.

assertWithinMaxInputBytes()

Rejects an input file whose size exceeds the configured `$maxInputBytes` cap.

private assertWithinMaxInputBytes(string $inputFile) : void

No-op when $maxInputBytes is null (default).

Parameters
$inputFile : string
Tags
throws
RuntimeException

If the file size strictly exceeds the cap.

decryptLegacy()

Decrypts a legacy V1 payload (no magic, no MAC, raw passphrase as key).

private decryptLegacy(string $data) : string

Kept for backward-compatibility only. Files in this format have no tampering detection; the integrity of the plaintext depends on the integrity of the storage medium.

Parameters
$data : string
Return values
string

decryptV2()

Decrypts a V2 payload: reads the header, derives the key, validates the tag.

private decryptV2(string $data) : string
Parameters
$data : string
Return values
string

isV2Payload()

Inspects the first bytes of an encrypted payload to determine whether it is a V2 file (carries magic + version + KDF).

private isV2Payload(string $data) : bool
Parameters
$data : string
Return values
bool
On this page

Search results