<?php
declare(strict_types=1);
namespace ParagonIE\Blakechain;
use ParagonIE_Sodium_Compat as SodiumCompat;
use ParagonIE_Sodium_Core_Util as Util;
use ParagonIE\ConstantTime\Base64UrlSafe;
/**
* Class Blakechain
* @package ParagonIE\Blakechain
*/
class Blakechain
{
// Maximum is 64 byte // 512 bit
const HASH_SIZE = 32; // 256 bit
/** @var string $firstPrevHash */
protected $firstPrevHash = '';
/** @var string $summaryHashState */
protected $summaryHashState = '';
/** @var array<int, Node> */
protected $nodes = [];
/**
* Blakechain constructor.
*
* @param Node ...$nodes
*
* @throws \Error
* @throws \SodiumException
*/
public function __construct(Node ...$nodes)
{
$this->firstPrevHash = '';
$this->summaryHashState = '';
$this->nodes = $nodes;
$this->recalculate();
}
/**
* Append a new Node.
*
* @param string $data
* @return self
*
* @throws \SodiumException
*/
public function appendData(string $data): self
{
if (empty($this->nodes)) {
$prevHash = $this->firstPrevHash;
} else {
$last = $this->getLastNode();
$prevHash = $last->getHash(true);
}
$newNode = new Node($data, $prevHash);
$this->nodes[] = $newNode;
SodiumCompat::crypto_generichash_update(
$this->summaryHashState,
$newNode->getHash(true)
);
return $this;
}
/**
* @param bool $rawBinary
* @return string
*
* @throws \SodiumException
*/
public function getLastHash(bool $rawBinary = false): string
{
return $this->getLastNode()->getHash($rawBinary);
}
/**
* @return Node
* @throws \Error
*/
public function getLastNode(): Node
{
$keys = \array_keys($this->nodes);
$last = \array_pop($keys);
return $this->nodes[$last];
}
/**
* @return array<int, Node>
*/
public function getNodes(): array
{
return \array_values($this->nodes);
}
/**
* Get the summary hash
*
* @param bool $rawBinary
* @return string
*
* @throws \Exception
*/
public function getSummaryHash(bool $rawBinary = false): string
{
/* Make a XOR-encrypted copy of the hash state to prevent PHP's
* interned strings from overwriting the hash state and causing
* corruption. */
$len = Util::strlen($this->summaryHashState);
$pattern = \random_bytes($len);
$tmp = $pattern ^ $this->summaryHashState;
$finalHash = SodiumCompat::crypto_generichash_final($this->summaryHashState);
/* Restore hash state */
$this->summaryHashState = $tmp ^ $pattern;
if ($rawBinary) {
return $finalHash;
}
return Base64UrlSafe::encode($finalHash);
}
/**
* Get a string representing the internals of a crypto_generichash state.
*
* @param bool $rawBinary
* @return string
*/
public function getSummaryHashState(bool $rawBinary = false): string
{
if ($rawBinary) {
return '' . $this->summaryHashState;
}
return Base64UrlSafe::encode($this->summaryHashState);
}
/**
* @param int $offset
* @param int $limit
* @return array
*
* @throws \SodiumException
*/
public function getPartialChain(int $offset = 0, int $limit = PHP_INT_MAX): array
{
$chain = [];
$num = \count($this->nodes);
for ($i = 0; $i < $limit && $i < $num; ++$i) {
$chain[] = [
'prev' => $this->nodes[$offset]->getPrevHash(),
'data' => $this->nodes[$offset]->getData(),
'hash' => $this->nodes[$offset]->getHash()
];
++$offset;
}
return $chain;
}
/**
* Recalculate the summary hash and summary hash state.
* @return self
*
* @throws \SodiumException
*/
public function recalculate(): self
{
$num = \count($this->nodes);
$this->summaryHashState = SodiumCompat::crypto_generichash_init();
$prevHash = $this->firstPrevHash;
for ($i = 0; $i < $num; ++$i) {
$thisNodesPrev = $this->nodes[$i]->getPrevHash();
if (empty($thisNodesPrev)) {
$this->nodes[$i]->setPrevHash($prevHash);
}
$prevHash = $this->nodes[$i]->getHash(true);
SodiumCompat::crypto_generichash_update(
$this->summaryHashState,
$prevHash
);
}
return $this;
}
/**
* @param string $first
* @return self
*
* @throws \SodiumException
*/
public function setFirstPrevHash(string $first = ''): self
{
$this->firstPrevHash = $first;
return $this->recalculate();
}
/**
* @param string $hashState
* @return self
*
* @throws \RangeException
*/
public function setSummaryHashState(string $hashState): self
{
$len = Util::strlen($hashState);
if ($len !== 384 && $len !== 361) {
throw new \RangeException(
'Expected exactly 361 or 384 bytes, ' . $len . ' given.'
);
}
$this->summaryHashState = $hashState;
return $this;
}
}
|