Browse Source

Add a signer class for signing

Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
tags/v11.0RC2
Lukas Reschke 7 years ago
parent
commit
fb91bf6a5b
No account linked to committer's email address

+ 13
- 1
apps/lookup_server_connector/appinfo/app.php View 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);
});

+ 36
- 61
apps/lookup_server_connector/lib/UpdateLookupServer.php View 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
*
@@ -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,
]
);

}
}

+ 2
- 2
core/Controller/OCSController.php View 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);
}
}

+ 28
- 9
lib/private/Security/IdentityProof/Manager.php View 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);

+ 120
- 0
lib/private/Security/IdentityProof/Signer.php View File

@@ -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;
}
}

+ 12
- 6
settings/Controller/UsersController.php View 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
) {



+ 1
- 0
tests/lib/AppTest.php View File

@@ -422,6 +422,7 @@ class AppTest extends \Test\TestCase {
'appforgroup2',
'dav',
'federatedfilesharing',
'lookup_server_connector',
'provisioning_api',
'twofactor_backupcodes',
'workflowengine',

+ 166
- 0
tests/lib/Security/IdentityProof/ManagerTest.php View File

@@ -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']);
}
}

Loading…
Cancel
Save