Passed
Push — master ( bdfe7b...8f2aab )
by Alexander
01:12
created

Crypt::decrypt()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 17
nc 6
nop 4
dl 0
loc 28
rs 9.7
c 1
b 0
f 0
ccs 16
cts 16
cp 1
crap 4
1
<?php declare(strict_types=1);
2
3
namespace Yiisoft\Security;
4
5
use Yiisoft\Strings\StringHelper;
6
7
final class Crypt
8
{
9
    /**
10
     * @var string The cipher to use for encryption and decryption.
11
     */
12
    private $cipher;
13
14
    /**
15
     * @var array[] Look-up table of block sizes and key sizes for each supported OpenSSL cipher.
16
     *
17
     * In each element, the key is one of the ciphers supported by OpenSSL {@see openssl_get_cipher_methods()}.
18
     * The value is an array of two integers, the first is the cipher's block size in bytes and the second is
19
     * the key size in bytes.
20
     *
21
     * > Note: Yii's encryption protocol uses the same size for cipher key, HMAC signature key and key
22
     * derivation salt.
23
     */
24
    private const ALLOWED_CIPHERS = [
25
        'AES-128-CBC' => [16, 16],
26
        'AES-192-CBC' => [16, 24],
27
        'AES-256-CBC' => [16, 32],
28
    ];
29
30
    /**
31
     * @var string Hash algorithm for key derivation. Recommend sha256, sha384 or sha512.
32
     * @see http://php.net/manual/en/function.hash-algos.php
33
     */
34
    private $kdfAlgorithm = 'sha256';
35
36
    /**
37
     * @var string HKDF info value for derivation of message authentication key.
38
     */
39
    private $authKeyInfo = 'AuthorizationKey';
40
    /**
41
     * @var int derivation iterations count.
42
     * Set as high as possible to hinder dictionary password attacks.
43
     */
44
    private $derivationIterations = 100000;
45
46 48
    public function __construct(string $cipher = 'AES-128-CBC')
47
    {
48 48
        if (!extension_loaded('openssl')) {
49 1
            throw new \RuntimeException('Encryption requires the OpenSSL PHP extension');
50
        }
51 47
        if (!isset(self::ALLOWED_CIPHERS[$cipher][0], self::ALLOWED_CIPHERS[$cipher][1])) {
52 1
            throw new \RuntimeException($cipher . ' is not an allowed cipher');
53
        }
54
55 46
        $this->cipher = $cipher;
56 46
    }
57
58 1
    public function withKdfAlgorithm(string $algorithm): self
59
    {
60 1
        $new = clone $this;
61 1
        $new->kdfAlgorithm = $algorithm;
62 1
        return $new;
63
    }
64
65 1
    public function withAuthKeyInfo(string $info): self
66
    {
67 1
        $new = clone $this;
68 1
        $new->authKeyInfo = $info;
69 1
        return $new;
70
    }
71
72 44
    public function withDerivationIterations(int $iterations): self
73
    {
74 44
        $new = clone $this;
75 44
        $new->derivationIterations = $iterations;
76 44
        return $new;
77
    }
78
79
    /**
80
     * Encrypts data using a password.
81
     * Derives keys for encryption and authentication from the password using PBKDF2 and a random salt,
82
     * which is deliberately slow to protect against dictionary attacks. Use {@see encryptByKey()} to
83
     * encrypt fast using a cryptographic key rather than a password. Key derivation time is
84
     * determined by {@see $derivationIterations}}, which should be set as high as possible.
85
     * The encrypted data includes a keyed message authentication code (MAC) so there is no need
86
     * to hash input or output data.
87
     * > Note: Avoid encrypting with passwords wherever possible. Nothing can protect against
88
     * poor-quality or compromised passwords.
89
     * @param string $data the data to encrypt
90
     * @param string $password the password to use for encryption
91
     * @return string the encrypted data as byte string
92
     * @throws \RuntimeException on OpenSSL not loaded
93
     * @throws \Exception on OpenSSL error
94
     * @see decryptByPassword()
95
     * @see encryptByKey()
96
     */
97 4
    public function encryptByPassword(string $data, string $password): string
98
    {
99 4
        return $this->encrypt($data, true, $password, '');
100
    }
101
102
    /**
103
     * Encrypts data using a cryptographic key.
104
     * Derives keys for encryption and authentication from the input key using HKDF and a random salt,
105
     * which is very fast relative to {@see encryptByPassword()}. The input key must be properly
106
     * random — use {@see random_bytes()} to generate keys.
107
     * The encrypted data includes a keyed message authentication code (MAC) so there is no need
108
     * to hash input or output data.
109
     * @param string $data the data to encrypt
110
     * @param string $inputKey the input to use for encryption and authentication
111
     * @param string $info context/application specific information, e.g. a user ID
112
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
113
     * @return string the encrypted data as byte string
114
     * @throws \RuntimeException on OpenSSL not loaded
115
     * @throws \Exception on OpenSSL error
116
     * @see decryptByKey()
117
     * @see encryptByPassword()
118
     */
119 4
    public function encryptByKey(string $data, string $inputKey, string $info = ''): string
120
    {
121 4
        return $this->encrypt($data, false, $inputKey, $info);
122
    }
123
124
    /**
125
     * Verifies and decrypts data encrypted with {@see encryptByPassword()}.
126
     * @param string $data the encrypted data to decrypt
127
     * @param string $password the password to use for decryption
128
     * @return string the decrypted data
129
     * @throws \RuntimeException on OpenSSL not loaded
130
     * @throws \Exception on OpenSSL errors
131
     * @throws AuthenticationException on authentication failure
132
     * @see encryptByPassword()
133
     */
134 21
    public function decryptByPassword(string $data, string $password): string
135
    {
136 21
        return $this->decrypt($data, true, $password, '');
137
    }
138
139
    /**
140
     * Verifies and decrypts data encrypted with {@see encryptByKey()}.
141
     * @param string $data the encrypted data to decrypt
142
     * @param string $inputKey the input to use for encryption and authentication
143
     * @param string $info context/application specific information, e.g. a user ID
144
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
145
     * @return string the decrypted data
146
     * @throws \RuntimeException on OpenSSL not loaded
147
     * @throws \Exception on OpenSSL errors
148
     * @throws AuthenticationException on authentication failure
149
     * @see encryptByKey()
150
     */
151 22
    public function decryptByKey($data, $inputKey, $info = ''): string
152
    {
153 22
        return $this->decrypt($data, false, $inputKey, $info);
154
    }
155
156
    /**
157
     * Encrypts data.
158
     *
159
     * @param string $data data to be encrypted
160
     * @param bool $passwordBased set true to use password-based key derivation
161
     * @param string $secret the encryption password or key
162
     * @param string $info context/application specific information, e.g. a user ID
163
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
164
     *
165
     * @return string the encrypted data as byte string
166
     * @throws \RuntimeException on OpenSSL not loaded
167
     * @throws \Exception on OpenSSL error
168
     * @see decrypt()
169
     */
170 8
    private function encrypt(string $data, bool $passwordBased, string $secret, string $info = ''): string
171
    {
172 8
        [$blockSize, $keySize] = self::ALLOWED_CIPHERS[$this->cipher];
173
174 8
        $keySalt = random_bytes($keySize);
175 8
        if ($passwordBased) {
176 4
            $key = hash_pbkdf2($this->kdfAlgorithm, $secret, $keySalt, $this->derivationIterations, $keySize, true);
177
        } else {
178 4
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, $info, $keySalt);
179
        }
180
181 8
        $iv = random_bytes($blockSize);
182
183 8
        $encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
184 8
        if ($encrypted === false) {
0 ignored issues
show
introduced by
The condition $encrypted === false is always false.
Loading history...
185 1
            throw new \RuntimeException('OpenSSL failure on encryption: ' . openssl_error_string());
186
        }
187
188 7
        $authKey = hash_hkdf($this->kdfAlgorithm, $key, $keySize, $this->authKeyInfo);
189 7
        $signed = (new Mac())->sign($iv . $encrypted, $authKey);
190
191
        /*
192
         * Output: [keySalt][MAC][IV][ciphertext]
193
         * - keySalt is KEY_SIZE bytes long
194
         * - MAC: message authentication code, length same as the output of MAC_HASH
195
         * - IV: initialization vector, length $blockSize
196
         */
197 7
        return $keySalt . $signed;
198
    }
199
200
    /**
201
     * Decrypts data.
202
     *
203
     * @param string $data encrypted data to be decrypted.
204
     * @param bool $passwordBased set true to use password-based key derivation
205
     * @param string $secret the decryption password or key
206
     * @param string $info context/application specific information, @see encrypt()
207
     *
208
     * @return string the decrypted data
209
     * @throws \RuntimeException on OpenSSL not loaded
210
     * @throws \Exception on OpenSSL errors
211
     * @throws AuthenticationException on authentication failure
212
     * @see encrypt()
213
     */
214 43
    private function decrypt(string $data, bool $passwordBased, string $secret, string $info): string
215
    {
216 43
        [$blockSize, $keySize] = self::ALLOWED_CIPHERS[$this->cipher];
217
218 43
        $keySalt = StringHelper::byteSubstr($data, 0, $keySize);
219 43
        if ($passwordBased) {
220 21
            $key = hash_pbkdf2($this->kdfAlgorithm, $secret, $keySalt, $this->derivationIterations, $keySize, true);
221
        } else {
222 22
            $key = hash_hkdf($this->kdfAlgorithm, $secret, $keySize, $info, $keySalt);
223
        }
224
225 43
        $authKey = hash_hkdf($this->kdfAlgorithm, $key, $keySize, $this->authKeyInfo);
226
227
        try {
228 43
            $data = (new Mac())->getMessage(StringHelper::byteSubstr($data, $keySize), $authKey);
229 3
        } catch (DataIsTamperedException $e) {
230 3
            throw new AuthenticationException('Failed to decrypt data');
231
        }
232
233 40
        $iv = StringHelper::byteSubstr($data, 0, $blockSize);
234 40
        $encrypted = StringHelper::byteSubstr($data, $blockSize);
235
236 40
        $decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
237 40
        if ($decrypted === false) {
0 ignored issues
show
introduced by
The condition $decrypted === false is always false.
Loading history...
238 1
            throw new \RuntimeException('OpenSSL failure on decryption: ' . openssl_error_string());
239
        }
240
241 39
        return $decrypted;
242
    }
243
}
244