Passed
Push — b2.0.0 ( 59ba4e...bb1327 )
by Sebastian
02:46
created

EncryptionTokenProvider::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 10
cc 1
nc 1
nop 3
crap 1
1
<?php
2
3
/**
4
 * Linna Cross-site Request Forgery Guard
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2020, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\CsrfGuard\Provider;
13
14
use Linna\CsrfGuard\Exception\BadExpireException;
15
use Linna\CsrfGuard\Exception\BadStorageSizeException;
16
use Linna\CsrfGuard\Exception\BadExpireTrait;
17
use Linna\CsrfGuard\Exception\BadStorageSizeTrait;
18
19
/**
20
 * Csrf Encryption Based Token Pattern Provider.
21
 *
22
 * It use sodium_crypto_aead_xchacha20poly1305_ietf_encrypt fuction to encrypt
23
 * the token. Key change every session, nonce change for every token.
24
 */
25
class EncryptionTokenProvider implements TokenProviderInterface
26
{
27
    use BadExpireTrait, BadStorageSizeTrait;
28
29
    /**
30
     * @var string CSRF_ENCRYPTION_KEY Encryption key name in session array
31
     */
32
    private const CSRF_ENCRYPTION_KEY = 'csrf_encryption_key';
33
34
    /**
35
     * @var string CSRF_ENCRYPTION_NONCE Encryption nonce name in session array
36
     */
37
    private const CSRF_ENCRYPTION_NONCE = 'csrf_encryption_nonce';
38
39
    /**
40
     * @var string $sessionId Session id of the current session
41
     */
42
    private string $sessionId = '';
43
44
    /**
45
     * @var int $sessionIdLen Session id lenght
46
     */
47
    private int $sessionIdLen = 0;
48
49
    /**
50
     * @var int $expire Token validity in seconds, default 600 -> 10 minutes
51
     */
52
    private int $expire = 0;
53
54
    /**
55
     * @var int $storageSize Maximum token nonces stored in session
56
     */
57
    private int $storageSize = 0;
58
59
    /**
60
     * Class constructor.
61
     *
62
     * @param string $sessionId   Session id of the current session
63
     * @param int    $expire      Token validity in seconds, default 600 -> 10 minutes
64
     * @param int    $storageSize Maximum token nonces stored in session
65
     *
66
     * @throws BadExpireException      If $expire is less than 0 and greater than 86400
67
     * @throws BadStorageSizeException If $storageSize is less than 2 and greater than 64
68
     */
69 5
    public function __construct(string $sessionId, int $expire = 600, int $storageSize = 10)
70
    {
71
        // from BadExpireTrait, BadStorageSizeTrait
72
        /** @throws BadExpireException */
73 5
        $this->checkBadExpire($expire);
74
        /** @throws BadStorageSizeException */
75 3
        $this->checkBadStorageSize($storageSize);
76
77 1
        $this->sessionId = $sessionId;
78 1
        $this->sessionIdLen = \strlen($sessionId);
79 1
        $this->expire = $expire;
80 1
        $this->storageSize = $storageSize;
81
82
        //if no nonce stored, initialize the session storage
83 1
        $_SESSION[static::CSRF_ENCRYPTION_NONCE] ??= [];
84 1
    }
85
86
    /**
87
     * Return new Encryption based Token.
88
     *
89
     * @return string A hex token
90
     */
91
    public function getToken(): string
92
    {
93
        //get the key for encryption
94
        $key = $this->getKey(); //random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
95
        //get a new nonce for encryption
96
        $nonce = $this->getNonce(); //random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
97
98
        //get current time
99
        $time = \base_convert((string) \time(), 10, 16);
100
        //build message
101
        $message = $this->sessionId.$time;
102
103
        //create ciphertext
104
        //https://www.php.net/manual/en/function.sodium-crypto-aead-xchacha20poly1305-ietf-encrypt.php
105
        //https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction
106
        $ciphertext = \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($message, '', $nonce, $key);
0 ignored issues
show
Bug introduced by
The function sodium_crypto_aead_xchacha20poly1305_ietf_encrypt was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

106
        $ciphertext = /** @scrutinizer ignore-call */ \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($message, '', $nonce, $key);
Loading history...
107
108
        return \sodium_bin2hex($ciphertext);
109
    }
110
111
    /**
112
     * Validate Encryption based Token.
113
     *
114
     * @param string $token Token must be validated, hex format
115
     *
116
     * @return bool
117
     */
118
    public function validate(string $token): bool
119
    {
120
        //convert hex token to raw bytes
121
        $hex_token = \sodium_hex2bin($token);
122
123
        // plain text returned from check encryption
124
        $plainText = '';
125
126
        //plainText variable is passed as reference,
127
        //if checkEncrption method end without errors then checkTime method
128
        //receive a filled plainText variable as argument else
129
        //short circuiting make the if block skipped
130
        if ($this->checkEncryption($hex_token, $plainText) && $this->checkTime($plainText)) {
131
            return true;
132
        }
133
134
        return false;
135
    }
136
137
    /**
138
     * Check if token is expired.
139
     *
140
     * @param string $token Token after decryption
141
     *
142
     * @return bool
143
     */
144
    private function checkTime(string $token): bool
145
    {
146
        $time = \substr($token, $this->sessionIdLen);
147
148
        //timestamp from token time
149
        $timestamp = (int) \base_convert($time, 16, 10);
150
151
        //token expiration check
152
        if ($timestamp + $this->expire < \time()) {
153
            return false;
154
        }
155
156
        return true;
157
    }
158
159
    /**
160
     * Try to decrypt a token.
161
     *
162
     * @param string $encryptedToken Encrypted token
163
     * @param string $plainText      Variable passed as reference to store the result
164
     *
165
     * @return bool
166
     */
167
    private function checkEncryption(string $encryptedToken, string &$plainText): bool
168
    {
169
        //get the key for encryption
170
        $key = $this->getKey();
171
        //reference to nonce storage in session
172
        $nonces = &$_SESSION[static::CSRF_ENCRYPTION_NONCE];
173
        //get current size of nonce storage in session
174
        $size = \count($nonces);
175
176
        //try to decrypt starting from last stored nonce
177
        for ($i = $size -1; $i > -1; $i--) {
178
            //for successful decryption return true
179
            if (($tmpPlainText = \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encryptedToken, '', $nonces[$i], $key))) {
0 ignored issues
show
Bug introduced by
The function sodium_crypto_aead_xchacha20poly1305_ietf_decrypt was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

179
            if (($tmpPlainText = /** @scrutinizer ignore-call */ \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encryptedToken, '', $nonces[$i], $key))) {
Loading history...
180
                //plainText will remain string if sodium_crypto return false
181
                //todo, check in php source code if sodium_crypto
182
                //return false if fail
183
                $plainText = $tmpPlainText;
184
                return true;
185
            }
186
        }
187
188
        return false;
189
    }
