Signed-off-by: Lukas Reschke <lukas@statuscode.ch>tags/v11.0RC2
@@ -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); | |||
}); |
@@ -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 | |||
* | |||
@@ -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, | |||
] | |||
); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); |
@@ -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; | |||
} | |||
} |
@@ -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 | |||
) { | |||
@@ -422,6 +422,7 @@ class AppTest extends \Test\TestCase { | |||
'appforgroup2', | |||
'dav', | |||
'federatedfilesharing', | |||
'lookup_server_connector', | |||
'provisioning_api', | |||
'twofactor_backupcodes', | |||
'workflowengine', |
@@ -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']); | |||
} | |||
} |