<?php

namespace Hwapx\Core\Library;

use InvalidArgumentException;

class EOpensslException extends \Exception {}
class Openssl {
    protected $publicKey = null;
    protected $privateKey = null;
    protected $publicKeySize = null;
    protected $privateKeySize = null;
    protected $privateKeyUsableSize = null;
    protected $signatureSize = 512;
    protected $padding = OPENSSL_PKCS1_PADDING;

    public function setPadding($padding) {
        $this->padding = $padding;
    }

    public function setKey($key, $password = '') {
        $this->setPublicKey($key);
        $this->setPrivateKey($key, $password);
    }

    public function setPublicKey($key) {
        if($this->publicKey)
            openssl_free_key($this->publicKey);

        $this->publicKey = openssl_get_publickey($key);

        $this->checkPublicKey();

        $details = openssl_pkey_get_details($this->publicKey);

        $this->publicKeySize = $details['bits'] / 8;
    }

    public function setPrivateKey($key, $password = '') {
        if($this->privateKey)
            openssl_free_key($this->privateKey);

        $this->privateKey = openssl_get_privateKey($key, $password);

        $this->checkprivateKey();

        $details = openssl_pkey_get_details($this->privateKey);

        $this->privateKeySize = $details['bits'] / 8;
        $this->privateKeyUsableSize = ($details['bits'] / 8) - 11;
    }

    protected function checkPublicKey() {
        if($this->publicKey === null)
            throw new InvalidArgumentException("Public key is not set");
        else if ($this->publicKey === false)
            throw new InvalidArgumentException("Invalid public key");
    }

    protected function checkprivateKey() {
        if($this->privateKey === null)
            throw new InvalidArgumentException("Private key is not set");
        else if ($this->privateKey === false)
            throw new InvalidArgumentException("Invalid private key");
    }

    public function privateEncode($data) {
        $data = $this->privateEncrypt($data);
        $signature = $this->sign($data);
        return base64_encode($data.$signature);
    }

    public function publicDecode($data) {
        $data = base64_decode($data);
        $signature = substr($data, -$this->signatureSize);
        $data = substr($data, 0, -$this->signatureSize);

        if(!$this->verify($data, $signature))
            return false;

        return $this->publicDecrypt($data);
    }

    public function publicEncode($data) {
        $data = $this->publicEncrypt($data);
        $signature = $this->sign($data);
        return base64_encode($data.$signature);
    }

    public function privateDecode($data) {
        $data = base64_decode($data);
        $signature = substr($data, -$this->signatureSize);
        $data = substr($data, 0, -$this->signatureSize);

        if(!$this->verify($data, $signature))
            return false;

        return $this->privateDecrypt($data);
    }

    public function privateEncrypt($data) {
        $this->checkprivateKey();
        if(strlen($data) > $this->privateKeyUsableSize) {
            $chunks = str_split($data, $this->privateKeyUsableSize);
            $result = array_map(array($this, 'privateEncrypt'), $chunks);
            return implode('', $result);
        }

        if(!openssl_private_encrypt($data, $out, $this->privateKey, $this->padding))
            throw new EOpensslException(openssl_error_string());

        return $out;
    }

    public function publicEncrypt($data) {
        $this->checkpublicKey();
        if(strlen($data) > $this->publicKeySize) {
            $chunks = str_split($data, $this->publicKeySize);
            $result = array_map(array($this, 'publicEncrypt'), $chunks);
            return implode('', $result);
        }

        if(!openssl_public_encrypt($data, $out, $this->publicKey, $this->padding))
            throw new EOpensslException(openssl_error_string());

        return $out;
    }

    public function publicDecrypt($data) {
        $this->checkPublicKey();

        if(strlen($data) > $this->publicKeySize) {
            $chunks = str_split($data, $this->publicKeySize);
            $result = array_map(array($this, 'publicDecrypt'), $chunks);
            return implode('', $result);
        }

        if(!openssl_public_decrypt($data, $out, $this->publicKey, $this->padding))
            throw new EOpensslException(openssl_error_string());

        return $out;
    }

    public function privateDecrypt($data) {
        $this->checkPrivateKey();

        if(strlen($data) > $this->privateKeySize) {
            $chunks = str_split($data, $this->privateKeySize);
            $result = array_map(array($this, 'privateDecrypt'), $chunks);
            return implode('', $result);
        }

        if(!openssl_private_decrypt($data, $out, $this->privateKey, $this->padding))
            throw new EOpensslException(openssl_error_string());

        return $out;
    }

    public function sign($data) {
        $this->checkprivateKey();

        openssl_sign($data, $signature, $this->privateKey, OPENSSL_ALGO_SHA512);

        return $signature;
    }

    public function verify($data, $signature) {
        $this->checkPublicKey();

        $ok = openssl_verify($data, $signature, $this->publicKey, OPENSSL_ALGO_SHA512);

        return $ok === 1;
    }
}