Skip to content

Commit 796d8e7

Browse files
committed
Merge branch 'encryption-aead-fixes' into 8.x
2 parents a8e8b49 + e0eef8d commit 796d8e7

File tree

2 files changed

+69
-41
lines changed

2 files changed

+69
-41
lines changed

src/Illuminate/Encryption/Encrypter.php

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ class Encrypter implements EncrypterContract, StringEncrypter
2424
*/
2525
protected $cipher;
2626

27+
/**
28+
* The supported cipher algorithms and their properties.
29+
*
30+
* @var array
31+
*/
32+
private static $supportedCiphers = [
33+
'AES-128-CBC' => ['size' => 16, 'aead' => false],
34+
'AES-256-CBC' => ['size' => 32, 'aead' => false],
35+
'AES-128-GCM' => ['size' => 16, 'aead' => true],
36+
'AES-256-GCM' => ['size' => 32, 'aead' => true],
37+
];
38+
2739
/**
2840
* Create a new encrypter instance.
2941
*
@@ -37,12 +49,14 @@ public function __construct($key, $cipher = 'AES-128-CBC')
3749
{
3850
$key = (string) $key;
3951

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+
55+
throw new RuntimeException("Unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}.");
4556
}
57+
58+
$this->key = $key;
59+
$this->cipher = $cipher;
4660
}
4761

4862
/**
@@ -54,12 +68,11 @@ public function __construct($key, $cipher = 'AES-128-CBC')
5468
*/
5569
public static function supported($key, $cipher)
5670
{
57-
$length = mb_strlen($key, '8bit');
71+
if (! isset(self::$supportedCiphers[$cipher])) {
72+
return false;
73+
}
5874

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);
75+
return mb_strlen($key, '8bit') === self::$supportedCiphers[$cipher]['size'];
6376
}
6477

6578
/**
@@ -70,7 +83,7 @@ public static function supported($key, $cipher)
7083
*/
7184
public static function generateKey($cipher)
7285
{
73-
return random_bytes($cipher === 'AES-128-CBC' ? 16 : 32);
86+
return random_bytes(self::$supportedCiphers[$cipher]['size']);
7487
}
7588

7689
/**
@@ -86,32 +99,28 @@ public function encrypt($value, $serialize = true)
8699
{
87100
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
88101

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-
);
102+
$tag = '';
103+
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+
);
104113

105114
if ($value === false) {
106115
throw new EncryptException('Could not encrypt the data.');
107116
}
108117

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);
115124

116125
$json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);
117126

@@ -184,12 +193,11 @@ public function decryptString($payload)
184193
*
185194
* @param string $iv
186195
* @param mixed $value
187-
* @param string $tag
188196
* @return string
189197
*/
190-
protected function hash($iv, $value, $tag = '')
198+
protected function hash($iv, $value)
191199
{
192-
return hash_hmac('sha256', $tag.$iv.$value, $this->key);
200+
return hash_hmac('sha256', $iv.$value, $this->key);
193201
}
194202

195203
/**
@@ -211,7 +219,7 @@ protected function getJsonPayload($payload)
211219
throw new DecryptException('The payload is invalid.');
212220
}
213221

214-
if (! $this->validMac($payload)) {
222+
if (! self::$supportedCiphers[$this->cipher]['aead'] && ! $this->validMac($payload)) {
215223
throw new DecryptException('The MAC is invalid.');
216224
}
217225

@@ -239,7 +247,7 @@ protected function validPayload($payload)
239247
protected function validMac(array $payload)
240248
{
241249
return hash_equals(
242-
$this->hash($payload['iv'], $payload['value'], $payload['tag'] ?? ''), $payload['mac']
250+
$this->hash($payload['iv'], $payload['value']), $payload['mac']
243251
);
244252
}
245253

tests/Encryption/EncrypterTest.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,54 @@ public function testWithCustomCipher()
5656
$this->assertSame('foo', $e->decrypt($encrypted));
5757
}
5858

59+
public function testThatAnAeadCipherIncludesTag()
60+
{
61+
$e = new Encrypter(str_repeat('b', 32), 'AES-256-GCM');
62+
$encrypted = $e->encrypt('foo');
63+
$data = json_decode(base64_decode($encrypted));
64+
65+
$this->assertEmpty($data->mac);
66+
$this->assertNotEmpty($data->tag);
67+
}
68+
69+
public function testThatANonAeadCipherIncludesMac()
70+
{
71+
$e = new Encrypter(str_repeat('b', 32), 'AES-256-CBC');
72+
$encrypted = $e->encrypt('foo');
73+
$data = json_decode(base64_decode($encrypted));
74+
75+
$this->assertEmpty($data->tag);
76+
$this->assertNotEmpty($data->mac);
77+
}
78+
5979
public function testDoNoAllowLongerKey()
6080
{
6181
$this->expectException(RuntimeException::class);
62-
$this->expectExceptionMessage('The only supported ciphers are AES-128-CBC, AES-256-CBC, AES-128-GCM, and AES-256-GCM with the correct key lengths.');
82+
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: AES-128-CBC, AES-256-CBC, AES-128-GCM, AES-256-GCM.');
6383

6484
new Encrypter(str_repeat('z', 32));
6585
}
6686

6787
public function testWithBadKeyLength()
6888
{
6989
$this->expectException(RuntimeException::class);
70-
$this->expectExceptionMessage('The only supported ciphers are AES-128-CBC, AES-256-CBC, AES-128-GCM, and AES-256-GCM with the correct key lengths.');
90+
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: AES-128-CBC, AES-256-CBC, AES-128-GCM, AES-256-GCM.');
7191

7292
new Encrypter(str_repeat('a', 5));
7393
}
7494

7595
public function testWithBadKeyLengthAlternativeCipher()
7696
{
7797
$this->expectException(RuntimeException::class);
78-
$this->expectExceptionMessage('The only supported ciphers are AES-128-CBC, AES-256-CBC, AES-128-GCM, and AES-256-GCM with the correct key lengths.');
98+
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: AES-128-CBC, AES-256-CBC, AES-128-GCM, AES-256-GCM.');
7999

80100
new Encrypter(str_repeat('a', 16), 'AES-256-CFB8');
81101
}
82102

83103
public function testWithUnsupportedCipher()
84104
{
85105
$this->expectException(RuntimeException::class);
86-
$this->expectExceptionMessage('The only supported ciphers are AES-128-CBC, AES-256-CBC, AES-128-GCM, and AES-256-GCM with the correct key lengths.');
106+
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: AES-128-CBC, AES-256-CBC, AES-128-GCM, AES-256-GCM.');
87107

88108
new Encrypter(str_repeat('c', 16), 'AES-256-CFB8');
89109
}

0 commit comments

Comments
 (0)