package GPLVote::SignDoc::Client;

# Copyright (c) 2014, Andrey Velikoredchanin.
# This library is free software released under the GNU Lesser General
# Public License, Version 3.  Please read the important licensing and
# disclaimer information included below.

# $Id: Client.pm,v 0.1 2014/12/17 00:10:00 Andrey Velikoredchanin $

use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::AES;
use Crypt::CBC;
use Bytes::Random::Secure qw(random_bytes);
use MIME::Base64;
use LWP::UserAgent;
use Digest::SHA qw(sha256_base64);
use JSON;
use utf8;
use Encode;


use strict;
use Exporter;
use vars qw($VERSION);
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);

@ISA         = qw(Exporter);
@EXPORT      = qw(user_sign_is_valid calc_pub_key_id encrypt send_doc get_docs split_base64);
@EXPORT_OK   = qw(user_sign_is_valid calc_pub_key_id encrypt send_doc get_docs split_base64);
%EXPORT_TAGS = (DEFAULT => [qw(&user_sign_is_valid &calc_pub_key_id &encrypt &send_doc &get_docs &split_base64)]);

BEGIN {
  $VERSION = '0.5';
}

=head1 NAME

GPLVote::SignDoc::Client -  module for helping create GPLVote SignDoc client software.

=head1 SYNOPSIS

 use GPLVote::SignDoc::Client;

 if (user_sign_is_valid($public_key, $sign, $data)) {
    print "Sign of document is CORRECT\n";
 } else {
    print "BAD SIGN!!!\n";
 };

 my $pub_key_id = calc_pub_key_id($public_key);

 my $enc_data = encrypt($public_key, $data);


 def get_one_doc {
    my ($doc) = @_;

    push(@global_array, $doc);
 };

 get_doc('123', \&get_one_doc);
 foreach (my $doc (@global_array)) {
    ... process $doc
 };
 
=head1 Methods

=head2 user_sign_is_valid(base64_plain public_key, base64_plain sign, raw data)

Check signature of data.

public_key - RSA public key for check signature. Encoded in Base64 in one string without special
begin/finish strings and without line breaks.

sign - RSA signature. Some format like public_key.

data - signing data for verify signature.

Returning true if signature is valid.

=head2 split_base64(base_64 string)

Helping method for separate one long line Base64 on different lines with length 72 chars.

=head2 calc_pub_key_id(base64_plain public_key)

Calculate ID of public key.

=head2 encrypt(base64_plain public_key, raw data)

Encrypt data over public key.

Returning plain Base64 string with encrypted data.

=head2 send_doc(hash document, client_password)

Send document to proxy server.

document - hash with structured document description like:

* type - type of document (for example, "SIGN_REQUEST")

* site - client site identification name (for example, "www.site.com")

* data - encrypted document data like JSON array of strings (for example, encode('["string data1", "string data 2", ...]'))

* template - template for show document data, started from template type line (for example, "LIST\nTitle for data1\nTitle for data 2\n...")

* doc_id - internal client document identificator. Will be present in answered SIGN document.

client_password - client password

Return true, if success delivering

=head2 get_docs(site, client_password, get_one_doc function, unixtime from_time)

site - client site identification name

client_password - client password

function - reference to function with one argument: hash with structured document.

from_time - unix_time of moment after that get all documents

Return 1 if docs present.

=head1 BUGS

No known bugs, but this does not mean no bugs exist.

=head1 SEE ALSO

http://gplvote.org/

=head1 MAINTAINER

Andrey Velikoredchanin <andy@andyhost.ru>

=head1 AUTHOR

Andrey Velikoredchanin

=head1 COPYRIGHT

GPLVote::SignDoc::Client - module for helping create GPLVote SignDoc client software
Copyright (c) 2014, Andrey Velikoredchanin.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.

BECAUSE THIS LIBRARY IS LICENSED FREE OF CHARGE, THIS LIBRARY IS
BEING PROVIDED "AS IS WITH ALL FAULTS," WITHOUT ANY WARRANTIES
OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT
LIMITATION, ANY IMPLIED WARRANTIES OF TITLE, NONINFRINGEMENT,
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, AND THE
ENTIRE RISK AS TO SATISFACTORY QUALITY, PERFORMANCE, ACCURACY,
AND EFFORT IS WITH THE YOU.  See the GNU Lesser General Public
License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA

=cut


sub user_sign_is_valid {
  my ($b64_pub_key, $b64_sign, $data) = @_;

  my $b64_open_key = "-----BEGIN PUBLIC KEY-----\n".split_base64($b64_pub_key)."\n-----END PUBLIC KEY-----";
  my $dec_sign = decode_base64($b64_sign);
    
  my $rsa = Crypt::OpenSSL::RSA->new_public_key($b64_open_key);

  return($rsa->verify($data, $dec_sign));
}

