Lines 100.00% 32 / 32
Methods 100.00% 6 / 6
Classes 100.00% 1 / 1
Covered by tests of size
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
33class 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}