]> source.dussan.org Git - nextcloud-server.git/commitdiff
Add a signer class for signing
authorLukas Reschke <lukas@statuscode.ch>
Fri, 18 Nov 2016 09:10:05 +0000 (10:10 +0100)
committerRoeland Jago Douma <roeland@famdouma.nl>
Mon, 21 Nov 2016 10:30:00 +0000 (11:30 +0100)
Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
apps/lookup_server_connector/appinfo/app.php
apps/lookup_server_connector/lib/UpdateLookupServer.php
core/Controller/OCSController.php
lib/private/Security/IdentityProof/Manager.php
lib/private/Security/IdentityProof/Signer.php [new file with mode: 0644]
settings/Controller/UsersController.php
tests/lib/AppTest.php
tests/lib/Security/IdentityProof/ManagerTest.php [new file with mode: 0644]

index dc076b78828b902eb40a477d9d7f8162b603bb75..d757284f7d540c2c92d13379387ea70a317f1780 100644 (file)
@@ -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);
 });
index b6d8b9782a42e4425b69b9abb35d116e79f25f50..fc20ddcd143e96a0b69850ebdc29cd8cec8061e5 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 /**
  * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org>
+ * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
  *
  * @license GNU AGPL version 3 or any later version
  *
  *
  */
 
-
 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,
-                       ]
-               );
-
-       }
 }
index b1c4f377a1433c37e461ddb7e2e6955c3212a28e..c59b0d7ad3fc15dd41925d10aad4a7c1f195e617 100644 (file)
@@ -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);
        }
 }
index 223af05410b9c4a4f76cb164306a64b3422ba782..d2a9e57e3387e646f48cb4e65f8204a4632da8f1 100644 (file)
@@ -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 (file)
index 0000000..50c36b2
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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;
+       }
+}
index 94f97a356130fb116b55862f55cb36aceec6a722..41f3bac733dea1da86685ee981cfdea748faaf12 100644 (file)
@@ -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
        ) {
 
 
index 971d86cf6a4807b5d872a56b97a00485a5f8fead..6017c029644fac5335022f0603ff10b877143745 100644 (file)
@@ -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 (file)
index 0000000..d93f675
--- /dev/null
@@ -0,0 +1,166 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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']);
+       }
+}