Add encoding to Totp Secret and Backup Codes

- Remove double call for backup codes
This commit is contained in:
Jack Anderson 2025-01-10 09:48:28 +00:00 committed by j.anderson
parent f829346938
commit bab7d9a202
7 changed files with 195 additions and 25 deletions

View file

@ -97,7 +97,6 @@ export class TwoFactorComponent implements OnInit {
next: (response) => {
this.qrCodeUrl = response?.url;
this.qrCodeSvg = response?.svg;
this.backupCodes = response?.backupCodes;
this.areRecoveryCodesGenerated.set(true);
this.isQrCodeGenerated.set(true);
},

View file

@ -31,7 +31,6 @@ namespace App\Authentication\Controller;
use App\Authentication\LegacyHandler\UserHandler;
use App\Data\LegacyHandler\PreparedStatementHandler;
use App\Engine\LegacyHandler\CacheManagerHandler;
use App\Security\TwoFactor\BackupCodeGenerator;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManagerInterface;
use Endroid\QrCode\Builder\Builder;
@ -77,7 +76,6 @@ class SecurityController extends AbstractController
private $entityManager;
private PreparedStatementHandler $preparedStatementHandler;
private BackupCodeGenerator $backupCodeGenerator;
private UserHandler $userHandler;
@ -92,7 +90,6 @@ class SecurityController extends AbstractController
RequestStack $requestStack,
EntityManagerInterface $entityManager,
PreparedStatementHandler $preparedStatementHandler,
BackupCodeGenerator $backupCodeGenerator,
UserHandler $userHandler,
CacheManagerHandler $cacheManagerHandler
)
@ -101,7 +98,6 @@ class SecurityController extends AbstractController
$this->requestStack = $requestStack;
$this->entityManager = $entityManager;
$this->preparedStatementHandler = $preparedStatementHandler;
$this->backupCodeGenerator = $backupCodeGenerator;
$this->userHandler = $userHandler;
$this->cacheManagerHandler = $cacheManagerHandler;
}
@ -155,9 +151,7 @@ class SecurityController extends AbstractController
$user->setTotpSecret($secret);
$backupCodes = $this->backupCodeGenerator->generate();
$this->setupBackupCodes($user,$backupCodes);
$qrCodeUrl = $totpAuthenticator->getQRContent($user);
$this->preparedStatementHandler->update(
'UPDATE users SET totp_secret = :totp_secret WHERE id = :id',
@ -167,12 +161,9 @@ class SecurityController extends AbstractController
$this->entityManager->flush();
$qrCodeUrl = $totpAuthenticator->getQRContent($user);
$response = [
'url' => $qrCodeUrl,
'svg' => $this->displayQRCode($qrCodeUrl),
'backupCodes' => $backupCodes
];
return new Response(json_encode($response), Response::HTTP_OK);
@ -184,6 +175,8 @@ class SecurityController extends AbstractController
$id = $user->getId();
$this->userHandler->setUserPreference('is_two_factor_enabled', false);
$user->setTotpSecret(null);
$user->setBackupCodes(null);
$this->preparedStatementHandler->update(
'UPDATE users SET totp_secret = NULL, is_totp_enabled = 0, backup_codes = NULL WHERE id = :id',
@ -371,12 +364,4 @@ class SecurityController extends AbstractController
return $result->getString();
}
protected function setupBackupCodes($user, $backupCodes): void
{
$this->preparedStatementHandler->update("UPDATE users SET backup_codes = :backup_codes WHERE id = :id",
['id' => $user->getId(), 'backup_codes' => $backupCodes],
[['param' => 'id', 'type' => 'string'], ['param' => 'backup_codes', 'type' => 'json']]
);
}
}

View file

