<?php 
declare(strict_types=1); 
namespace ParagonIE\Blakechain; 
 
use ParagonIE_Sodium_Compat as SodiumCompat; 
use ParagonIE\ConstantTime\Base64UrlSafe; 
 
/** 
 * Class Verifier 
 * @package ParagonIE\Blakechain 
 */ 
class Verifier 
{ 
    const FINAL_HASH_MISMATCH = 'The final hash for this Blakechain does not match what was expected'; 
    const HASH_DOES_NOT_MATCH = 'The hash for this item does not match its contents'; 
    const PREV_DOES_NOT_MATCH = 'The previous hash for this item does not match the previous hash'; 
 
    /** 
     * @var array 
     */ 
    protected $lastErrorData = []; 
 
    /** 
     * @return array 
     */ 
    public function getLastError(): array 
    { 
        return $this->lastErrorData; 
    } 
 
    /** 
     * Walk down the entire chain, recalculate the final hash, then 
     * verify that it matches what we expect. 
     * 
     * @param Blakechain $chain 
     * @param string $lastHash 
     * @return bool 
     * 
     * @throws \SodiumException 
     */ 
    public function verifyLastHash( 
        Blakechain $chain, 
        string $lastHash 
    ): bool { 
        /** 
         * @var array<int, Node> $nodes 
         */ 
        $nodes = $chain->getNodes(); 
        $count = \count($nodes); 
 
        $prevHash = ''; 
        for ($i = 0; $i < $count; ++$i) { 
            /** @var Node $curr */ 
            $curr = $nodes[$i]; 
            $actualHash = SodiumCompat::crypto_generichash( 
                $curr->getData(), 
                $prevHash 
            ); 
            if (!\hash_equals($actualHash, $curr->getHash(true))) { 
                $this->lastErrorData = [ 
                    'index' => $i, 
                    'item' => [ 
                        'prev' => $curr->getPrevHash(), 
                        'data' => $curr->getData(), 
                        'hash' => $curr->getHash() 
                    ], 
                    'failure' => static::HASH_DOES_NOT_MATCH 
                ]; 
                return false; 
            } 
            $prevHash = $curr->getHash(true); 
        } 
        $decoded = Base64UrlSafe::decode($lastHash); 
        if (!\hash_equals($prevHash, $decoded)) { 
            $this->lastErrorData = [ 
                'item' => null, 
                'expected' => $lastHash, 
                'calculated' => Base64UrlSafe::encode($prevHash), 
                'failure' => static::FINAL_HASH_MISMATCH 
            ]; 
            return false; 
        } 
        return true; 
    } 
 
    /** 
     * This is a self-consistency check for a subset of a Blakechain. 
     * 
     * @param Blakechain $chain 
     * @param int $offset 
     * @param int $limit 
     * @return bool 
     * 
     * @throws \SodiumException 
     */ 
    public function verifySequenceHashes( 
        Blakechain $chain, 
        int $offset = 0, 
        int $limit = PHP_INT_MAX 
    ): bool { 
        $subchain = $chain->getPartialChain($offset, $limit); 
 
        /** @var string $prev */ 
        $prev = ''; 
 
        /** 
         * @var int $idx 
         * @var array<string, string> $item 
         */ 
        foreach ($subchain as $idx => $item) { 
            $prevHash = Base64UrlSafe::decode($item['prev']); 
            $storedHash = Base64UrlSafe::decode($item['hash']); 
            $actualHash = SodiumCompat::crypto_generichash( 
                $item['data'], 
                $prevHash 
            ); 
            if (!\hash_equals($actualHash, $storedHash)) { 
                $this->lastErrorData = [ 
                    'index' => $idx, 
                    'item' => $item, 
                    'failure' => static::HASH_DOES_NOT_MATCH 
                ]; 
                return false; 
            } 
            if (!empty($prev)) { 
                if (!\hash_equals($prev, $item['prev'])) { 
                    $this->lastErrorData = [ 
                        'index' => $idx, 
                        'prev' => $prev, 
                        'item' => $item, 
                        'failure' => static::PREV_DOES_NOT_MATCH 
                    ]; 
                    return false; 
                } 
            } 
            /** @var string $prev */ 
            $prev = $item['hash']; 
        } 
        return true; 
    } 
} 
 
 |