mirror of
https://github.com/wallabag/wallabag.git
synced 2025-01-09 00:15:27 +00:00
Crypt site credential password
This commit is contained in:
parent
9de9f1e5ce
commit
906424c1b6
15 changed files with 169 additions and 9 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -56,3 +56,4 @@ app/Resources/build/
|
||||||
# Test-generated files
|
# Test-generated files
|
||||||
admin-export.json
|
admin-export.json
|
||||||
specialexport.json
|
specialexport.json
|
||||||
|
/data/site-credentials-secret-key.txt
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Version20170501115751 extends AbstractMigration implements ContainerAwareI
|
||||||
$table->addColumn('user_id', 'integer');
|
$table->addColumn('user_id', 'integer');
|
||||||
$table->addColumn('host', 'string', ['length' => 255]);
|
$table->addColumn('host', 'string', ['length' => 255]);
|
||||||
$table->addColumn('username', 'string', ['length' => 255]);
|
$table->addColumn('username', 'string', ['length' => 255]);
|
||||||
$table->addColumn('password', 'string', ['length' => 255]);
|
$table->addColumn('password', 'text');
|
||||||
$table->addColumn('createdAt', 'datetime');
|
$table->addColumn('createdAt', 'datetime');
|
||||||
$table->addIndex(['user_id'], 'idx_user');
|
$table->addIndex(['user_id'], 'idx_user');
|
||||||
$table->setPrimaryKey(['id']);
|
$table->setPrimaryKey(['id']);
|
||||||
|
|
|
@ -26,6 +26,7 @@ wallabag_core:
|
||||||
fetching_error_message: |
|
fetching_error_message: |
|
||||||
wallabag can't retrieve contents for this article. Please <a href="http://doc.wallabag.org/en/user/errors_during_fetching.html#how-can-i-help-to-fix-that">troubleshoot this issue</a>.
|
wallabag can't retrieve contents for this article. Please <a href="http://doc.wallabag.org/en/user/errors_during_fetching.html#how-can-i-help-to-fix-that">troubleshoot this issue</a>.
|
||||||
api_limit_mass_actions: 10
|
api_limit_mass_actions: 10
|
||||||
|
encryption_key_path: "%kernel.root_dir%/../data/site-credentials-secret-key.txt"
|
||||||
default_internal_settings:
|
default_internal_settings:
|
||||||
-
|
-
|
||||||
name: share_public
|
name: share_public
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
"kphoen/rulerz-bundle": "~0.13",
|
"kphoen/rulerz-bundle": "~0.13",
|
||||||
"guzzlehttp/guzzle": "^5.3.1",
|
"guzzlehttp/guzzle": "^5.3.1",
|
||||||
"doctrine/doctrine-migrations-bundle": "^1.0",
|
"doctrine/doctrine-migrations-bundle": "^1.0",
|
||||||
"paragonie/random_compat": "~1.0",
|
"paragonie/random_compat": "~2.0",
|
||||||
"craue/config-bundle": "~2.0",
|
"craue/config-bundle": "~2.0",
|
||||||
"mnapoli/piwik-twig-extension": "^1.0",
|
"mnapoli/piwik-twig-extension": "^1.0",
|
||||||
"ocramius/proxy-manager": "1.*",
|
"ocramius/proxy-manager": "1.*",
|
||||||
|
@ -83,7 +83,8 @@
|
||||||
"javibravo/simpleue": "^1.0",
|
"javibravo/simpleue": "^1.0",
|
||||||
"symfony/dom-crawler": "^3.1",
|
"symfony/dom-crawler": "^3.1",
|
||||||
"friendsofsymfony/jsrouting-bundle": "^1.6",
|
"friendsofsymfony/jsrouting-bundle": "^1.6",
|
||||||
"bdunogier/guzzle-site-authenticator": "^1.0.0@dev"
|
"bdunogier/guzzle-site-authenticator": "^1.0.0@dev",
|
||||||
|
"defuse/php-encryption": "^2.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/doctrine-fixtures-bundle": "~2.2",
|
"doctrine/doctrine-fixtures-bundle": "~2.2",
|
||||||
|
|
|
@ -313,6 +313,8 @@ class InstallCommand extends ContainerAwareCommand
|
||||||
|
|
||||||
$this
|
$this
|
||||||
->runCommand('doctrine:migrations:migrate', ['--no-interaction' => true]);
|
->runCommand('doctrine:migrations:migrate', ['--no-interaction' => true]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -45,6 +45,8 @@ class SiteCredentialController extends Controller
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$credential->setPassword($this->get('wallabag_core.helper.crypto_proxy')->crypt($credential->getPassword()));
|
||||||
|
|
||||||
$em = $this->getDoctrine()->getManager();
|
$em = $this->getDoctrine()->getManager();
|
||||||
$em->persist($credential);
|
$em->persist($credential);
|
||||||
$em->flush($credential);
|
$em->flush($credential);
|
||||||
|
|
|
@ -63,6 +63,8 @@ class Configuration implements ConfigurationInterface
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->scalarNode('encryption_key_path')
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ class WallabagCoreExtension extends Extension
|
||||||
$container->setParameter('wallabag_core.fetching_error_message_title', $config['fetching_error_message_title']);
|
$container->setParameter('wallabag_core.fetching_error_message_title', $config['fetching_error_message_title']);
|
||||||
$container->setParameter('wallabag_core.api_limit_mass_actions', $config['api_limit_mass_actions']);
|
$container->setParameter('wallabag_core.api_limit_mass_actions', $config['api_limit_mass_actions']);
|
||||||
$container->setParameter('wallabag_core.default_internal_settings', $config['default_internal_settings']);
|
$container->setParameter('wallabag_core.default_internal_settings', $config['default_internal_settings']);
|
||||||
|
$container->setParameter('wallabag_core.site_credentials.encryption_key_path', $config['encryption_key_path']);
|
||||||
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
|
||||||
$loader->load('services.yml');
|
$loader->load('services.yml');
|
||||||
|
|
|
@ -46,8 +46,7 @@ class SiteCredential
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank()
|
||||||
* @Assert\Length(max=255)
|
* @ORM\Column(name="password", type="text")
|
||||||
* @ORM\Column(name="password", type="string", length=255)
|
|
||||||
*/
|
*/
|
||||||
private $password;
|
private $password;
|
||||||
|
|
||||||
|
|
86
src/Wallabag/CoreBundle/Helper/CryptoProxy.php
Normal file
86
src/Wallabag/CoreBundle/Helper/CryptoProxy.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wallabag\CoreBundle\Helper;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Defuse\Crypto\Key;
|
||||||
|
use Defuse\Crypto\Crypto;
|
||||||
|
use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a proxy to crypt and decrypt password used by SiteCredential entity.
|
||||||
|
* BTW, It might be re-use for sth else.
|
||||||
|
*/
|
||||||
|
class CryptoProxy
|
||||||
|
{
|
||||||
|
private $logger;
|
||||||
|
private $encryptionKey;
|
||||||
|
|
||||||
|
public function __construct($encryptionKeyPath, LoggerInterface $logger)
|
||||||
|
{
|
||||||
|
$this->logger = $logger;
|
||||||
|
|
||||||
|
if (!file_exists($encryptionKeyPath)) {
|
||||||
|
$key = Key::createNewRandomKey();
|
||||||
|
|
||||||
|
file_put_contents($encryptionKeyPath, $key->saveToAsciiSafeString());
|
||||||
|
chmod($encryptionKeyPath, 0600);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->encryptionKey = file_get_contents($encryptionKeyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the given value will be crypted.
|
||||||
|
*
|
||||||
|
* @param string $secretValue Secret valye to crypt
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function crypt($secretValue)
|
||||||
|
{
|
||||||
|
$this->logger->debug('Crypto: crypting value: '.$this->mask($secretValue));
|
||||||
|
|
||||||
|
return Crypto::encrypt($secretValue, $this->loadKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the given crypted value will be decrypted.
|
||||||
|
*
|
||||||
|
* @param string $cryptedValue The value to be decrypted
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function decrypt($cryptedValue)
|
||||||
|
{
|
||||||
|
$this->logger->debug('Crypto: decrypting value: '.$this->mask($cryptedValue));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Crypto::decrypt($cryptedValue, $this->loadKey());
|
||||||
|
} catch (WrongKeyOrModifiedCiphertextException $e) {
|
||||||
|
throw new \RuntimeException('Decrypt fail: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the private key.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function loadKey()
|
||||||
|
{
|
||||||
|
return Key::loadFromAsciiSafeString($this->encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep first and last character and put some stars in between.
|
||||||
|
*
|
||||||
|
* @param string $value Value to mask
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function mask($value)
|
||||||
|
{
|
||||||
|
return $value[0].'*****'.$value[strlen($value) - 1];
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,20 @@
|
||||||
|
|
||||||
namespace Wallabag\CoreBundle\Repository;
|
namespace Wallabag\CoreBundle\Repository;
|
||||||
|
|
||||||
|
use Wallabag\CoreBundle\Helper\CryptoProxy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SiteCredentialRepository.
|
* SiteCredentialRepository.
|
||||||
*/
|
*/
|
||||||
class SiteCredentialRepository extends \Doctrine\ORM\EntityRepository
|
class SiteCredentialRepository extends \Doctrine\ORM\EntityRepository
|
||||||
{
|
{
|
||||||
|
private $cryptoProxy;
|
||||||
|
|
||||||
|
public function setCrypto(CryptoProxy $cryptoProxy)
|
||||||
|
{
|
||||||
|
$this->cryptoProxy = $cryptoProxy;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve one username/password for the given host and userId.
|
* Retrieve one username/password for the given host and userId.
|
||||||
*
|
*
|
||||||
|
@ -17,12 +26,21 @@ class SiteCredentialRepository extends \Doctrine\ORM\EntityRepository
|
||||||
*/
|
*/
|
||||||
public function findOneByHostAndUser($host, $userId)
|
public function findOneByHostAndUser($host, $userId)
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('s')
|
$res = $this->createQueryBuilder('s')
|
||||||
->select('s.username', 's.password')
|
->select('s.username', 's.password')
|
||||||
->where('s.host = :hostname')->setParameter('hostname', $host)
|
->where('s.host = :hostname')->setParameter('hostname', $host)
|
||||||
->andWhere('s.user = :userId')->setParameter('userId', $userId)
|
->andWhere('s.user = :userId')->setParameter('userId', $userId)
|
||||||
->setMaxResults(1)
|
->setMaxResults(1)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getOneOrNullResult();
|
->getOneOrNullResult();
|
||||||
|
|
||||||
|
if (null === $res) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt password before returning it
|
||||||
|
$res['password'] = $this->cryptoProxy->decrypt($res['password']);
|
||||||
|
|
||||||
|
return $res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,8 @@ services:
|
||||||
factory: [ "@doctrine.orm.default_entity_manager", getRepository ]
|
factory: [ "@doctrine.orm.default_entity_manager", getRepository ]
|
||||||
arguments:
|
arguments:
|
||||||
- WallabagCoreBundle:SiteCredential
|
- WallabagCoreBundle:SiteCredential
|
||||||
|
calls:
|
||||||
|
- [ setCrypto, [ "@wallabag_core.helper.crypto_proxy" ] ]
|
||||||
|
|
||||||
wallabag_core.helper.entries_export:
|
wallabag_core.helper.entries_export:
|
||||||
class: Wallabag\CoreBundle\Helper\EntriesExport
|
class: Wallabag\CoreBundle\Helper\EntriesExport
|
||||||
|
@ -208,3 +210,9 @@ services:
|
||||||
|
|
||||||
wallabag_core.entry.download_images.client:
|
wallabag_core.entry.download_images.client:
|
||||||
class: GuzzleHttp\Client
|
class: GuzzleHttp\Client
|
||||||
|
|
||||||
|
wallabag_core.helper.crypto_proxy:
|
||||||
|
class: Wallabag\CoreBundle\Helper\CryptoProxy
|
||||||
|
arguments:
|
||||||
|
- "%wallabag_core.site_credentials.encryption_key_path%"
|
||||||
|
- "@logger"
|
||||||
|
|
|
@ -1341,7 +1341,7 @@ class EntryControllerTest extends WallabagCoreTestCase
|
||||||
$credential = new SiteCredential($user);
|
$credential = new SiteCredential($user);
|
||||||
$credential->setHost('monde-diplomatique.fr');
|
$credential->setHost('monde-diplomatique.fr');
|
||||||
$credential->setUsername('foo');
|
$credential->setUsername('foo');
|
||||||
$credential->setPassword('bar');
|
$credential->setPassword($client->getContainer()->get('wallabag_core.helper.crypto_proxy')->crypt('bar'));
|
||||||
|
|
||||||
$em->persist($credential);
|
$em->persist($credential);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
|
@ -6,12 +6,11 @@ use Monolog\Handler\TestHandler;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use BD\GuzzleSiteAuthenticator\SiteConfig\SiteConfig;
|
use BD\GuzzleSiteAuthenticator\SiteConfig\SiteConfig;
|
||||||
use Graby\SiteConfig\SiteConfig as GrabySiteConfig;
|
use Graby\SiteConfig\SiteConfig as GrabySiteConfig;
|
||||||
use PHPUnit_Framework_TestCase;
|
|
||||||
use Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder;
|
use Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||||
|
|
||||||
class GrabySiteConfigBuilderTest extends PHPUnit_Framework_TestCase
|
class GrabySiteConfigBuilderTest extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
/** @var \Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder */
|
/** @var \Wallabag\CoreBundle\GuzzleSiteAuthenticator\GrabySiteConfigBuilder */
|
||||||
protected $builder;
|
protected $builder;
|
||||||
|
|
40
tests/Wallabag/CoreBundle/Helper/CryptoProxyTest.php
Normal file
40
tests/Wallabag/CoreBundle/Helper/CryptoProxyTest.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Wallabag\CoreBundle\Helper;
|
||||||
|
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Handler\TestHandler;
|
||||||
|
use Wallabag\CoreBundle\Helper\CryptoProxy;
|
||||||
|
|
||||||
|
class CryptoProxyTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function testCrypto()
|
||||||
|
{
|
||||||
|
$logHandler = new TestHandler();
|
||||||
|
$logger = new Logger('test', [$logHandler]);
|
||||||
|
|
||||||
|
$crypto = new CryptoProxy(sys_get_temp_dir().'/'.uniqid('', true).'.txt', $logger);
|
||||||
|
$crypted = $crypto->crypt('test');
|
||||||
|
$decrypted = $crypto->decrypt($crypted);
|
||||||
|
|
||||||
|
$this->assertSame('test', $decrypted);
|
||||||
|
|
||||||
|
$records = $logHandler->getRecords();
|
||||||
|
$this->assertCount(2, $records);
|
||||||
|
$this->assertContains('Crypto: crypting value', $records[0]['message']);
|
||||||
|
$this->assertContains('Crypto: decrypting value', $records[1]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException RuntimeException
|
||||||
|
* @expectedExceptionMessage Decrypt fail
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function testDecryptBadValue()
|
||||||
|
{
|
||||||
|
$crypto = new CryptoProxy(sys_get_temp_dir().'/'.uniqid('', true).'.txt', new NullLogger());
|
||||||
|
$crypto->decrypt('badvalue');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue