EncryptionTokenProvider::getNonce()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 15
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of the Linna Csrf Guard.
7
 *
8
 * @author Sebastian Rapetti <[email protected]>
9
 * @copyright (c) 2020, Sebastian Rapetti
10
 * @license http://opensource.org/licenses/MIT MIT License
11
 */
12
13
namespace Linna\CsrfGuard\Provider;
14
15
use Linna\CsrfGuard\Exception\BadExpireException;
16
use Linna\CsrfGuard\Exception\BadStorageSizeException;
17
use Linna\CsrfGuard\Exception\BadExpireTrait;
18
use Linna\CsrfGuard\Exception\BadStorageSizeTrait;
19
use Linna\CsrfGuard\Exception\SessionNotStartedException;
20
use Linna\CsrfGuard\Exception\SessionNotStartedTrait;
21
22
/**
23
 * Csrf Encryption Based Token Pattern Provider.
24
 *
25
 * <p>It use sodium_crypto_aead_xchacha20poly1305_ietf_encrypt fuction to encrypt
26
 * the token.</p>
27
 *
28
 * <p>This token works storing a different key for session and a different nonce for every token in session, store the
29
 * complete encrypted token isn't stored because the token is valid only if the server is able to decrypt it.</p>
30
 *
31
 * <p>An attacker should know the key and the nonce and the time to craft a valid token for the specific session.</p>
32
 *
33
 * <p>The space needed is token-length indipendent, 32 bytes for the key and 24 bytes for the nonce. Neet to consider
34
 * that the key is stored once in session, nonce is stored for every token.</p>
35
 *
36
 */
37
class EncryptionTokenProvider implements TokenProviderInterface
38
{
39
    use BadExpireTrait;
40
    use BadStorageSizeTrait;
41
    use SessionNotStartedTrait;
42
43
    /** @var string CSRF_ENCRYPTION_KEY Encryption key name in session array. */
44
    private const CSRF_ENCRYPTION_KEY = 'csrf_encryption_key';
45
46
    /** @var string CSRF_ENCRYPTION_NONCE Encryption nonce name in session array. */
47
    private const CSRF_ENCRYPTION_NONCE = 'csrf_encryption_nonce';
48
49
    /** @var int CSRF_MESSAGE_LEN Message lenght in bytes. */
50
    private const CSRF_MESSAGE_LEN = 32;
51
52
    /**
53
     * Class constructor.
54
     *
55
     * @param int $expire      Token validity in seconds, default 600 -> 10 minutes.
56
     * @param int $storageSize Maximum token nonces stored for a session.
57
     *
58
     * @throws BadExpireException         If <code>$expire</code> is less than 0 and greater than 86400.
59
     * @throws BadStorageSizeException    If <code>$storageSize</code> is less than 2 and greater than 64.
60
     * @throws SessionNotStartedException If sessions are disabled or no session is started.
61
     */
62
    public function __construct(
63
        /** @var int $expire Token validity in seconds, default 600 -> 10 minutes. */
64
        private int $expire = 600,
65
        /** @var int $storageSize Maximum token nonces stored in session. */
66
        private int $storageSize = 10
67
    ) {
68
        // from BadExpireTrait, BadStorageSizeTrait and SessionNotStartedTrait
69
        /** @throws BadExpireException */
70
        $this->checkBadExpire($expire);
71
        /** @throws BadStorageSizeException */
72
        $this->checkBadStorageSize($storageSize);
73
        /** @throws SessionNotStartedException */
74
        $this->checkSessionNotStarted();
75
76
        $this->expire = $expire;
77
        $this->storageSize = $storageSize;
78
79
        //if no nonce stored, initialize the session storage
80
        $_SESSION[self::CSRF_ENCRYPTION_NONCE] ??= [];
81
    }
82
83
    /**
84
     * Return new Encryption based Token.
85
     *
86
     * @return string The token in hex format.
87
     */
88
    public function getToken(): string
89
    {
90
        //get the key for encryption
91
        $key = $this->getKey();
92
        //get a new nonce for encryption as result of
93
        //random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
94
        $nonce = $this->getNonce();
95
        $additionlData = \sodium_bin2hex($nonce);
96
97
        //get current time
98
        $time = \dechex(\time());
99
100
        //build message
101
        $message = \sodium_bin2hex(\random_bytes(self::CSRF_MESSAGE_LEN)).$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, $additionlData, $nonce, $key);
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 True if the token is valid, false otherwise.
117
     */
118
    public function validate(string $token): bool
119
    {
120
        //convert hex token to raw bytes
121
        $hexToken = \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($hexToken, $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 True if token isn't expired, false otherwise.
143
     */
144
    private function checkTime(string $token): bool
145
    {
146
        $time = \substr($token, self::CSRF_MESSAGE_LEN * 2);
147
148
        //timestamp from token time
149
        $timestamp = \hexdec($time);
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 an encrypted token.
161
     *
162
     * @param string $encryptedToken Encrypted token.
163
     * @param string $plainText      Variable passed as reference to store the result.
164
     *
165
     * @return bool True in the encrypted token decrypt successfully, false otherwise.
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[self::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
            //get nonce
179
            $nonce = $nonces[$i];
180
            //generate addition data
181
            $additionlData = \sodium_bin2hex($nonce);
182
183
            //for successful decryption return true
184
            if (($tmpPlainText = \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encryptedToken, $additionlData, $nonce, $key))) {
185
                //plainText will remain string if sodium_crypto return false
186
                //todo, check in php source code if sodium_crypto
187
                //return false if fail
188
                $plainText = $tmpPlainText;
189
                //no need to check if the plaintext is the same because if the token is tampered decryption doesn't
190
                //work
191
                return true;
192
            }
193
        }
194
195
        return false;
196
    }
197
198
    /**
199
     * Return the encryption key for the currente session.
200
     *
201
     * <p>Generate a different key for every session.</p>
202
     *
203
     * @return string The encryption key.
204
     */
205
    private function getKey(): string
206
    {
207
        //if key is already stored, return it
208
        if (isset($_SESSION[self::CSRF_ENCRYPTION_KEY])) {
209
            return $_SESSION[self::CSRF_ENCRYPTION_KEY];
210
        }
211
212
        //generate new key
213
        $key = \sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
214
215
        //store new key
216
        $_SESSION[self::CSRF_ENCRYPTION_KEY] = $key;
217
218
        return $key;
219
    }
220
221
    /**
222
     * Generate a new nonce to encrypt the token.
223
     *
224
     * @return string The new nonce.
225
     */
226
    private function getNonce(): string
227
    {
228
        //generate new nonce
229
        $nonce = \random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
230
231
        //store new nonce
232
        $_SESSION[self::CSRF_ENCRYPTION_NONCE][] = $nonce;
233
234
        //check if the storage is growt beyond the maximun size
235
        if (\count($_SESSION[self::CSRF_ENCRYPTION_NONCE]) > $this->storageSize) {
236
            //remove the oldest stores nonce
237
            \array_shift($_SESSION[self::CSRF_ENCRYPTION_NONCE]);
238
        }
239
240
        return $nonce;
241
    }
242
}
243