. */ namespace SP\Domain\Import\Services; use Defuse\Crypto\Exception\CryptoException; use DOMDocument; use DOMElement; use DOMNodeList; use DOMXPath; use Exception; use SP\Core\Crypt\Crypt; use SP\Core\Crypt\Hash; use SP\Core\Events\Event; use SP\Core\Events\EventMessage; use SP\Core\Exceptions\SPException; use SP\DataModel\CategoryData; use SP\DataModel\ClientData; use SP\DataModel\TagData; use SP\Domain\Account\Services\AccountRequest; use SP\Domain\Export\Services\XmlVerifyService; use SP\Domain\Import\ImportInterface; use SP\Util\VersionUtil; defined('APP_ROOT') || die(); /** * Esta clase es la encargada de importar cuentas desde sysPass */ final class SyspassImport extends XmlImportBase implements ImportInterface { /** * Iniciar la importación desde sysPass. * * @throws ImportException */ public function doImport(): ImportInterface { try { $this->eventDispatcher->notifyEvent( 'run.import.syspass', new Event($this, EventMessage::factory()->addDescription(__u('sysPass XML Import'))) ); if (!empty($this->importParams->getImportMasterPwd())) { $this->mPassValidHash = Hash::checkHashKey( $this->importParams->getImportMasterPwd(), $this->configService->getByParam('masterPwd') ); } $this->version = $this->getXmlVersion(); if ($this->detectEncrypted()) { if ($this->importParams->getImportPwd() === '') { throw new ImportException(__u('Encryption password not set'), SPException::INFO); } $this->processEncrypted(); } $this->checkIntegrity(); $this->processCategories(); if ($this->version >= VersionUtil::versionToInteger('3.0.0')) { $this->processClients(); } else { $this->processCustomers(); } $this->processTags(); $this->processAccounts(); return $this; } catch (ImportException $e) { throw $e; } catch (Exception $e) { throw new ImportException( $e->getMessage(), SPException::CRITICAL ); } } /** * Obtener la versión del XML */ protected function getXmlVersion() { return VersionUtil::versionToInteger( (new DOMXPath($this->xmlDOM)) ->query('/Root/Meta/Version')->item(0)->nodeValue ); } /** * Verificar si existen datos encriptados */ protected function detectEncrypted(): bool { return $this->xmlDOM->getElementsByTagName('Encrypted')->length > 0; } /** * Procesar los datos encriptados y añadirlos al árbol DOM desencriptados * * @throws ImportException */ protected function processEncrypted(): void { $hash = $this->xmlDOM ->getElementsByTagName('Encrypted') ->item(0) ->getAttribute('hash'); if (!empty($hash) && !Hash::checkHashKey($this->importParams->getImportPwd(), $hash) ) { throw new ImportException(__u('Wrong encryption password')); } /** @var DOMElement $node */ foreach ($this->xmlDOM->getElementsByTagName('Data') as $node) { try { if ($this->version >= 210 && $this->version <= 310) { $xmlDecrypted = Crypt::decrypt( base64_decode($node->nodeValue), $node->getAttribute('key'), $this->importParams->getImportPwd() ); } else { if ($this->version >= 320) { $xmlDecrypted = Crypt::decrypt( $node->nodeValue, $node->getAttribute('key'), $this->importParams->getImportPwd() ); } else { throw new ImportException(__u('The file was exported with an old sysPass version (<= 2.10).')); } } } catch (CryptoException $e) { processException($e); $this->eventDispatcher->notifyEvent('exception', new Event($e)); continue; } $newXmlData = new DOMDocument(); if ($newXmlData->loadXML($xmlDecrypted) === false) { throw new ImportException(__u('Wrong encryption password')); } $this->xmlDOM->documentElement ->appendChild( $this->xmlDOM->importNode($newXmlData->documentElement, true) ); } // Eliminar los datos encriptados tras desencriptar los mismos if ($this->xmlDOM->getElementsByTagName('Data')->length > 0) { $nodeData = $this->xmlDOM ->getElementsByTagName('Encrypted') ->item(0); $nodeData->parentNode->removeChild($nodeData); } $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.decryption', new Event($this, EventMessage::factory()->addDescription(__u('Data unencrypted'))) ); } /** * Checks XML file's data integrity using the signed hash */ protected function checkIntegrity(): void { $key = $this->importParams->getImportPwd() ?: sha1($this->configData->getPasswordSalt()); if (!XmlVerifyService::checkXmlHash($this->xmlDOM, $key)) { $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.verify', new Event( $this, EventMessage::factory() ->addDescription(__u('Error while checking integrity hash')) ->addDescription( __u( 'If you are importing an exported file from the same origin, the data could be compromised.' ) ) ) ); } } /** * Obtener las categorías y añadirlas a sysPass. * * @throws ImportException */ protected function processCategories(): void { $this->getNodesData( 'Categories', 'Category', function (DOMElement $category) { $categoryData = new CategoryData(); foreach ($category->childNodes as $node) { if (isset($node->tagName)) { switch ($node->tagName) { case 'name': $categoryData->setName($node->nodeValue); break; case 'description': $categoryData->setDescription($node->nodeValue); break; } } } try { $this->addWorkingItem( 'category', (int)$category->getAttribute('id'), $this->addCategory($categoryData) ); $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.category', new Event( $this, EventMessage::factory() ->addDetail(__u('Category imported'), $categoryData->getName()) ) ); } catch (Exception $e) { processException($e); $this->eventDispatcher->notifyEvent('exception', new Event($e)); } } ); } /** * Obtener los clientes y añadirlos a sysPass. * * @throws ImportException */ protected function processClients(): void { $this->getNodesData( 'Clients', 'Client', function (DOMElement $client) { $clientData = new ClientData(); foreach ($client->childNodes as $node) { if (isset($node->tagName)) { switch ($node->tagName) { case 'name': $clientData->setName($node->nodeValue); break; case 'description': $clientData->setDescription($node->nodeValue); break; } } } try { $this->addWorkingItem( 'client', (int)$client->getAttribute('id'), $this->addClient($clientData) ); $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.client', new Event( $this, EventMessage::factory() ->addDetail(__u('Client imported'), $clientData->getName()) ) ); } catch (Exception $e) { processException($e); $this->eventDispatcher->notifyEvent('exception', new Event($e)); } } ); } /** * Obtener los clientes y añadirlos a sysPass. * * @throws ImportException * @deprecated */ protected function processCustomers(): void { $this->getNodesData( 'Customers', 'Customer', function (DOMElement $client) { $clientData = new ClientData(); foreach ($client->childNodes as $node) { if (isset($node->tagName)) { switch ($node->tagName) { case 'name': $clientData->setName($node->nodeValue); break; case 'description': $clientData->setDescription($node->nodeValue); break; } } } try { $this->addWorkingItem( 'client', (int)$client->getAttribute('id'), $this->addClient($clientData) ); $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.customer', new Event( $this, EventMessage::factory()->addDetail(__u('Client imported'), $clientData->getName()) ) ); } catch (Exception $e) { processException($e); $this->eventDispatcher->notifyEvent('exception', new Event($e)); } } ); } /** * Obtener las etiquetas y añadirlas a sysPass. * * @throws ImportException */ protected function processTags(): void { $this->getNodesData( 'Tags', 'Tag', function (DOMElement $tag) { $tagData = new TagData(); foreach ($tag->childNodes as $node) { if (isset($node->tagName) && $node->tagName === 'name') { $tagData->setName($node->nodeValue); } } try { $this->addWorkingItem( 'tag', (int)$tag->getAttribute('id'), $this->addTag($tagData) ); $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.tag', new Event( $this, EventMessage::factory()->addDetail(__u('Tag imported'), $tagData->getName()) ) ); } catch (Exception $e) { processException($e); $this->eventDispatcher->notifyEvent('exception', new Event($e)); } }, false ); } /** * Obtener los datos de las cuentas de sysPass y crearlas. * * @throws ImportException */ protected function processAccounts(): void { $this->getNodesData( 'Accounts', 'Account', function (DOMElement $account) { $accountRequest = new AccountRequest(); /** @var DOMElement $node */ foreach ($account->childNodes as $node) { if (isset($node->tagName)) { switch ($node->tagName) { case 'name'; $accountRequest->name = $node->nodeValue; break; case 'login'; $accountRequest->login = $node->nodeValue; break; case 'categoryId'; $accountRequest->categoryId = $this->getWorkingItem('category', (int)$node->nodeValue); break; case 'clientId'; case 'customerId'; $accountRequest->clientId = $this->getWorkingItem('client', (int)$node->nodeValue); break; case 'url'; $accountRequest->url = $node->nodeValue; break; case 'pass'; $accountRequest->pass = $node->nodeValue; break; case 'key'; $accountRequest->key = $node->nodeValue; break; case 'notes'; $accountRequest->notes = $node->nodeValue; break; case 'tags': $accountRequest->tags = $this->processAccountTags($node->childNodes); } } } try { $this->addAccount($accountRequest); $this->eventDispatcher->notifyEvent( 'run.import.syspass.process.account', new Event( $this, EventMessage::factory()->addDetail(__u('Account imported'), $accountRequest->name) ) ); } catch (Exception $e) { processException($e); $this->eventDispatcher->notifyEvent('exception', new Event($e)); } } ); } /** * Procesar las etiquetas de la cuenta */ protected function processAccountTags(DOMNodeList $nodes): array { $tags = []; if ($nodes->length > 0) { /** @var DOMElement $node */ foreach ($nodes as $node) { if (isset($node->tagName)) { $tags[] = $this->getWorkingItem('tag', (int)$node->getAttribute('id')); } } } return $tags; } }