Oihana PHP

openssl

Table of Contents

Namespaces

enums

Classes

OpenSSLFileEncryption
File-level symmetric encryption powered by OpenSSL.

Functions

assertBlockSizeExponent()  : int
Asserts that a V3 block-size exponent is one this library will act on, and returns it.
availableMemoryBytes()  : int|null
Returns the number of bytes this process may reasonably allocate, or `null` when it cannot be told.
deriveKey()  : string
Derives a 32-byte symmetric encryption key from a passphrase using a slow KDF.
bestAvailableKdf()  : int
Returns the strongest KDF algorithm available in the current PHP environment.
encryptionBlockAad()  : string
Builds the associated data that binds one V3 block to its file, its position and the end of the stream.
encryptionBlockNonce()  : string
Builds the GCM nonce of one V3 block: the file's random prefix followed by the block index.
isAeadCipher()  : bool
Determines whether an OpenSSL cipher name designates an AEAD mode.
kdfIsAvailable()  : bool
Tells whether the KDF a file names can actually be run here.
parseMemoryLimit()  : int|null
Parses a `memory_limit`-style size into bytes, or `null` when the value names no usable ceiling.

Functions

assertBlockSizeExponent()

Asserts that a V3 block-size exponent is one this library will act on, and returns it.

assertBlockSizeExponent(int $exponent) : int

The exponent is a single byte of the V3 header, so a file can declare any value from 0 to 255 — and the reader meets it before a single byte has been authenticated. Both ends of the range are hostile:

  • Too small. An exponent of 0 asks for 1-byte blocks, which turns a 100 MB file into a hundred million frames, each with its own 16-byte tag and its own openssl_decrypt() call. The file would be 1.6 GB of tags and the read would run for hours. EncryptionFormat::MIN_BLOCK_SIZE_EXPONENT (64 KiB) is the floor.
  • Too large. An exponent of 255 asks the reader to allocate 2^255 bytes for the first block. EncryptionFormat::MAX_BLOCK_SIZE_EXPONENT (1 GiB) is the ceiling, and 1 << 255 is undefined ground this never reaches.

Called on both sides: on write so the library never produces a file it would refuse to read, and on read so a crafted header is refused before it costs anything.

Parameters
$exponent : int

The block-size exponent, as read from a header or chosen for one.

Tags
throws
InvalidArgumentException

If the exponent falls outside the accepted range.

example
use function oihana\files\openssl\assertBlockSizeExponent;

$blockSize = 1 << assertBlockSizeExponent( 23 ) ; // 8388608
assertBlockSizeExponent( 255 ) ;                  // InvalidArgumentException
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
int —

The same exponent, so it can be validated in place: 1 << assertBlockSizeExponent( $e ).

availableMemoryBytes()

Returns the number of bytes this process may reasonably allocate, or `null` when it cannot be told.

availableMemoryBytes([string|null $memoryLimit = null ][, string $meminfoPath = '/proc/meminfo' ]) : int|null

Why this exists. The legacy V1/V2 read path loads the whole file into memory and peaks at about EncryptionFormat::LEGACY_MEMORY_FACTOR times its size. Past the budget, PHP either throws a fatal "Allowed memory size exhausted" or — when memory_limit is -1 — the kernel OOM killer takes the process down with no exception and no stack trace, so nothing can be reported. Measured on 2026-10-24 in a 3.8 GB VM: a 1 GB file logged Out of memory: Killed process (php) in dmesg and nothing anywhere else. Knowing the budget is what lets OpenSSLFileEncryption::decrypt() refuse with a message instead of disappearing.

Two sources, in order:

  1. memory_limit — the limit PHP enforces itself, read through parseMemoryLimit. Any positive value wins: it is the ceiling that actually applies, however much RAM the host has.
  2. /proc/meminfo's MemAvailable — consulted only when memory_limit is -1 (unlimited), where the real ceiling is the machine's free memory. Linux only; elsewhere the function returns null.

null means unknown, not unlimited: callers should let the operation through rather than refuse on a number they do not have.

Parameters
$memoryLimit : string|null = null

A memory_limit-style value to read instead of the ini setting ('512M', '-1', '1G'…). Testing seam; null reads ini_get('memory_limit').

$meminfoPath : string = '/proc/meminfo'

Path to read MemAvailable from. Testing seam.

Tags
example
use function oihana\files\openssl\availableMemoryBytes;

availableMemoryBytes() ;              // 268435456 with memory_limit = 256M
availableMemoryBytes( '-1' ) ;        // MemAvailable on Linux, null on macOS
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
int|null —

The budget in bytes, or null when no source gives a figure.

deriveKey()

Derives a 32-byte symmetric encryption key from a passphrase using a slow KDF.

deriveKey(string $passphrase, string $salt, int $algorithm) : string

Why a KDF ? A passphrase like 'chocolat' has roughly 8 bytes of entropy. Using it directly as an AES-256 key (zero-padded to 32 bytes) makes brute-force trivial. A KDF deliberately slows the key derivation down (hundreds of thousands of iterations or memory-hard computation) so that an attacker who tries 1 million passphrases must do 1 million × N work — increasing brute-force cost by orders of magnitude.

