Merge branch 'signer' into 'master'

Signer

See merge request lyseontech/assinador-digital!16
This commit is contained in:
jalxes 2020-10-28 16:31:12 +00:00
commit 7d9cfcf76a
64 changed files with 15759 additions and 52 deletions

View file

@ -1,3 +1,14 @@
FROM golang:1.14 as cfssl
WORKDIR /workdir
RUN git clone https://github.com/cloudflare/cfssl.git /workdir && \
git clone https://github.com/cloudflare/cfssl_trust.git /etc/cfssl && \
make clean && \
make bin/rice && ./bin/rice embed-go -i=./cli/serve && \
make all && cp bin/* /usr/bin/
RUN mkdir /home/cfssl/ && \
chown 1000:1000 /home/cfssl/
EXPOSE 8888
FROM nextcloud:stable-fpm as prod
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN apt update && apt install -y git
@ -7,6 +18,9 @@ RUN apt update && apt install -y cmake poppler-data libopenjp2-7-dev libfreetype
&& cd poppler-20.08.0 && mkdir build && cd build \
&& cmake .. -DBUILD_GTK_TESTS=OFF -DBUILD_QT5_TESTS=OFF -DBUILD_QT6_TESTS=OFF -DBUILD_CPP_TESTS=OFF -DENABLE_SPLASH=OFF -DENABLE_CPP=OFF -DENABLE_GLIB=OFF -DENABLE_GOBJECT_INTROSPECTION=OFF -DENABLE_QT5=OFF -DENABLE_QT6=OFF -DENABLE_CMS=lcms2 -DENABLE_LIBOPENJPEG=openjpeg2 \
&& make
RUN mkdir /tmp/cfssl/ && \
chmod 777 /tmp/cfssl/
COPY --from=cfssl /workdir/bin /usr/bin
FROM prod as dev
RUN yes | pecl install xdebug-2.9.6

19
.docker/cfssl/entrypoint.sh Executable file
View file

@ -0,0 +1,19 @@
#! /bin/bash
while [ ! -f /tmp/cfssl/csr_server.json ] || [ ! -f /tmp/cfssl/config_server.json ]; do
echo "no /tmp/cfssl/csr_server.json or /tmp/cfssl/config_server.json detected!";
sleep 10;
done;
cd /home/cfssl/;
if [ ! -f csr_server.json ]
then
cp /tmp/cfssl/csr_server.json /home/cfssl/csr_server.json;
cfssl genkey -initca=true csr_server.json | cfssljson -bare ca;
fi
if [ ! -f config_server.json ]
then
cp /tmp/cfssl/config_server.json /home/cfssl/config_server.json;
fi
cfssl serve -address=0.0.0.0 -ca-key ca-key.pem -ca ca.pem -config config_server.json

View file

@ -28,7 +28,7 @@ set-configs:
init-cron:
docker-compose up -d cron
install-dsv: fix-database build-dsv
install-dsv: build-dsv
fix-database:
docker-compose run --rm --user www-data app sh -c "php occ config:system:set dbname --value \$$POSTGRES_DB"
@ -42,4 +42,41 @@ build-dsv:
docker-compose exec app bash -c "cd /tmp/dsv/lib; composer install --no-interaction --no-dev"
docker-compose exec app sh -c "cp -r /tmp/dsv /var/www/html/apps/"
docker-compose exec --user www-data app php occ app:enable dsv
install-signer:
docker-compose build
docker-compose down
docker-compose up -d
docker-compose exec app sh -c "mkdir /var/www/html/apps/signer && \
cp -r /tmp/signer/appinfo \
/tmp/signer/img \
/tmp/signer/js \
/tmp/signer/lib \
/tmp/signer/src \
/tmp/signer/templates \
/tmp/signer/composer.json \
/tmp/signer/composer.lock /var/www/html/apps/signer"
docker-compose exec -w /var/www/html/apps/signer app bash -c "composer install --no-interaction --no-dev"
docker-compose exec -w /var/www/html/apps/signer app bash -c "chmod +x vendor/jeidison/jsignpdf-php/bin/jre1.8.0_241_linux/bin/java"
docker-compose exec --user www-data app php occ app:enable signer
update-signer:
docker-compose exec app sh -c "rsync -av /tmp/signer /var/www/html/apps/ --delete --exclude node_modules --exclude vendor"
test-signer: update-signer
docker-compose exec -w /var/www/html/apps/signer app sh -c "vendor/phpunit/phpunit/phpunit -c phpunit.xml"
build-signer-frontend:
cd ./signer; make
lint:
cd ./signer; make lint-fix
make update-signer
INSTALL=0
serve-signer-frontend-dev: update-signer
ifeq ($(INSTALL), 1)
cd ./volumes/nextcloud/apps/signer; make serve
else
cd ./volumes/nextcloud/apps/signer; make watch
endif

View file

@ -15,4 +15,8 @@
## Documentação do app Digital Signature Validator
- [Instalação e uso do app de validação de assinaturas](./docs/instalacao-uso-dsv.md)
- [Instalação e uso do app de validação de assinaturas](./docs/instalacao-uso-dsv.md)
## Documentação do app Signer
- [Instalação e uso do app assinador e gerador de assinaturas](./docs/instalacao-uso-signer.md)

View file

@ -1,57 +1,71 @@
version: "3.7"
volumes:
html:
html:
tmp:
services:
db:
image: postgres:12.3
restart: always
volumes:
- ./volumes/postgres/data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
db:
image: postgres:12.3
restart: always
volumes:
- ./volumes/postgres/data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
app:
build:
context: ./.docker/app
target: prod
restart: always
volumes:
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./volumes/nextcloud:/var/www/html
- ./dsv:/tmp/dsv
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin}
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS:-mydomain.coop}
depends_on:
- db
app:
build:
context: ./.docker/app
target: prod
restart: always
volumes:
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./volumes/nextcloud:/var/www/html
- ./dsv:/tmp/dsv
- ./signer:/tmp/signer
- tmp:/tmp/cfssl:rw
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB:-nextcloud}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-SECRET_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER:-nextcloud}
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin}
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin}
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS:-mydomain.coop}
depends_on:
- db
web:
image: nginx:1.18
restart: always
ports:
- 443:443
volumes:
- ./.docker/web/nginx.conf:/etc/nginx/nginx.conf
- ./.docker/web/conf.d/nextcloud.conf:/etc/nginx/conf.d/nextcloud.conf
- ./volumes/nextcloud:/var/www/html:ro
- ./certs/default.crt:/etc/nginx/certs/default.crt
- ./certs/default.key:/etc/nginx/certs/default.key
depends_on:
- app
web:
image: nginx:1.18
restart: always
ports:
- 443:443
volumes:
- ./.docker/web/nginx.conf:/etc/nginx/nginx.conf
- ./.docker/web/conf.d/nextcloud.conf:/etc/nginx/conf.d/nextcloud.conf
- ./volumes/nextcloud:/var/www/html:ro
- ./certs/default.crt:/etc/nginx/certs/default.crt
- ./certs/default.key:/etc/nginx/certs/default.key
depends_on:
- app
cron:
image: nextcloud:stable-fpm-alpine
restart: unless-stopped
volumes:
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./volumes/nextcloud:/var/www/html
entrypoint: /cron.sh
cron:
image: nextcloud:stable-fpm-alpine
restart: unless-stopped
volumes:
- ./.docker/app/conf.d/php.ini:/usr/local/etc/php/conf.d/php.ini
- ./volumes/nextcloud:/var/www/html
entrypoint: /cron.sh
cfssl:
build:
context: ./.docker/app
target: cfssl
working_dir: /home/cfssl
command: /home/cfssl/entrypoint.sh
volumes:
- ./.docker/cfssl/entrypoint.sh:/home/cfssl/entrypoint.sh
- ./volumes/cfssl:/home/cfssl
- tmp:/tmp/cfssl:ro

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,37 @@
# Signer
Aplicativo NextCloud que permite assinar documentos PDF e gerar novas assinaturas de uso interno.
## Instalação
Para instalar o aplicativo, basta rodar na pasta raiz do projeto `make install-signer`.
## Configuração para Geração de Assinaturas
Para a geração de novos assinaturas de uso interno, é preciso primeiro configurar o certificado raiz.
Este certificado deve conter as informações da Entidade Certificadora Raiz que será responsável por todas as assinaturas geradas, e será por isso, será vinculado a todas as assinaturas geradas.
Para configurar, acesse o painel de configurações com um usuário administrador, e vá para a opção de "Segurança" na sessão "Administração".
![Configuração do certificado raiz no painel administrativo](./certificado_raiz_painel_administrativo.png)
**Importante:** no momento, não é possivel alterar as informações do certificado raiz.
## Geração de Assinaturas
Uma vez que o certificado raiz foi gerado pelo administrador do sistema, qualquer usuário pode gerar uma assinatura.
Para isso, basta acessar o icone "Signer" no topo da página, e informar os dados do formulário. Estes dados serão usados para compôr as ĩnformações do certificado, que irão ser adicionados aos arquivos assinados com a assinatura gerada.
![Formulário para geração de nova assinatura](./formulario_nova_assinatura.png)
A assinatura será salva no sistema de arquivos do NextCloud, conforme selecionado no formulário, e pode ser baixada clicando com o botão direito e selecionando a opção "Baixar".
## Assinatura de documentos
Ao abrir os detalhes de um arquivo PDF, haverá uma nova aba com o label `Assinar Documento`. Nesta aba, basta informar qual a assinatura a ser utilizada e a senha desta assinatura e clicar no botão de "Assinar Documento". Feito isso, será gerado um novo arquivo no mesmo local onde o arquivo original está, com o prefixo "signed_".
![Aba onde é possivel assinar documentos](./assinar_documento_aba.png)
Os arquivos serão armazenados no sistema de arquivos do NextCloud, e podem ser baixados clicando com o botão direito e selecionando a opção "Baixar".

5
signer/.eslintrc.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
extends: [
'@nextcloud',
]
}

1
signer/.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
/js/* binary

6
signer/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.idea
*.iml
/vendor/
/build/
node_modules/
/.php_cs.cache

28
signer/Makefile Normal file
View file

@ -0,0 +1,28 @@
all: dev-setup build-production
serve: dev-setup watch
dev-setup: clean npm-init
npm-init:
docker-compose run --rm node npm ci
npm-update:
docker-compose run --rm node npm update
watch: npm-update
docker-compose up
build-production: lint
docker-compose run --rm node npm run build
lint-fix:
docker-compose run --rm node npm run lint:fix
docker-compose run --rm node npm run stylelint:fix
lint:
docker-compose run --rm node npm run lint
docker-compose run --rm node npm run stylelint
clean:
rm -rf js/*
rm -rf node_modules

6
signer/appinfo/app.php Normal file
View file

@ -0,0 +1,6 @@
<?php
if (false === (@include_once __DIR__.'/../vendor/autoload.php')) {
throw new Exception('Cannot include autoload. Did you run install dependencies using composer?');
}
$app = \OC::$server->query(OCA\Signer\AppInfo\Application::class);

23
signer/appinfo/info.xml Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>signer</id>
<name>Signer</name>
<summary>App for signing documents.</summary>
<description><![CDATA[This is a app for signing documents]]></description>
<version>1.0.1</version>
<licence>agpl</licence>
<namespace>Signer</namespace>
<dependencies>
<nextcloud min-version="17" max-version="20"/>
</dependencies>
<navigations>
<navigation>
<name>Signer</name>
<route>signer.page.index</route>
</navigation>
</navigations>
<settings>
<admin>OCA\Signer\Settings\AdminSettings</admin>
</settings>
</info>

14
signer/appinfo/routes.php Normal file
View file

@ -0,0 +1,14 @@
<?php
return [
'routes' => [
['name' => 'api#preflighted_cors', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+'], ],
['name' => 'signer#sign', 'url' => '/api/0.1/sign', 'verb' => 'POST'],
['name' => 'signature#generate', 'url' => '/api/0.1/signature/generate', 'verb' => 'POST'],
['name' => 'signature#check', 'url' => '/api/0.1/signature/check', 'verb' => 'GET'],
['name' => 'admin#generateCertificate', 'url' => '/api/0.1/admin/certificate', 'verb' => 'POST'],
['name' => 'admin#loadCertificate', 'url' => '/api/0.1/admin/certificate', 'verb' => 'GET'],
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
],
];

6
signer/babel.config.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: [
'@babel/plugin-syntax-dynamic-import',
],
presets: ['@babel/preset-env'],
}

18
signer/composer.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "lt/signer",
"description": "Signer",
"type": "project",
"license": "AGPL",
"require": {
"jeidison/jsignpdf-php": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5",
"nextcloud/coding-standard": "^0.3.0"
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix"
}
}

3219
signer/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

10
signer/docker-compose.yml Normal file
View file

@ -0,0 +1,10 @@
version: "3.7"
services:
node:
image: node
user: node
command: npm run watch
working_dir: /home/node/app
volumes:
- ./:/home/node/app

4
signer/img/app.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0" viewBox="0 0 32 32" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
<path fill="#FFF" d="m6 2c-2.216 0-4 1.784-4 4v20c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4v-16.719l-0.906 0.9068-5.282-5.2818 2.907-2.9062h-20.719zm15.812 4.9062l5.282 5.2818-8.313 8.312-8.781 3.5 3.5-8.781 8.312-8.3128zm-7.406 9.1878l-2.656 4.406 1.75 1.75 4.406-2.656-3.5-3.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 629 B

216
signer/js/signer-main.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

10
signer/js/signer-tab.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,28 @@
<?php
namespace OCA\Signer\AppInfo;
use OCA\Files\Event\LoadSidebar;
use OCA\Signer\Listener\LoadSidebarListener;
use OCA\Signer\Storage\ClientStorage;
use OCP\AppFramework\App;
use OCP\EventDispatcher\IEventDispatcher;
class Application extends App
{
public const APP_ID = 'signer';
public function __construct()
{
parent::__construct(self::APP_ID);
$container = $this->getContainer();
$dispatcher = $container->query(IEventDispatcher::class);
$dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class);
$container->registerService(ClientStorage::class, function ($c) {
return new ClientStorage(
$c->query('ServerContainer')->getUserFolder()
);
});
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace OCA\Signer\Controller;
use OCA\Signer\AppInfo\Application;
use OCA\Signer\Service\AdminSignatureService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class AdminController extends Controller
{
use HandleErrorsTrait;
use HandleParamsTrait;
/** @var AdminSignatureService */
private $service;
/** @var string */
private $userId;
public function __construct(
IRequest $request,
AdminSignatureService $service,
$userId
) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @NoCSRFRequired
*
* @todo remove NoCSRFRequired
*/
public function generateCertificate(
string $commonName = null,
string $country = null,
string $organization = null,
string $organizationUnit = null
): DataResponse {
try {
$this->checkParams([
'commonName' => $commonName,
'country' => $country,
'organization' => $organization,
'organizationUnit' => $organizationUnit,
]);
$this->service->generate(
$commonName,
$country,
$organization,
$organizationUnit
);
return new DataResponse([]);
} catch (\Exception $exception) {
return $this->handleErrors($exception);
}
}
/**
* @NoCSRFRequired
*
* @todo remove NoCSRFRequired
*/
public function loadCertificate(): DataResponse
{
try {
$certificate = $this->service->loadKeys();
return new DataResponse($certificate);
} catch (\Exception $exception) {
return $this->handleErrors($exception);
}
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace OCA\Signer\Controller;
use OCA\Signer\Exception\SignerException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
trait HandleErrorsTrait
{
protected function handleErrors(\Exception $exception): DataResponse
{
if ($exception instanceof SignerException) {
return new DataResponse($exception->jsonSerialize(), $exception->getCode());
}
return new DataResponse(
['message' => $exception->getMessage()],
Http::STATUS_INTERNAL_SERVER_ERROR
);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace OCA\Signer\Controller;
use OCA\Signer\Exception\SignerException;
trait HandleParamsTrait
{
protected function checkParams(array $params): void
{
foreach ($params as $key => $param) {
if (empty($param)) {
throw new SignerException("parameter '{$key}' is required!", 400);
}
}
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace OCA\Signer\Controller;
use OC\Config;
use OCA\Signer\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Util;
class PageController extends Controller
{
/** @var IConfig */
private $config;
public function __construct(IRequest $request, IConfig $config)
{
parent::__construct(Application::APP_ID, $request);
$this->config = $config;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Render default template
*/
public function index()
{
Util::addScript(Application::APP_ID, 'signer-main');
$response = new TemplateResponse(Application::APP_ID, 'main');
if ($this->config->getSystemValue('debug')) {
$csp = new ContentSecurityPolicy();
$csp->allowInlineScript(true);
$response->setContentSecurityPolicy($csp);
}
return $response;
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace OCA\Signer\Controller;
use OCA\Signer\AppInfo\Application;
use OCA\Signer\Service\SignatureService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class SignatureController extends Controller
{
use HandleErrorsTrait;
use HandleParamsTrait;
/** @var SignatureService */
private $service;
/** @var string */
private $userId;
public function __construct(
IRequest $request,
SignatureService $service,
$userId
) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @todo remove NoCSRFRequired
*/
public function generate(
string $commonName = null,
string $hosts = null,
string $country = null,
string $organization = null,
string $organizationUnit = null,
string $path = null,
string $password = null
): DataResponse {
try {
$this->checkParams([
'commonName' => $commonName,
'hosts' => $hosts,
'country' => $country,
'organization' => $organization,
'organizationUnit' => $organizationUnit,
'path' => $path,
'password' => $password,
]);
$hosts = explode(',', $hosts);
$signaturePath = $this->service->generate(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$path,
$password
);
return new DataResponse(['signature' => $signaturePath]);
} catch (\Exception $exception) {
return $this->handleErrors($exception);
}
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @todo remove NoCSRFRequired
*/
public function check(){
try{
$checkData = $this->service->check();
return new DataResponse($checkData);
} catch (\Exception $exception) {
return $this->handleErrors($exception);
}
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace OCA\Signer\Controller;
use OCA\Signer\AppInfo\Application;
use OCA\Signer\Exception\SignerException;
use OCA\Signer\Service\SignerService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class SignerController extends Controller
{
use HandleErrorsTrait;
use HandleParamsTrait;
/** @var SignerService */
private $service;
/** @var string */
private $userId;
public function __construct(
IRequest $request,
SignerService $service,
$userId
) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @todo remove NoCSRFRequired
*/
public function sign(
string $inputFilePath = null,
string $outputFolderPath = null,
string $certificatePath = null,
string $password = null
): DataResponse {
try {
$this->checkParams([
'inputFilePath' => $inputFilePath,
'outputFolderPath' => $outputFolderPath,
'certificatePath' => $certificatePath,
'password' => $password,
]);
$fileSigned = $this->service->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
return new DataResponse(['fileSigned' => $fileSigned->getInternalPath()]);
} catch (\Exception $exception) {
return $this->handleErrors($exception);
}
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace OCA\Signer\Exception;
use JsonSerializable;
class SignerException extends \Exception implements JsonSerializable
{
public function jsonSerialize()
{
return ['message' => $this->getMessage()];
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace OCA\Signer\Handler;
use GuzzleHttp\Client;
use OCA\Signer\Exception\SignerException;
class CfsslHandler
{
public function generateCertificate(
string $commonName,
array $hosts,
string $country,
string $organization,
string $organizationUnit,
string $password
) {
$certKeys = $this->newCert(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit
);
$certContent = null;
$isCertGenerated = openssl_pkcs12_export($certKeys['certificate'], $certContent, $certKeys['private_key'], $password);
if (!$isCertGenerated) {
throw new SignerException('Error while creating certificate file', 500);
}
return $certContent;
}
private function newCert(
string $commonName,
array $hosts,
string $country,
string $organization,
string $organizationUnit
) {
$response = (new Client(['base_uri' => 'http://cfssl:8888/api/v1/cfssl/']))
->request('POST', 'newcert', [
'json' => [
'profile' => 'CA',
'request' => [
'hosts' => $hosts,
'CN' => $commonName,
'key' => [
'algo' => 'rsa',
'size' => 2048,
],
'names' => [
[
'C' => $country,
'O' => $organization,
'OU' => $organizationUnit,
'CN' => $commonName,
],
],
],
],
]
)
;
$responseDecoded = json_decode($response->getBody(), true);
if (!$responseDecoded['success']) {
throw new SignerException('Error while generating certificate keys!', 500);
}
return $responseDecoded['result'];
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace OCA\Signer\Handler;
use OCA\Signer\Exception\SignerException;
class CfsslServerHandler
{
const CSR_FILE = 'csr_server.json';
const CONFIG_FILE = 'config_server.json';
const TMP_DIR = '/tmp/cfssl/';
public function createConfigServer(
$commonName,
$country,
$organization,
$organizationUnit,
$key
) {
$this->putCsrServer(
$commonName,
$country,
$organization,
$organizationUnit
);
$this->putConfigServer($key);
}
private function putCsrServer(
$commonName,
$country,
$organization,
$organizationUnit
)
{
$filename = self::TMP_DIR.self::CSR_FILE;
$content = [
'CN' => $commonName,
'key' => [
'algo' => 'rsa',
'size' => 2048,
],
'names' => [
[
'C' => $country,
'O' => $organization,
'OU' => $organizationUnit,
'CN' => $commonName,
],
],
];
$response = file_put_contents($filename, json_encode($content));
if ($response === false) {
throw new SignerException("Error while writing CSR server file!", 500);
}
}
private function putConfigServer(string $key)
{
$filename = self::TMP_DIR.self::CONFIG_FILE;
$content = [
'signing' => [
'profiles' => [
'CA' => [
'auth_key' => 'key1',
'expiry' => '8760h',
'usages' => [
"signing",
"digital signature",
"cert sign"
],
],
],
],
'auth_keys' => [
'key1' => [
'key' => $key,
'type' => 'standard',
],
],
];
$response = file_put_contents($filename, json_encode($content));
if ($response === false) {
throw new SignerException("Error while writing config server file!", 500);
}
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace OCA\Signer\Handler;
use Jeidison\JSignPDF\JSignPDF;
use Jeidison\JSignPDF\Sign\JSignParam;
use OC\Files\Node\File;
use OCA\Signer\Storage\ClientStorage;
class JSignerHandler
{
public function signExistingFile(
File $inputFile,
File $certificate,
string $password
): array {
$param = (new JSignParam())
->setCertificate($certificate->getContent())
->setPdf($inputFile->getContent())
->setPassword($password)
->setTempPath('/tmp/')
;
$jSignPdf = new JSignPDF($param);
$contentFileSigned = $jSignPdf->sign();
return [
'signed_'.$inputFile->getName(),
$contentFileSigned,
];
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace OCA\Signer\Listener;
use OCA\Files\Event\LoadSidebar;
use OCA\Signer\AppInfo\Application;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
class LoadSidebarListener implements IEventListener
{
public function handle(Event $event): void
{
if (!($event instanceof LoadSidebar)) {
return;
}
Util::addScript(Application::APP_ID, 'signer-tab');
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace OCA\Signer\Service;
use OCA\Signer\AppInfo\Application;
use OCA\Signer\Handler\CfsslServerHandler;
use OCP\IAppConfig;
class AdminSignatureService
{
/** @var CfsslServerHandler */
private $cfsslHandler;
/** @var IAppConfig */
private $config;
public function __construct(CfsslServerHandler $cfsslHandler, IAppConfig $config)
{
$this->cfsslHandler = $cfsslHandler;
$this->config = $config;
}
public function generate(
string $commonName,
string $country,
string $organization,
string $organizationUnit
) {
$key = bin2hex(random_bytes(16));
$this->config->setValue(Application::APP_ID, 'authkey', $key);
$this->config->setValue(Application::APP_ID, 'commonName', $commonName);
$this->config->setValue(Application::APP_ID, 'country', $country);
$this->config->setValue(Application::APP_ID, 'organization', $organization);
$this->config->setValue(Application::APP_ID, 'organizationUnit', $organizationUnit);
$this->cfsslHandler->createConfigServer(
$commonName,
$country,
$organization,
$organizationUnit,
$key
);
}
public function loadKeys(){
return [
'commonName' => $this->config->getValue(Application::APP_ID, 'commonName'),
'country' => $this->config->getValue(Application::APP_ID, 'country'),
'organization' => $this->config->getValue(Application::APP_ID, 'organization'),
'organizationUnit' => $this->config->getValue(Application::APP_ID, 'organizationUnit'),
];
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace OCA\Signer\Service;
use OCA\Signer\AppInfo\Application;
use OCA\Signer\Handler\CfsslHandler;
use OCA\Signer\Storage\ClientStorage;
use OCP\IAppConfig;
class SignatureService
{
/** @var CfsslHandler */
private $cfsslHandler;
/** @var ClientStorage */
private $clientStorage;
/** @var IAppConfig */
private $config;
public function __construct(
CfsslHandler $cfsslHandler,
ClientStorage $clientStorage,
IAppConfig $config
) {
$this->cfsslHandler = $cfsslHandler;
$this->clientStorage = $clientStorage;
$this->config = $config;
}
public function generate(
string $commonName,
array $hosts,
string $country,
string $organization,
string $organizationUnit,
string $certificatePath,
string $password
) {
$content = $this->cfsslHandler->generateCertificate(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$password
);
$folder = $this->clientStorage->createFolder($certificatePath);
$certificateFile = $this->clientStorage->saveFile($commonName.'.pfx', $content, $folder);
return $certificateFile->getInternalPath();
}
public function check()
{
return [
'hasRootCert' => null !== $this->config->getValue(Application::APP_ID, 'authkey'),
];
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace OCA\Signer\Service;
use OCA\Signer\Handler\JSignerHandler;
use OCA\Signer\Storage\ClientStorage;
class SignerService
{
/** @var JSignerHandler */
private $signerHandler;
/** @var ClientStorage */
private $clientStorage;
public function __construct(JSignerHandler $signerHandler, ClientStorage $clientStorage)
{
$this->signerHandler = $signerHandler;
$this->clientStorage = $clientStorage;
}
public function sign(string $inputFilePath, string $outputFolderPath, string $certificatePath, string $password){
$file = $this->clientStorage->getFile($inputFilePath);
$certificate = $this->clientStorage->getFile($certificatePath);
list($filename, $content) = $this->signerHandler->signExistingFile($file, $certificate, $password);
$folder = $this->clientStorage->createFolder($outputFolderPath);
$certificateFile = $this->clientStorage->saveFile($filename, $content, $folder);
return $certificateFile;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace OCA\Signer\Settings;
use OCA\Signer\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\BackgroundJob\IJobList;
use OCP\IAppConfig;
use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\Settings\ISettings;
class AdminSettings implements ISettings
{
public function getForm()
{
return new TemplateResponse(Application::APP_ID, 'settings');
}
public function getSection()
{
return 'security';
}
public function getPriority()
{
return 00;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace OCA\Signer\Storage;
use OC\Files\Node\File;
use OC\Files\Node\Node;
use OCA\Signer\Exception\SignerException;
use OCP\Files\Folder;
class ClientStorage
{
/** @var Folder */
private $storage;
public function __construct(Folder $userStorage)
{
$this->storage = $userStorage;
}
public function getFile(string $path): ?File
{
$node = $this->getNode($path);
if (!$node instanceof File) {
throw new SignerException("path {$path} is not a valid file!", 400);
}
return $node;
}
public function createFolder(string $path): Folder
{
if (!$this->storage->nodeExists($path)) {
return $this->storage->newFolder($path);
}
$node = $this->storage->get($path);
if (!$node instanceof Folder) {
throw new SignerException("path {$path} already exists and is not a folder!", 400);
}
return $node;
}
public function saveFile(string $filePath, $content = null, Folder $folder = null): File
{
if (null === $folder) {
$folder = $this->storage;
}
if (null !== $content && $folder->nodeExists($filePath)) {
$node = $folder->get($filePath);
if (!$node instanceof File) {
throw new SignerException("path {$filePath} already exists and is not a file!", 400);
}
$node->putContent($content);
return $node;
}
if (null !== $content) {
$file = $folder->newFile($filePath);
$file->putContent($content);
return $file;
}
return $folder->newFile($filePath);
}
private function getNode(string $path): Node
{
if (!$this->storage->nodeExists($path)) {
throw new SignerException("path {$path} is not valid!", 400);
}
return $this->storage->get($path);
}
}

10020
signer/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

66
signer/package.json Normal file
View file

@ -0,0 +1,66 @@
{
"name": "signer",
"description": "A app for signing documents",
"version": "1.0.0",
"license": "agpl",
"private": true,
"scripts": {
"build": "NODE_ENV=production webpack --progress --hide-modules --config webpack.js",
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue",
"stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix"
},
"dependencies": {
"@nextcloud/axios": "^1.4.0",
"@nextcloud/dialogs": "^2.0.1",
"@nextcloud/l10n": "^1.4.1",
"@nextcloud/logger": "^1.1.2",
"@nextcloud/router": "^1.2.0",
"@nextcloud/vue": "^2.6.5",
"style-loader": "^1.2.1",
"vue": "^2.6.12",
"webpack-merge": "^5.2.0"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": ">=10.0.0"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.11.5",
"@nextcloud/browserslist-config": "^1.0.0",
"@nextcloud/eslint-config": "^2.2.0",
"@nextcloud/eslint-plugin": "^1.4.0",
"@nextcloud/webpack-vue-config": "^1.4.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"css-loader": "^4.3.0",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.1",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.1.0",
"node-sass": "^4.14.1",
"sass-loader": "^8.0.2",
"stylelint": "^13.7.1",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.18.0",
"stylelint-webpack-plugin": "^2.1.0",
"url-loader": "^4.1.0",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
}
}

7
signer/phpunit.xml Normal file
View file

@ -0,0 +1,7 @@
<phpunit bootstrap="tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="unit">
<directory>./tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -0,0 +1,152 @@
<template>
<div id="formSigner" class="form-signer">
<div class="form-group">
<label for="commonName">{{ t('signer', 'Nome (CN)') }}</label>
<input
id="commonName"
ref="commonName"
v-model="certificate.commonName"
type="text"
:disabled="formDisabled">
</div>
<div class="form-group">
<label for="country">{{ t('signer', 'País (C)') }}</label>
<input
id="country"
ref="country"
v-model="certificate.country"
type="text"
:disabled="formDisabled">
</div>
<div class="form-group">
<label for="organization">{{ t('signer', 'Organização (O)') }}</label>
<input
id="organization"
ref="organization"
v-model="certificate.organization"
type="text"
:disabled="formDisabled">
</div>
<div class="form-group">
<label for="organizationUnit">{{ t('signer', 'Unidade da organização (OU)') }}</label>
<input
id="organizationUnit"
ref="organizationUnit"
v-model="certificate.organizationUnit"
type="text"
:disabled="formDisabled">
</div>
<input
type="button"
class="primary"
:value="submitLabel"
:disabled="formDisabled || !savePossible"
@click="generateCertificate">
</div>
</template>
<script>
import '@nextcloud/dialogs/styles/toast.scss'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
export default {
name: 'AdminFormSigner',
components: {
},
data() {
return {
certificate: {
commonName: '',
country: '',
organization: '',
organizationUnit: '',
},
submitLabel: t('signer', 'Gerar Certificado Raiz'),
formDisabled: false,
loading: true,
}
},
computed: {
savePossible() {
return (
this.certificate
&& this.certificate.commonName !== ''
&& this.certificate.country !== ''
&& this.certificate.organization !== ''
&& this.certificate.organizationUnit !== ''
)
},
},
async mounted() {
this.loading = false
this.loadRootCertificate()
},
methods: {
async generateCertificate() {
this.formDisabled = true
this.submitLabel = 'Gerando Certificado...'
try {
const response = await axios.post(
generateUrl('/apps/signer/api/0.1/admin/certificate'),
this.certificate
)
if (!response.data || response.data.message) {
throw new Error(response.data)
}
this.submitLabel = 'Certificado gerado!'
return
} catch (e) {
console.error(e)
showError(t('signer', 'Não foi possivel gerar certificado'))
this.submitLabel = 'Gerar Certificado Raiz'
}
this.formDisabled = false
},
async loadRootCertificate() {
this.formDisabled = true
try {
const response = await axios.get(
generateUrl('/apps/signer/api/0.1/admin/certificate'),
)
if (!response.data || response.data.message) {
throw new Error(response.data)
}
if (response.data.commonName
&& response.data.country
&& response.data.organization
&& response.data.organizationUnit
) {
this.certificate = response.data
this.submitLabel = 'Certificado gerado!'
return
}
} catch (e) {
console.error(e)
}
this.formDisabled = false
},
},
}
</script>
<style>
#formSigner{
width: 60%;
text-align: left;
margin: 20px;
}
.form-group > input[type='text'] {
width: 100%;
}
</style>

68
signer/src/App.vue Normal file
View file

@ -0,0 +1,68 @@
<template>
<AppContent>
<div v-if="error" class="emptycontent">
<div class="icon icon-error" />
<h2>{{ error }}</h2>
</div>
<div v-else id="content" class="app-signer">
<h2>{{ t('signer', 'Criar nova assinatura') }}</h2>
<FormSigner />
</div>
</AppContent>
</template>
<script>
import FormSigner from './FormSigner'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import '@nextcloud/dialogs/styles/toast.scss'
export default {
name: 'App',
components: {
FormSigner,
AppContent,
},
data() {
return {
loading: true,
error: '',
}
},
computed: {},
async mounted() {
await this.checkRootCertificate()
},
methods: {
async checkRootCertificate() {
this.error = ''
try {
const response = await axios.get(
generateUrl('/apps/signer/api/0.1/signature/check'),
)
if (!response.data || !response.data.hasRootCert) {
this.error = t('signer', 'Certificado raiz não foi configurado pelo Administrador!')
}
} catch (e) {
console.error(e)
}
},
},
}
</script>
<style scoped>
#content {
width: 100vw;
padding: 20px;
padding-top: 70px;
display: flex;
flex-direction: column;
flex-grow: 1;
align-items: center;
}
</style>

220
signer/src/FormSigner.vue Normal file
View file

@ -0,0 +1,220 @@
<template>
<div id="formSigner" class="form-signer">
<div class="form-group">
<label for="hosts">{{ t('signer', 'Email') }}</label>
<input
id="hosts"
ref="hosts"
v-model="signature.hosts"
type="email"
:disabled="updating">
</div>
<div class="form-group">
<label for="commonName">{{ t('signer', 'Nome (CN)') }}</label>
<input
id="commonName"
ref="commonName"
v-model="signature.commonName"
type="text"
:disabled="updating">
</div>
<div class="form-group">
<label for="country">{{ t('signer', 'País (C)') }}</label>
<input
id="country"
ref="country"
v-model="signature.country"
type="text"
:disabled="updating">
</div>
<div class="form-group">
<label for="organization">{{ t('signer', 'Organização (O)') }}</label>
<input
id="organization"
ref="organization"
v-model="signature.organization"
type="text"
:disabled="updating">
</div>
<div class="form-group">
<label for="organizationUnit">{{ t('signer', 'Unidade da organização (OU)') }}</label>
<input
id="organizationUnit"
ref="organizationUnit"
v-model="signature.organizationUnit"
type="text"
:disabled="updating">
</div>
<div class="form-group">
<label for="password">{{ t('signer', 'Senha da assinatura') }}</label>
<input
id="password"
v-model="signature.password"
type="text"
:disabled="updating">
</div>
<div class="form-group">
<label for="path">{{ t('signer', 'Armazenamento da assinatura') }}</label>
<div>
<input
id="path"
ref="path"
v-model="signature.path"
type="text"
:disabled="1">
<button
id="pickFromCloud"
:class="'icon-folder'"
:title="t('signer', 'Selecionar pasta onde assinatura será salva')"
:disabled="updating"
@click.stop="pickFromCloud">
{{ t('signer', 'Selecionar Pasta') }}
</button>
</div>
</div>
<input
type="button"
class="primary"
:value="t('signer', 'Gerar Assinatura')"
:disabled="updating || !savePossible"
@click="saveSignature">
<Modal
v-if="modal"
dark=""
@close="closeModal">
<div class="modal_content">
{{ t('signer','Assinatura gerada e disponivel em ') }} {{ signature.path }} !
</div>
</Modal>
</div>
</template>
<script>
import '@nextcloud/dialogs/styles/toast.scss'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { showError, getFilePickerBuilder } from '@nextcloud/dialogs'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import { translate as t } from '@nextcloud/l10n'
export default {
name: 'FormSigner',
components: {
Modal,
},
data() {
return {
signature: {
commonName: '',
hosts: '',
country: '',
organization: '',
organizationUnit: '',
path: '',
password: '',
},
updating: false,
loading: true,
modal: false,
}
},
computed: {
savePossible() {
return (
this.signature
&& this.signature.commonName !== ''
&& this.signature.hosts !== ''
&& this.signature.country !== ''
&& this.signature.organization !== ''
&& this.signature.organizationUnit !== ''
&& this.signature.password !== ''
&& this.signature.path !== ''
)
},
},
async mounted() {
this.loading = false
this.$refs.hosts.focus()
},
methods: {
async saveSignature() {
this.updating = true
try {
const response = await axios.post(
generateUrl('/apps/signer/api/0.1/signature/generate'),
this.signature
)
if (!response.data || !response.data.signature) {
throw new Error(response.data)
}
this.signature.path = response.data.signature
this.showModal()
} catch (e) {
console.error(e)
showError(t('signer', 'Não foi possivel criar assinatura'))
}
this.updating = false
},
showModal() {
this.modal = true
},
closeModal() {
this.modal = false
this.signature = {
commonName: '',
hosts: '',
country: '',
organization: '',
organizationUnit: '',
path: '',
password: '',
}
},
pickFromCloud() {
const picker = getFilePickerBuilder(t('signer', 'Escolha uma pasta para armazenar a assinatura'))
.setMultiSelect(false)
.addMimeTypeFilter('httpd/unix-directory')
.setModal(true)
.setType(1)
.allowDirectories(true)
.build()
picker.pick().then((path) => {
if (!path) {
path = '/'
}
this.signature.path = path
})
},
},
}
</script>
<style>
#formSigner{
width: 60%;
text-align: left;
margin: 20px;
}
.form-group > input[type='text'],
.form-group > input[type='email'] {
width: 100%;
}
#path {
width: 80%;
}
#pickFromCloud{
display: inline-block;
margin: 16px;
background-position: 16px center;
padding: 12px;
padding-left: 44px;
}
.modal_content {
text-align: center;
margin: 40px;
}
</style>

44
signer/src/Settings.vue Normal file
View file

@ -0,0 +1,44 @@
<template>
<SettingsSection
:title="title"
:description="description">
<AdminFormSigner />
</SettingsSection>
</template>
<script>
import AdminFormSigner from './AdminFormSigner'
import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
import { translate as t } from '@nextcloud/l10n'
export default {
name: 'Settings',
components: {
AdminFormSigner,
SettingsSection,
},
data() {
return {
loading: true,
title: t('signer', 'Assinador Digital: Dados Certificado Raiz'),
description: t('signer', 'Para gerar novas assinaturas, é preciso primeiro gerar o ceritificado raiz'),
}
},
computed: {},
async mounted() {},
methods: {},
}
</script>
<style scoped>
#signer-admin-settings {
width: 100vw;
padding: 20px;
padding-top: 70px;
display: flex;
flex-direction: column;
flex-grow: 1;
align-items: center;
}
</style>

166
signer/src/SignerTab.vue Normal file
View file

@ -0,0 +1,166 @@
<template>
<AppSidebarTab
:id="id"
:icon="icon"
:name="name">
<div v-if="error" class="emptycontent">
<div class="icon icon-error" />
<h2>{{ error }}</h2>
</div>
<div v-else-if="response" class="emptycontent">
<div class="icon icon-checkmark" />
<h2>{{ response }}</h2>
</div>
<div v-else id="signerTabContent">
<label for="path">{{ t('signer', 'Local da assinatura') }}</label>
<div class="form-group">
<input
id="path"
ref="path"
v-model="signaturePath"
type="text"
:disabled="1">
<button
id="pickFromCloud"
:class="'icon-folder'"
:title="t('signer', 'Selecionar local da assinatura')"
:disabled="updating"
@click.stop="pickFromCloud">
{{ t('signer', 'Selecionar Assinatura') }}
</button>
</div>
<label for="password">{{ t('signer', 'Senha da assinatura') }}</label>
<div class="form-group">
<input
id="password"
v-model="password"
type="password"
:disabled="updating">
</div>
<input
type="button"
class="primary"
:value="t('signer', 'Assinar Documento')"
:disabled="updating || !savePossible"
@click="sign">
</div>
</AppSidebarTab>
</template>
<script>
import AppSidebarTab from '@nextcloud/vue/dist/Components/AppSidebarTab'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
export default {
name: 'SignerTab',
components: {
AppSidebarTab,
},
mixins: [],
props: {
fileInfo: {
type: Object,
default: () => {},
required: true,
},
},
data() {
return {
signaturePath: '',
password: '',
response: '',
icon: 'icon-rename',
updating: false,
loading: true,
name: t('signer', 'Assinar Documento'),
}
},
computed: {
id() {
return 'signerTab'
},
activeTab() {
return this.$parent.activeTab
},
savePossible() {
return (
this.password !== ''
&& this.signaturePath !== ''
)
},
},
methods: {
async sign() {
this.updating = true
this.response = ''
this.error = ''
try {
const response = await axios.post(
generateUrl('/apps/signer/api/0.1/sign'),
{
inputFilePath: OC.joinPaths(this.fileInfo.get('path'), this.fileInfo.get('name')),
outputFolderPath: this.fileInfo.get('path'),
certificatePath: this.signaturePath,
password: this.password,
}
)
if (!response.data || !response.data.fileSigned) {
throw new Error(response.data)
}
this.response = t('signer', 'Documento assinado disponivel em ') + response.data.fileSigned
} catch (e) {
console.error(e)
this.error = t('signer', 'Não foi possivel assinar documento!')
}
this.updating = false
},
pickFromCloud() {
const picker = getFilePickerBuilder(t('signer', 'Escolha o local da assinatura'))
.setMultiSelect(false)
.addMimeTypeFilter('application/octet-stream')
.setModal(true)
.setType(1)
.allowDirectories(false)
.build()
picker.pick().then((path) => {
this.signaturePath = path
})
},
},
}
</script>
<style>
#signerTabContent {
display: flex;
flex-direction: column;
}
.form-group > input {
width: 50%;
}
.form-group > input[type='button'] {
width: 80%;
margin: 2em;
}
#pickFromCloud{
display: inline-block;
background-position: 16px center;
padding: 12px;
padding-left: 44px;
}
</style>

9
signer/src/main.js Normal file
View file

@ -0,0 +1,9 @@
import Vue from 'vue'
import App from './App'
Vue.mixin({ methods: { t, n } })
export default new Vue({
el: '#content',
render: h => h(App),
})

9
signer/src/settings.js Normal file
View file

@ -0,0 +1,9 @@
import Vue from 'vue'
import Settings from './Settings'
Vue.mixin({ methods: { t, n } })
export default new Vue({
el: '#signer-admin-settings',
render: h => h(Settings),
})

14
signer/src/tab.js Normal file
View file

@ -0,0 +1,14 @@
import SignerTab from './SignerTab'
window.addEventListener('DOMContentLoaded', () => {
if (OCA.Files && OCA.Files.Sidebar) {
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab('signer', SignerTab, (fileInfo) => {
if (!fileInfo || fileInfo.isDirectory()) {
return false
}
const mimetype = fileInfo.get('mimetype') || ''
return mimetype === 'application/pdf'
}))
}
})

View file

@ -0,0 +1,32 @@
module.exports = {
extends: 'stylelint-config-recommended-scss',
rules: {
indentation: 'tab',
'selector-type-no-unknown': null,
'number-leading-zero': null,
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'inside-block'],
},
],
'declaration-empty-line-before': [
'never',
{
ignore: ['after-declaration'],
},
],
'comment-empty-line-before': null,
'selector-type-case': null,
'selector-list-comma-newline-after': null,
'no-descending-specificity': null,
'string-quotes': 'single',
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep'],
},
],
},
plugins: ['stylelint-scss'],
}

View file

@ -0,0 +1 @@
<div id="content"></div>

View file

@ -0,0 +1,5 @@
<?php
script('signer', 'signer-settings');
?>
<div id="signer-admin-settings" class="section">
</div>

View file

@ -0,0 +1,121 @@
<?php
namespace OCA\Signer\Tests\Unit\Controller;
use OCA\Signer\Controller\SignatureController;
use OCA\Signer\Service\SignatureService;
use OCP\IRequest;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class SignatureControllerTest extends TestCase
{
public function testGenerate()
{
$userId = 'john';
$request = $this->prophesize(IRequest::class);
$service = $this->prophesize(SignatureService::class);
$commonName = 'someCommonName';
$hosts = 'someHosts';
$country = 'someCountry';
$organization = 'someOrganization';
$organizationUnit = 'someOrganizationUnit';
$path = '/path/to/somePath';
$password = 'somePassword';
$service->generate(
$commonName,
[$hosts],
$country,
$organization,
$organizationUnit,
$path,
$password
)
->shouldBeCalled()
->willReturn('/path/to/someSignature')
;
$controller = new SignatureController(
$request->reveal(),
$service->reveal(),
$userId
);
$result = $controller->generate(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$path,
$password
);
static::assertSame(['signature' => '/path/to/someSignature'], $result->getData());
}
public function failParameterMissingProvider()
{
$commonName = 'someCommonName';
$hosts = 'someHosts';
$country = 'someCountry';
$organization = 'someOrganization';
$organizationUnit = 'someOrganizationUnit';
$path = '/path/to/somePath';
$password = 'somePassword';
return [
[null, $hosts, $country, $organization, $organizationUnit, $path, $password, 'commonName'],
[$commonName, null, $country, $organization, $organizationUnit, $path, $password, 'hosts'],
[$commonName, $hosts, null, $organization, $organizationUnit, $path, $password, 'country'],
[$commonName, $hosts, $country, null, $organizationUnit, $path, $password, 'organization'],
[$commonName, $hosts, $country, $organization, null, $path, $password, 'organizationUnit'],
[$commonName, $hosts, $country, $organization, $organizationUnit, null, $password, 'path'],
[$commonName, $hosts, $country, $organization, $organizationUnit, $path, null, 'password'],
];
}
/** @dataProvider failParameterMissingProvider */
public function testGenerateFailParameterMissing(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$path,
$password,
$paramenterMissing
) {
$userId = 'john';
$request = $this->prophesize(IRequest::class);
$service = $this->prophesize(SignatureService::class);
$service->generate(\Prophecy\Argument::cetera())
->shouldNotBeCalled()
;
$controller = new SignatureController(
$request->reveal(),
$service->reveal(),
$userId
);
$result = $controller->generate(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$path,
$password,
);
static::assertSame(['message' => "parameter '{$paramenterMissing}' is required!"], $result->getData());
static::assertSame(400, $result->getStatus());
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace OCA\Signer\Tests\Unit\Controller;
use OC\Files\Node\File;
use OCA\Signer\Controller\SignerController;
use OCA\Signer\Service\SignerService;
use OCP\IRequest;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class SignerControllerTest extends TestCase
{
public function testSignFile()
{
$userId = 'john';
$request = $this->prophesize(IRequest::class);
$service = $this->prophesize(SignerService::class);
$file = $this->prophesize(File::class);
$file->getInternalPath()->willReturn("/path/to/someFileSigned");
$inputFilePath = '/path/to/someInputFilePath';
$outputFolderPath = '/path/to/someOutputFolderPath';
$certificatePath = '/path/to/someCertificatePath';
$password = 'somePassword';
$service->sign($inputFilePath, $outputFolderPath, $certificatePath, $password)
->shouldBeCalled()
->willReturn($file->reveal())
;
$controller = new SignerController(
$request->reveal(),
$service->reveal(),
$userId
);
$result = $controller->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
static::assertSame(['fileSigned' => '/path/to/someFileSigned'], $result->getData());
}
public function failParameterMissingProvider()
{
$inputFilePath = '/path/to/someInputFilePath';
$outputFolderPath = '/path/to/someOutputFolderPath';
$certificatePath = '/path/to/someCertificatePath';
$password = 'somePassword';
return [
[null, $outputFolderPath, $certificatePath, $password, 'inputFilePath'],
[$inputFilePath, null, $certificatePath, $password, 'outputFolderPath'],
[$inputFilePath, $outputFolderPath, null, $password, 'certificatePath'],
[$inputFilePath, $outputFolderPath, $certificatePath, null, 'password'],
];
}
/** @dataProvider failParameterMissingProvider */
public function testSignFileFailParameterMissing(
$inputFilePath,
$outputFolderPath,
$certificatePath,
$password,
$paramenterMissing
){
$userId = 'john';
$request = $this->prophesize(IRequest::class);
$service = $this->prophesize(SignerService::class);
$service->sign(\Prophecy\Argument::cetera())
->shouldNotBeCalled();
$controller = new SignerController(
$request->reveal(),
$service->reveal(),
$userId
);
$result = $controller->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
static::assertSame(['message' => "parameter '{$paramenterMissing}' is required!"], $result->getData());
static::assertSame(400, $result->getStatus());
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace OCA\Signer\Tests\Unit\Service;
use OCA\Signer\Handler\CfsslHandler;
use OCA\Signer\Service\SignatureService;
use OCA\Signer\Storage\ClientStorage;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class SignatureServiceTest extends TestCase
{
public function testGenerate()
{
$commonName = 'someCommonName';
$hosts = ['someHosts'];
$country = 'someCountry';
$organization = 'someOrganization';
$organizationUnit = 'someOrganizationUnit';
$path = '/path/to/somePath';
$password = 'somePassword';
$content = 'someContent';
$cfsslHandler = $this->prophesize(CfsslHandler::class);
$clientStorage = $this->prophesize(ClientStorage::class);
$cfsslHandler
->generateCertificate(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$password
)
->shouldBeCalled()
->willReturn($content)
;
$clientStorage->createFolder($path)
->shouldBeCalled()
;
$clientStorage->saveFile($commonName.'.pfx', $content, \Prophecy\Argument::any())
->shouldBeCalled()
;
$service = new SignatureService($cfsslHandler->reveal(), $clientStorage->reveal());
$service->generate(
$commonName,
$hosts,
$country,
$organization,
$organizationUnit,
$path,
$password
);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace OCA\Signer\Tests\Unit\Service;
use OCA\Signer\Handler\JSignerHandler;
use OCA\Signer\Service\SignerService;
use OCA\Signer\Storage\ClientStorage;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class SignerServiceTest extends TestCase
{
public function testSignFile()
{
$inputFilePath = '/path/to/someInputFilePath';
$outputFolderPath = '/path/to/someOutputFolderPath';
$certificatePath = '/path/to/someCertificatePath';
$password = 'somePassword';
$signerHandler = $this->prophesize(JSignerHandler::class);
$clientStorage = $this->prophesize(ClientStorage::class);
$filename = 'someFilename';
$content = 'someContent';
$clientStorage->getFile($inputFilePath)
->shouldBeCalled()
;
$clientStorage->getFile($certificatePath)
->shouldBeCalled()
;
$signerHandler->signExistingFile(\Prophecy\Argument::any(), \Prophecy\Argument::any(), $password)
->shouldBeCalled()
->willReturn([$filename, $content])
;
$clientStorage->createFolder($outputFolderPath)
->shouldBeCalled()
;
$clientStorage->saveFile($filename, $content, \Prophecy\Argument::any())
->shouldBeCalled()
;
$service = new SignerService($signerHandler->reveal(), $clientStorage->reveal());
$service->sign($inputFilePath, $outputFolderPath, $certificatePath, $password);
}
}

View file

@ -0,0 +1,7 @@
<?php
require_once __DIR__.'/../../../lib/base.php';
require_once __DIR__.'/../vendor/autoload.php';
\OC::$loader->addValidRoot(OC::$SERVERROOT . '/tests');
\OC_App::loadApp('signer');

12
signer/webpack.js Normal file
View file

@ -0,0 +1,12 @@
const { merge } = require('webpack-merge')
const path = require('path')
const webpackConfig = require('@nextcloud/webpack-vue-config')
const config = {
entry: {
tab: path.resolve(path.join('src', 'tab.js')),
settings: path.resolve(path.join('src', 'settings.js')),
},
}
module.exports = merge(config, webpackConfig)