Passed
Push — b2.0.0 ( 2f7a91 )
by Sebastian
08:01
created

EncryptionTokenProvider   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Test Coverage

Coverage 17.65%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 18
eloc 50
c 1
b 0
f 0
dl 0
loc 203
ccs 9
cts 51
cp 0.1765
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getKey() 0 14 2
A __construct() 0 18 5
A validate() 0 13 3
A getNonce() 0 15 2
A getToken() 0 18 1
A checkTime() 0 13 2
A checkEncryption() 0 20 3
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
17
/**
18
 * Csrf Encryption Based Token Pattern Provider.
19
 *
20
 * It use sodium_crypto_aead_xchacha20poly1305_ietf_encrypt fuction to encrypt
21
 * the token. Key change every session, nonce change for every token.
22
 */
23
class EncryptionTokenProvider implements TokenProviderInterface
24
{
25
    /**
26
     * @var string CSRF_ENCRYPTION_KEY Encryption key name in session array
27
     */
28
    private const CSRF_ENCRYPTION_KEY = 'csrf_encryption_key';
29
30
    /**
31
     * @var string CSRF_ENCRYPTION_NONCE Encryption nonce name in session array
32
     */
33
    private const CSRF_ENCRYPTION_NONCE = 'csrf_encryption_nonce';
34
35
    /**
36
     * @var string $sessionId Session id of the current session
37
     */
38
    private string $sessionId = '';
39
40
    /**
41
     * @var int $sessionIdLen Session id lenght
42
     */
43
    private int $sessionIdLen = 0;
44
45
    /**
46
     * @var $expire Token validity in seconds, default 600 -> 10 minutes
0 ignored issues
show
Documentation Bug introduced by
The doc comment $expire at position 0 could not be parsed: Unknown type name '$expire' at position 0 in $expire.
Loading history...
47
     */
48
    private int $expire = 0;
49
50
    /**
51
     * @var $storageSize Maximum token nonces stored in session
0 ignored issues
show
Documentation Bug introduced by
The doc comment $storageSize at position 0 could not be parsed: Unknown type name '$storageSize' at position 0 in $storageSize.
Loading history...
52
     */
53
    private int $storageSize = 0;
54
55
    /**
56
     * Class constructor.
57
     *
58
     * @param string $sessionId   Session id of the current session
59
     * @param int    $expire      Token validity in seconds, default 600 -> 10 minutes
60
     * @param int    $storageSize Maximum token nonces stored in session
61
     *
62
     * @throws BadExpireException      If $expire is less than 0 and greater than 86400
63
     * @throws BadStorageSizeException If $storageSize is less than 2 and greater than 64
64
     */
65 1
    public function __construct(string $sessionId, int $expire = 600, int $storageSize = 10)
66
    {
67
        // expire maximum tim is one day
68 1
        if ($expire < 0 || $expire > 86400) {
69
            throw new BadExpireException('Expire time must be between 0 and PHP_INT_MAX');
70
        }
71
72 1
        if ($storageSize < 2 || $storageSize > 64) {
73
            throw new BadStorageSizeException('Storage size must be between 2 and 64');
74
        }
75
76 1
        $this->sessionId = $sessionId;
77 1
        $this->sessionIdLen = \strlen($sessionId);
78 1
        $this->expire = $expire;
79 1
        $this->storageSize = $storageSize;
80
81
        //if no nonce stored, initialize the session storage
82 1
        $_SESSION[static::CSRF_ENCRYPTION_NONCE] ??= [];
83 1
    }
84
85
    /**
86
     * Return new Encryption based Token.
87
     *
88
     * @return string A hex token
89
     */
90
    public function getToken(): string
91
    {
92
        //get the key for encryption
93
        $key = $this->getKey(); //random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES);
94
        //get a new nonce for encryption
95
        $nonce = $this->getNonce(); //random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
96
97
        //get current time
98
        $time = \base_convert((string) \time(), 10, 16);
99
        //build message
100
        $message = $this->sessionId.$time;
101
102
        //create ciphertext
103
        //https://www.php.net/manual/en/function.sodium-crypto-aead-xchacha20poly1305-ietf-encrypt.php
104
        //https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction
105
        $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

105
        $ciphertext = /** @scrutinizer ignore-call */ \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($message, '', $nonce, $key);
Loading history...
106
107
        return \sodium_bin2hex($ciphertext);
108
    }
109
110
    /**
111
     * Validate Encryption based Token.
112
     *
113
     * @param string $token Token must be validated, hex format
114
     *
115
     * @return bool
116
     */
117
    public function validate(string $token): bool
118
    {
119
        //convert hex token to raw bytes
120
        $hex_token = \sodium_hex2bin($token);
121
122
        // plain text returned from check encryption
123
        $plainText = '';
124
125
        if ($this->checkEncryption($hex_token, $plainText) && $this->checkTime($plainText)) {
126
            return true;
127
        }
128
129
        return false;
130
    }
131
132
    /**
133
     * Check if token is expired.
134
     *
135
     * @param string $token Token after decryption
136
     *
137
     * @return bool
138
     */
139
    private function checkTime(string $token): bool
140
    {
141
        $time = \substr($token, $this->sessionIdLen);
142
143
        //timestamp from token time
144
        $timestamp = (int) \base_convert($time, 16, 10);
145
146
        //token expiration check
147
        if ($timestamp + $this->expire < \time()) {
148
            return false;
149
        }
150
151
        return true;
152
    }
153
154
    /**
155
     * Try to decrypt a token.
156
     *
157
     * @param string $encryptedToken Encrypted token
158
     * @param string $plainText      Variable passed as reference to store the result
159
     *
160
     * @return bool
161
     */
162
    private function checkEncryption(string $encryptedToken, string &$plainText): bool
163
    {
164
        //get the key for encryption
165
        $key = $this->getKey();
166
        //reference to nonce storage in session
167
        $nonces = &$_SESSION[static::CSRF_ENCRYPTION_NONCE];
168
        //get current size of nonce storage in session
169
        $size = \count($nonces);
170
171
        //try to decrypt starting from last stored nonce
172
        for ($i = $size -1; $i > -1; $i--) {
173
            //for successful decryption return true
174
            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

174
            if (($tmpPlainText = /** @scrutinizer ignore-call */ \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encryptedToken, '', $nonces[$i], $key))) {
Loading history...
175
                //plainText will remain string
176
                $plainText = $tmpPlainText;
177
                return true;
178
            }
179
        }
180
181
        return false;
182
    }
