Lines
100.00%
32 / 32
Methods
100.00%
6 / 6
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 1 | ||
| generateSecret | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| saveSecret | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| getSecret | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 2 | ||
| validateToken | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 5 | ||
| getQrCode | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 1 | ||
| 33 | class TwoFactor | |
| 34 | { | |
| 35 | private TwoFactorAuth $twoFactorAuth; | |
| 36 | ||
| 37 | private EndroidQrCodeProvider $endroidQrCodeProvider; | |
| 38 | ||
| 39 | /** | |
| 40 | * Number of adjacent time slices accepted besides the current one. | |
| 41 | * | |
| 42 | * RobThree defaults to 1, which keeps a code usable for roughly 90 seconds and | |
| 43 | * leaves a captured code replayable for that whole span. Accepting only the | |
| 44 | * current slice shortens that to at most 30 seconds. The trade-off is that | |
| 45 | * authenticator apps whose clock has drifted by more than one period are no | |
| 46 | * longer tolerated. | |
| 47 | */ | |
| 48 | private const int VERIFY_DISCREPANCY = 0; | |
| 49 | ||
| 50 | /** | |
| 51 | * @throws TwoFactorAuthException | |
| 52 | */ | |
| 53 | public function __construct( | |
| 54 | private readonly Configuration $configuration, | |
| 55 | private readonly CurrentUser $currentUser, | |
| 56 | ) { | |
| 57 | $this->endroidQrCodeProvider = new EndroidQrCodeProvider(); | |
| 58 | $this->twoFactorAuth = new TwoFactorAuth( | |
| 59 | $this->endroidQrCodeProvider, | |
| 60 | (string) $this->configuration->get(item: 'main.titleFAQ'), | |
| 61 | 6, | |
| 62 | 30, | |
| 63 | Algorithm::Sha1, | |
| 64 | ); | |
| 65 | } | |
| 66 | ||
| 67 | /** | |
| 68 | * Generates and returns a new secret without saving | |
| 69 | */ | |
| 70 | public function generateSecret(): string | |
| 71 | { | |
| 72 | return $this->twoFactorAuth->createSecret(); | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Saves a given secret to the current user from the session. | |
| 77 | */ | |
| 78 | public function saveSecret(#[\SensitiveParameter] string $secret): bool | |
| 79 | { | |
| 80 | if ($secret === '') { | |
| 81 | return false; | |
| 82 | } | |
| 83 | ||
| 84 | return $this->currentUser->setUserData(['secret' => $secret]); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Returns the secret of the current user | |
| 89 | */ | |
| 90 | public function getSecret(CurrentUser $currentUser): ?string | |
| 91 | { | |
| 92 | $secret = $currentUser->getUserData('secret'); | |
| 93 | ||
| 94 | return is_string($secret) ? $secret : null; | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Validates a given token. Returns true if the token is correct. | |
| 99 | */ | |
| 100 | public function validateToken(#[\SensitiveParameter] string $token, int $userId): bool | |
| 101 | { | |
| 102 | if (strlen($token) !== 6 || $userId <= 0) { | |
| 103 | return false; | |
| 104 | } | |
| 105 | ||
| 106 | $this->currentUser->getUserById($userId); | |
| 107 | ||
| 108 | $secret = $this->currentUser->getUserData('secret'); | |
| 109 | if (!is_string($secret) || $secret === '') { | |
| 110 | return false; | |
| 111 | } | |
| 112 | ||
| 113 | return $this->twoFactorAuth->verifyCode($secret, $token, self::VERIFY_DISCREPANCY); | |
| 114 | } | |
| 115 | ||
| 116 | /** | |
| 117 | * Returns a QR-Code to a given secret for transmitting the secret to the Authenticator-App | |
| 118 | */ | |
| 119 | public function getQrCode(#[\SensitiveParameter] string $secret): string | |
| 120 | { | |
| 121 | $label = $this->configuration->getTitle() . ':' . (string) $this->currentUser->getUserData('email'); | |
| 122 | $qrCodeText = sprintf( | |
| 123 | '%s&image=%sassets/templates/images/logo.png', | |
| 124 | $this->twoFactorAuth->getQrText($label, $secret), | |
| 125 | $this->configuration->getDefaultUrl(), | |
| 126 | ); | |
| 127 | ||
| 128 | return sprintf( | |
| 129 | 'data:%s;base64,%s', | |
| 130 | $this->endroidQrCodeProvider->getMimeType(), | |
| 131 | base64_encode($this->endroidQrCodeProvider->getQRCodeImage($qrCodeText, 200)), | |
| 132 | ); | |
| 133 | } | |
| 134 | } |