Supported algorithms (passed via $algorithm):

Use bestAvailableKdf() to pick the strongest algorithm available in the current PHP environment.

The chosen algorithm is explicit so that the encrypted file can carry it and any reader (even on a different PHP build) can reproduce the derivation.

Security boundary: this function protects the cost of brute-forcing the passphrase. It does not protect against:

  • a leaked salt + ciphertext + passphrase (anyone with all three can decrypt) ;
  • a passphrase observable by an attacker (e.g. typed in front of a camera) ;
  • side-channel attacks on the host (memory dump while passphrase is in RAM).
Parameters
$passphrase : string

The user-provided passphrase. Must be non-empty.

$salt : string

A per-file random salt of EncryptionFormat::SALT_LENGTH bytes.

$algorithm : int

One of EncryptionFormat::KDF_ARGON2ID or EncryptionFormat::KDF_PBKDF2_SHA256.

Tags
throws
RuntimeException

If the passphrase is empty, the salt has wrong length, the algorithm is unknown, or Argon2id is requested but the sodium extension is not loaded.

example
use function oihana\files\openssl\{ deriveKey , bestAvailableKdf } ;
use oihana\files\openssl\enums\EncryptionFormat;

$salt = random_bytes( EncryptionFormat::SALT_LENGTH ) ;
$algo = bestAvailableKdf() ;
$key  = deriveKey( 'my passphrase' , $salt , $algo ) ;
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
string —

A 32-byte raw binary key.

bestAvailableKdf()

Returns the strongest KDF algorithm available in the current PHP environment.

bestAvailableKdf() : int

Use this when writing new encrypted files; for reading, the algorithm is dictated by the file header.

Tags
example
use function oihana\files\openssl\bestAvailableKdf;

$kdf = bestAvailableKdf() ;
// → KDF_ARGON2ID on PHP 8.4 with sodium (the usual case)
// → KDF_PBKDF2_SHA256 on stripped-down builds
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
int —

One of EncryptionFormat::KDF_ARGON2ID or EncryptionFormat::KDF_PBKDF2_SHA256.

encryptionBlockAad()

Builds the associated data that binds one V3 block to its file, its position and the end of the stream.

encryptionBlockAad(string $header, int $index, bool $isFinal) : string
AAD(i) = header (31 bytes) ‖ pack('N', i) (4 bytes) ‖ chr( isFinal ? 1 : 0 ) (1 byte)

This function is the integrity of the format. A GCM tag proves that a block was sealed with the key — nothing more. Seal each block with no associated data and every frame authenticates on its own, which leaves four rewrites of a file undetectable, all of them trivial to perform with no key at all:

Attack What stops it
Cut the file short the final flag: the reader computes final from the cursor, so the new last frame was sealed as non-final and fails
Swap two frames the block index
Drop a frame from the middle the block index of every frame after it
Splice two files from the same passphrase the header, whose salt and nonce prefix are per-file

The flag is one byte rather than a bit of the index because it has to be decided on the read side, from the cursor position against the file size, before the block is opened. Writer and reader must agree on it, and that agreement is what a truncation breaks.

Parameters
$header : string

The file's V3 header, exactly EncryptionFormat::V3_HEADER_LENGTH bytes.

$index : int

The zero-based block index.

$isFinal : bool

Whether this block is the last of the file.

Tags
throws
InvalidArgumentException

If the header has the wrong length, or the index is negative or past what the counter can hold.

example
use function oihana\files\openssl\encryptionBlockAad;

$aad = encryptionBlockAad( $header , 0 , false ) ; // first block of several
$end = encryptionBlockAad( $header , 7 , true  ) ; // the one that closes the file
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
string —

The associated data to pass to openssl_encrypt() / openssl_decrypt().

encryptionBlockNonce()

Builds the GCM nonce of one V3 block: the file's random prefix followed by the block index.

encryptionBlockNonce(string $noncePrefix, int $index) : string
nonce(i) = NONCE PREFIX (8 bytes) ‖ pack('N', i) (4 bytes) = 12 bytes

Why a counter and not 12 random bytes per block. A nonce must never repeat under one key, and with AES-GCM a repeat is not a degradation but a collapse: two blocks sharing a nonce leak their XOR and, worse, hand an attacker the material to forge tags for any message under that key. Random 96-bit nonces collide by birthday after about 2^32 blocks; a counter cannot collide at all within a file, and the per-file random prefix keeps two files apart.

Which is why the index is bounded here. EncryptionFormat::BLOCK_COUNTER_LENGTH is 4 bytes, and pack('N', 2**32) wraps silently back to zero — the one arithmetic accident in this format that would reuse a nonce. At the default block size that ceiling sits at 32 EiB, so the guard costs nothing and removes the accident.

Parameters
$noncePrefix : string

The file's random prefix, exactly EncryptionFormat::NONCE_PREFIX_LENGTH bytes.

$index : int

The zero-based block index.

Tags
throws
InvalidArgumentException

If the prefix has the wrong length, or the index is negative or past what the counter can hold.

