Ozzy47
Well-known member
I'm working on an addon that will allow a user to login using a secret phrase. They save the phrase from their user details page like this:
Then if they forgot their password and no longer have access to their email, they can go to the recovery page, enter their secret phrase, and if correct it logs them in. We do this like so:
This just does not seem secure to me.
PHP:
protected function accountDetailsSaveProcess(\XF\Entity\User $visitor)
{
$form = parent::accountDetailsSaveProcess($visitor);
$secretPhrase = $this->filter('secret_phrase', 'str');
\XF::logError('Secret phrase: ' . $secretPhrase);
if ($secretPhrase !== '')
{
// Generate a unique salt for the user (32 bytes = 64 characters)
$salt = bin2hex(random_bytes(32));
// Generate a random pepper (32 characters)
$pepper = bin2hex(random_bytes(16)); // Generates 32 hexadecimal characters
// Combine the secret phrase, salt, and pepper
$combined = $secretPhrase . $salt . $pepper;
// Hash the combined value using Argon2 (stronger hashing)
$hashedSecretPhrase = password_hash($combined, PASSWORD_ARGON2ID);
// Store the hashed secret phrase, the salt, and the pepper
$visitor->ozzmodz_secret_phrase_hash = $hashedSecretPhrase;
$visitor->ozzmodz_secret_phrase_salt = $salt;
$visitor->ozzmodz_secret_phrase_pepper = $pepper;
// Save the changes
$form->basicEntitySave($visitor, []);
\XF::logError('Saving hashed phrase: ' . $visitor->ozzmodz_secret_phrase_hash);
}
return $form;
}
Then if they forgot their password and no longer have access to their email, they can go to the recovery page, enter their secret phrase, and if correct it logs them in. We do this like so:
PHP:
public function actionRecover()
{
$input = $this->filter([
'username' => 'str',
'secret_phrase' => 'str'
]);
/** @var \XF\Entity\User|null $user */
$user = $this->finder('XF:User')
->where('username', $input['username'])
->fetchOne();
if (!$user || !$user->ozzmodz_secret_phrase_hash || !$user->ozzmodz_secret_phrase_salt || !$user->ozzmodz_secret_phrase_pepper) {
// Log missing data if needed for debugging purposes (this shouldn't be exposed to the user)
\XF::logError("Missing data during secret phrase recovery for user: " . $input['username']);
return $this->error(\XF::phrase('secret_phrase_invalid'));
}
// Retrieve the stored salt and pepper (they should be used only internally)
$storedSalt = $user->ozzmodz_secret_phrase_salt;
$storedPepper = $user->ozzmodz_secret_phrase_pepper;
// Combine the user-entered secret phrase with the stored salt and pepper
$combined = $input['secret_phrase'] . $storedSalt . $storedPepper;
// Hash the combined value using Argon2
if (password_verify($combined, $user->ozzmodz_secret_phrase_hash)) {
// Success: Log the user in
\XF::asVisitor($user, function() {
// Switching the visitor context
});
$this->session()->changeUser($user);
// Log IP if needed for security reasons
$this->repository('XF:Ip')->logIp($user->user_id, $this->request->getIp(), 'user', $user->user_id, 'secret_phrase_login');
return $this->redirect($this->buildLink('index'), \XF::phrase('you_have_been_logged_in'));
} else {
// Log the failed attempt for debugging purposes
\XF::logError("Failed secret phrase verification for user: " . $input['username']);
return $this->error(\XF::phrase('secret_phrase_invalid'));
}
}
This just does not seem secure to me.