Encrypter::encryptString()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SGP\IronBox\Encryption;
4
5
use SGP\IronBox\Exceptions\DecryptException;
6
use SGP\IronBox\Exceptions\EncryptException;
7
use RuntimeException;
8
9
class Encrypter
10
{
11
    /**
12
     * The encryption key.
13
     *
14
     * @var string
15
     */
16
    protected $key;
17
18
    /**
19
     * The algorithm used for encryption.
20
     *
21
     * @var string
22
     */
23
    protected $cipher;
24
25
    /**
26
     * The initialization vector to use.
27
     *
28
     * @var string
29
     */
30
    protected $iv;
31
32
    /**
33
     * Create a new encrypter instance.
34
     *
35
     * @param string $key
36
     * @param string $cipher
37
     * @param string $iv
38
     *
39
     * @return void
40
     *
41
     * @throws \RuntimeException
42
     */
43
    public function __construct($key, $cipher = 'AES-128-CBC', $iv = null)
44
    {
45
        $key = (string) $key;
46
47
        if (static::supported($key, $cipher)) {
48
            $this->key = $key;
49
            $this->cipher = $cipher;
50
            $this->iv = $iv;
51
        } else {
52
            throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC');
53
        }
54
    }
55
56
    /**
57
     * Determine if the given key and cipher combination is valid.
58
     *
59
     * @param string $key
60
     * @param string $cipher
61
     *
62
     * @return bool
63
     */
64
    public static function supported($key, $cipher)
65
    {
66
        $length = mb_strlen($key, '8bit');
67
68
        return ($cipher === 'AES-128-CBC' && $length === 16) ||
69
            ($cipher === 'AES-256-CBC' && $length === 32);
70
    }
71
72
    /**
73
     * Create a new encryption key for the given cipher.
74
     *
75
     * @param string $cipher
76
     *
77
     * @return string
78
     * @throws \Exception
79
     */
80
    public static function generateKey($cipher)
81
    {
82
        return random_bytes($cipher === 'AES-128-CBC' ? 16 : 32);
83
    }
84
85
    /**
86
     * @param $value
87
     * @param bool $serialize
88
     *
89
     * @return string
90
     *
91
     * @throws \SGP\IronBox\Exceptions\EncryptException
92
     */
93
    public function encrypt($value, $serialize = true)
94
    {
95
        $iv = $this->iv ?? random_bytes(openssl_cipher_iv_length($this->cipher));
96
97
        // First we will encrypt the value using OpenSSL. After this is encrypted we
98
        // will proceed to calculating a MAC for the encrypted value so that this
99
        // value can be verified later as not having been changed by the users.
100
        $value = \openssl_encrypt(
101
            $serialize ? serialize($value) : $value,
102
            $this->cipher,
103
            $this->key,
104
            0,
105
            $iv
106
        );
107
108
        if ($value === false) {
109
            throw new EncryptException('Could not encrypt the data.');
110
        }
111
112
        // Once we get the encrypted value we'll go ahead and base64_encode the input
113
        // vector and create the MAC for the encrypted value so we can then verify
114
        // its authenticity. Then, we'll JSON the data into the "payload" array.
115
        $mac = $this->hash($iv = base64_encode($iv), $value);
116
117
        $json = json_encode(compact('iv', 'value', 'mac'));
118
119
        if (json_last_error() !== JSON_ERROR_NONE) {
120
            throw new EncryptException('Could not encrypt the data.');
121
        }
122
123
        return base64_encode($json);
124
    }
125
126
    /**
127
     * Encrypt a string without serialization.
128
     *
129
     * @param string $value
130
     *
131
     * @return string
132
     *
133
     * @throws \SGP\IronBox\Exceptions\EncryptException
134
     */
135
    public function encryptString($value)
136
    {
137
        return $this->encrypt($value, false);
138
    }
139
140
    /**
141
     * Decrypt the given value.
142
     *
143
     * @param mixed $payload
144
     * @param bool $unserialize
145
     *
146
     * @return mixed
147
     *
148
     * @throws \SGP\IronBox\Exceptions\DecryptException
149
     */
150
    public function decrypt($payload, $unserialize = true)
151
    {
152
        $payload = $this->getJsonPayload($payload);
153
154
        $iv = base64_decode($payload['iv']);
155
156
        // Here we will decrypt the value. If we are able to successfully decrypt it
157
        // we will then unserialize it and return it out to the caller. If we are
158
        // unable to decrypt this value we will throw out an exception message.
159
        $decrypted = \openssl_decrypt(
160
            $payload['value'],
161
            $this->cipher,
162
            $this->key,
163
            0,
164
            $iv
165
        );
166
167
        if ($decrypted === false) {
168
            throw new DecryptException('Could not decrypt the data.');
169
        }
170
171
        return $unserialize ? unserialize($decrypted) : $decrypted;
172
    }
173
174
    /**
175
     * Decrypt the given string without unserialization.
176
     *
177
     * @param string $payload
178
     *
179
     * @return string
180
     *
181
     * @throws \SGP\IronBox\Exceptions\DecryptException
182
     */
183
    public function decryptString($payload)
184
    {
185
        return $this->decrypt($payload, false);
186
    }
187
188
    /**
189
     * Get the encryption key.
190
     *
191
     * @return string
192
     */
193
    public function getKey()
194
    {
195
        return $this->key;
196
    }
197
198
    /**
199
     * Create a MAC for the given value.
200
     *
201
     * @param string $iv
202
     * @param mixed $value
203
     *
204
     * @return string
205
     */
206
    protected function hash($iv, $value)
207
    {
208
        return hash_hmac('sha256', $iv . $value, $this->key);
209
    }
210
211
    /**
212
     * Get the JSON array from the given payload.
213
     *
214
     * @param string $payload
215
     *
216
     * @return array
217
     *
218
     * @throws \SGP\IronBox\Exceptions\DecryptException
219
     */
220
    protected function getJsonPayload($payload)
221
    {
222
        $payload = json_decode(base64_decode($payload), true);
223
224
        // If the payload is not valid JSON or does not have the proper keys set we will
225
        // assume it is invalid and bail out of the routine since we will not be able
226
        // to decrypt the given value. We'll also check the MAC for this encryption.
227
        if (! $this->validPayload($payload)) {
228
            throw new DecryptException('The payload is invalid.');
229
        }
230
231
        if (! $this->validMac($payload)) {
232
            throw new DecryptException('The MAC is invalid.');
233
        }
234
235
        return $payload;
236
    }
237
238
    /**
239
     * Verify that the encryption payload is valid.
240
     *
241
     * @param mixed $payload
242
     *
243
     * @return bool
244
     */
245
    protected function validPayload($payload)
246
    {
247
        return is_array($payload) &&
248
            isset($payload['iv'], $payload['value'], $payload['mac']) &&
249
            strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length($this->cipher);
250
    }
251
252
    /**
253
     * Determine if the MAC for the given payload is valid.
254
     *
255
     * @param array $payload
256
     *
257
     * @return bool
258
     * @throws \Exception
259
     */
260
    protected function validMac(array $payload)
261
    {
262
        $calculated = $this->calculateMac($payload, $bytes = random_bytes(16));
263
264
        return hash_equals(
265
            hash_hmac('sha256', $payload['mac'], $bytes, true),
266
            $calculated
267
        );
268
    }
269
270
    /**
271
     * Calculate the hash of the given payload.
272
     *
273
     * @param array $payload
274
     * @param string $bytes
275
     *
276
     * @return string
277
     */
278
    protected function calculateMac($payload, $bytes)
279
    {
280
        return hash_hmac(
281
            'sha256',
282
            $this->hash($payload['iv'], $payload['value']),
283
            $bytes,
284
            true
285
        );
286
    }
287
}
288