sub split_base64 {
  my $text = $_[0];
      
  my $res = '';
  while ($text ne '') {
    if (length($text) > 72) {
      $res .= substr($text, 0, 72)."\n";
      $text = substr($text, 72, length($text) - 72);
    } else {
      $res .= $text;
      $text = '';
    };
  };

  return($res);
};

sub calc_pub_key_id {
  my $b64_pub_key = $_[0];

  my $pub_key = decode_base64($b64_pub_key);

  return(sha256_base64($pub_key));
};

sub encrypt {
  my ($b64_pub_key, $data) = @_;

  my $b64_open_key = "-----BEGIN PUBLIC KEY-----\n".split_base64($b64_pub_key)."\n-----END PUBLIC KEY-----";

  my $rsa = Crypt::OpenSSL::RSA->new_public_key($b64_open_key);
  $rsa->use_pkcs1_padding();
  if (length($data) <= 256) {
    return(encode_base64($rsa->encrypt($data), ''));
  } else {
    # TODO: реализовать блочное шифрование большого файла
    # Первые 256 байт - зашифрованный 256-битный AES ключ
    # Дальше - данные зашифрованные AES с помощью этого ключа
    my $enc_data = '';

    # Случайный ключ AES (256 bit)
    my $aes_key = random_bytes(32);
    # Случайный вектор инициализации IV (
    my $aes_iv = random_bytes(16);
    $enc_data .= $rsa->encrypt($aes_key.$aes_iv);

    my $aes_cbc = Crypt::CBC->new( -key => $aes_key,
                                    -literal_key => 1,
                                    -keysize => 32,
                                    -cipher => "Crypt::OpenSSL::AES",
                                    -iv => $aes_iv,
                                    -header => 'none' );
    
    $enc_data .= $aes_cbc->encrypt($data);

    return(encode_base64($enc_data, ''));
  };
};

sub send_doc {
  my ($doc, $client_password) = @_;

  my $server = "http://signdoc.gplvote.org/send";
  my $req = HTTP::Request->new(POST => $server);
  $req->header('content-type' => 'application/json');
  $req->header('x-password' => $client_password);

  my $data_request = to_json($doc);

  $req->content(encode('UTF-8', $data_request));

  my $ua = LWP::UserAgent->new;

  my $resp = $ua->request($req);

  if (!$resp->is_success) {
    warn "HTTP request error ".$resp->code." - ".$resp->message;
  }

  return($resp->is_success);
};

sub get_docs {
  my ($site, $client_password, $get_doc_func, $from_time) = @_;

  $from_time = time() - 3600*24 if !defined($from_time);
  
  my $doc_request = {
    type => 'CLIENT_REQUEST',
    site => $site,
    from_time => $from_time,
  };
  
  my $server = "http://signdoc.gplvote.org/get";
  my $req = HTTP::Request->new(POST => $server);
  $req->header('content-type' => 'application/json');
  $req->header('x-password' => $client_password);
  
  $req->content(to_json($doc_request));

  my $ua = LWP::UserAgent->new;

  my $resp = $ua->request($req);

  my $docs_present;
  if (!$resp->is_success) {
    warn "HTTP request error ".$resp->code." - ".$resp->message;
  } else {
    my $response = from_json($resp->content);
  
    if ($response->{status} eq '0') {
      # Перебираем документы и в зависимости от типа обрабатываем
      foreach my $doc (@{$response->{documents}}) {
        $docs_present = 1;
        $get_doc_func->($doc) if defined($get_doc_func);
      };
    } else {
      warn "Bad request auth status";
    };
  };
  
  return($docs_present);
};

sub to_hash {
  my ($json) = @_;
  my $h;
  my $js = JSON->new();
  # позволяет обработать невалидный json
  $js->relaxed(1);
  # преобразование в utf-8
  $js->utf8;
  eval {
    # eval нужен для того что-бы не падало приложение при ошибках обработки json
    $h = $js->decode($json);
  };
  undef($js);
  return($h);
};

sub from_hash {
  my ($h, $pretty) = @_;

  my $s = '';
  my $js = JSON->new();
  # позволяет обработать невалидный json
  $js->relaxed(1);
  # преобразование в utf-8
  # $js->utf8;
  $js->pretty(1) if ($pretty);
  eval {
    # eval нужен для того что-бы не падало приложение при ошибках обработки json
    $s = $js->encode($h);
  };
  undef($js);

  return($s);
};


1;