Passed
Pull Request — master (#91)
by Robbie
01:53
created

BackupCodeGenerator::hash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Service;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Extensible;
7
use SilverStripe\Core\Injector\Injectable;
8
9
class BackupCodeGenerator implements BackupCodeGeneratorInterface
10
{
11
    use Configurable;
12
    use Extensible;
13
    use Injectable;
14
15
    /**
16
     * The number of back-up codes that should be generated for a user. Note that changing this value will not
17
     * regenerate or generate new codes to meet the new number. The user will have to manually regenerate codes to
18
     * receive the new number of codes.
19
     *
20
     * @config
21
     * @var int
22
     */
23
    private static $backup_code_count = 15;
0 ignored issues
show
introduced by
The private property $backup_code_count is not used, and could be removed.
Loading history...
24
25
    /**
26
     * The length of each individual backup code.
27
     *
28
     * @config
29
     * @var int
30
     */
31
    private static $backup_code_length = 9;
0 ignored issues
show
introduced by
The private property $backup_code_length is not used, and could be removed.
Loading history...
32
33
    /**
34
     * Generates a list of backup codes
35
     *
36
     * {@inheritDoc}
37
     */
38
    public function generate(): array
39
    {
40
        $codeCount = (int) $this->config()->get('backup_code_count');
41
        $codeLength = (int) $this->config()->get('backup_code_length');
42
        $charset = $this->getCharacterSet();
43
44
        $codes = [];
45
        while (count($codes) < $codeCount) {
46
            $code = $this->generateCode($charset, $codeLength);
47
            if (!in_array($code, $codes)) {
48
                $codes[] = $code;
49
            }
50
        }
51
52
        // Create hashes for the codes
53
        $hashedCodes = array_map([$this, 'hash'], $codes);
54
55
        return array_combine($codes, $hashedCodes);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($codes, $hashedCodes) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
56
    }
57
58
    /**
59
     * Hash a back-up code for storage. This uses the native PHP password_hash API by default, but can be extended to
60
     * implement a custom hash requirement callback.
61
     *
62
     * {@inheritDoc}
63
     */
64
    public function hash(string $code): string
65
    {
66
        $hash = (string) password_hash($code, PASSWORD_DEFAULT);
67
68
        $this->extend('updateHash', $code, $hash);
69
70
        return $hash;
71
    }
72
73
    public function getCharacterSet(): array
74
    {
75
        $characterSet = array_merge(
76
            range('a', 'z'),
77
            range('A', 'Z'),
78
            range(0, 9)
79
        );
80
81
        $this->extend('updateCharacterSet', $characterSet);
82
83
        return $characterSet;
84
    }
85
86
    /**
87
     * Generates a backup code at the specified string length, using a mixture of digits and mixed case letters
88
     *
89
     * @param array $charset
90
     * @param int $codeLength
91
     * @return string
92
     */
93
    protected function generateCode(array $charset, int $codeLength = 9): string
94
    {
95
        $characters = [];
96
        $numberOfOptions = count($charset);
97
        while (count($characters) < $codeLength) {
98
            $key = random_int(0, $numberOfOptions - 1); // zero based array
99
            $characters[] = $charset[$key];
100
        }
101
        return implode($characters);
102
    }
103
}
104