example
use function oihana\files\openssl\encryptionBlockNonce;

$prefix = random_bytes( 8 ) ;
encryptionBlockNonce( $prefix , 0 ) ; // $prefix . "\x00\x00\x00\x00"
encryptionBlockNonce( $prefix , 1 ) ; // $prefix . "\x00\x00\x00\x01"
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
string —

The EncryptionFormat::GCM_IV_LENGTH-byte nonce of that block.

isAeadCipher()

Determines whether an OpenSSL cipher name designates an AEAD mode.

isAeadCipher(string $cipher) : bool

AEAD (Authenticated Encryption with Associated Data) ciphers compute an authentication tag in addition to the ciphertext. PHP's openssl_encrypt() / openssl_decrypt() API requires the &$tag parameter for these ciphers; without it, integrity is not protected.

Currently recognised modes:

  • GCM (Galois/Counter Mode) — preferred for general use.
  • CCM (Counter with CBC-MAC).
  • OCB (Offset Codebook) — modern, less ubiquitous.

The check is purely textual on the cipher name (case-insensitive).

Parameters
$cipher : string

Cipher name as returned by openssl_get_cipher_methods(), e.g. 'aes-256-gcm', 'aes-128-ccm', 'aes-256-cbc'.

Tags
example
use function oihana\files\openssl\isAeadCipher;

isAeadCipher('aes-256-gcm') ;  // true
isAeadCipher('aes-128-ccm') ;  // true
isAeadCipher('aes-256-cbc') ;  // false
isAeadCipher('chacha20-poly1305') ; // false — handled separately by sodium
author

Marc Alcaraz (ekameleon)

since
1.0.0
Return values
bool —

True if the cipher operates in an AEAD mode.

kdfIsAvailable()

Tells whether the KDF a file names can actually be run here.

kdfIsAvailable(int $algorithm[, bool|null $sodiumLoaded = null ]) : bool

Every encrypted file stores the identifier of the KDF its key was derived with, so a reader never has to guess. What that byte cannot do is make the KDF available: EncryptionFormat::KDF_ARGON2ID needs ext-sodium at decryption time exactly as much as at encryption time, and ext-sodium is a suggestion of this package, not a requirement.

Which produces the one case worth a function of its own: a file written on a host that has sodium, read on a host that does not. Measured on 2026-10-24, that read failed with "Decryption failed due to incorrect passphrase or corrupted data" — on an intact archive, with the right passphrase, because the real cause sat in a wrapped exception nobody prints. For someone restoring a backup, that message sends them looking for a lost passphrase or a corrupt file instead of a missing extension.

EncryptionFormat::KDF_PBKDF2_SHA256 is native to PHP, so it is available everywhere.

An unknown identifier answers true, and that is deliberate: it names no implementation that could be missing. A byte that is neither of the two known values is a corrupt or tampered header, or one from a newer release — all of which deriveKey() must reject, and under the generic message, since a forged byte must not get an answer of its own.

Parameters
$algorithm : int
$sodiumLoaded : bool|null = null

Whether ext-sodium is loaded. Testing seam; null asks function_exists().

Tags
example
use function oihana\files\openssl\kdfIsAvailable;
use oihana\files\openssl\enums\EncryptionFormat;

kdfIsAvailable( EncryptionFormat::KDF_ARGON2ID ) ;             // false without ext-sodium
kdfIsAvailable( EncryptionFormat::KDF_PBKDF2_SHA256 ) ;        // always true
kdfIsAvailable( EncryptionFormat::KDF_ARGON2ID , false ) ;     // false
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
bool

parseMemoryLimit()

Parses a `memory_limit`-style size into bytes, or `null` when the value names no usable ceiling.

parseMemoryLimit(string $value) : int|null

Accepts a plain byte count, optionally suffixed with K, M or G (case-insensitive), with optional surrounding whitespace — the shapes PHP itself reads as a size. null comes back for:

  • -1, the value that means unlimited: there is no ceiling to report;
  • 0 or a negative count, which name no usable budget;
  • an empty string, or anything that is not a number followed by at most one of those three suffixes.

That last case includes '128MB', and deliberately. PHP reads a quantity's multiplier from what follows the digits, accepts only a single k, m or g there, and warns then ignores anything else — so memory_limit = 128MB is 128 bytes to PHP, not 128 mebibytes. Returning the number this function would like it to mean would overstate the budget by a factor of a million; returning null says unknown, which is the one answer that cannot mislead a caller.

Parameters
$value : string

The raw setting value, e.g. '512M', '1G', '134217728', '-1'.

Tags
example
use function oihana\files\openssl\parseMemoryLimit;

parseMemoryLimit('256M')      ; // 268435456
parseMemoryLimit('1G')        ; // 1073741824
parseMemoryLimit('134217728') ; // 134217728
parseMemoryLimit('-1')        ; // null — unlimited, so no ceiling
parseMemoryLimit('128MB')     ; // null — PHP would read 128 bytes here
author

Marc Alcaraz (ekameleon)

since
1.5.0
Return values
int|null —

The size in bytes, or null when the value names no usable ceiling.

On this page

Search results