Skip to content
This repository was archived by the owner on Mar 3, 2020. It is now read-only.

Commit e880251

Browse files
justinwraygsingh93
authored andcommitted
Updated LiveSync Security (#494)
* Updated LiveSync Security * Live Sync API is now disabled by default. * Admins can enable or disable the Live Sync API from the Administration Configuration page. * Live Sync API now has an optional 'Auth Key.' When the auth key is set, anyone attempting to pull from the API must supply the auth key value in their request. Without the auth key, no data is provided by the Live Sync API endpoint. * When using the Auth Key, it must be added as a parameter to the URL value in the `liveimport` script: ```?auth=XXXXX_``` * Example (with an auth key of `1234567890`: * `hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php --url 'https://10.10.10.101/data/livesync.php?auth=1234567890'` * Note: When using the Auth Key you should use a secure key. * The `livesync` API endpoint will provide error messages if the API is disabled, the key is missing or invalid, or if any general error is encountered. * The `liveimport` script will check for errors and display those in the output if any are encountered. * Updated LiveSync Security * Combined Awaitables throughout LiveSync endpoint. * Used hash_equals() for API key verification, mitigating timing attacks on the key.# Please enter the commit message for your changes. Lines starting
1 parent d326564 commit e880251

File tree

5 files changed

+220
-64
lines changed

5 files changed

+220
-64
lines changed

database/schema.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ INSERT INTO `configuration` (field, value, description) VALUES("password_type",
229229
INSERT INTO `configuration` (field, value, description) VALUES("default_bonus", "30", "(Integer) Default value for bonus in levels");
230230
INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec", "10", "(Integer) Default bonus decrement in levels");
231231
INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system");
232+
INSERT INTO `configuration` (field, value, description) VALUES("livesync", "0", "(Boolean) LiveSync functionality");
233+
INSERT INTO `configuration` (field, value, description) VALUES("livesync_auth_key", "", "(String) Optional LiveSync Auth Key");
232234
UNLOCK TABLES;
233235

234236
--

database/test_schema.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ INSERT INTO `configuration` (field, value, description) VALUES("password_type",
229229
INSERT INTO `configuration` (field, value, description) VALUES("default_bonus", "30", "(Integer) Default value for bonus in levels");
230230
INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec", "10", "(Integer) Default bonus decrement in levels");
231231
INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system");
232+
INSERT INTO `configuration` (field, value, description) VALUES("livesync", "0", "(Boolean) LiveSync functionality");
233+
INSERT INTO `configuration` (field, value, description) VALUES("livesync_auth_key", "", "(String) Optional LiveSync Auth Key");
232234
UNLOCK TABLES;
233235

234236
--

src/controllers/AdminController.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ class="fb-cta cta--yellow"
293293
'autorun_cycle' => Configuration::gen('autorun_cycle'),
294294
'start_ts' => Configuration::gen('start_ts'),
295295
'end_ts' => Configuration::gen('end_ts'),
296+
'livesync' => Configuration::gen('livesync'),
297+
'livesync_auth_key' => Configuration::gen('livesync_auth_key'),
296298
};
297299

298300
$results = await \HH\Asio\m($awaitables);
@@ -318,6 +320,8 @@ class="fb-cta cta--yellow"
318320
$autorun_cycle = $results['autorun_cycle'];
319321
$start_ts = $results['start_ts'];
320322
$end_ts = $results['end_ts'];
323+
$livesync = $results['livesync'];
324+
$livesync_auth_key = $results['livesync_auth_key'];
321325

322326
$registration_on = $registration->getValue() === '1';
323327
$registration_off = $registration->getValue() === '0';
@@ -337,6 +341,8 @@ class="fb-cta cta--yellow"
337341
$gameboard_off = $gameboard->getValue() === '0';
338342
$timer_on = $timer->getValue() === '1';
339343
$timer_off = $timer->getValue() === '0';
344+
$livesync_on = $livesync->getValue() === '1';
345+
$livesync_off = $livesync->getValue() === '0';
340346

341347
$game_start_array = array();
342348
if ($start_ts->getValue() !== '0' && $start_ts->getValue() !== 'NaN') {
@@ -887,6 +893,43 @@ class="fb-cta cta--yellow"
887893
</div>
888894
</div>
889895
</section>
896+
<section class="admin-box">
897+
<header class="admin-box-header">
898+
<h3>{tr('LiveSync')}</h3>
899+
<div class="admin-section-toggle radio-inline">
900+
<input
901+
type="radio"
902+
name="fb--conf--livesync"
903+
id="fb--conf--livesync--on"
904+
checked={$livesync_on}
905+
/>
906+
<label for="fb--conf--livesync--on">
907+
{tr('On')}
908+
</label>
909+
<input
910+
type="radio"
911+
name="fb--conf--livesync"
912+
id="fb--conf--livesync--off"
913+
checked={$livesync_off}
914+
/>
915+
<label for="fb--conf--livesync--off">
916+
{tr('Off')}
917+
</label>
918+
</div>
919+
</header>
920+
<div class="fb-column-container">
921+
<div class="col col-pad col-1-4">
922+
<div class="form-el el--block-label el--full-text">
923+
<label>{tr('Optional LiveSync Auth Key')}</label>
924+
<input
925+
type="text"
926+
value={$livesync_auth_key->getValue()}
927+
name="fb--conf--livesync_auth_key"
928+
/>
929+
</div>
930+
</div>
931+
</div>
932+
</section>
890933
<section class="admin-box">
891934
<header class="admin-box-header">
892935
<h3>{tr('Language')}</h3>

src/data/livesync.php

Lines changed: 169 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,188 @@ class LiveSyncDataController extends DataController {
66

77
public async function genGenerateData(): Awaitable<void> {
88
$data = array();
9+
await tr_start();
10+
$input_auth_key = idx(Utils::getGET(), 'auth', '');
11+
$livesync_awaits = Map {
12+
'livesync_enabled' => Configuration::gen('livesync'),
13+
'livesync_auth_key' => Configuration::gen('livesync_auth_key'),
14+
};
15+
$livesync_awaits_results = await \HH\Asio\m($livesync_awaits);
16+
$livesync_enabled = $livesync_awaits_results['livesync_enabled'];
17+
$livesync_auth_key = $livesync_awaits_results['livesync_auth_key'];
918

10-
$teams_array = array();
11-
$all_teams = await Team::genAllTeams();
12-
foreach ($all_teams as $team) {
13-
$team_livesync_exists =
14-
await Team::genLiveSyncExists($team->getId(), "fbctf");
15-
if ($team_livesync_exists === true) {
16-
$team_livesync_key =
17-
await Team::genGetLiveSyncKey($team->getId(), "fbctf");
18-
$teams_array[$team->getId()] = strval($team_livesync_key);
19-
}
20-
}
19+
if ($livesync_enabled->getValue() === '1' &&
20+
hash_equals(
21+
strval($livesync_auth_key->getValue()),
22+
strval($input_auth_key),
23+
)) {
24+
25+
$livesync_enabled_awaits = Map {
26+
'all_teams' => Team::genAllTeams(),
27+
'all_scores' => ScoreLog::genAllScores(),
28+
'all_hints' => HintLog::genAllHints(),
29+
'all_levels' => Level::genAllLevels(),
30+
};
31+
$livesync_enabled_awaits_results =
32+
await \HH\Asio\m($livesync_enabled_awaits);
33+
$all_teams = $livesync_enabled_awaits_results['all_teams'];
34+
invariant(
35+
is_array($all_teams),
36+
'all_teams should be an array and not null',
37+
);
38+
39+
$all_scores = $livesync_enabled_awaits_results['all_scores'];
40+
invariant(
41+
is_array($all_scores),
42+
'all_scores should be an array and not null',
43+
);
44+
45+
$all_hints = $livesync_enabled_awaits_results['all_hints'];
46+
invariant(
47+
is_array($all_hints),
48+
'all_hints should be an array and not null',
49+
);
2150

22-
$scores_array = array();
23-
$scored_teams = array();
24-
$all_scores = await ScoreLog::genAllScores();
25-
foreach ($all_scores as $score) {
26-
if (in_array($score->getTeamId(), array_keys($teams_array)) === false) {
27-
continue;
51+
$all_levels = $livesync_enabled_awaits_results['all_levels'];
52+
invariant(
53+
is_array($all_levels),
54+
'all_levels should be an array and not null',
55+
);
56+
57+
$data = array();
58+
$teams_array = array();
59+
$team_livesync_exists = Map {};
60+
$team_livesync_key = Map {};
61+
foreach ($all_teams as $team) {
62+
$team_id = $team->getId();
63+
$team_livesync_exists->add(
64+
Pair {$team_id, Team::genLiveSyncExists($team_id, "fbctf")},
65+
);
2866
}
29-
$scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['timestamp'] =
30-
$score->getTs();
31-
$scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['capture'] =
32-
true;
33-
$scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['hint'] =
34-
false;
35-
$scored_teams[$score->getLevelId()][] = $score->getTeamId();
36-
}
37-
$all_hints = await HintLog::genAllHints();
38-
foreach ($all_hints as $hint) {
39-
if ($hint->getPenalty()) {
40-
if (in_array($hint->getTeamId(), array_keys($teams_array)) ===
67+
$team_livesync_exists_results = await \HH\Asio\m($team_livesync_exists);
68+
foreach ($team_livesync_exists_results as $team_id => $livesync_exists) {
69+
if ($livesync_exists === true) {
70+
$team_livesync_key->add(
71+
Pair {$team_id, Team::genGetLiveSyncKey($team_id, "fbctf")},
72+
);
73+
}
74+
}
75+
$team_livesync_key_results = await \HH\Asio\m($team_livesync_key);
76+
$teams_array = $team_livesync_key_results->toArray();
77+
78+
$scores_array = array();
79+
$scored_teams = array();
80+
81+
foreach ($all_scores as $score) {
82+
if (in_array($score->getTeamId(), array_keys($teams_array)) ===
4183
false) {
4284
continue;
4385
}
44-
$scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['hint'] =
86+
$scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['timestamp'] =
87+
$score->getTs();
88+
$scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['capture'] =
4589
true;
46-
if (in_array(
47-
$hint->getTeamId(),
48-
$scored_teams[$hint->getLevelId()],
49-
) ===
50-
false) {
51-
$scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['capture'] =
52-
false;
53-
$scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['timestamp'] =
54-
$hint->getTs();
90+
$scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['hint'] =
91+
false;
92+
$scored_teams[$score->getLevelId()][] = $score->getTeamId();
93+
}
94+
foreach ($all_hints as $hint) {
95+
if ($hint->getPenalty()) {
96+
if (in_array($hint->getTeamId(), array_keys($teams_array)) ===
97+
false) {
98+
continue;
99+
}
100+
$scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['hint'] =
101+
true;
102+
if (in_array(
103+
$hint->getTeamId(),
104+
$scored_teams[$hint->getLevelId()],
105+
) ===
106+
false) {
107+
$scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['capture'] =
108+
false;
109+
$scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['timestamp'] =
110+
$hint->getTs();
111+
}
55112
}
56113
}
57-
}
58114

59-
$levels_array = array();
60-
$all_levels = await Level::genAllLevels();
61-
foreach ($all_levels as $level) {
62-
$entity = await Country::gen($level->getEntityId());
63-
$category = await Category::genSingleCategory($level->getCategoryId());
64-
if (array_key_exists($level->getId(), $scores_array)) {
65-
$score_level_array = $scores_array[$level->getId()];
66-
} else {
67-
$score_level_array = array();
115+
$levels_array = array();
116+
$entities = Map {};
117+
$categories = Map {};
118+
foreach ($all_levels as $level) {
119+
$level_id = $level->getId();
120+
$entities->add(Pair {$level_id, Country::gen($level->getEntityId())});
121+
$categories->add(
122+
Pair {
123+
$level_id,
124+
Category::genSingleCategory($level->getCategoryId()),
125+
},
126+
);
68127
}
69-
$one_level = array(
70-
'active' => $level->getActive(),
71-
'type' => $level->getType(),
72-
'title' => $level->getTitle(),
73-
'description' => $level->getDescription(),
74-
'entity_iso_code' => $entity->getIsoCode(),
75-
'category' => $category->getCategory(),
76-
'points' => $level->getPoints(),
77-
'bonus' => $level->getBonusFix(),
78-
'bonus_dec' => $level->getBonusDec(),
79-
'penalty' => $level->getPenalty(),
80-
'teams' => $score_level_array,
128+
$entities_results = await \HH\Asio\m($entities);
129+
invariant(
130+
$entities_results instanceof Map,
131+
'entities_results should of type Map and not null',
81132
);
82-
$levels_array[] = $one_level;
83-
}
84133

85-
$data = $levels_array;
134+
$categories_results = await \HH\Asio\m($categories);
135+
invariant(
136+
$categories_results instanceof Map,
137+
'categories_results should of type Map and not null',
138+
);
139+
140+
foreach ($all_levels as $level) {
141+
$level_id = $level->getId();
142+
$entity = $entities_results->get($level_id);
143+
invariant(
144+
$entity instanceof Country,
145+
'entity should of type Country and not null',
146+
);
147+
148+
$category = $categories_results->get($level_id);
149+
invariant(
150+
$category instanceof Category,
151+
'category should of type Category and not null',
152+
);
153+
154+
if (array_key_exists($level->getId(), $scores_array)) {
155+
$score_level_array = $scores_array[$level_id];
156+
} else {
157+
$score_level_array = array();
158+
}
159+
$one_level = array(
160+
'active' => $level->getActive(),
161+
'type' => $level->getType(),
162+
'title' => $level->getTitle(),
163+
'description' => $level->getDescription(),
164+
'entity_iso_code' => $entity->getIsoCode(),
165+
'category' => $category->getCategory(),
166+
'points' => $level->getPoints(),
167+
'bonus' => $level->getBonusFix(),
168+
'bonus_dec' => $level->getBonusDec(),
169+
'penalty' => $level->getPenalty(),
170+
'teams' => $score_level_array,
171+
);
172+
$levels_array[] = $one_level;
173+
}
174+
175+
$data = $levels_array;
176+
} else if ($livesync_enabled->getValue() === '0') {
177+
$data['error'] = tr(
178+
'LiveSync is disabled, please contact the administrator for access.',
179+
);
180+
} else if (strval($input_auth_key) !==
181+
strval($livesync_auth_key->getValue())) {
182+
$data['error'] =
183+
tr(
184+
'LiveSync auth key is invalid, please contact the administrator for access.',
185+
);
186+
} else {
187+
$data['error'] = tr(
188+
'LiveSync failed, please contact the administrator for assistance.',
189+
);
190+
}
86191
$this->jsonSend($data);
87192
}
88193

src/scripts/liveimport.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ class LiveSyncImport {
6060
$json = await self::genDownloadData($url, $check_certificates);
6161
$data = json_decode($json);
6262
if (empty($data) === false) {
63+
if ((!is_array($data)) && (property_exists($data, 'error'))) {
64+
self::debug(true, $url, '!!!', strval($data->error));
65+
continue;
66+
}
6367
foreach ($data as $level) {
6468
$mandatories_set = await self::genMandatoriesSet($level);
6569
if ($mandatories_set === false) {

0 commit comments

Comments
 (0)