183
184
    /**
185
     * Return the encryption key for the currente session.
186
     * Generate a different key for every session.
187
     *
188
     * @return string
189
     */
190
    private function getKey(): string
191
    {
192
        //if key is already stored, return it
193
        if (isset($_SESSION[static::CSRF_ENCRYPTION_KEY])) {
194
            return $_SESSION[static::CSRF_ENCRYPTION_KEY];
195
        }
196
197
        //generate new key
198
        $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...
199
200
        //store new key
201
        $_SESSION[static::CSRF_ENCRYPTION_KEY] = $key;
202
203
        return $key;
204
    }
205
206
    /**
207
     * Generate a new nonce for encrypt the token.
208
     *
209
     * @return string
210
     */
211
    private function getNonce(): string
212
    {
213
        //generate new nonce
214
        $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...
215
216
        //store new nonce
217
        $_SESSION[static::CSRF_ENCRYPTION_NONCE][] = $nonce;
218
219
        //check if the storage is growt beyond the maximun size
220
        if (\count($_SESSION[static::CSRF_ENCRYPTION_NONCE]) > $this->storageSize) {
221
            //remove the oldest stores nonce
222
            \array_shift($_SESSION[static::CSRF_ENCRYPTION_NONCE]);
223
        }
224
225
        return $nonce;
226
    }
227
}
228