$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);
});
<?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;
* @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) {
}
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
*
* @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,
- ]
- );
-
- }
}
public function getIdentityProof($cloudId) {
$userObject = $this->userManager->get($cloudId);
- if($cloudId !== null) {
+ if($userObject !== null) {
$key = $this->keyManager->getKey($userObject);
$data = [
'public' => $key->getPublic(),
return new DataResponse($data);
}
- return new DataResponse(101);
+ return new DataResponse('User not found', 404);
}
}
}
/**
- * 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,
$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);
*/
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);
--- /dev/null
+<?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;
+ }
+}
* @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
) {
'appforgroup2',
'dav',
'federatedfilesharing',
+ 'lookup_server_connector',
'provisioning_api',
'twofactor_backupcodes',
'workflowengine',
--- /dev/null
+<?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']);
+ }
+}