Răsfoiți Sursa

PPRF and PPCL support

Billy Barrow 1 an în urmă
părinte
comite
1fff9d80e1
5 a modificat fișierele cu 409 adăugiri și 0 ștergeri
  1. 6 0
      config.php
  2. 10 0
      index.php
  3. 130 0
      ppcl.php
  4. 260 0
      pprf.php
  5. 3 0
      router.php

+ 6 - 0
config.php

@@ -9,6 +9,12 @@ define("PUBLICATION_DIR", "ppubs");
 define("PUBLICATION_NAME", "Post");
 define("DATE_FORMAT", "l d F Y, H:i");
 define("USE_PPIX", false);
+define("USE_PPCL", true);
+define("ENABLE_PPRF", true);
+define("ENABLE_PPRF_BSPATCH", true);
+define("BSPATCH_PATH", "/usr/bin/bspatch");
+define("PPRF_DATA_DIR", "pprf-data");
+define("PPRF_MAX_UPLOAD_CHUNK_SIZE", 1024);
 define("THEME", "baseline");
 
 ?>

+ 10 - 0
index.php

@@ -42,6 +42,16 @@ function get_ppub_file_list() {
             return $list;
         }
     }
+    else if(USE_PPCL) {
+        include_once("ppcl.php");
+        $ppcl = new Ppcl();
+        $ppcl->from_string(file_get_contents(PUBLICATION_DIR . "/collection.ppcl"));
+        $list = array();
+        foreach($ppcl->publications as $pub) {
+            array_push($list, $pub->name);
+        }
+        return array_reverse($list);
+    }
     else {
         $dir = opendir(PUBLICATION_DIR . "/");
         $list = array();

+ 130 - 0
ppcl.php

@@ -0,0 +1,130 @@
+<?php
+
+class Ppcl {
+    public $identifier;
+    public $serial;
+    public $domains = array();
+    public $members = array();
+    public $last_modified;
+    public $signature;
+    public $shared_signature_key;
+    public $shared_signature;
+
+    public $publications = array();
+
+    public function from_string($str) {
+        $lines = explode("\n", $str);
+        $header = explode(" ", $lines[0]);
+        if($header[0] != "PPCL") {
+            throw new Exception("Header does not start with PPCL magic number", 1);
+        }
+        $this->identifier = base64_decode($header[1]);
+        $this->serial = (int)$header[2];
+
+        foreach ($lines as $line) {
+            $entry = explode(" ", $line);
+            if($entry[0] == "PPCL") {
+                continue;
+            }
+            if($entry[0] == "SSK") {
+                $this->shared_signature_key = base64_decode($entry[1]);
+            }
+            else if($entry[0] == "DOM") {
+                array_push($this->domains, $entry[1]);
+            }
+            else if($entry[0] == "MEM") {
+                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") {
+                $this->signature = base64_decode($entry[1]);
+            }
+            else if($entry[0] == "MOD") {
+                $this->last_modified = new DateTime($entry[1]);
+            }
+            else if($entry[0] == "PUB") {
+                array_push($this->publications, new CollectionPublication($line, $this->members));
+            }
+            else if($entry[0] == "SSG") {
+                $this->shared_signature = base64_decode($entry[1]);
+            }
+        }
+
+        // Verify authoritative signature
+        $parts = explode("\nSIG ", $str, 2);
+        $hash = hash("sha512", $parts[0], true);
+        $sig_data = sodium_crypto_sign_open($this->signature, $this->identifier);
+        if($hash != $sig_data) {
+            throw new Exception("Invalid authoritative signature", 1);
+        }
+
+        // Verify shared signature
+        $parts = explode("\nSSG ", $str, 2);
+        $hash = hash("sha512", $parts[0], true);
+        $sig_data = sodium_crypto_sign_open($this->shared_signature, $this->shared_signature_key);
+        if($hash != $sig_data) {
+            throw new Exception("Invalid shared signature", 1);
+        }
+    }
+}
+
+class CollectionMember {
+    public $name;
+    public $signing_public_key;
+    public $sealing_public_key;
+    public $collection_secret;
+    
+    public function __construct($name, $sign_key, $seal_key, $secret) {
+        $this->name = $name;
+        $this->signing_public_key = $sign_key;
+        $this->sealing_public_key = $seal_key;
+        $this->collection_secret = $secret;
+    }
+}
+
+class CollectionPublication {
+    public $name;
+    public $member_name;
+    public $timestamp;
+    public $signature;
+    public $checksum;
+
+    public function __construct($line, $members) {
+        $parts = explode(": ", $line, 2);
+        $params = explode(" ", $parts[1]);
+
+        $this->name = substr($parts[0], 4);
+        $this->member_name = $params[0];
+        $this->timestamp = new DateTime($params[1]);
+        $this->signature = base64_decode($params[2]);
+        
+        $member = null;
+        foreach($members as $m) {
+            if($m->name == $this->member_name) {
+                $member = $m;
+                break;
+            }
+        }
+
+        if($member == null) {
+            throw new Exception("PUB entry references member (" . $this->member_name . ") that is not present in the collection", 1);
+        }
+
+        $hash_data = $this->name . ": " . $this->member_name . " " . $params[1];
+        $hash = hash("sha512", $hash_data, true);
+        $sig_data = sodium_crypto_sign_open($this->signature, $member->signing_public_key);
+        if(substr($sig_data, 0, 64) != $hash) {
+            throw new Exception("Invalid member signature on publication " . $this->name , 1);
+        }
+
+        $this->checksum = substr($sig_data, 64, 64);
+    }
+    
+    public function verify_ppub() {
+        $file_hash = hash_file("sha512", PUBLICATION_DIR . "/" . $this->name, true);
+        return $file_hash == $this->checksum;
+    }
+
+}
+
+
+?>

+ 260 - 0
pprf.php

@@ -0,0 +1,260 @@
+<?php
+include("config.php");
+include_once("ppcl.php");
+
+function send_message($type, $message) {
+    header("Content-type: application/pprf");
+    echo("PPRF\x00");
+    $len = strlen($message) + 1;
+    echo(pack("PC", $len, $type));
+    echo($message);
+    exit();
+}
+
+function send_failure($code, $message) {
+    $len = strlen($message);
+    $msg = pack("vv", $code, $len);
+    $msg .= $message;
+    send_message(193, $msg);
+}
+
+function send_confirmation() {
+    send_message(192, "");
+}
+
+function get_ppcl() {
+    $ppcl = new Ppcl();
+    $ppcl->from_string(file_get_contents(PUBLICATION_DIR . "/collection.ppcl"));
+    return $ppcl;
+}
+
+function get_member($ppcl, $name) {
+    foreach($ppcl->members as $m) {
+        if($m->name == $name) {
+            return $m;
+        }
+    }
+    send_failure(3, "No member \"" . $member_name . "\" in collection");
+}
+
+function verify_collection_message($handle, $ppcl) {
+    $id = fread($handle, SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES);
+    if($ppcl->identifier != $id) {
+        send_failure(2, "Provided collection not hosted by this server");
+    }
+}
+
+function read_upload_session_message($handle) {
+    $data = array();
+    $tsize = unpack("v", fread($handle, 2))[1];
+    $data["token"] = fread($handle, $tsize);
+
+    $asize = unpack("v", fread($handle, 2))[1];
+    $data["auth"] = fread($handle, $asize);
+    return $data;
+}
+
+function get_upload_session($session_header) {
+    $file = fopen(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($session_header["token"]), 'rb');
+    if($file == null) {
+        send_failure(4, "Invalid upload session token");
+    }
+    $session = array();
+    $session["file_size"] = unpack("P", fread($file, 8))[1];
+    $session["key"] = fread($file, SODIUM_CRYPTO_BOX_KEYPAIRBYTES);
+    $name_size = unpack("C", fread($file, 1))[1];
+    $session["member_name"] = fread($file, $name_size);
+    return $session;
+}
+
+function verify_upload_auth($session_header, $session, $member, $checksum) {
+    $decrypted = sodium_crypto_box_seal_open($session_header["auth"], $session["key"]);
+    if($decrypted == null) {
+        send_failure(5, "Could not decrypt upload chunk auth");
+    }
+    $verified = sodium_crypto_sign_open($decrypted, $member->signing_public_key);
+    if($verified == null) {
+        send_failure(5, "Could not verify upload chunk auth");
+    }
+
+    if($verified != $checksum) {
+        send_failure(5, "Invalid checksum on upload chunk auth");
+    }
+}
+
+function cleanup_upload_session($token) {
+    unlink(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($token));
+    unlink(PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($token));
+}
+
+
+if(!ENABLE_PPRF) {
+    send_failure(1, "PPRF has been disabled on this server");
+}
+
+$handle = fopen("php://input", "rb");
+if(fread($handle, 5) != "PPRF\x00") {
+    send_failure(0, "Stream did not begin with PPRF magic number.");
+}
+
+$message_info = unpack("Psize/Ctype", fread($handle, 9));
+
+// Register name
+if($message_info["type"] == 32) {
+    $ppcl = get_ppcl();
+    verify_collection_message($handle, $ppcl);
+    $member_name_size = unpack("C", fread($handle, 1))[1];
+    $member_name = fread($handle, $member_name_size);
+    $member = get_member($ppcl, $member_name);
+
+    $signed_name_size = unpack("v", fread($handle, 2))[1];
+    $signed_name = fread($handle, $signed_name_size);
+
+    $name = sodium_crypto_sign_open($signed_name, $member->signing_public_key);
+    if($name == null) {
+        send_failure(11, "Could not verify signed name");
+    }
+
+    if(strtolower(substr($name, strlen($name)-5, 5)) != ".ppub") {
+        send_failure(7, "Name must end in \".ppub\"");
+    }
+
+    $path = PUBLICATION_DIR . "/" . $name;
+    if(file_exists($path)) {
+        send_failure(8, "Name already exists");
+    }
+
+    file_put_contents($path, "");
+    send_confirmation();
+}
+
+// Begin upload
+if($message_info["type"] == 33) {
+    $ppcl = get_ppcl();
+    verify_collection_message($handle, $ppcl);
+    $details = unpack("Pfile_size/Cname_size", fread($handle, 9));
+    $member_name = fread($handle, $details["name_size"]);
+    $member = get_member($ppcl, $member_name);
+
+    // Create upload session
+    $token = random_bytes(16);
+    $key = sodium_crypto_box_keypair();
+
+    $session_data = pack("P", $details["file_size"]);
+    $session_data .= $key;
+    $session_data .= pack("C", $details["name_size"]);
+    $session_data .= $member_name;
+    file_put_contents(PPRF_DATA_DIR . "/upload-session-info-" . bin2hex($token), $session_data);
+    file_put_contents(PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($token), "");
+
+    $session_auth = sodium_crypto_box_publickey($key);
+    $session_auth .= $token;
+    
+    // Build and send reply
+    $challenge = sodium_crypto_box_seal($session_auth, $member->sealing_public_key);
+    $reply = pack("v", strlen($challenge));
+    $reply .= $challenge;
+    $reply .= pack("V", PPRF_MAX_UPLOAD_CHUNK_SIZE);
+    send_message(160, $reply);
+}
+
+// Upload message
+if($message_info["type"] == 35) {
+    $ppcl = get_ppcl();
+    verify_collection_message($handle, $ppcl);
+    $session_header = read_upload_session_message($handle);
+    $session = get_upload_session($session_header);
+    $path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
+    $offset = unpack("P", fread($handle, 8))[1];
+
+    $member = get_member($ppcl, $session["member_name"]);
+
+    if($offset != filesize($path)) {
+        send_failure(6, "Invalid offset, got " . $offset . " expected " . filesize($path));
+    }
+
+    $data_size = $message_info["size"] - 9; // Message size minus headers
+    $data = fread($handle, $data_size);
+
+    // Verify data
+    $checksum = hash("sha512", $data, true);
+    verify_upload_auth($session_header, $session, $member, $checksum);
+
+    file_put_contents($path, $data, FILE_APPEND);
+    send_confirmation();
+}
+
+// Finalise upload message
+if($message_info["type"] == 34) {
+    $ppcl = get_ppcl();
+    verify_collection_message($handle, $ppcl);
+    $session_header = read_upload_session_message($handle);
+    $session = get_upload_session($session_header);
+    $upload_path = PPRF_DATA_DIR . "/upload-session-file-" . bin2hex($session_header["token"]);
+    $member = get_member($ppcl, $session["member_name"]);
+    
+    $flags = unpack('C', fread($handle, 1))[1];
+    $dest_len = unpack('v', fread($handle, 2))[1];
+    $dest_name = fread($handle, $dest_len);
+    $dest_path = PUBLICATION_DIR . "/" . $dest_name;
+    $bsdiff_old_checksum = null;
+
+    // If cancel flag set
+    if(($flags & 1 << 0) != 0) {    
+        cleanup_upload_session($session_header["token"]);
+        send_confirmation();
+    }
+
+    // If BSDIFF flag set
+    if(($flags & 1 << 2) != 0) {
+        $bsdiff_old_checksum = fread($handle, 64);
+    }
+
+    if(!file_exists($dest_path)) {
+        cleanup_upload_session($session_header["token"]);
+        send_failure(9, "The name \"" . $dest_name . "\" is not registered");
+    }
+
+    // If overwrite flag NOT set, and file is not empty
+    if(($flags & 1 << 1) == 0 && filesize($dest_path) != 0) {
+        cleanup_upload_session($session_header["token"]);
+        send_failure(10, "The destination \"" . $dest_name . "\" is not empty");
+    }
+
+    // Verify data
+    $checksum = hash_file("sha512", $upload_path, true);
+    verify_upload_auth($session_header, $session, $member, $checksum);
+
+    // Copy to destination
+    if($bsdiff_old_checksum == null) {
+        rename($upload_path, $dest_path);
+    }
+    else if(!ENABLE_PPRF_BSPATCH) {
+        send_failure(1, "BSPATCH is not supported by this server");
+    }
+    else {
+        $old_hash = hash_file("sha512", $dest_path, true);
+        if($bsdiff_old_checksum != $old_hash) {
+            cleanup_upload_session($session_header["token"]);
+            send_failure(12, "Destination file does not match provided checksum");
+        }
+        $patched_path = PPRF_DATA_DIR . "/upload-session-patch-" . bin2hex($session_header["token"]);
+        $output=null;
+        $retval=null;
+        exec(BSPATCH_PATH . " " . escapeshellarg($dest_path) . " " . escapeshellarg($patched_path) . " " . escapeshellarg($upload_path), $output, $retval);
+        if($retval != 0) {
+            cleanup_upload_session($session_header["token"]);
+            send_failure(13, "bspatch failed with exit code " . $retval);
+        }
+
+        rename($patched_path, $dest_path);
+    }
+
+    cleanup_upload_session($session_header["token"]);
+    send_confirmation();
+}
+
+
+send_failure(1, "Messages of type " . $message_info["type"] . " are not supported by this server");
+
+?>

+ 3 - 0
router.php

@@ -6,6 +6,9 @@ $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
 if (str_starts_with(dirname($path), "/themes") || (dirname($path) == '/' && (pathinfo($path, PATHINFO_EXTENSION) == 'css' || pathinfo($path, PATHINFO_EXTENSION) == 'js'))) {
   return false;
 }
+else if ($_SERVER["REQUEST_METHOD"] === "POST" && $_SERVER["CONTENT_TYPE"] === "application/pprf") {
+  $file = 'pprf.php';
+}
 else {
   $file = 'index.php';