Skip to content

Commit

Permalink
Merge pull request cypht-org#819 from Yannick243/2FA-configuration-co…
Browse files Browse the repository at this point in the history
…nfirm

Make sure the user has configured Google Authenticator before enabling 2FA authentication
  • Loading branch information
kroky authored Feb 2, 2024
2 parents 056a74b + c787c4a commit a0bf30d
Show file tree
Hide file tree
Showing 4 changed files with 479 additions and 27 deletions.
86 changes: 68 additions & 18 deletions modules/2fa/modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,33 @@ public function process() {
}
}

/**
* Verify 2fa code is paired with Authenticator app before enabling 2fa
* @subpackage 2fa/handler
*/
class Hm_Handler_2fa_setup_check extends Hm_Handler_Module {
public function process() {

list($secret, $simple) = get_2fa_key($this->config);
if (!$secret) {
Hm_Debug::add('2FA module set enabled, but no shared secret configured');
return;
}

$verified = false;
$len = $simple ? 15 : 64;

$username = $this->session->get('username', false);
$secret = create_secret($secret, $username, $len);

if (check_2fa_pin($this->request->post['2fa_code'], $secret)) {
$verified = true;
}

$this->out('ajax_2fa_verified', $verified);
}
}

/**
* @subpackage 2fa/output
*/
Expand All @@ -120,40 +147,63 @@ protected function output() {
if (array_key_exists('2fa_enable', $settings)) {
$enabled = $settings['2fa_enable'];
}
$res = '<tr><td colspan="2" data-target=".tfa_setting" class="settings_subtitle">'.
'<img alt="" src="'.Hm_Image_Sources::$unlocked.'" width="16" height="16" />'.$this->trans('2 Factor Authentication').'</td></tr>';
$res = '<tr>
<td colspan="2" data-target=".tfa_setting" class="settings_subtitle">'.
'<img alt="" src="'.Hm_Image_Sources::$unlocked.'" width="16" height="16" />'.$this->trans('2 Factor Authentication').'
</td>
</tr>';

$res .= '<tr class="tfa_setting">
<td>
'.$this->trans('Enable 2 factor authentication').'
<input value="1" type="checkbox" name="2fa_enable" '.($enabled ? 'checked="checked"' : '').' />';

$res .= '<tr class="tfa_setting"><td>'.$this->trans('Enable 2 factor authentication').
'</td><td><input value="1" type="checkbox" name="2fa_enable"';
if ($enabled) {
$res .= ' checked="checked"';
}
$res .= '></td></tr>';
$svg = $this->get('2fa_svg');

if ($svg) {
$qr_code = '<tr class="tfa_setting"><td></td><td>';
$qr_code = '';
if (!$enabled) {
$qr_code .= '<div class="err settings_wrap_text">'.
$this->trans('Configure your authentication app using the barcode below BEFORE enabling 2 factor authentication.').'</div>';
$qr_code .= '<div class="err settings_wrap_text tfa_mt_1">'.$this->trans('Configure your authentication app using the barcode below BEFORE enabling 2 factor authentication.').'</div>';
}
else {
$qr_code .= '<div>'.$this->trans('Update your settings with the code below').'</div>';
}

$qr_code .= $svg;
$qr_code .= '</td></tr>';
$qr_code .= '<tr class="tfa_setting"><td></td><td>'.$this->trans('If you can\'t use the QR code, you can enter the code below manually (no line breaks)').'</td></tr>';
$qr_code .= '<tr class="tfa_setting"><td></td><td>'.wordwrap($this->html_safe($this->get('2fa_secret', '')), 60, '<br />', true).'</td></tr>';
$qr_code .= '<div class="tfa_mb_1">'.$this->trans('If you can\'t use the QR code, you can enter the code below manually (no line breaks)').'</div>';
$qr_code .= wordwrap($this->html_safe($this->get('2fa_secret', '')), 60, '<br />', true);
}
else {
$qr_code = '<tr class="tfa_setting"><td></td><td class="err">'.$this->trans('Unable to generate 2 factor authentication QR code').'</td></tr>';
$qr_code = '<div class="tfa_mt_1">'.$this->trans('Unable to generate 2 factor authentication QR code').'</div>';
}
$res .= $qr_code;
$res .= '<tr class="tfa_setting"><td></td><td>'.$this->trans('The following backup codes can be used to access your account if you lose your device').'<br /><br />';
$res .= $qr_code . '</td>';

$res .= '<td><div class="tfa_mb_1">'.$this->trans('The following backup codes can be used to access your account if you lose your device'). '</div>';

foreach ($backup_codes as $val) {
$res .= ' '.$val.'<input type="hidden" name="2fa_backup_codes[]" value="'.$val.'" /></br >';
}
$res .= '</td></tr>';
$res .= '<div class="tfa_mt_1">
<fieldset class="tfa_confirmation_fieldset">
<legend>Enter the confirmation code</legend>
<div class="tfa_confirmation_wrapper">
<div class="tfa_confirmation_form">
<div class="tfa_confirmation_input_digits">
<input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 0" aria-required="true">
<input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 1" aria-required="true">
<input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 2" aria-required="true">
<input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 3" aria-required="true">
<input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 4" aria-required="true">
<input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 5" aria-required="true">
</div>
<button id="tfaConfirmationBtn" type="submit" class="tfa_confirmation_input_button">'.$this->trans('Verify code').'</button>
</div>
<div class="tfa_confirmation_hint"> '.$this->trans('Enter the 6 digit code from your Authenticator application').'</div>
</div>
</fieldset>
</div>
</td>
</tr>';
return $res;
}
}
Expand Down
22 changes: 17 additions & 5 deletions modules/2fa/setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,20 @@
add_handler('settings', 'process_enable_2fa', true, '2fa', 'save_user_settings', 'before');
add_output('settings', 'enable_2fa_setting', true, '2fa', 'end_settings_form', 'before');