@ -109,8 +109,9 @@ class GenerateBackupCodesHandler extends LegacyHandler implements ProcessHandler
*/
public function run(Process $process): void
{
$user = $this->security->getToken()->getUser();
$user = $this->security->getToken()?->getUser();
$backupCodes = $this->backupCodeGenerator->generate();
$user?->setBackupCodes($backupCodes);
$this->setupBackupCodes($user, $backupCodes);

View file

@ -0,0 +1,85 @@
<?php
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2025 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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/>.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
namespace App\Security\TwoFactor\EventListener;
use App\Security\TwoFactor\LegacyHandler\BlowfishCodeHandler;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class TotpEncryptionListener
{
protected BlowfishCodeHandler $blowfish;
public function __construct(
BlowfishCodeHandler $blowfish
)
{
$this->blowfish = $blowfish;
}
public function prePersist(LifecycleEventArgs $args): void
{
$this->encodeTotpFields($args);
}
public function preUpdate(LifecycleEventArgs $args): void
{
$this->encodeTotpFields($args);
}
public function postLoad(LifecycleEventArgs $args): void
{
$user = $args->getObject();
$backupCodes = $user->getBackupCodes();
$totpSecret = $user->getTotpSecret();
if (!empty($backupCodes)) {
$user->setBackupCodes($this->blowfish->decode('Users', $backupCodes));
}
if (!empty($totpSecret)) {
$user->setTotpSecret($this->blowfish->decode('Users', $totpSecret));
}
}
/**
* @param LifecycleEventArgs $args
* @return void
*/
public function encodeTotpFields(LifecycleEventArgs $args): void
{
$user = $args->getObject();
$backupCodes = $user->getBackupCodes();
$totpSecret = $user->getTotpSecret();
if (!empty($backupCodes)) {
$user->setBackupCodes($this->blowfish->encode('Users', $backupCodes));
}
if (!empty($totpSecret)) {
$user->setTotpSecret($this->blowfish->encode('Users', $totpSecret));
}
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2025 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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/>.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
namespace App\Security\TwoFactor\LegacyHandler;
use App\Engine\LegacyHandler\LegacyHandler;
class BlowfishCodeHandler extends LegacyHandler {
public const HANDLER_KEY = 'blowfish-code';
public function getHandlerKey(): string
{
return self::HANDLER_KEY;
}
public function getKey(string $key): string {
return blowfishGetKey($key);
}
public function encode($key, $value): array|string
{
$this->init();
$key = $this->getKey($key);
if (!is_array($value)) {
$value = blowfishEncode($key, $value);
$this->close();
return $value;
}
$result = [];
foreach ($value as $i => $item) {
$result[$i] = blowfishEncode($key, $item);
}
$this->close();
return $result;
}
public function decode($key, $value) {
$this->init();
$key = $this->getKey($key);
if (!is_array($value)) {
$value = blowfishDecode($key, $value);
$this->close();
return $value;
}
$result = [];
foreach ($value as $i => $item) {
$result[$i] = blowfishDecode($key, $item);
}
$this->close();
return $result;
}
}

View file

@ -1170,7 +1170,7 @@ class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUs
public function getBackupCodes(): array
{
return $this->backupCodes;
return $this->backupCodes ?? [];
}
public function setBackupCodes(?array $backupCodes): self
@ -1272,7 +1272,6 @@ class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUs
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
// You could persist the other configuration options in the user entity to make it individual per user.
return new TotpConfiguration($this->getTotpSecret(), TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
@ -1306,10 +1305,13 @@ class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUs
public function isBackupCode(string $code): bool
{
$correctCode = false;
if (in_array($code, $this->getBackupCodes())){
$backupCodes = $this->getBackupCodes();
if (in_array($code, $backupCodes, true)){
$correctCode = true;
$this->invalidateBackupCode($code);
}
return $correctCode;
}
@ -1318,10 +1320,11 @@ class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUs
*/
public function invalidateBackupCode(string $code): void
{
$key = array_search($code, $this->backupCodes);
$key = array_search($code, $this->getBackupCodes(), true);
if ($key !== false){
unset($this->backupCodes[$key]);
unset($this->getBackupCodes()[$key]);
}
$this->backupCodes = array_values($this->backupCodes);
}