<?php
$k1 = '1.key';
$k2 = '2.key';
$d = 'The quick brown fox jumped over the lazy dog';
VernamCipher::createTestKeyFile($k1, 1024);
copy($k1, $k2);
$c1 = new VernamCipher($k1);
$eD = $c1->encryptWithHMAC($d);
echo 'Encrypted: ', bin2hex($eD);
$c2 = new VernamCipher($k2);
echo PHP_EOL, 'Decrypted: ', $c2->decryptWithHMAC($eD);
class VernamCipher
{
    const DEFAULT_HMAC_ALGO = 'sha3-256';
    const DEFAULT_HMAC_KEY_LENGTH = 16;
    const DEFAULT_HMAC_HASH_LENGTH = 32;
    private $keyFilePath;
    private $keyFileHandler;
    private $deferredFtruncate = false;
    private $deferredFtruncatePos;
    private $hmacAlgo = self::DEFAULT_HMAC_ALGO;
    private $hmacKeyLength = self::DEFAULT_HMAC_KEY_LENGTH;
    private $hmacHashLength = self::DEFAULT_HMAC_HASH_LENGTH;
    function __construct(string $keyFilePath, string $hmacAlgo = self::DEFAULT_HMAC_ALGO, int $hmacKeyLength = self::DEFAULT_HMAC_KEY_LENGTH)
    {
        $this->keyFilePath = $keyFilePath;
        $this->openKeyFile();
        
        if($hmacAlgo !== self::DEFAULT_HMAC_ALGO) {
            if(in_array($hmacAlgo, hash_algos())) {
                $this->hmacAlgo = $hmacAlgo;
                $this->hmacHashLength = strlen(hash($this->hmacAlgo, '', true));
            }
            else {
                $this->hmacAlgo = self::DEFAULT_HMAC_ALGO;
                $this->hmacHashLength = self::DEFAULT_HMAC_HASH_LENGTH;
            }
        }
        
        if($hmacKeyLength !== self::DEFAULT_HMAC_KEY_LENGTH) {
            $this->hmacKeyLength = $hmacKeyLength;
        }
    }
    public function encryptWithHMAC(string $data)
    {
        $hmacKey = $this->getBytes($this->hmacKeyLength);
        $encData = $this->encrypt($data);
        $dataHmac = $this->hashHmac($encData, $hmacKey);
        
        return $dataHmac.$encData;
    }
    public function decryptWithHMAC(string $data)
    {
        $dataLength = strlen($data);
        if($dataLength < $this->hmacHashLength)
            throw new Exception('data length '.$dataLength.' < hmac length '. $this->hmacHashLength);
        
        $dataHmacRemote = substr($data, 0, $this->hmacHashLength);
        $dataOnly = substr($data, $this->hmacHashLength);
        
        $hmacKey = $this->getBytes($this->hmacKeyLength, false);
        $dataHmacLocal = $this->hashHmac($dataOnly, $hmacKey);
        
        if(hash_equals($dataHmacLocal, $dataHmacRemote) === false)
            throw new Exception('Hashes not equals, remote: '.bin2hex($dataHmacRemote).' local:'. bin2hex($dataHmacLocal));
    
        $this->deferredFtruncate();
        return $this->encrypt($dataOnly);
    }
    public function encrypt(string $data) : string
    {
        $dataLength = strlen($data);
        $bytes = $this->getBytes($dataLength);
        for($i=0;$i<$dataLength;$i++)
            $data{$i} = $data{$i} ^ $bytes{$i};
        
        return $data;
    }
    public function decrypt(string $data) : string
    {
        return $this->encrypt($data);
    }
    private function hashHmac($data, $key)
    {
        return hash_hmac($this->hmacAlgo, $data, $key, true);
    }
    public static function createTestKeyFile(string $filePath, int $size)
    {
        file_put_contents($filePath, random_bytes($size));
    }
    private function deferredFtruncate()
    {
        if(!$this->deferredFtruncate)
            return;
        
        ftruncate($this->keyFileHandler, $this->deferredFtruncatePos);
        $this->deferredFtruncate = false;
    }
    public function getBytes(int $length, $truncateNow = true) : string
    {
        fseek($this->keyFileHandler, 0, SEEK_END);
        $currentPos = ftell($this->keyFileHandler);
        if($currentPos < $length)
            throw new Exception('Not enough key materials, key size: '. $currentPos. ' needed: '.$length);
        fseek($this->keyFileHandler, -$length, SEEK_END);
        $bytes = fread($this->keyFileHandler, $length);
        
        if($truncateNow)
            ftruncate($this->keyFileHandler, $currentPos - $length);
        else {
            $this->deferredFtruncate = true;
            $this->deferredFtruncatePos = $currentPos - $length;
        }
        return $bytes;
    }
    private function openKeyFile()
    {
        if($this->keyFileHandler)
            return;
        if(($this->keyFileHandler = fopen($this->keyFilePath, 'rb+')) === false)
            throw new Exception('Cant open key file: '.$this->keyFilePath);
        
        if(!flock($this->keyFileHandler, LOCK_EX | LOCK_NB))
            throw new Exception('Cant lock key file: '.$this->keyFilePath);
    }
}
?>