return array( 'allowed_post' => array(
'2fa_code' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'2fa_enable' => FILTER_VALIDATE_INT,
'2fa_backup_codes' => array('filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_FORCE_ARRAY)
));
/* 2fa setup handler */
setup_base_ajax_page('ajax_2fa_setup_check', 'core');
add_handler('ajax_2fa_setup_check', '2fa_setup_check', true);

return array(
'allowed_pages' => array(
'ajax_2fa_setup_check',
),
'allowed_post' => array(
'2fa_code' => FILTER_SANITIZE_FULL_SPECIAL_CHARS,
'2fa_enable' => FILTER_VALIDATE_INT,
'2fa_backup_codes' => array('filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_FORCE_ARRAY)
),
'allowed_output' => array(
'ajax_2fa_verified' => array(FILTER_VALIDATE_BOOLEAN, false),
),
);
223 changes: 219 additions & 4 deletions modules/2fa/site.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,219 @@
.tfa_error, .tfa_input { margin-left: 20px; }
.tfa_error { color: red; margin-right: 20px; margin-bottom: 10px; }
.tfa_setting { display: none; }
.mobile .tfa_input { max-width: 340px; margin-bottom: 10px; }
:root {
--tfa-form-valid-color: #198754;
--tfa-form-valid-border-color: #198754;
--tfa-form-invalid-color: #dc3545;
--tfa-form-invalid-border-color: #dc3545;
--tfa-border-radius: 0.375rem;
--tfa-border-color: #dee2e6;
--tfa-base-color: #666;
--tfa-secondary-color: rgba(33, 37, 41, 0.75);
}

.tfa_error,
.tfa_input {
margin-left: 20px;
}

.tfa_error {
color: red;
margin-right: 20px;
margin-bottom: 10px;
}

.tfa_setting {
display: none;
}

.user_settings .tfa_setting td {
height: auto;
}

.mobile .tfa_input {
max-width: 340px;
margin-bottom: 10px;
}

.tfa_mt_1 {
margin-top: 0.7rem;
}

.tfa_mb_1 {
margin-bottom: 0.7rem;
}

.tfa_confirmation_fieldset {
color: var(--tfa-base-color);
border-radius: var(--tfa-border-radius);
border: 1px solid var(--tfa-border-color);
max-width: 365px;
}

.tfa_confirmation_wrapper .tfa_confirmation_hint {
font-style: italic;
font-size: 0.75em;
margin-top: 0.8rem;
color: var(--tfa-secondary-color);
}
.tfa_confirmation_wrapper .tfa_confirmation_hint .invalid {
color: var(--tfa-form-invalid-color);
}
.tfa_confirmation_wrapper .valid {
border-color: var(--tfa-form-valid-color);
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.tfa_confirmation_wrapper .invalid {
border-color: var(--tfa-form-invalid-color);
}
.tfa_confirmation_wrapper input::-webkit-outer-spin-button,
.tfa_confirmation_wrapper input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.tfa_confirmation_wrapper input[type=number] {
-moz-appearance: textfield;
}
.tfa_confirmation_wrapper input,
.tfa_confirmation_wrapper button,
.tfa_confirmation_wrapper a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

.tfa_confirmation_input_digits {
--total-digits: 6;
display: grid;
grid-template-columns: repeat(var(--total-digits), 1fr);
gap: 0.5rem;
}
.tfa_confirmation_input_digit {
display: block;
width: 2rem;
height: 2rem;
padding: 0.625rem;
border: 1px solid var(--tfa-border-color);
border-radius: var(--tfa-border-radius);
font-size: 1.5rem;
text-align: center;
}
.tfa_confirmation_input_digit:focus {
color: #212529;
background-color: #fff;
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.tfa_confirmation_input_button {
padding: 10px;
color: var(--tfa-base-color);
border-radius: var(--tfa-border-radius);
border: 1px solid var(--tfa-border-color);
background-color: #fff;
text-decoration: none;
margin-top: 0.8rem;
font-size: 0.9rem;
}
.tfa_confirmation_input_button:hover {
cursor: pointer;
}
.tfa_confirmation_input_button.invalid {
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
animation: shake 150ms 2 linear;
-moz-animation: shake 150ms 2 linear;
-webkit-animation: shake 150ms 2 linear;
-o-animation: shake 150ms 2 linear;
}
.tfa_confirmation_input_button.loading {
cursor: not-allowed;
opacity: 0.7;
padding-right: calc(1.75em + 0.75rem);
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiBub25lOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjIwMHB4IiBoZWlnaHQ9IjIwMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPgo8ZyB0cmFuc2Zvcm09InJvdGF0ZSgwIDUwIDUwKSI+CiAgPHJlY3QgeD0iNDciIHk9IjI0IiByeD0iMyIgcnk9IjYiIHdpZHRoPSI2IiBoZWlnaHQ9IjEyIiBmaWxsPSIjYTNhM2EzIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9Im9wYWNpdHkiIHZhbHVlcz0iMTswIiBrZXlUaW1lcz0iMDsxIiBkdXI9IjFzIiBiZWdpbj0iLTAuOTE2NjY2NjY2NjY2NjY2NnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGU+CiAgPC9yZWN0Pgo8L2c+PGcgdHJhbnNmb3JtPSJyb3RhdGUoMzAgNTAgNTApIj4KICA8cmVjdCB4PSI0NyIgeT0iMjQiIHJ4PSIzIiByeT0iNiIgd2lkdGg9IjYiIGhlaWdodD0iMTIiIGZpbGw9IiNhM2EzYTMiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ib3BhY2l0eSIgdmFsdWVzPSIxOzAiIGtleVRpbWVzPSIwOzEiIGR1cj0iMXMiIGJlZ2luPSItMC44MzMzMzMzMzMzMzMzMzM0cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZT4KICA8L3JlY3Q+CjwvZz48ZyB0cmFuc2Zvcm09InJvdGF0ZSg2MCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZT4KICA8L3JlY3Q+CjwvZz48ZyB0cmFuc2Zvcm09InJvdGF0ZSg5MCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjY2NjY2NjY2NjY2NjY2NjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPjxnIHRyYW5zZm9ybT0icm90YXRlKDEyMCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjU4MzMzMzMzMzMzMzMzMzRzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPjxnIHRyYW5zZm9ybT0icm90YXRlKDE1MCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjVzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPjxnIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjQxNjY2NjY2NjY2NjY2NjdzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPjxnIHRyYW5zZm9ybT0icm90YXRlKDIxMCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjMzMzMzMzMzMzMzMzMzMzNzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPjxnIHRyYW5zZm9ybT0icm90YXRlKDI0MCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49Ii0wLjI1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZT4KICA8L3JlY3Q+CjwvZz48ZyB0cmFuc2Zvcm09InJvdGF0ZSgyNzAgNTAgNTApIj4KICA8cmVjdCB4PSI0NyIgeT0iMjQiIHJ4PSIzIiByeT0iNiIgd2lkdGg9IjYiIGhlaWdodD0iMTIiIGZpbGw9IiNhM2EzYTMiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ib3BhY2l0eSIgdmFsdWVzPSIxOzAiIGtleVRpbWVzPSIwOzEiIGR1cj0iMXMiIGJlZ2luPSItMC4xNjY2NjY2NjY2NjY2NjY2NnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGU+CiAgPC9yZWN0Pgo8L2c+PGcgdHJhbnNmb3JtPSJyb3RhdGUoMzAwIDUwIDUwKSI+CiAgPHJlY3QgeD0iNDciIHk9IjI0IiByeD0iMyIgcnk9IjYiIHdpZHRoPSI2IiBoZWlnaHQ9IjEyIiBmaWxsPSIjYTNhM2EzIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9Im9wYWNpdHkiIHZhbHVlcz0iMTswIiBrZXlUaW1lcz0iMDsxIiBkdXI9IjFzIiBiZWdpbj0iLTAuMDgzMzMzMzMzMzMzMzMzMzNzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPjxnIHRyYW5zZm9ybT0icm90YXRlKDMzMCA1MCA1MCkiPgogIDxyZWN0IHg9IjQ3IiB5PSIyNCIgcng9IjMiIHJ5PSI2IiB3aWR0aD0iNiIgaGVpZ2h0PSIxMiIgZmlsbD0iI2EzYTNhMyI+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJvcGFjaXR5IiB2YWx1ZXM9IjE7MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49IjBzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlPgogIDwvcmVjdD4KPC9nPgo8IS0tIFtsZGlvXSBnZW5lcmF0ZWQgYnkgaHR0cHM6Ly9sb2FkaW5nLmlvLyAtLT48L3N2Zz4=");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(1.5em + 0.375rem) calc(1.5em + 0.375rem);
}

@media (max-width: 767px) {
.hidden-xs {
display: none !important;
}

.tfa_confirmation_fieldset {
max-width: 100vh;
}
.tfa_confirmation_fieldset .tfa_confirmation_input_digit {
width: 1.5rem;
height: 1.5rem;
}

.user_settings .tfa_setting td {
display: block;
}
}
/**
** Shake Annimation Starts Here.
**/
@keyframes shake {
0% {
transform: translate(3px, 0);
}
50% {
transform: translate(-3px, 0);
}
100% {
transform: translate(0, 0);
}
}
@-moz-keyframes shake {
0% {
-moz-transform: translate(3px, 0);
}
50% {
-moz-transform: translate(-3px, 0);
}
100% {
-moz-transform: translate(0, 0);
}
}
@-webkit-keyframes shake {
0% {
-webkit-transform: translate(3px, 0);
}
50% {
-webkit-transform: translate(-3px, 0);
}
100% {
-webkit-transform: translate(0, 0);
}
}
@-ms-keyframes shake {
0% {
-ms-transform: translate(3px, 0);
}
50% {
-ms-transform: translate(-3px, 0);
}
100% {
-ms-transform: translate(0, 0);
}
}
@-o-keyframes shake {
0% {
-o-transform: translate(3px, 0);
}
50% {
-o-transform: translate(-3px, 0);
}
100% {
-o-transform: translate(0, 0);
}
}
/**
** Shake Annimation Ends Here.
**/
Loading

0 comments on commit a0bf30d

Please sign in to comment.