From b9bef69977b5ae66d0ee5a521409a9f976fd967f Mon Sep 17 00:00:00 2001 From: jelhan Date: Sun, 23 Aug 2015 18:56:41 +0200 Subject: [PATCH] user has to proof that he knows encryption key when he participates Therefore sha256 hash of encryption key is validated against one which is stored on server on poll creation. This one is transfered as X-Croodle-Proof-Key-Knowledge HTTP HEADER. Prevents an attacker of transmitting data with wrong encryption key, which would cause decryption errors for legit users. --- api/classes/model.php | 84 ++++++++++++++++++- api/classes/poll.php | 12 ++- api/classes/user.php | 16 +++- api/index.php | 6 ++ api/tests/api/CreateAnotherUserCept.php | 11 ++- api/tests/api/CreatePollCept.php | 14 +++- api/tests/api/CreateUserCept.php | 6 +- ...rFailsIfKeyKnowledgeHeaderIsNotSetCept.php | 29 +++++++ ...UserFailsIfKeyKnowledgeIsNotProvedCept.php | 30 +++++++ api/tests/api/ExpiredPollGetsDeletedCept.php | 2 +- api/tests/api/GetNotExistingPollCept.php | 2 +- api/tests/api/GetPollCept.php | 3 +- api/tests/api/GetPollWithUsersCept.php | 2 +- app/adapters/application.js | 8 ++ app/initializers/inject-encryption-key.js | 1 + app/models/encryption.js | 11 ++- 16 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 api/tests/api/CreateUserFailsIfKeyKnowledgeHeaderIsNotSetCept.php create mode 100644 api/tests/api/CreateUserFailsIfKeyKnowledgeIsNotProvedCept.php diff --git a/api/classes/model.php b/api/classes/model.php index c85a23c..1e52299 100644 --- a/api/classes/model.php +++ b/api/classes/model.php @@ -3,19 +3,28 @@ class Model { const ENCRYPTED_PROPERTIES = []; const PLAIN_PROPERTIES = []; + const PROOF_KEY_KNOWLEDGE = 'validate'; const SERVER_PROPERTIES = []; protected $data; + protected $proofKeyKnowledge; public function __construct() { - if(!defined('DATA_FOLDER')) { + if (!defined('DATA_FOLDER')) { throw new Exception('DATA_FOLDER is not defined'); } - if(!is_writable(DATA_FOLDER)) { + if (!is_writable(DATA_FOLDER)) { throw new Exception('DATA_FOLDER (' . DATA_FOLDER . ') is not writeable'); } + if ( + static::PROOF_KEY_KNOWLEDGE !== 'save' && + static::PROOF_KEY_KNOWLEDGE !== 'validate' + ) { + throw new Exception('PROOF_KEY_KNOWLEDGE must be "save" or "validate" but is ' . static::PROOF_KEY_KNOWLEDGE); + } + $this->data = new stdClass(); } @@ -102,6 +111,10 @@ class Model { throw new Exception ('getPath must be implemented by model'); } + private function getPathToKeyKnowledgeFile() { + return $this->getPollDir() . 'key_knowledge'; + } + /* * Checks if a json string is a proper SJCL encrypted message. * False if format is incorrect. @@ -168,7 +181,7 @@ class Model { catch (Exception $e) { // no poll with this id return false; - } + } $data = self::convertFromStorage($storageObject); @@ -181,6 +194,10 @@ class Model { $model->set($property, $data->$property); } + if (static::PROOF_KEY_KNOWLEDGE === 'save') { + $model->restoreKeyKnowledge($data); + } + if (method_exists($model, 'restoreHook')) { if ($model->restoreHook() === false) { return false; @@ -190,11 +207,34 @@ class Model { return $model; } + private function restoreKeyKnowledge() { + try { + $data = file_get_contents( + $this->getPathToKeyKnowledgeFile() + ); + + if ($data) { + return $data; + } + else { + throw new Exception('key knowledge file could not be read'); + } + } + catch (Exception $e) { + return false; + } + } + /* * save object to storage * gives back new id */ public function save() { + // proof key knowledge before save + if (static::PROOF_KEY_KNOWLEDGE === 'validate') { + $this->validateKeyKnowledge(); + } + // create dir for data if it does not exists $counter = 0; while (true) { @@ -238,9 +278,47 @@ class Model { // successfully run break; } + + // save key knowledge after poll is saved + if (static::PROOF_KEY_KNOWLEDGE === 'save') { + $this->saveKeyKnowledge(); + } + } + + private function saveKeyKnowledge() { + if ( + file_put_contents( + $this->getPathToKeyKnowledgeFile(), + $this->proofKeyKnowledge, + LOCK_EX + ) === false + ) { + throw new Exception('failed to save key knowledge'); + } } private function set($key, $value) { $this->data->$key = $value; } + + public function setProofKeyKnowledge($value) { + $this->proofKeyKnowledge = $value; + } + + private function validateKeyKnowledge() { + if (empty($this->proofKeyKnowledge)) { + throw new Exception('proof key knowledge is not set'); + } + + $keyKnowledge = $this->restoreKeyKnowledge(); + + if ( + $keyKnowledge !== false && + $keyKnowledge !== $this->proofKeyKnowledge + ) { + throw new Exception( + 'key knowledge not proofed: ' . $this->proofKeyKnowledge . ' does not equal ' . var_export($keyKnowledge, true) + ); + } + } } diff --git a/api/classes/poll.php b/api/classes/poll.php index aea1867..c53190b 100644 --- a/api/classes/poll.php +++ b/api/classes/poll.php @@ -26,6 +26,8 @@ class Poll extends model { 'version' ]; + const PROOF_KEY_KNOWLEDGE = 'save'; + const SERVER_PROPERTIES = [ 'serverExpirationDate' ]; @@ -72,8 +74,15 @@ class Poll extends model { } protected function getDir() { + if (($this->get('id') === null)) { + throw new Exception('id must be set before calling getDir'); + } return DATA_FOLDER . $this->get('id') . '/'; } + + protected function getPollDir() { + return $this->getDir(); + } protected function getPath() { return $this->getDir() . 'poll_data'; @@ -110,7 +119,8 @@ class Poll extends model { public static function isValidId($id) { $idCharacters = str_split($id); - return count(array_diff($idCharacters, str_split(self::ID_CHARACTERS))) === 0; + return strlen($id) === 10 && + count(array_diff($idCharacters, str_split(self::ID_CHARACTERS))) === 0; } protected function restoreHook() { diff --git a/api/classes/user.php b/api/classes/user.php index 60d8d43..25036e9 100644 --- a/api/classes/user.php +++ b/api/classes/user.php @@ -38,16 +38,28 @@ class User extends Model { } protected function getDir() { + return $this->getPollDir() . 'users/'; + } + + protected function getPollDir() { if ($this->get('poll') !== null) { $pollId = $this->get('poll'); } else { $pollId = explode('_', $this->get('id'))[0]; } - return DATA_FOLDER . $pollId . '/users/'; + + if (!Poll::isValidId($pollId)) { + throw new Exception('cound not get a valid id when getPollDir was called'); + } + + return DATA_FOLDER . $pollId . '/'; } - protected function getPath() { + protected function getPath() { + if (!self::isValidId($this->get('id'))) { + throw new Exception('no valid user id when getPath was called'); + } return $this->getDir() . explode('_', $this->get('id'))[1]; } diff --git a/api/index.php b/api/index.php index d2a1ae9..93b3361 100644 --- a/api/index.php +++ b/api/index.php @@ -55,6 +55,9 @@ $app->post('/polls', function() use ($app) { $app->request->getBody() )->poll ); + $poll->setProofKeyKnowledge( + $app->request->headers->get('X-Croodle-Proof-Key-Knowledge') + ); $poll->save(); $app->response->setBody( @@ -72,6 +75,9 @@ $app->post('/users', function() use ($app) { $app->request->getBody() )->user ); + $user->setProofKeyKnowledge( + $app->request->headers->get('X-Croodle-Proof-Key-Knowledge') + ); $user->save(); $app->response->setBody( diff --git a/api/tests/api/CreateAnotherUserCept.php b/api/tests/api/CreateAnotherUserCept.php index 584d6ab..2880915 100644 --- a/api/tests/api/CreateAnotherUserCept.php +++ b/api/tests/api/CreateAnotherUserCept.php @@ -1,19 +1,22 @@ wantTo('create a user'); +$I->haveHTTPHeader('X-Croodle-Proof-Key-Knowledge', $proofKeyKnowledge); $I->sendPOST('/users', $userJson); $I->seeResponseCodeIs(200); $I->seeResponseIsJson(); diff --git a/api/tests/api/CreatePollCept.php b/api/tests/api/CreatePollCept.php index ef8c90e..736ec2f 100644 --- a/api/tests/api/CreatePollCept.php +++ b/api/tests/api/CreatePollCept.php @@ -1,9 +1,11 @@ wantTo('create a poll'); +$I->haveHTTPHeader('X-Croodle-Proof-Key-Knowledge', $proofKeyKnowledge); $I->sendPOST('/polls', $pollJson); $I->seeResponseCodeIs(200); $I->seeResponseIsJson(); @@ -38,5 +40,15 @@ $I->seeResponseContainsJson( ); $I->dontSeeResponseJsonMatchesJsonPath( 'poll.serverExpirationDate', - 'serverExpirationDate is not in response.' + 'serverExpirationDate is not in response payload.' +); +$I->dontSeeResponseJsonMatchesJsonPath( + 'poll.proofKeyKnowledge', + 'proofKeyKnowledge is not in response payload.' +); + +\PHPUnit_Framework_Assert::assertEquals( + file_get_contents(TEST_DATA_DIR . $pollId . '/key_knowledge'), + $proofKeyKnowledge, + 'user array should be empty' ); diff --git a/api/tests/api/CreateUserCept.php b/api/tests/api/CreateUserCept.php index ae91ed5..0aff8d0 100644 --- a/api/tests/api/CreateUserCept.php +++ b/api/tests/api/CreateUserCept.php @@ -1,14 +1,18 @@ wantTo('create a user'); +$I->haveHTTPHeader('X-Croodle-Proof-Key-Knowledge', $proofKeyKnowledge); $I->sendPOST('/users', $userJson); $I->seeResponseCodeIs(200); $I->seeResponseIsJson(); diff --git a/api/tests/api/CreateUserFailsIfKeyKnowledgeHeaderIsNotSetCept.php b/api/tests/api/CreateUserFailsIfKeyKnowledgeHeaderIsNotSetCept.php new file mode 100644 index 0000000..838ccec --- /dev/null +++ b/api/tests/api/CreateUserFailsIfKeyKnowledgeHeaderIsNotSetCept.php @@ -0,0 +1,29 @@ +wantTo('see that create a new user fails if key knowledge header is not set'); +$I->sendPOST('/users', $userJson); +$I->seeResponseCodeIs(500); +$I->seeResponseIsJson(); + +try { + $result = file_get_contents($usersDir . '0'); +} +catch (Exception $e) { + $result = false; +} +\PHPUnit_Framework_Assert::assertFalse( + $result, + 'no user is saved to disc' +); diff --git a/api/tests/api/CreateUserFailsIfKeyKnowledgeIsNotProvedCept.php b/api/tests/api/CreateUserFailsIfKeyKnowledgeIsNotProvedCept.php new file mode 100644 index 0000000..51e4af7 --- /dev/null +++ b/api/tests/api/CreateUserFailsIfKeyKnowledgeIsNotProvedCept.php @@ -0,0 +1,30 @@ +wantTo('see that create a new user fails if key knowledge is wrong'); +$I->haveHTTPHeader('X-Croodle-Proof-Key-Knowledge', $wrongKeyKnowledge); +$I->sendPOST('/users', $userJson); +$I->seeResponseCodeIs(500); +$I->seeResponseIsJson(); + +try { + $result = file_get_contents($usersDir . '0'); +} +catch (Exception $e) { + $result = false; +} +\PHPUnit_Framework_Assert::assertFalse( + $result, + 'no user is saved to disc' +); diff --git a/api/tests/api/ExpiredPollGetsDeletedCept.php b/api/tests/api/ExpiredPollGetsDeletedCept.php index 6ad7ad2..16d45fb 100644 --- a/api/tests/api/ExpiredPollGetsDeletedCept.php +++ b/api/tests/api/ExpiredPollGetsDeletedCept.php @@ -1,6 +1,6 @@ wantTo('get an not existing poll'); -$I->sendGet('/polls/abcdEFGH12'); +$I->sendGet('/polls/notExistin'); $I->seeResponseCodeIs(404); $I->seeResponseEquals(''); diff --git a/api/tests/api/GetPollCept.php b/api/tests/api/GetPollCept.php index 0372038..0bb6094 100644 --- a/api/tests/api/GetPollCept.php +++ b/api/tests/api/GetPollCept.php @@ -1,5 +1,6 @@