190
191
    /**
192
     * Return the encryption key for the currente session.
193
     * Generate a different key for every session.
194
     *
195
     * @return string
196
     */
197
    private function getKey(): string
198
    {
199
        //if key is already stored, return it
200
        if (isset($_SESSION[static::CSRF_ENCRYPTION_KEY])) {
201
            return $_SESSION[static::CSRF_ENCRYPTION_KEY];
202
        }
203
204
        //generate new key
205
        $key = \random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
0 ignored issues
show
Bug introduced by
The constant Linna\CsrfGuard\Provider...0POLY1305_IETF_KEYBYTES was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
206
207
        //store new key
208
        $_SESSION[static::CSRF_ENCRYPTION_KEY] = $key;
209
210
        return $key;
211
    }
212
213
    /**
214
     * Generate a new nonce for encrypt the token.
215
     *
216
     * @return string
217
     */
218
    private function getNonce(): string
219
    {
220
        //generate new nonce
221
        $nonce = \random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
0 ignored issues
show
Bug introduced by
The constant Linna\CsrfGuard\Provider...POLY1305_IETF_NPUBBYTES was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
222
223
        //store new nonce
224
        $_SESSION[static::CSRF_ENCRYPTION_NONCE][] = $nonce;
225
226
        //check if the storage is growt beyond the maximun size
227
        if (\count($_SESSION[static::CSRF_ENCRYPTION_NONCE]) > $this->storageSize) {
228
            //remove the oldest stores nonce
229
            \array_shift($_SESSION[static::CSRF_ENCRYPTION_NONCE]);
230
        }
231
232
        return $nonce;
233
    }
234
}
235