PHP Classes

File: src/Stream/ReadOnlyFile.php

Recommend this page to a friend!
  Packages of Scott Arciszewski   Halite   src/Stream/ReadOnlyFile.php   Download  
File: src/Stream/ReadOnlyFile.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Halite
Perform cryptography operations with libsodium
Author: By
Last change: Limit the File API.
For version 2, let's use strict types!
Switch passwords and key derivation to crypto_pwhash() (Argon2i)
Date: 10 years ago
Size: 6,123 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace ParagonIE\Halite\Stream;

use \ParagonIE\Halite\Contract\{
    StreamInterface,
    KeyInterface
};
use \ParagonIE\Halite\Alerts as CryptoException;
use \ParagonIE\Halite\Util as CryptoUtil;

class ReadOnlyFile implements StreamInterface
{
    const CHUNK = 8192; // PHP's fread() buffer is set to 8192 by default

    private $closeAfter = false;
    private $fp;
    private $hash;
    private $pos;
    private $hashKey = null;
    private $stat = [];
    
    public function __construct($file, KeyInterface $key = null)
    {
        if (is_string($file)) {
            $this->fp = \fopen($file, 'rb');
            $this->closeAfter = true;
            $this->pos = 0;
            $this->stat = \fstat($this->fp);
        } elseif (is_resource($file)) {
            $this->fp = $file;
            $this->pos = \ftell($this->fp);
            $this->stat = \fstat($this->fp);
        } else {
            throw new \ParagonIE\Halite\Alerts\InvalidType(
                'Argument 1: Expected a filename or resource'
            );
        }
        $this->hashKey = !empty($key) 
            ? $key->get()
            : '';
        $this->hash = $this->getHash();
    }

    public function close()
    {
        if ($this->closeAfter) {
            $this->closeAfter = false;
            \fclose($this->fp);
            \clearstatcache();
        }
    }

    public function __destruct()
    {
        $this->close();
    }

    /**
     * Where are we in the buffer?
     * 
     * @return int
     */
    public function getPos()
    {
        return $this->pos;
    }
    /**
     * How big is this buffer?
     * 
     * @return int
     */
    public function getSize()
    {
        return $this->stat['size'];
    }
    
    /**
     * Read from a stream; prevent partial reads (also uses run-time testing to
     * prevent partial reads -- you can turn this off if you need performance
     * and aren't concerned about race condition attacks, but this isn't a
     * decision to make lightly!)
     * 
     * @param int $num
     * @param bool $skipTests Only set this to TRUE if you're absolutely sure
     *                           that you don't want to defend against TOCTOU /
     *                           race condition attacks on the filesystem!
     * @return string
     * @throws CryptoException\AccessDenied
     */
    public function readBytes(int $num, bool $skipTests = false): string
    {
        if ($num <= 0) {
            throw new \Exception('num < 0');
        }
        if (($this->pos + $num) > $this->stat['size']) {
            throw new \Exception('Out-of-bounds read');
        }
        $buf = '';
        $remaining = $num;
        if (!$skipTests) {
            $this->toctouTest();
        }
        do {
            if ($remaining <= 0) {
                break;
            }
            $read = \fread($this->fp, $remaining);
            if ($read === false) {
                throw new CryptoException\FileAccessDenied(
                    'Could not read from the file'
                );
            }
            $buf .= $read;
            $readSize = CryptoUtil::safeStrlen($read);
            $this->pos += $readSize;
            $remaining -= $readSize;
        } while ($remaining > 0);
        return $buf;
    }
    
    /**
     * Get number of bytes remaining
     * 
     * @return int
     */
    public function remainingBytes(): int
    {
        return (PHP_INT_MAX & ($this->stat['size'] - $this->pos));
    }
    
    /**
     * This is a meaningless operation for a Read-Only File!
     * 
     * @param string $buf
     * @param int $num (number of bytes)
     * @throws CryptoException\AccessDenied
     */
    public function writeBytes(string $buf, int $num = null): int
    {
        unset($buf);
        unset($num);
        throw new CryptoException\FileAccessDenied(
            'This is a read-only file handle.'
        );
    }
    
    /**
     * Set the current cursor position to the desired location
     * 
     * @param int $i
     * @return boolean
     * @throws CryptoException\CannotPerformOperation
     */
    public function reset(int $i = 0): bool
    {
        $this->pos = $i;
        if (\fseek($this->fp, $i, SEEK_SET) === 0) {
            return true;
        }
        throw new CryptoException\CannotPerformOperation(
            'fseek() failed'
        );
    }
    
    /**
     * Calculate a BLAKE2b hash of a file
     * 
     * @return string
     */
    public function getHash(): string
    {
        $init = $this->pos;
        \fseek($this->fp, 0, SEEK_SET);
        
        // Create a hash context:
        $h = \Sodium\crypto_generichash_init(
            $this->hashKey,
            \Sodium\CRYPTO_GENERICHASH_BYTES_MAX
        );
        for ($i = 0; $i < $this->stat['size']; $i += self::CHUNK) {
            if (($i + self::CHUNK) > $this->stat['size']) {
                $c = \fread($this->fp, ($this->stat['size'] - $i));
            } else {
                $c = \fread($this->fp, self::CHUNK);
            }
            \Sodium\crypto_generichash_update($h, $c);
        }
        // Reset the file pointer's internal cursor to where it was:
        \fseek($this->fp, $init, SEEK_SET);
        return \Sodium\crypto_generichash_final($h);
    }
    
    /**
     * Run-time test to prevent TOCTOU attacks (race conditions) through
     * verifying that the hash matches and the current cursor position/file
     * size matches their values when the file was first opened.
     * 
     * @throws CryptoException\FileModified
     * @return true
     */
    public function toctouTest()
    {
        if (\ftell($this->fp) !== $this->pos) {
            throw new CryptoException\FileModified(
                'Read-only file has been modified since it was opened for reading'
            );
        }
        $stat = \fstat($this->fp);
        if ($stat['size'] !== $this->stat['size']) {
            throw new CryptoException\FileModified(
                'Read-only file has been modified since it was opened for reading'
            );
        }
    }
}