Skip to content

Commit

Permalink
Introduce summary hashes.
Browse files Browse the repository at this point in the history
Allow a Blakechain to be initiated from the latest node, and still carry
forward the desired properties (i.e. you can start from scratch and still
have the entire chain be authenticated).
  • Loading branch information
paragonie-security committed Jun 27, 2017
1 parent 3b65c3c commit 3f595e5
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 28 deletions.
151 changes: 123 additions & 28 deletions src/Blakechain.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
namespace ParagonIE\Blakechain;
use ParagonIE\ConstantTime\Base64UrlSafe;

/**
* Class Blakechain
Expand All @@ -10,6 +11,16 @@ class Blakechain
{
const HASH_SIZE = 32;

/**
* @var string
*/
protected $firstPrevHash = '';

/**
* @var string
*/
protected $summaryHashState = '';

/**
* @var array<int, Node>
*/
Expand All @@ -22,19 +33,34 @@ class Blakechain
*/
public function __construct(Node ...$nodes)
{
$num = \count($nodes);
if ($num < 1) {
throw new \Error('Nodes expected.');
}
$prevHash = '';
for ($i = 0; $i < $num; ++$i) {
$thisNodesPrev = $nodes[$i]->getPrevHash();
if (empty($thisNodesPrev)) {
$nodes[$i]->setPrevHash($prevHash);
}
$prevHash = $nodes[$i]->getHash(true);
}
$this->firstPrevHash = '';
$this->summaryHashState = '';
$this->nodes = $nodes;
$this->recalculate();
}

/**
* Append a new Node.
*
* @param string $data
* @return self
*/
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;

\ParagonIE_Sodium_Compat::crypto_generichash_update(
$this->summaryHashState,
$newNode->getHash(true)
);
return $this;
}

/**
Expand All @@ -47,17 +73,14 @@ public function getLastHash(bool $rawBinary = false): string
}

/**
* Append a new Node.
*
* @param string $data
* @return self
* @return Node
* @throws \Error
*/
public function appendData(string $data): self
public function getLastNode(): Node
{
$last = $this->getLastNode();
$prevHash = $last->getHash(true);
$this->nodes[] = new Node($data, $prevHash);
return $this;
$keys = \array_keys($this->nodes);
$last = \array_pop($keys);
return $this->nodes[$last];
}

/**
Expand All @@ -68,6 +91,45 @@ public function getNodes(): array
return \array_values($this->nodes);
}

/**
* Get the summary hash
*
* @param bool $rawBinary
* @return string
*/
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 = \ParagonIE_Sodium_Core_Util::strlen($this->summaryHashState);
$pattern = \random_bytes($len);
$tmp = $pattern ^ $this->summaryHashState;

$finalHash = \ParagonIE_Sodium_Compat::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
Expand All @@ -89,15 +151,48 @@ public function getPartialChain(int $offset = 0, int $limit = PHP_INT_MAX): arra
}

/**
* @return Node
* Recalculate the summary hash and summary hash.
* @return self
*/
public function getLastNode(): Node
public function recalculate(): self
{
if (empty($this->nodes)) {
throw new \Error('Blakechain has no nodes');
$num = \count($this->nodes);
$this->summaryHashState = \ParagonIE_Sodium_Compat::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);
\ParagonIE_Sodium_Compat::crypto_generichash_update(
$this->summaryHashState,
$prevHash
);
}
$keys = \array_keys($this->nodes);
$last = \array_pop($keys);
return $this->nodes[$last];
return $this;
}

/**
* @param string $first
* @return self
*/
public function setFirstPrevHash(string $first = ''): self
{
$this->firstPrevHash = $first;
return $this->recalculate();
}

/**
* @param string $hashState
* @return self
*/
public function setSummaryHashState(string $hashState): self
{
if (\ParagonIE_Sodium_Core_Util::strlen($hashState) !== 361) {
throw new \RangeException('Expected exactly 361 bytes');
}
$this->summaryHashState = $hashState;
return $this;
}
}
114 changes: 114 additions & 0 deletions tests/BlakechainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use ParagonIE\Blakechain\Blakechain;
use ParagonIE\Blakechain\Node;
use ParagonIE\ConstantTime\Base64UrlSafe;
use PHPUnit\Framework\TestCase;

class BlakechainTest extends TestCase
Expand Down Expand Up @@ -94,4 +95,117 @@ public function testChaining()
);
}
}

public function testSummaryHashUpdate()
{
$chain = new Blakechain(
new Node(\random_bytes(128))
);

for ($i = 0; $i < 100; ++$i) {
$chain->appendData(random_bytes(128));
}
$prevSummaryState = $chain->getSummaryHashState(true);
$prevSummaryHash = $chain->getSummaryHash();

$random = random_bytes(33);
$chain->appendData($random);

$newSummaryState = $chain->getSummaryHashState(true);

$this->assertNotSame(
$prevSummaryHash,
$chain->getSummaryHash()
);
$this->assertNotSame(
$prevSummaryState,
Base64UrlSafe::encode($newSummaryState)
);
}

/**
* Verify that we get the same summary hash piecewise as we
* do in one fell swoop.
*/
public function testSummaryHash()
{
$chainA = new Blakechain(
new Node('abcdef'),
new Node('abcdefg'),
new Node('abcdefh'),
new Node('abcde'),
new Node('abcdefj')
);

$chainB = new Blakechain(
new Node('abcdef'),
new Node('abcdefg'),
new Node('abcdefh'),
new Node('abcde'),
new Node('abcdefj')
);

$this->assertSame(
$chainA->getSummaryHash(),
$chainB->getSummaryHash()
);

$chainC = (new Blakechain(new Node('abcdef')))
->appendData('abcdefg')
->appendData('abcdefh')
->appendData('abcde');

$clone = [
'prev' =>
$chainC->getLastHash(true),
'state' =>
$chainC->getSummaryHashState(true)
];

$cloneChain = new Blakechain();
$cloneChain->setFirstPrevHash($clone['prev']);
$cloneChain->setSummaryHashState($clone['state']);

$chainC->appendData('abcdefj');
$cloneChain->appendData('abcdefj');

$this->assertSame(
$cloneChain->getSummaryHash(),
$chainC->getSummaryHash()
);
$this->assertSame(
$cloneChain->getLastHash(),
$chainC->getLastHash()
);

$this->assertSame(
$chainA->getSummaryHash(),
$chainC->getSummaryHash()
);

$this->assertSame(
$chainA->getSummaryHashState(), $chainB->getSummaryHashState()
);
$this->assertSame(
$chainB->getSummaryHashState(), $chainC->getSummaryHashState()
);
$this->assertSame(
$chainA->getSummaryHashState(), $chainC->getSummaryHashState()
);

$chainA->appendData('');

$this->assertNotSame(
$chainA->getSummaryHashState(), $chainB->getSummaryHashState()
);
$this->assertNotSame(
$chainA->getSummaryHashState(), $chainC->getSummaryHashState()
);
$this->assertNotSame(
$chainA->getSummaryHash(), $chainB->getSummaryHash()
);
$this->assertNotSame(
$chainA->getSummaryHash(), $chainC->getSummaryHash()
);
}
}

0 comments on commit 3f595e5

Please sign in to comment.