Passed
Push — master ( 25a926...39145a )
by
unknown
20:30
created

RecoveryCodes::generatedHashedRecoveryCodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 10
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
declare(strict_types=1);
17
18
namespace TYPO3\CMS\Core\Authentication\Mfa\Provider;
19
20
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
21
use TYPO3\CMS\Core\Crypto\Random;
22
use TYPO3\CMS\Core\Utility\GeneralUtility;
23
24
/**
25
 * Implementation for generation and validation of recovery codes
26
 *
27
 * @internal should only be used by the TYPO3 Core
28
 */
29
class RecoveryCodes
30
{
31
    private const MIN_LENGTH = 8;
32
33
    protected string $mode;
34
    protected PasswordHashFactory $passwordHashFactory;
35
36
    public function __construct(string $mode)
37
    {
38
        $this->mode = $mode;
39
        $this->passwordHashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
40
    }
41
42
    /**
43
     * Generate plain and hashed recovery codes and return them as key/value
44
     *
45
     * @return array
46
     */
47
    public function generateRecoveryCodes(): array
48
    {
49
        $plainCodes = $this->generatePlainRecoveryCodes();
50
        return array_combine($plainCodes, $this->generatedHashedRecoveryCodes($plainCodes));
51
    }
52
53
    /**
54
     * Generate given amount of plain recovery codes with the given length
55
     *
56
     * @param int $length
57
     * @param int $quantity
58
     * @return string[]
59
     */
60
    public function generatePlainRecoveryCodes(int $length = 8, int $quantity = 8): array
61
    {
62
        if ($length < self::MIN_LENGTH) {
63
            throw new \InvalidArgumentException(
64
                $length . ' is not allowed as length for recovery codes. Must be at least ' . self::MIN_LENGTH,
65
                1613666803
66
            );
67
        }
68
69
        $codes = [];
70
        $random = GeneralUtility::makeInstance(Random::class);
71
72
        while ($quantity >= 1 && count($codes) < $quantity) {
73
            $number = (int)hexdec($random->generateRandomHexString(16));
74
            // We only want to work with positive integers
75
            if ($number < 0) {
76
                continue;
77
            }
78
            // Create a $length long string from the random number
79
            $code = str_pad((string)($number % (10 ** $length)), $length, '0', STR_PAD_LEFT);
80
            // Prevent duplicate codes which is however very unlikely to happen
81
            if (!in_array($code, $codes, true)) {
82
                $codes[] = $code;
83
            }
84
        }
85
86
        return $codes;
87
    }
88
89
    /**
90
     * Hash the given plain recovery codes with the default hash instance and return them
91
     *
92
     * @param array $codes
93
     * @return array
94
     */
95
    public function generatedHashedRecoveryCodes(array $codes): array
96
    {
97
        // Use the current default hash instance for hashing the recovery codes
98
        $hashInstance = $this->passwordHashFactory->getDefaultHashInstance($this->mode);
99
100
        foreach ($codes as &$code) {
101
            $code = $hashInstance->getHashedPassword($code);
102
        }
103
        unset($code);
104
        return $codes;
105
    }
106
107
    /**
108
     * Compare given recovery code against all hashed codes and
109
     * unset the corresponding code on success.
110
     *
111
     * @param string $recoveryCode
112
     * @param array $codes
113
     * @return bool
114
     */
115
    public function verifyRecoveryCode(string $recoveryCode, array &$codes): bool
116
    {
117
        if ($codes === []) {
118
            return false;
119
        }
120
121
        // Get the hash instance which was initially used to generate these codes.
122
        // This could differ from the current default has instance. We however only need
123
        // to check the first code since recovery codes can not be generated individually.
124
        $hasInstance = $this->passwordHashFactory->get(reset($codes), $this->mode);
125
126
        foreach ($codes as $key => $code) {
127
            // Compare hashed codes
128
            if ($hasInstance->checkPassword($recoveryCode, $code)) {
129
                // Unset the matching code
130
                unset($codes[$key]);
131
                return true;
132
            }
133
        }
134
        return false;
135
    }
136
}
137