From fb91bf6a5b55fa39414add74f86f3f5af21b6552 Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Fri, 18 Nov 2016 10:10:05 +0100 Subject: [PATCH] Add a signer class for signing Signed-off-by: Lukas Reschke --- apps/lookup_server_connector/appinfo/app.php | 14 +- .../lib/UpdateLookupServer.php | 97 ++++------ core/Controller/OCSController.php | 4 +- .../Security/IdentityProof/Manager.php | 37 +++- lib/private/Security/IdentityProof/Signer.php | 120 +++++++++++++ settings/Controller/UsersController.php | 18 +- tests/lib/AppTest.php | 1 + .../Security/IdentityProof/ManagerTest.php | 166 ++++++++++++++++++ 8 files changed, 378 insertions(+), 79 deletions(-) create mode 100644 lib/private/Security/IdentityProof/Signer.php create mode 100644 tests/lib/Security/IdentityProof/ManagerTest.php diff --git a/apps/lookup_server_connector/appinfo/app.php b/apps/lookup_server_connector/appinfo/app.php index dc076b7882..d757284f7d 100644 --- a/apps/lookup_server_connector/appinfo/app.php +++ b/apps/lookup_server_connector/appinfo/app.php @@ -23,11 +23,23 @@ $dispatcher = \OC::$server->getEventDispatcher(); $dispatcher->addListener('OC\AccountManager::userUpdated', function(\Symfony\Component\EventDispatcher\GenericEvent $event) { $user = $event->getSubject(); + + $keyManager = new \OC\Security\IdentityProof\Manager( + \OC::$server->getAppDataDir('identityproof'), + \OC::$server->getCrypto() + ); $updateLookupServer = new \OCA\LookupServerConnector\UpdateLookupServer( new \OC\Accounts\AccountManager(\OC::$server->getDatabaseConnection(), \OC::$server->getEventDispatcher()), \OC::$server->getConfig(), \OC::$server->getSecureRandom(), - \OC::$server->getHTTPClientService() + \OC::$server->getHTTPClientService(), + $keyManager, + new \OC\Security\IdentityProof\Signer( + $keyManager, + new \OC\AppFramework\Utility\TimeFactory(), + \OC::$server->getURLGenerator(), + \OC::$server->getUserManager() + ) ); $updateLookupServer->userUpdated($user); }); diff --git a/apps/lookup_server_connector/lib/UpdateLookupServer.php b/apps/lookup_server_connector/lib/UpdateLookupServer.php index b6d8b9782a..fc20ddcd14 100644 --- a/apps/lookup_server_connector/lib/UpdateLookupServer.php +++ b/apps/lookup_server_connector/lib/UpdateLookupServer.php @@ -1,6 +1,7 @@ + * @copyright Copyright (c) 2016 Lukas Reschke * * @license GNU AGPL version 3 or any later version * @@ -19,11 +20,11 @@ * */ - namespace OCA\LookupServerConnector; - use OC\Accounts\AccountManager; +use OC\Security\IdentityProof\Manager; +use OC\Security\IdentityProof\Signer; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IUser; @@ -35,45 +36,48 @@ use OCP\Security\ISecureRandom; * @package OCA\LookupServerConnector */ class UpdateLookupServer { - - /** @var AccountManager */ + /** @var AccountManager */ private $accountManager; - /** @var IConfig */ private $config; - /** @var ISecureRandom */ private $secureRandom; - /** @var IClientService */ private $clientService; - + /** @var Manager */ + private $keyManager; + /** @var Signer */ + private $signer; /** @var string URL point to lookup server */ - private $lookupServer = 'http://192.168.56.102'; + private $lookupServer = 'http://192.168.176.105/lookup-server/server/'; /** - * UpdateLookupServer constructor. - * * @param AccountManager $accountManager * @param IConfig $config * @param ISecureRandom $secureRandom * @param IClientService $clientService + * @param Manager $manager + * @param Signer $signer */ public function __construct(AccountManager $accountManager, IConfig $config, ISecureRandom $secureRandom, - IClientService $clientService) { + IClientService $clientService, + Manager $manager, + Signer $signer) { $this->accountManager = $accountManager; $this->config = $config; $this->secureRandom = $secureRandom; $this->clientService = $clientService; + $this->keyManager = $manager; + $this->signer = $signer; } - + /** + * @param IUser $user + */ public function userUpdated(IUser $user) { $userData = $this->accountManager->getUser($user); - $authKey = $this->config->getUserValue($user->getUID(), 'lookup_server_connector', 'authKey'); - $publicData = []; foreach ($userData as $key => $data) { @@ -83,13 +87,15 @@ class UpdateLookupServer { } if (empty($publicData) && !empty($authKey)) { - $this->removeFromLookupServer($user, $authKey); + $this->removeFromLookupServer($user); } else { - $this->sendToLookupServer($user, $publicData, $authKey); + $this->sendToLookupServer($user, $publicData); } } /** + * TODO: FIXME. Implement removal from lookup server. + * * remove user from lookup server * * @param IUser $user @@ -103,56 +109,25 @@ class UpdateLookupServer { * * @param IUser $user * @param array $publicData - * @param string $authKey */ - protected function sendToLookupServer(IUser $user, $publicData, $authKey) { - if (empty($authKey)) { - $authKey = $this->secureRandom->generate(16); - $this->sendNewRecord($user, $publicData, $authKey); - $this->config->setUserValue($user->getUID(), 'lookup_server_connector', 'authKey', $authKey); - } else { - $this->updateExistingRecord($user, $publicData, $authKey); - } - } - - protected function sendNewRecord(IUser $user, $publicData, $authKey) { + protected function sendToLookupServer(IUser $user, array $publicData) { + $dataArray = [ + 'federationId' => $user->getCloudId(), + 'name' => isset($publicData[AccountManager::PROPERTY_DISPLAYNAME]) ? $publicData[AccountManager::PROPERTY_DISPLAYNAME]['value'] : '', + 'email' => isset($publicData[AccountManager::PROPERTY_EMAIL]) ? $publicData[AccountManager::PROPERTY_EMAIL]['value'] : '', + 'address' => isset($publicData[AccountManager::PROPERTY_ADDRESS]) ? $publicData[AccountManager::PROPERTY_ADDRESS]['value'] : '', + 'website' => isset($publicData[AccountManager::PROPERTY_WEBSITE]) ? $publicData[AccountManager::PROPERTY_WEBSITE]['value'] : '', + 'twitter' => isset($publicData[AccountManager::PROPERTY_TWITTER]) ? $publicData[AccountManager::PROPERTY_TWITTER]['value'] : '', + 'phone' => isset($publicData[AccountManager::PROPERTY_PHONE]) ? $publicData[AccountManager::PROPERTY_PHONE]['value'] : '', + ]; + $dataArray = $this->signer->sign('lookupserver', $dataArray, $user); $httpClient = $this->clientService->newClient(); - $response = $httpClient->post($this->lookupServer, + $httpClient->post($this->lookupServer, [ - 'body' => [ - 'key' => $authKey, - 'federationid' => $publicData[$user->getCloudId()], - 'name' => isset($publicData[AccountManager::PROPERTY_DISPLAYNAME]) ? $publicData[AccountManager::PROPERTY_DISPLAYNAME]['value'] : '', - 'email' => isset($publicData[AccountManager::PROPERTY_EMAIL]) ? $publicData[AccountManager::PROPERTY_EMAIL]['value'] : '', - 'address' => isset($publicData[AccountManager::PROPERTY_ADDRESS]) ? $publicData[AccountManager::PROPERTY_ADDRESS]['value'] : '', - 'website' => isset($publicData[AccountManager::PROPERTY_WEBSITE]) ? $publicData[AccountManager::PROPERTY_WEBSITE]['value'] : '', - 'twitter' => isset($publicData[AccountManager::PROPERTY_TWITTER]) ? $publicData[AccountManager::PROPERTY_TWITTER]['value'] : '', - 'phone' => isset($publicData[AccountManager::PROPERTY_PHONE]) ? $publicData[AccountManager::PROPERTY_PHONE]['value'] : '', - ], + 'body' => $dataArray, 'timeout' => 3, 'connect_timeout' => 3, ] ); } - - protected function updateExistingRecord(IUser $user, $publicData, $authKey) { - $httpClient = $this->clientService->newClient(); - $httpClient->put($this->lookupServer, - [ - 'body' => [ - 'key' => $authKey, - 'federationid' => $publicData[$user->getCloudId()], - 'name' => isset($publicData[AccountManager::PROPERTY_DISPLAYNAME]) ? $publicData[AccountManager::PROPERTY_DISPLAYNAME]['value'] : '', - 'email' => isset($publicData[AccountManager::PROPERTY_EMAIL]) ? $publicData[AccountManager::PROPERTY_EMAIL]['value'] : '', - 'address' => isset($publicData[AccountManager::PROPERTY_ADDRESS]) ? $publicData[AccountManager::PROPERTY_ADDRESS]['value'] : '', - 'website' => isset($publicData[AccountManager::PROPERTY_WEBSITE]) ? $publicData[AccountManager::PROPERTY_WEBSITE]['value'] : '', - 'twitter' => isset($publicData[AccountManager::PROPERTY_TWITTER]) ? $publicData[AccountManager::PROPERTY_TWITTER]['value'] : '', - 'phone' => isset($publicData[AccountManager::PROPERTY_PHONE]) ? $publicData[AccountManager::PROPERTY_PHONE]['value'] : '', - ], - 'timeout' => 3, - 'connect_timeout' => 3, - ] - ); - - } } diff --git a/core/Controller/OCSController.php b/core/Controller/OCSController.php index b1c4f377a1..c59b0d7ad3 100644 --- a/core/Controller/OCSController.php +++ b/core/Controller/OCSController.php @@ -151,7 +151,7 @@ class OCSController extends \OCP\AppFramework\OCSController { public function getIdentityProof($cloudId) { $userObject = $this->userManager->get($cloudId); - if($cloudId !== null) { + if($userObject !== null) { $key = $this->keyManager->getKey($userObject); $data = [ 'public' => $key->getPublic(), @@ -159,6 +159,6 @@ class OCSController extends \OCP\AppFramework\OCSController { return new DataResponse($data); } - return new DataResponse(101); + return new DataResponse('User not found', 404); } } diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php index 223af05410..d2a9e57e33 100644 --- a/lib/private/Security/IdentityProof/Manager.php +++ b/lib/private/Security/IdentityProof/Manager.php @@ -42,13 +42,12 @@ class Manager { } /** - * Generate a key for $user - * Note: If a key already exists it will be overwritten + * Calls the openssl functions to generate a public and private key. + * In a separate function for unit testing purposes. * - * @param IUser $user - * @return Key + * @return array [$publicKey, $privateKey] */ - public function generateKey(IUser $user) { + protected function generateKeyPair() { $config = [ 'digest_alg' => 'sha512', 'private_key_bits' => 2048, @@ -62,10 +61,27 @@ class Manager { $publicKey = openssl_pkey_get_details($res); $publicKey = $publicKey['key']; + return [$publicKey, $privateKey]; + } + + /** + * Generate a key for $user + * Note: If a key already exists it will be overwritten + * + * @param IUser $user + * @return Key + */ + protected function generateKey(IUser $user) { + list($publicKey, $privateKey) = $this->generateKeyPair(); + // Write the private and public key to the disk - $this->appData->getFolder($user->getUID())->newFile('private') + try { + $this->appData->newFolder($user->getUID()); + } catch (\Exception $e) {} + $folder = $this->appData->getFolder($user->getUID()); + $folder->newFile('private') ->putContent($this->crypto->encrypt($privateKey)); - $this->appData->getFolder($user->getUID())->newFile('public') + $folder->newFile('public') ->putContent($publicKey); return new Key($publicKey, $privateKey); @@ -79,8 +95,11 @@ class Manager { */ public function getKey(IUser $user) { try { - $privateKey = $this->crypto->decrypt($this->appData->getFolder($user->getUID())->getFile('private')->getContent()); - $publicKey = $this->appData->getFolder($user->getUID())->getFile('public')->getContent(); + $folder = $this->appData->getFolder($user->getUID()); + $privateKey = $this->crypto->decrypt( + $folder->getFile('private')->getContent() + ); + $publicKey = $folder->getFile('public')->getContent(); return new Key($publicKey, $privateKey); } catch (\Exception $e) { return $this->generateKey($user); diff --git a/lib/private/Security/IdentityProof/Signer.php b/lib/private/Security/IdentityProof/Signer.php new file mode 100644 index 0000000000..50c36b2696 --- /dev/null +++ b/lib/private/Security/IdentityProof/Signer.php @@ -0,0 +1,120 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Security\IdentityProof; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; + +class Signer { + /** @var Manager */ + private $keyManager; + /** @var ITimeFactory */ + private $timeFactory; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var IUserManager */ + private $userManager; + + /** + * @param Manager $keyManager + * @param ITimeFactory $timeFactory + * @param IURLGenerator $urlGenerator + * @param IUserManager $userManager + */ + public function __construct(Manager $keyManager, + ITimeFactory $timeFactory, + IURLGenerator $urlGenerator, + IUserManager $userManager) { + $this->keyManager = $keyManager; + $this->timeFactory = $timeFactory; + $this->userManager = $userManager; + } + + /** + * Returns a signed blob for $data + * + * @param string $type + * @param array $data + * @param IUser $user + * @return array ['message', 'signature'] + */ + public function sign($type, array $data, IUser $user) { + $privateKey = $this->keyManager->getKey($user)->getPrivate(); + $data = [ + 'data' => $data, + 'type' => $type, + 'signer' => $user->getCloudId(), + 'timestamp' => $this->timeFactory->getTime(), + ]; + openssl_sign(json_encode($data), $signature, $privateKey, OPENSSL_ALGO_SHA512); + + return [ + 'message' => $data, + 'signature' => base64_encode($signature), + ]; + } + + /** + * @param string $url + * @return string + */ + private function removeProtocolFromUrl($url) { + if (strpos($url, 'https://') === 0) { + return substr($url, strlen('https://')); + } else if (strpos($url, 'http://') === 0) { + return substr($url, strlen('http://')); + } + + return $url; + } + + /** + * Whether the data is signed properly + * + * @param array $data + * @return bool + */ + public function verify(array $data) { + if(isset($data['message']) + && isset($data['signature']) + && isset($data['message']['signer']) + ) { + $server = $this->urlGenerator->getAbsoluteURL('/'); + $postfix = strlen('@' . rtrim($this->removeProtocolFromUrl($server), '/')); + $userId = substr($data['message']['signer'], -$postfix); + + $user = $this->userManager->get($userId); + if($user !== null) { + $key = $this->keyManager->getKey($user); + return (bool)openssl_verify( + json_encode($data['message']), + base64_decode($data['signature']), + $key->getPublic() + ); + } + } + + return false; + } +} diff --git a/settings/Controller/UsersController.php b/settings/Controller/UsersController.php index 94f97a3561..41f3bac733 100644 --- a/settings/Controller/UsersController.php +++ b/settings/Controller/UsersController.php @@ -524,12 +524,18 @@ class UsersController extends Controller { * @return DataResponse */ public function setUserSettings($avatarScope, - $displayname, $displaynameScope, - $phone, $phoneScope, - $email, $emailScope, - $website, $websiteScope, - $address, $addressScope, - $twitter, $twitterScope + $displayname, + $displaynameScope, + $phone, + $phoneScope, + $email, + $emailScope, + $website, + $websiteScope, + $address, + $addressScope, + $twitter, + $twitterScope ) { diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index 971d86cf6a..6017c02964 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -422,6 +422,7 @@ class AppTest extends \Test\TestCase { 'appforgroup2', 'dav', 'federatedfilesharing', + 'lookup_server_connector', 'provisioning_api', 'twofactor_backupcodes', 'workflowengine', diff --git a/tests/lib/Security/IdentityProof/ManagerTest.php b/tests/lib/Security/IdentityProof/ManagerTest.php new file mode 100644 index 0000000000..d93f675825 --- /dev/null +++ b/tests/lib/Security/IdentityProof/ManagerTest.php @@ -0,0 +1,166 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace Test\Security; + +use OC\Security\IdentityProof\Key; +use OC\Security\IdentityProof\Manager; +use OCP\Files\IAppData; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\IUser; +use OCP\Security\ICrypto; +use Test\TestCase; + +class ManagerTest extends TestCase { + /** @var IAppData|\PHPUnit_Framework_MockObject_MockObject */ + private $appData; + /** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */ + private $crypto; + /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ + private $manager; + + public function setUp() { + parent::setUp(); + $this->appData = $this->createMock(IAppData::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->appData, + $this->crypto + ]) + ->setMethods(['generateKeyPair']) + ->getMock(); + } + + public function testGetKeyWithExistingKey() { + $user = $this->createMock(IUser::class); + $user + ->expects($this->once()) + ->method('getUID') + ->willReturn('MyUid'); + $folder = $this->createMock(ISimpleFolder::class); + $privateFile = $this->createMock(ISimpleFile::class); + $privateFile + ->expects($this->once()) + ->method('getContent') + ->willReturn('EncryptedPrivateKey'); + $publicFile = $this->createMock(ISimpleFile::class); + $publicFile + ->expects($this->once()) + ->method('getContent') + ->willReturn('MyPublicKey'); + $this->crypto + ->expects($this->once()) + ->method('decrypt') + ->with('EncryptedPrivateKey') + ->willReturn('MyPrivateKey'); + $folder + ->expects($this->at(0)) + ->method('getFile') + ->with('private') + ->willReturn($privateFile); + $folder + ->expects($this->at(1)) + ->method('getFile') + ->with('public') + ->willReturn($publicFile); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('MyUid') + ->willReturn($folder); + + $expected = new Key('MyPublicKey', 'MyPrivateKey'); + $this->assertEquals($expected, $this->manager->getKey($user)); + } + + public function testGetKeyWithNotExistingKey() { + $user = $this->createMock(IUser::class); + $user + ->expects($this->exactly(3)) + ->method('getUID') + ->willReturn('MyUid'); + $this->appData + ->expects($this->at(0)) + ->method('getFolder') + ->with('MyUid') + ->willThrowException(new \Exception()); + $this->manager + ->expects($this->once()) + ->method('generateKeyPair') + ->willReturn(['MyNewPublicKey', 'MyNewPrivateKey']); + $this->appData + ->expects($this->at(1)) + ->method('newFolder') + ->with('MyUid'); + $folder = $this->createMock(ISimpleFolder::class); + $this->crypto + ->expects($this->once()) + ->method('encrypt') + ->with('MyNewPrivateKey') + ->willReturn('MyNewEncryptedPrivateKey'); + $privateFile = $this->createMock(ISimpleFile::class); + $privateFile + ->expects($this->once()) + ->method('putContent') + ->with('MyNewEncryptedPrivateKey'); + $publicFile = $this->createMock(ISimpleFile::class); + $publicFile + ->expects($this->once()) + ->method('putContent') + ->with('MyNewPublicKey'); + $folder + ->expects($this->at(0)) + ->method('newFile') + ->with('private') + ->willReturn($privateFile); + $folder + ->expects($this->at(1)) + ->method('newFile') + ->with('public') + ->willReturn($publicFile); + $this->appData + ->expects($this->at(2)) + ->method('getFolder') + ->with('MyUid') + ->willReturn($folder); + + + $expected = new Key('MyNewPublicKey', 'MyNewPrivateKey'); + $this->assertEquals($expected, $this->manager->getKey($user)); + } + + public function testGenerateKeyPair() { + $manager = new Manager( + $this->appData, + $this->crypto + ); + $data = 'MyTestData'; + + list($resultPublicKey, $resultPrivateKey) = $this->invokePrivate($manager, 'generateKeyPair'); + openssl_sign($data, $signature, $resultPrivateKey); + $details = openssl_pkey_get_details(openssl_pkey_get_public($resultPublicKey)); + + $this->assertSame(1, openssl_verify($data, $signature, $resultPublicKey)); + $this->assertSame(2048, $details['bits']); + } +}