PHP Classes

File: src/Quill.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   PHP Quill   src/Quill.php   Download  
File: src/Quill.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PHP Quill
Write data to a Chronicle server using encryption
Author: By
Last change:
Date: 1 year ago
Size: 10,117 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace
ParagonIE\Quill;

use
GuzzleHttp\Client;
use
ParagonIE\Certainty\Exception\CertaintyException;
use
GuzzleHttp\Psr7\{
   
Request,
   
Response
};
use
GuzzleHttp\Exception\GuzzleException;
use
ParagonIE\Certainty\RemoteFetch;
use
ParagonIE\Sapient\Adapter\Guzzle;
use
ParagonIE\Sapient\CryptographyKeys\{
   
SealingPublicKey,
   
SharedEncryptionKey,
   
SigningPublicKey,
   
SigningSecretKey
};
use
ParagonIE\Sapient\Exception\{
   
HeaderMissingException,
   
InvalidMessageException
};
use
ParagonIE\Sapient\Sapient;
use
ParagonIE\Sapient\Simple;
use
Psr\Http\Message\ResponseInterface;

/**
 * Class Quill
 * @package ParagonIE\Quill
 */
class Quill
{
    const
CLIENT_ID_HEADER = 'Chronicle-Client-Key-ID';

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

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

   
/**
     * @var SigningSecretKey $clientSSK
     */
   
protected $clientSSK = null;

   
/**
     * @var Client
     */
   
protected $http = null;

   
/**
     * @var SigningPublicKey $serverSPK
     */
   
protected $serverSPK = null;

   
/**
     * Quill constructor.
     *
     * @param string $url
     * @param string $clientId
     * @param SigningPublicKey|null $serverPublicKey
     * @param SigningSecretKey|null $clientSecretKey
     * @param Client|null $http
     *
     * @throws CertaintyException
     * @throws \SodiumException
     * @throws \TypeError
     */
   
public function __construct(
       
string $url = '',
       
string $clientId = '',
       
SigningPublicKey $serverPublicKey = null,
       
SigningSecretKey $clientSecretKey = null,
       
Client $http = null
   
) {
        if (
$url) {
           
$this->chronicleUrl = $url;
        }
        if (
$clientId) {
           
$this->clientID = $clientId;
        }
        if (
$serverPublicKey) {
           
$this->serverSPK = $serverPublicKey;
        }
        if (
$clientSecretKey) {
           
$this->clientSSK = $clientSecretKey;
        }
        if (
$http) {
           
$this->http = $http;
        } else {
           
$this->http = new Client([
               
'curl.options' => [
                   
// https://github.com/curl/curl/blob/6aa86c493bd77b70d1f5018e102bc3094290d588/include/curl/curl.h#L1927
                   
CURLOPT_SSLVERSION =>
                       
CURL_SSLVERSION_TLSv1_2 | (CURL_SSLVERSION_TLSv1 << 16)
                ],
               
'verify' => (new RemoteFetch())->getLatestBundle()->getFilePath()
            ]);
        }
    }

   
/**
     * Write data to the Chronicle Instance. Return a boolean indicating
     * success or failure, discarding the response body after verification.
     *
     * @param string $data
     * @return bool
     */
   
public function blindWrite(string $data): bool
   
{
        try {
           
$response = $this->write($data);
           
// If we're here, the data was written successfully.
           
return $response instanceof ResponseInterface;
        } catch (
InvalidMessageException $ex) {
            return
false;
        } catch (
HeaderMissingException $ex) {
            return
false;
        }
    }

   
/**
     * Encrypt data with a shared (symmetric) encryption key, then write it
     * to a Chronicle. Returns TRUE if published successfully.
     *
     * @param string $data
     * @param SharedEncryptionKey $sharedEncryptionKey
     * @return bool
     */
   
public function blindWriteEncrypted(
       
string $data,
       
SharedEncryptionKey $sharedEncryptionKey
   
): bool
   
{
        try {
           
$response = $this->writeEncrypted($data, $sharedEncryptionKey);
           
// If we're here, the data was written successfully.
           
return $response instanceof ResponseInterface;
        } catch (
InvalidMessageException $ex) {
            return
false;
        } catch (
HeaderMissingException $ex) {
            return
false;
        }
    }

   
/**
     * Encrypt data with an public key (asymmetric encryption), then write it
     * to a Chronicle. Returns TRUE if published successfully.
     *
     * @param string $data
     * @param SealingPublicKey $publicKey
     * @return bool
     */
   
public function blindWriteSealed(
       
string $data,
       
SealingPublicKey $publicKey
   
): bool
   
{
        try {
           
$response = $this->writeSealed($data, $publicKey);
           
// If we're here, the data was written successfully.
           
return $response instanceof ResponseInterface;
        } catch (
InvalidMessageException $ex) {
            return
false;
        } catch (
HeaderMissingException $ex) {
            return
false;
        }
    }

   
/**
     * @return string
     */
   
public function getChronicleURL(): string
   
{
        return
$this->chronicleUrl;
    }

   
/**
     * @return string
     */
   
public function getClientID(): string
   
{
        return
$this->clientID;
    }

   
/**
     * @return SigningSecretKey
     */
   
public function getClientSecretKey(): SigningSecretKey
   
{
        return
$this->clientSSK;
    }

   
/**
     * @return SigningPublicKey
     */
   
public function getServerPublicKey(): SigningPublicKey
   
{
        return
$this->serverSPK;
    }

   
/**
     * @param string $url
     * @return self
     */
   
public function setChronicleURL(string $url): self
   
{
       
$this->chronicleUrl = $url;
        return
$this;
    }

   
/**
     * @param string $clientID
     * @return self
     */
   
public function setClientID(string $clientID): self
   
{
       
$this->clientID = $clientID;
        return
$this;
    }

   
/**
     * @param SigningSecretKey $secretKey
     * @return self
     */
   
public function setClientSecretKey(SigningSecretKey $secretKey): self
   
{
       
$this->clientSSK = $secretKey;
        return
$this;
    }

   
/**
     * @param SigningPublicKey $publicKey
     * @return self
     */
   
public function setServerPublicKey(SigningPublicKey $publicKey): self
   
{
       
$this->serverSPK = $publicKey;
        return
$this;
    }

   
/**
     * Encrypt a message and publish its contents onto a Chronicle instance,
     * using a shared encryption key. (Symmetric cryptography.)
     *
     * @param string $data
     * @param SharedEncryptionKey $sharedEncryptionKey
     * @return ResponseInterface
     * @throws HeaderMissingException
     * @throws InvalidMessageException
     */
   
public function writeEncrypted(
       
string $data,
       
SharedEncryptionKey $sharedEncryptionKey
   
): ResponseInterface {
        return
$this->write(
           
Simple::encrypt($data, $sharedEncryptionKey)
        );
    }

   
/**
     * Encrypt a message and publish its contents onto a Chronicle instance,
     * using a public encryption key. (Asymmetric cryptography.)
     *
     * @param string $data
     * @param SealingPublicKey $publicKey
     * @return ResponseInterface
     * @throws HeaderMissingException
     * @throws InvalidMessageException
     */
   
public function writeSealed(
       
string $data,
       
SealingPublicKey $publicKey
   
): ResponseInterface {
        return
$this->write(
           
Simple::seal($data, $publicKey)
        );
    }

   
/**
     * Write data to the Chronicle instance. Return the Response object.
     *
     * @param string $data
     * @return ResponseInterface
     *
     * @throws HeaderMissingException
     * @throws InvalidMessageException
     */
   
public function write(string $data): ResponseInterface
   
{
       
/** @psalm-suppress RedundantConditionGivenDocblockType */
       
$this->assertValid();
       
$sapient = new Sapient(new Guzzle($this->http));

       
$url = $this->chronicleUrl;
       
$pieces = \explode('/', \trim($this->chronicleUrl, '/'));
       
$last = \array_pop($pieces);
        if (
$last !== 'publish') {
           
$precursor = \array_pop($pieces);
            if (
$precursor === 'chronicle') {
               
$url = $this->chronicleUrl . '/publish';
            } else {
               
$url = $this->chronicleUrl . '/chronicle/publish';
            }
        }

       
$header = (string) static::CLIENT_ID_HEADER;
       
/** @var Request $request */
       
$request = $sapient->createSignedRequest(
           
'POST',
           
$url,
           
$data,
           
$this->clientSSK,
            [
               
$header => $this->clientID
           
]
        );
       
/** @var Response $response */
       
$response = $this->http->send($request);

       
/** @var Response $verified */
       
$verified = $sapient->verifySignedResponse(
           
$response,
           
$this->serverSPK
       
);
        return
$this->validateResponse($verified);
    }

   
/**
     * @throws \Error
     * @psalm-suppress DocblockTypeContradiction
     */
   
protected function assertValid(): void
   
{
        if (!
$this->clientID) {
            throw new \
Error('Client ID is not populated');
        }
        if (!
$this->chronicleUrl) {
            throw new \
Error('Chronicle URL is not populated');
        }
        if (!
$this->clientSSK) {
            throw new \
Error('Client signing secret key is not populated');
        }
        if (!
$this->serverSPK) {
            throw new \
Error('Server signing public key is not populated');
        }
    }

   
/**
     * Validate the Chronicle's JSON response.
     *
     * @param Response $response
     * @return Response
     * @throws InvalidMessageException
     */
   
protected function validateResponse(Response $response): Response
   
{
       
/** @var string $body */
       
$body = (string) $response->getBody();
       
/** @var array|false $decoded */
       
$decoded = \json_decode($body, true);
        if (!\
is_array($decoded)) {
            throw new
InvalidMessageException('Could not parse JSON body');
        }
        if (
$decoded['status'] !== 'OK') {
            throw new
InvalidMessageException(
                (string) (
$decoded['message'] ?? 'An unknown error has occurred.')
            );
        }
        return
$response;
    }
}