|
@@ -5,38 +5,55 @@ class Ppcl {
|
|
public $serial;
|
|
public $serial;
|
|
public $domains = array();
|
|
public $domains = array();
|
|
public $members = array();
|
|
public $members = array();
|
|
|
|
+ public $agents = array();
|
|
public $last_modified;
|
|
public $last_modified;
|
|
public $signature;
|
|
public $signature;
|
|
public $shared_signature_key;
|
|
public $shared_signature_key;
|
|
public $shared_signature;
|
|
public $shared_signature;
|
|
|
|
+ public $name;
|
|
|
|
+ public $current_state_token;
|
|
|
|
|
|
public $publications = array();
|
|
public $publications = array();
|
|
|
|
+ private $authoritative_part;
|
|
|
|
|
|
public function from_string($str) {
|
|
public function from_string($str) {
|
|
$lines = explode("\n", $str);
|
|
$lines = explode("\n", $str);
|
|
$header = explode(" ", $lines[0]);
|
|
$header = explode(" ", $lines[0]);
|
|
|
|
+ $this->authoritative_part = explode("\nMOD ", $str, 2)[0];
|
|
|
|
+
|
|
if($header[0] != "PPCL") {
|
|
if($header[0] != "PPCL") {
|
|
throw new Exception("Header does not start with PPCL magic number", 1);
|
|
throw new Exception("Header does not start with PPCL magic number", 1);
|
|
}
|
|
}
|
|
$this->identifier = base64_decode($header[1]);
|
|
$this->identifier = base64_decode($header[1]);
|
|
$this->serial = (int)$header[2];
|
|
$this->serial = (int)$header[2];
|
|
|
|
|
|
|
|
+ $authoritative = true;
|
|
foreach ($lines as $line) {
|
|
foreach ($lines as $line) {
|
|
$entry = explode(" ", $line);
|
|
$entry = explode(" ", $line);
|
|
if($entry[0] == "PPCL") {
|
|
if($entry[0] == "PPCL") {
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
- if($entry[0] == "SSK") {
|
|
|
|
|
|
+ if($entry[0] == "SSK" && $authoritative) {
|
|
$this->shared_signature_key = base64_decode($entry[1]);
|
|
$this->shared_signature_key = base64_decode($entry[1]);
|
|
}
|
|
}
|
|
- else if($entry[0] == "DOM") {
|
|
|
|
|
|
+ else if($entry[0] == "NAM" && $authoritative) {
|
|
|
|
+ $this->name = explode(" ", $line, 2)[1];
|
|
|
|
+ }
|
|
|
|
+ else if($entry[0] == "DOM" && $authoritative) {
|
|
array_push($this->domains, $entry[1]);
|
|
array_push($this->domains, $entry[1]);
|
|
}
|
|
}
|
|
- else if($entry[0] == "MEM") {
|
|
|
|
|
|
+ else if($entry[0] == "MEM" && $authoritative) {
|
|
array_push($this->members, new CollectionMember($entry[1], base64_decode($entry[2]), base64_decode($entry[3]), base64_decode($entry[4])));
|
|
array_push($this->members, new CollectionMember($entry[1], base64_decode($entry[2]), base64_decode($entry[3]), base64_decode($entry[4])));
|
|
}
|
|
}
|
|
- else if($entry[0] == "SIG") {
|
|
|
|
|
|
+ else if($entry[0] == "AGT" && $authoritative) {
|
|
|
|
+ array_push($this->agents, new CollectionAgent($entry[1], base64_decode($entry[2]), base64_decode($entry[3])));
|
|
|
|
+ }
|
|
|
|
+ else if($entry[0] == "SIG" && $authoritative) {
|
|
$this->signature = base64_decode($entry[1]);
|
|
$this->signature = base64_decode($entry[1]);
|
|
|
|
+ $authoritative = false;
|
|
|
|
+ }
|
|
|
|
+ else if($entry[0] == "CST") {
|
|
|
|
+ $this->current_state_token = base64_decode($entry[1]);
|
|
}
|
|
}
|
|
else if($entry[0] == "MOD") {
|
|
else if($entry[0] == "MOD") {
|
|
$this->last_modified = new DateTime($entry[1]);
|
|
$this->last_modified = new DateTime($entry[1]);
|
|
@@ -65,6 +82,39 @@ class Ppcl {
|
|
throw new Exception("Invalid shared signature", 1);
|
|
throw new Exception("Invalid shared signature", 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ public function to_string() {
|
|
|
|
+ $str = $this->authoritative_part;
|
|
|
|
+ $now = new DateTime();
|
|
|
|
+ $token = random_bytes(256);
|
|
|
|
+ $str .= "\nCST " . $token . "\nMOD " . $now->format(DateTime::ATOM) . "\n";
|
|
|
|
+ usort($this->publications, function ($a, $b) { return $a->timestamp <=> $b->timestamp; });
|
|
|
|
+ foreach($this->publications as $pub) {
|
|
|
|
+ $str .= "\n" . $pub->raw_data;
|
|
|
|
+ }
|
|
|
|
+ $checksum = hash("sha512", $str, true);
|
|
|
|
+
|
|
|
|
+ $agent = null;
|
|
|
|
+ foreach($this->agents as $agt) {
|
|
|
|
+ if($agt->public_key == base64_decode(PPCL_AGENT_PUBLIC_KEY)) {
|
|
|
|
+ $agent = $agt;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if($agent == null) {
|
|
|
|
+ throw new Exception("Public key defined in config (PPCL_AGENT_PUBLIC_KEY) not present in PPCL file", 1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ $key = sodium_crypto_box_keypair_from_secretkey_and_publickey(base64_decode(PPCL_AGENT_SECRET_KEY), $agent->public_key);
|
|
|
|
+ $secret = sodium_crypto_box_seal_open($agent->collection_secret, $key);
|
|
|
|
+ if($secret == null) {
|
|
|
|
+ throw new Exception("Could not decrypt collection secret", 1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ $shared_signature = sodium_crypto_sign($checksum, $secret);
|
|
|
|
+ $str .= "\nSSG " . base64_encode($shared_signature);
|
|
|
|
+ return $str;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
class CollectionMember {
|
|
class CollectionMember {
|
|
@@ -81,14 +131,28 @@ class CollectionMember {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+class CollectionAgent {
|
|
|
|
+ public $name;
|
|
|
|
+ public $public_key;
|
|
|
|
+ public $collection_secret;
|
|
|
|
+
|
|
|
|
+ public function __construct($name, $key, $secret) {
|
|
|
|
+ $this->name = $name;
|
|
|
|
+ $this->public_key = $key;
|
|
|
|
+ $this->collection_secret = $secret;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
class CollectionPublication {
|
|
class CollectionPublication {
|
|
public $name;
|
|
public $name;
|
|
public $member_name;
|
|
public $member_name;
|
|
public $timestamp;
|
|
public $timestamp;
|
|
public $signature;
|
|
public $signature;
|
|
public $checksum;
|
|
public $checksum;
|
|
|
|
+ public $raw_data;
|
|
|
|
|
|
public function __construct($line, $members) {
|
|
public function __construct($line, $members) {
|
|
|
|
+ $this->raw_data = $line;
|
|
$parts = explode(": ", $line, 2);
|
|
$parts = explode(": ", $line, 2);
|
|
$params = explode(" ", $parts[1]);
|
|
$params = explode(" ", $parts[1]);
|
|
|
|
|