Issues (186)

ScratchTokenCredentialProvider.php (1 issue)

Severity
1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 * ACC Development Team. Please see team.json for a list of contributors.     *
5
 *                                                                            *
6
 * This is free and unencumbered software released into the public domain.    *
7
 * Please see LICENSE.md for the full licencing statement.                    *
8
 ******************************************************************************/
9
10
namespace Waca\Security\CredentialProviders;
11
12
use ParagonIE\ConstantTime\Base32;
13
use Waca\DataObjects\User;
14
use Waca\Exceptions\ApplicationLogicException;
15
use Waca\Exceptions\OptimisticLockFailedException;
16
use Waca\PdoDatabase;
17
use Waca\Security\EncryptionHelper;
18
use Waca\SessionAlert;
19
use Waca\SiteConfiguration;
20
use Waca\WebRequest;
21
22
class ScratchTokenCredentialProvider extends CredentialProviderBase
23
{
24
    /** @var EncryptionHelper */
25
    private $encryptionHelper;
26
    /** @var array the tokens generated in the last generation round. */
27
    private $generatedTokens;
28
29
    /**
30
     * ScratchTokenCredentialProvider constructor.
31
     *
32
     * @param PdoDatabase       $database
33
     * @param SiteConfiguration $configuration
34
     */
35
    public function __construct(PdoDatabase $database, SiteConfiguration $configuration)
36
    {
37
        parent::__construct($database, $configuration, 'scratch');
38
        $this->encryptionHelper = new EncryptionHelper($configuration);
39
    }
40
41
    /**
42
     * Validates a user-provided credential
43
     *
44
     * @param User   $user The user to test the authentication against
45
     * @param string $data The raw credential data to be validated
46
     *
47
     * @return bool
48
     * @throws ApplicationLogicException|OptimisticLockFailedException
49
     */
50
    public function authenticate(User $user, $data)
51
    {
52
        if (is_array($data)) {
0 ignored issues
show
The condition is_array($data) is always false.
Loading history...
53
            return false;
54
        }
55
56
        $storedData = $this->getCredentialData($user->getId());
57
58
        if ($storedData === null) {
59
            throw new ApplicationLogicException('Credential data not found');
60
        }
61
62
        $scratchTokens = unserialize($this->encryptionHelper->decryptData($storedData->getData()));
63
64
        $usedToken = null;
65
        foreach ($scratchTokens as $scratchToken) {
66
            if (password_verify($data, $scratchToken)) {
67
                $usedToken = $scratchToken;
68
                SessionAlert::quick("Hey, it looks like you used a scratch token to log in. Would you like to change your multi-factor authentication configuration?", 'alert-warning');
69
                WebRequest::setPostLoginRedirect($this->getConfiguration()->getBaseUrl() . "/internal.php/multiFactor");
70
                break;
71
            }
72
        }
73
74
        if ($usedToken === null) {
75
            return false;
76
        }
77
78
        $scratchTokens = array_diff($scratchTokens, [$usedToken]);
79
80
        $storedData->setData($this->encryptionHelper->encryptData(serialize($scratchTokens)));
81
        $storedData->save();
82
83
        return true;
84
    }
85
86
    /**
87
     * @param User   $user   The user the credential belongs to
88
     * @param int    $factor The factor this credential provides
89
     * @param string $data   Unused.
90
     *
91
     * @throws OptimisticLockFailedException
92
     */
93
    public function setCredential(User $user, $factor, $data)
94
    {
95
        $plaintextScratch = array();
96
        $storedScratch = array();
97
        for ($i = 0; $i < 5; $i++) {
98
            $token = Base32::encodeUpper(openssl_random_pseudo_bytes(10));
99
            $plaintextScratch[] = $token;
100
101
            $storedScratch[] = password_hash(
102
                $token,
103
                PasswordCredentialProvider::PASSWORD_ALGO,
104
                array('cost' => PasswordCredentialProvider::PASSWORD_COST)
105
            );
106
        }
107
108
        $storedData = $this->getCredentialData($user->getId(), null);
109
110
        if ($storedData !== null) {
111
            $storedData->delete();
112
        }
113
114
        $storedData = $this->createNewCredential($user);
115
116
        $storedData->setData($this->encryptionHelper->encryptData(serialize($storedScratch)));
117
        $storedData->setFactor($factor);
118
        $storedData->setVersion(1);
119
        $storedData->setPriority(9);
120
121
        $storedData->save();
122
        $this->generatedTokens = $plaintextScratch;
123
    }
124
125
    /**
126
     * Gets the count of remaining valid tokens
127
     *
128
     * @param int $userId
129
     *
130
     * @return int
131
     */
132
    public function getRemaining($userId)
133
    {
134
        $storedData = $this->getCredentialData($userId);
135
136
        if ($storedData === null) {
137
            return 0;
138
        }
139
140
        $scratchTokens = unserialize($this->encryptionHelper->decryptData($storedData->getData()));
141
142
        return count($scratchTokens);
143
    }
144
145
    /**
146
     * @return array
147
     */
148
    public function getTokens()
149
    {
150
        if ($this->generatedTokens != null) {
151
            return $this->generatedTokens;
152
        }
153
154
        return array();
155
    }
156
}
157