10
10
11
11
class Encrypter implements EncrypterContract, StringEncrypter
12
12
{
13
+ /**
14
+ * The supported cipher algorithms and their properties.
15
+ *
16
+ * @var array
17
+ */
18
+ private static $ supportedCiphers = [
19
+ 'AES-128-CBC ' => ['size ' => 16 , 'aead ' => false ],
20
+ 'AES-256-CBC ' => ['size ' => 32 , 'aead ' => false ],
21
+ 'AES-128-GCM ' => ['size ' => 16 , 'aead ' => true ],
22
+ 'AES-256-GCM ' => ['size ' => 32 , 'aead ' => true ],
23
+ ];
24
+
13
25
/**
14
26
* The encryption key.
15
27
*
@@ -37,12 +49,13 @@ public function __construct($key, $cipher = 'AES-128-CBC')
37
49
{
38
50
$ key = (string ) $ key ;
39
51
40
- if (static ::supported ($ key , $ cipher )) {
41
- $ this ->key = $ key ;
42
- $ this ->cipher = $ cipher ;
43
- } else {
44
- throw new RuntimeException ('The only supported ciphers are AES-128-CBC, AES-256-CBC, AES-128-GCM, and AES-256-GCM with the correct key lengths. ' );
52
+ if (! static ::supported ($ key , $ cipher )) {
53
+ $ ciphers = implode (', ' , array_keys (self ::$ supportedCiphers ));
54
+ throw new RuntimeException ("Unsupported cipher or incorrect key length. Supported ciphers are: $ ciphers. " );
45
55
}
56
+
57
+ $ this ->key = $ key ;
58
+ $ this ->cipher = $ cipher ;
46
59
}
47
60
48
61
/**
@@ -54,12 +67,11 @@ public function __construct($key, $cipher = 'AES-128-CBC')
54
67
*/
55
68
public static function supported ($ key , $ cipher )
56
69
{
57
- $ length = mb_strlen ($ key , '8bit ' );
70
+ if (! isset (self ::$ supportedCiphers [$ cipher ])) {
71
+ return false ;
72
+ }
58
73
59
- return ($ cipher === 'AES-128-CBC ' && $ length === 16 ) ||
60
- ($ cipher === 'AES-256-CBC ' && $ length === 32 ) ||
61
- ($ cipher === 'AES-128-GCM ' && $ length === 16 ) ||
62
- ($ cipher === 'AES-256-GCM ' && $ length === 32 );
74
+ return mb_strlen ($ key , '8bit ' ) === self ::$ supportedCiphers [$ cipher ]['size ' ];
63
75
}
64
76
65
77
/**
@@ -70,7 +82,7 @@ public static function supported($key, $cipher)
70
82
*/
71
83
public static function generateKey ($ cipher )
72
84
{
73
- return random_bytes ($ cipher === ' AES-128-CBC ' ? 16 : 32 );
85
+ return random_bytes (self :: $ supportedCiphers [ $ cipher][ ' size ' ] );
74
86
}
75
87
76
88
/**
@@ -86,33 +98,31 @@ public function encrypt($value, $serialize = true)
86
98
{
87
99
$ iv = random_bytes (openssl_cipher_iv_length ($ this ->cipher ));
88
100
89
- $ tag = in_array ($ this ->cipher , ['AES-128-GCM ' , 'AES-256-GCM ' ]) ? '' : null ;
90
-
91
- // First we will encrypt the value using OpenSSL. After this is encrypted we
92
- // will proceed to calculating a MAC for the encrypted value so that this
93
- // value can be verified later as not having been changed by the users.
94
- $ value =
95
- in_array ($ this ->cipher , ['AES-128-GCM ' , 'AES-256-GCM ' ]) ?
96
- \openssl_encrypt (
97
- $ serialize ? serialize ($ value ) : $ value ,
98
- $ this ->cipher , $ this ->key , 0 , $ iv , $ tag
99
- ) :
100
- \openssl_encrypt (
101
- $ serialize ? serialize ($ value ) : $ value ,
102
- $ this ->cipher , $ this ->key , 0 , $ iv
103
- );
101
+ // A tag (mac) is returned by openssl_encrypt for AEAD ciphers.
102
+ // Including $tag in the call for non-AEAD ciphers results in a warning before PHP 8.1.
103
+ $ tag = '' ;
104
+ $ value = self ::$ supportedCiphers [$ this ->cipher ]['aead ' ]
105
+ ? \openssl_encrypt (
106
+ $ serialize ? serialize ($ value ) : $ value ,
107
+ $ this ->cipher , $ this ->key , 0 , $ iv , $ tag
108
+ )
109
+ : \openssl_encrypt (
110
+ $ serialize ? serialize ($ value ) : $ value ,
111
+ $ this ->cipher , $ this ->key , 0 , $ iv
112
+ );
104
113
105
114
if ($ value === false ) {
106
115
throw new EncryptException ('Could not encrypt the data. ' );
107
116
}
108
117
109
- // Once we get the encrypted value we'll go ahead and base64_encode the input
110
- // vector and create the MAC for the encrypted value so we can then verify
111
- // its authenticity. Then, we'll JSON the data into the "payload" array.
112
- $ mac = $ this ->hash (
113
- $ iv = base64_encode ( $ iv ), $ value , $ tag = $ tag ? base64_encode ( $ tag ) : ''
114
- );
118
+ $ iv = base64_encode ( $ iv );
119
+ $ tag = base64_encode ( $ tag );
120
+
121
+ $ mac = self :: $ supportedCiphers [ $ this ->cipher ][ ' aead ' ]
122
+ ? '' // For AEAD-algoritms, the tag/mac is returned by openssl_encrypt
123
+ : $ this -> hash ( $ iv , $ value );
115
124
125
+ // Both tag and mac are included for compatibility reasons. A breaking update could use the same name for these.
116
126
$ json = json_encode (compact ('iv ' , 'value ' , 'mac ' , 'tag ' ), JSON_UNESCAPED_SLASHES );
117
127
118
128
if (json_last_error () !== JSON_ERROR_NONE ) {
@@ -184,12 +194,11 @@ public function decryptString($payload)
184
194
*
185
195
* @param string $iv
186
196
* @param mixed $value
187
- * @param string $tag
188
197
* @return string
189
198
*/
190
- protected function hash ($ iv , $ value, $ tag = '' )
199
+ protected function hash ($ iv , $ value )
191
200
{
192
- return hash_hmac ('sha256 ' , $ tag . $ iv .$ value , $ this ->key );
201
+ return hash_hmac ('sha256 ' , $ iv .$ value , $ this ->key );
193
202
}
194
203
195
204
/**
@@ -211,7 +220,8 @@ protected function getJsonPayload($payload)
211
220
throw new DecryptException ('The payload is invalid. ' );
212
221
}
213
222
214
- if (! $ this ->validMac ($ payload )) {
223
+ // We only need to check for the valid MAC if a non-AEAD algorithm is used
224
+ if (! self ::$ supportedCiphers [$ this ->cipher ]['aead ' ] && ! $ this ->validMac ($ payload )) {
215
225
throw new DecryptException ('The MAC is invalid. ' );
216
226
}
217
227
@@ -239,7 +249,7 @@ protected function validPayload($payload)
239
249
protected function validMac (array $ payload )
240
250
{
241
251
return hash_equals (
242
- $ this ->hash ($ payload ['iv ' ], $ payload ['value ' ], $ payload [ ' tag ' ] ?? '' ), $ payload ['mac ' ]
252
+ $ this ->hash ($ payload ['iv ' ], $ payload ['value ' ]), $ payload ['mac ' ]
243
253
);
244
254
}
245
255
0 commit comments