signing metadata to ensure integrity on remote instances

This commit is contained in:
Maxence Lange 2021-09-17 11:09:54 -01:00
Родитель 9d8d323f3d
Коммит 8b8aa24f32
11 изменённых файлов: 308 добавлений и 129 удалений

12
composer.lock сгенерированный
Просмотреть файл

@ -8,16 +8,16 @@
"packages": [
{
"name": "artificial-owl/my-small-php-tools",
"version": "v23.0.6",
"version": "v23.0.7",
"source": {
"type": "git",
"url": "https://github.com/ArtificialOwl/my-small-php-tools.git",
"reference": "f3ee2fab1ce33a1edc0993b42c7008196d75015d"
"reference": "0d9011b41954f072ba5ff721aee6b83b583a4da2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ArtificialOwl/my-small-php-tools/zipball/f3ee2fab1ce33a1edc0993b42c7008196d75015d",
"reference": "f3ee2fab1ce33a1edc0993b42c7008196d75015d",
"url": "https://api.github.com/repos/ArtificialOwl/my-small-php-tools/zipball/0d9011b41954f072ba5ff721aee6b83b583a4da2",
"reference": "0d9011b41954f072ba5ff721aee6b83b583a4da2",
"shasum": ""
},
"require": {
@ -42,9 +42,9 @@
"description": "My small PHP Tools",
"support": {
"issues": "https://github.com/ArtificialOwl/my-small-php-tools/issues",
"source": "https://github.com/ArtificialOwl/my-small-php-tools/tree/v23.0.6"
"source": "https://github.com/ArtificialOwl/my-small-php-tools/tree/v23.0.7"
},
"time": "2021-09-14T13:08:06+00:00"
"time": "2021-09-17T09:48:57+00:00"
},
{
"name": "ifsnop/mysqldump-php",

Просмотреть файл

@ -98,7 +98,7 @@ class PointUpload extends Base {
* @throws RestoringPointNotFoundException
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$point = $this->pointService->getPoint($input->getArgument('point'));
$point = $this->pointService->getRestoringPoint($input->getArgument('point'));
$checks = $this->remoteService->verifyPoint($point);
@ -124,7 +124,7 @@ class PointUpload extends Base {
}
$health = $item->getHealth();
$this->uploadMissingFiles($instance, $point, $item->getHealth(), $output);
$this->uploadMissingFiles($instance, $point, $health, $output);
if ($health->getStatus() === RestoringHealth::STATUS_OK) {
$output->writeln(' > RestoringPoint is fully uploaded to ' . $instance);
}
@ -260,7 +260,8 @@ class PointUpload extends Base {
}
$output->write(' * Uploading ' . $chunk->getDataName() . '/' . $chunk->getChunkName() . ': ');
$restoringChunk = $this->pointService->getChunkContent($point, $chunk->getDataName(), $chunk->getChunkName());
$restoringChunk =
$this->pointService->getChunkContent($point, $chunk->getDataName(), $chunk->getChunkName());
$this->remoteService->uploadChunk($instance, $point, $restoringChunk);
$output->writeln('<info>ok</info>');

Просмотреть файл

@ -72,7 +72,7 @@ class ArchiveFile implements JsonSerializable {
*
* @return ArchiveFile
*/
public function setName(string $name): ArchiveFile {
public function setName(string $name): self {
$this->name = $name;
return $this;
@ -94,13 +94,12 @@ class ArchiveFile implements JsonSerializable {
/**
* @return array
*/
public function jsonSerialize() {
public function jsonSerialize(): array {
return
[
'name' => $this->getName()
];
}
}

Просмотреть файл

@ -68,6 +68,9 @@ class RestoringChunk implements JsonSerializable, IDeserializable {
/** @var string */
private $checksum = '';
/** @var bool */
private $encrypted = false;
/** @var string */
private $encryptedChecksum = '';
@ -104,6 +107,17 @@ class RestoringChunk implements JsonSerializable, IDeserializable {
return $this;
}
/**
* @return string
*/
public function getFilename(): string {
if ($this->isEncrypted()) {
return $this->getName();
}
return $this->getName() . '.zip';
}
// /**
// * @return int
@ -184,6 +198,25 @@ class RestoringChunk implements JsonSerializable, IDeserializable {
}
/**
* @param bool $encrypted
*
* @return RestoringChunk
*/
public function setEncrypted(bool $encrypted): self {
$this->encrypted = $encrypted;
return $this;
}
/**
* @return bool
*/
public function isEncrypted(): bool {
return $this->encrypted;
}
/**
* @return string
*/
@ -246,13 +279,14 @@ class RestoringChunk implements JsonSerializable, IDeserializable {
* @return RestoringChunk
*/
public function import(array $data): IDeserializable {
$this->setName($this->get('name', $data, ''))
$this->setName($this->get('name', $data))
// ->setFiles($this->getArray('files', $data, []))
->setCount($this->getInt('count', $data, 0))
->setSize($this->getInt('size', $data, 0))
->setChecksum($this->get('checksum', $data, ''))
->setCount($this->getInt('count', $data))
->setSize($this->getInt('size', $data))
->setContent($this->get('content', $data))
->setEncryptedChecksum($this->get('encrypted', $data, ''));
->setEncrypted($this->getBool('encrypted', $data))
->setChecksum($this->get('checksum', $data))
->setEncryptedChecksum($this->get('encryptedChecksum', $data));
return $this;
}
@ -276,8 +310,9 @@ class RestoringChunk implements JsonSerializable, IDeserializable {
'name' => $this->getName(),
'count' => $this->getCount(),
'size' => $this->getSize(),
'encrypted' => $this->isEncrypted(),
'checksum' => $this->getChecksum(),
'encrypted' => $this->getEncryptedChecksum()
'encryptedChecksum' => $this->getEncryptedChecksum()
];
if ($this->getContent() !== '') {

Просмотреть файл

@ -35,6 +35,7 @@ namespace OCA\Backup\Model;
use ArtificialOwl\MySmallPhpTools\Db\Nextcloud\nc23\INC23QueryRow;
use ArtificialOwl\MySmallPhpTools\Exceptions\InvalidItemException;
use ArtificialOwl\MySmallPhpTools\IDeserializable;
use ArtificialOwl\MySmallPhpTools\ISignedModel;
use ArtificialOwl\MySmallPhpTools\Model\SimpleDataStore;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc23\TNC23Deserialize;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc23\TNC23Logger;
@ -48,7 +49,7 @@ use OCP\Files\SimpleFS\ISimpleFolder;
*
* @package OCA\Backup\Model
*/
class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable {
class RestoringPoint implements IDeserializable, INC23QueryRow, ISignedModel, JsonSerializable {
use TArrayTools;
@ -83,6 +84,9 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
/** @var RestoringHealth */
private $health;
/** @var string */
private $signature = '';
/** @var bool */
private $package = false;
@ -304,6 +308,25 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
}
/**
* @param string $signature
*
* @return RestoringPoint
*/
public function setSignature(string $signature): self {
$this->signature = $signature;
return $this;
}
/**
* @return string
*/
public function getSignature(): string {
return $this->signature;
}
/**
* @param bool $package
*
@ -347,6 +370,7 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
$metadata = new SimpleDataStore($this->getArray('metadata', $data));
$this->setNc($metadata->gArray('nc'));
$this->setSignature($metadata->g('signature'));
try {
/** @var RestoringHealth $health */
@ -377,7 +401,8 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
->setInstance($this->get('instance', $data))
->setRoot($this->get('root', $data))
->setStatus($this->getInt('status', $data))
->setDate($this->getInt('date', $data));
->setDate($this->getInt('date', $data))
->setSignature($this->get('signature', $data));
$this->setNc($this->getArray('nc', $data));
if ($this->getId() === '' || $this->getStatus() === -1) {
@ -402,6 +427,20 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
}
/**
* @return array
*/
public function signedData(): array {
return [
'id' => $this->getId(),
'nc' => $this->getNC(),
'root' => $this->getRoot(),
'data' => $this->getRestoringData(),
'date' => $this->getDate()
];
}
/**
* @return array
*/
@ -413,6 +452,7 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
'root' => $this->getRoot(),
'status' => $this->getStatus(),
'data' => $this->getRestoringData(),
'signature' => $this->getSignature(),
'date' => $this->getDate()
];
@ -424,4 +464,3 @@ class RestoringPoint implements IDeserializable, INC23QueryRow, JsonSerializable
}
}

Просмотреть файл

@ -110,6 +110,8 @@ class CreateRestoringPoint extends CoreRequest implements IRemoteRequest {
try {
$this->pointRequest->save($point);
$this->pointService->saveMetadata($point);
$stored = $this->pointRequest->getById($point->getId(), $signatory->getInstance());
$this->setOutcome(new SimpleDataStore([$point->getId() => $stored]));

Просмотреть файл

@ -32,14 +32,18 @@ declare(strict_types=1);
namespace OCA\Backup\RemoteRequest;
use ArtificialOwl\MySmallPhpTools\Exceptions\InvalidItemException;
use ArtificialOwl\MySmallPhpTools\IDeserializable;
use ArtificialOwl\MySmallPhpTools\Model\SimpleDataStore;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc23\TNC23Deserialize;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc23\TNC23Logger;
use OCA\Backup\AppInfo\Application;
use OCA\Backup\Db\PointRequest;
use OCA\Backup\Exceptions\RestoringPointNotFoundException;
use OCA\Backup\IRemoteRequest;
use OCA\Backup\Model\RestoringChunk;
use OCA\Backup\Service\ArchiveService;
use OCA\Backup\Service\PointService;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
/**
@ -54,37 +58,66 @@ class UploadRestoringPoint extends CoreRequest implements IRemoteRequest {
use TNC23Logger;
/** @var PointRequest */
private $pointRequest;
/** @var PointService */
private $pointService;
/** @var ArchiveService */
private $chunkService;
/**
* UploadRestoringPoint constructor.
*
* @param PointRequest $pointRequest
* @param PointService $pointService
* @param ArchiveService $chunkService
*/
public function __construct(PointRequest $pointRequest) {
public function __construct(
PointService $pointService,
ArchiveService $chunkService
) {
parent::__construct();
$this->pointRequest = $pointRequest;
$this->pointService = $pointService;
$this->chunkService = $chunkService;
$this->setup('app', Application::APP_ID);
}
/**
*
*/
public function execute(): void {
$chunk = $this->deserializeJson($this->getSignedRequest()->getBody(), RestoringChunk::class);
try {
$signedRequest = $this->getSignedRequest();
$signatory = $signedRequest->getSignatory();
$pointId = $signedRequest->getIncomingRequest()->getParam('pointId');
$point = $this->pointService->getRestoringPoint($pointId, $signatory->getInstance());
/** @var RestoringChunk $chunk */
$chunk = $this->deserializeJson($signedRequest->getBody(), RestoringChunk::class);
$this->pointService->initBaseFolder($point);
$this->chunkService->saveChunkContent($point, $chunk);
$this->pointService->generateHealth($point, true);
// $this->setOutcome(new SimpleDataStore(['dssd']));
} catch (RestoringPointNotFoundException
| InvalidItemException
| NotFoundException
| NotPermittedException $e) {
}
$this->log(3, '### ' . strlen($chunk->getContent()));
$this->setOutcome(new SimpleDataStore(['dssd']));
}
/**
* @param array $data
*
* @return IDeserializable
*/
public function import(array $data): IDeserializable {
return $this;
}
}

Просмотреть файл

@ -39,8 +39,8 @@ use OCA\Backup\Exceptions\ArchiveCreateException;
use OCA\Backup\Exceptions\ArchiveDeleteException;
use OCA\Backup\Exceptions\ArchiveNotFoundException;
use OCA\Backup\Exceptions\BackupAppCopyException;
use OCA\Backup\Exceptions\BackupFolderException;
use OCA\Backup\Exceptions\BackupScriptNotFoundException;
use OCA\Backup\Exceptions\ChunkNotFoundException;
use OCA\Backup\Exceptions\EncryptionKeyException;
use OCA\Backup\Model\ArchiveFile;
use OCA\Backup\Model\Backup;
@ -113,44 +113,44 @@ class ArchiveService {
}
}
/**
* @param Backup $backup
* @param RestoringChunk $archive
* @param string $root
*
* @throws ArchiveDeleteException
* @throws ArchiveNotFoundException
* @throws EncryptionKeyException
* @throws BackupFolderException
*/
public function extractAll(Backup $backup, RestoringChunk $archive, string $root): void {
if (!is_dir($root)) {
if (!@mkdir($root, 0755, true)) {
throw new BackupFolderException('could not create ' . $root);
}
}
$this->decryptArchive($backup, $archive);
$this->extractAllFromArchive($archive, $root);
$this->deleteArchive($backup, $archive, 'zip');
}
//
// /**
// * @param Backup $backup
// * @param RestoringChunk $archive
// * @param string $root
// *
// * @throws ArchiveDeleteException
// * @throws ArchiveNotFoundException
// * @throws EncryptionKeyException
// * @throws BackupFolderException
// */
// public function extractAll(Backup $backup, RestoringChunk $archive, string $root): void {
// if (!is_dir($root)) {
// if (!@mkdir($root, 0755, true)) {
// throw new BackupFolderException('could not create ' . $root);
// }
// }
//
// $this->decryptArchive($backup, $archive);
// $this->extractAllFromArchive($archive, $root);
// $this->deleteArchive($backup, $archive, 'zip');
// }
/**
* @param RestoringChunk $archive
* @param string $root
*
* @throws ArchiveNotFoundException
* @throws Exception
*/
public function extractAllFromArchive(RestoringChunk $archive, string $root): void {
$zip = $this->openZipArchive($archive);
$zip->extractTo($root);
$this->closeZipArchive($zip);
unlink($root . '.backup.' . $archive->getName() . '.json');
}
// /**
// * @param RestoringChunk $archive
// * @param string $root
// *
// * @throws ArchiveNotFoundException
// * @throws Exception
// */
// public function extractAllFromArchive(RestoringChunk $archive, string $root): void {
// $zip = $this->openZipArchive($archive);
// $zip->extractTo($root);
// $this->closeZipArchive($zip);
//
// unlink($root . '.backup.' . $archive->getName() . '.json');
// }
/**
@ -614,4 +614,61 @@ class ArchiveService {
}
/**
* @param RestoringPoint $point
* @param string $data
* @param string $chunk
*
* @return RestoringChunk
* @throws ChunkNotFoundException
*/
public function extractChunkFromRP(RestoringPoint $point, string $data, string $chunk): RestoringChunk {
foreach ($point->getRestoringData() as $restoringData) {
if ($restoringData->getName() !== $data) {
continue;
}
foreach ($restoringData->getChunks() as $restoringChunk) {
if ($restoringChunk->getName() === $chunk) {
return $restoringChunk;
}
}
}
throw new ChunkNotFoundException();
}
/**
* @param RestoringPoint $point
* @param RestoringChunk $restoringChunk
*/
public function getChunkContent(RestoringPoint $point, RestoringChunk $restoringChunk): void {
$folder = $point->getBaseFolder();
try {
$file = $folder->getFile($restoringChunk->getFilename());
$restoringChunk->setContent(base64_encode($file->getContent()));
} catch (NotFoundException | NotPermittedException $e) {
}
}
/**
* @param RestoringPoint $point
* @param RestoringChunk $chunk
*/
public function saveChunkContent(RestoringPoint $point, RestoringChunk $chunk) {
$folder = $point->getBaseFolder();
try {
try {
$file = $folder->getFile($chunk->getFilename());
} catch (NotFoundException $e) {
$file = $folder->newFile($chunk->getFilename());
}
$file->putContent(base64_decode($chunk->getContent()));
} catch (NotPermittedException | NotFoundException $e) {
}
}
}

Просмотреть файл

@ -32,7 +32,8 @@ declare(strict_types=1);
namespace OCA\Backup\Service;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22Logger;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc23\TNC23Logger;
use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc23\TNC23Signatory;
use ArtificialOwl\MySmallPhpTools\Traits\TStringTools;
use OC;
use OC\Files\AppData\Factory;
@ -42,6 +43,8 @@ use OCA\Backup\Exceptions\ArchiveCreateException;
use OCA\Backup\Exceptions\ArchiveNotFoundException;
use OCA\Backup\Exceptions\BackupAppCopyException;
use OCA\Backup\Exceptions\BackupScriptNotFoundException;
use OCA\Backup\Exceptions\ChunkNotFoundException;
use OCA\Backup\Exceptions\RestoringPointException;
use OCA\Backup\Exceptions\RestoringPointNotFoundException;
use OCA\Backup\Exceptions\SqlDumpException;
use OCA\Backup\Model\RestoringChunk;
@ -64,7 +67,8 @@ use OCP\Util;
class PointService {
use TNC22Logger;
use TNC23Signatory;
use TNC23Logger;
use TStringTools;
@ -76,11 +80,14 @@ class PointService {
/** @var PointRequest */
private $pointRequest;
/** @var ConfigService */
private $configService;
/** @var RemoteStreamService */
private $remoteStreamService;
/** @var ArchiveService */
private $archiveService;
private $chunkService;
/** @var ConfigService */
private $configService;
/** @var IAppData */
private $appData;
@ -93,16 +100,19 @@ class PointService {
* PointService constructor.
*
* @param PointRequest $pointRequest
* @param ArchiveService $archiveService
* @param RemoteStreamService $remoteStreamService
* @param ArchiveService $chunkService
* @param ConfigService $configService
*/
public function __construct(
PointRequest $pointRequest,
ArchiveService $archiveService,
RemoteStreamService $remoteStreamService,
ArchiveService $chunkService,
ConfigService $configService
) {
$this->pointRequest = $pointRequest;
$this->archiveService = $archiveService;
$this->chunkService = $chunkService;
$this->remoteStreamService = $remoteStreamService;
$this->configService = $configService;
$this->setup('app', 'backup');
@ -111,11 +121,22 @@ class PointService {
/**
* @param string $pointId
* @param string $instance
*
* @return RestoringPoint
* @throws RestoringPointNotFoundException
*/
public function getPoint(string $pointId): RestoringPoint {
return $this->pointRequest->getById($pointId);
public function getRestoringPoint(string $pointId, string $instance = ''): RestoringPoint {
return $this->pointRequest->getById($pointId, $instance);
}
/**
* @param string $instance
*
* @return RestoringPoint[]
*/
public function getRPByInstance(string $instance): array {
return $this->pointRequest->getByInstance($instance);
}
@ -130,15 +151,16 @@ class PointService {
* @throws BackupAppCopyException
* @throws BackupScriptNotFoundException
* @throws SqlDumpException
* @throws RestoringPointException
*/
public function create(bool $complete): RestoringPoint {
$point = $this->initRestoringPoint($complete);
$this->archiveService->copyApp($point);
$this->chunkService->copyApp($point);
// $backup->setEncryptionKey('12345');
$this->archiveService->createChunks($point);
$this->chunkService->createChunks($point);
$this->backupSql($point);
$this->generateMetadata($point);
$this->saveMetadata($point);
$this->pointRequest->save($point);
@ -163,9 +185,6 @@ class PointService {
$point->setNC(Util::getVersion());
$this->initBaseFolder($point);
$temp = $point->getBaseFolder()->newFile(self::METADATA_FILE);
$temp->putContent('');
$this->addingRestoringData($point, $complete);
return $point;
@ -240,7 +259,7 @@ class PointService {
$content = $this->generateSqlDump();
$data = new RestoringData(RestoringData::SQL_DUMP, '', 'sqldump');
$this->archiveService->createContentChunk(
$this->chunkService->createContentChunk(
$point,
$data,
self::SQL_DUMP_FILE,
@ -273,18 +292,21 @@ class PointService {
/**
* @param RestoringPoint $point
*
* @throws NotFoundException
* @throws NotPermittedException
* @throws NotFoundException
*/
private function generateMetadata(RestoringPoint $point) {
if (!$point->hasBaseFolder()) {
return;
}
public function saveMetadata(RestoringPoint $point) {
$this->initBaseFolder($point);
$folder = $point->getBaseFolder();
$json = $folder->getFile(self::METADATA_FILE);
$json->putContent(json_encode($point, JSON_PRETTY_PRINT));
try {
$file = $folder->getFile(self::METADATA_FILE);
} catch (NotFoundException $e) {
$file = $folder->newFile(self::METADATA_FILE);
}
$file->putContent(json_encode($point, JSON_PRETTY_PRINT));
}
@ -325,6 +347,11 @@ class PointService {
/**
* This will destroy all backup stored locally
* (from this instance and from remote instance using this instance as storage)
*
* This method is only called when the app is reset/uninstall using ./occ backup:reset
*
* @throws NotPermittedException
* @throws NotFoundException
*/
@ -344,7 +371,11 @@ class PointService {
* @throws NotFoundException
* @throws NotPermittedException
*/
private function initBaseFolder(RestoringPoint $point): void {
public function initBaseFolder(RestoringPoint $point): void {
if ($point->hasBaseFolder()) {
return;
}
$this->initBackupFS();
try {
@ -359,6 +390,7 @@ class PointService {
/**
* @param RestoringPoint $point
* @param bool $updateDb
*
* @throws NotFoundException
* @throws NotPermittedException
@ -402,7 +434,7 @@ class PointService {
*/
private function generateChunkHealthStatus(RestoringPoint $point, RestoringChunk $chunk): int {
try {
$checksum = $this->archiveService->getChecksum($point, $chunk, false);
$checksum = $this->chunkService->getChecksum($point, $chunk, false);
if ($checksum !== $chunk->getChecksum()) {
return RestoringChunkHealth::STATUS_CHECKSUM;
}
@ -422,44 +454,15 @@ class PointService {
* @return RestoringChunk
* @throws NotFoundException
* @throws NotPermittedException
* @throws ChunkNotFoundException
*/
public function getChunkContent(RestoringPoint $point, string $data, string $chunk): RestoringChunk {
$restoringChunk = $this->getChunk($point, $data, $chunk);
$this->initBaseFolder($point);
$folder = $point->getBaseFolder();
$file = $folder->getFile($chunk . '.zip');
$restoringChunk->setContent(base64_encode($file->getContent()));
$restoringChunk = $this->chunkService->extractChunkFromRP($point, $data, $chunk);
$this->chunkService->getChunkContent($point, $restoringChunk);
return $restoringChunk;
}
/**
* @param RestoringPoint $point
* @param string $data
* @param string $chunk
*
* @return RestoringChunk
* @throws ChunkNotFoundException
*/
private function getChunk(RestoringPoint $point, string $data, string $chunk): RestoringChunk {
foreach ($point->getRestoringData() as $restoringData) {
if ($restoringData->getName() !== $data) {
continue;
}
foreach ($restoringData->getChunks() as $restoringChunk) {
if ($restoringChunk->getName() === $chunk) {
return $restoringChunk;
}
}
}
throw new ChunkNotFoundException();
}
}

Просмотреть файл

@ -170,6 +170,8 @@ class RemoteService {
throw new RemoteInstanceException('instance not configured as outgoing');
}
$this->remoteStreamService->signPoint($point);
$result = $this->remoteStreamService->resultRequestRemoteInstance(
$remoteInstance->getInstance(),
RemoteInstance::RP_CREATE,

Просмотреть файл

@ -53,6 +53,7 @@ use OCA\Backup\Exceptions\RemoteInstanceException;
use OCA\Backup\Exceptions\RemoteInstanceNotFoundException;
use OCA\Backup\Exceptions\RemoteResourceNotFoundException;
use OCA\Backup\Model\RemoteInstance;
use OCA\Backup\Model\RestoringPoint;
use OCP\AppFramework\Http;
use OCP\IURLGenerator;
@ -369,5 +370,12 @@ class RemoteStreamService extends NC23Signature {
}
}
}
/**
* @throws SignatoryException
*/
public function signPoint(RestoringPoint $point) {
$this->signModel($point, $this->getAppSignatory(false));
}
}