Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Encrypter
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
6 / 6
19
100.00% covered (success)
100.00%
1 / 1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAvailable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValid
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 generateKey
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 encrypt
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 decrypt
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Crypt\Encryption;
15
16/**
17 * Pop Crypt encrypter
18 *
19 * @category   Pop
20 * @package    Pop\Crypt
21 * @author     Nick Sagona, III <dev@noladev.com>
22 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
23 * @license    https://www.popphp.org/license     New BSD License
24 * @version    3.0.0
25 */
26class Encrypter extends AbstractEncrypter
27{
28
29
30    /**
31     * Cipher constants
32     */
33    const AES_128_CBC = 'aes-128-cbc';
34    const AES_256_CBC = 'aes-256-cbc';
35    const AES_128_GCM = 'aes-128-gcm';
36    const AES_256_GCM = 'aes-256-gcm';
37
38    /**
39     * Available ciphers
40     *
41     * @var array
42     */
43    private static $ciphers = [
44        'aes-128-cbc' => ['size' => 16, 'aead' => false],
45        'aes-256-cbc' => ['size' => 32, 'aead' => false],
46        'aes-128-gcm' => ['size' => 16, 'aead' => true],
47        'aes-256-gcm' => ['size' => 32, 'aead' => true],
48    ];
49
50    /**
51     * Create encrypter object
52     *
53     * @param  string $cipher
54     * @return static
55     */
56    public static function create(string $cipher = 'aes-256-cbc'): static
57    {
58        return new static(static::generateKey($cipher), $cipher);
59    }
60
61    /**
62     * Determine if the cipher is available
63     *
64     * @param  string $cipher
65     * @return bool
66     */
67    public static function isAvailable(string $cipher): bool
68    {
69        return isset(static::$ciphers[strtolower($cipher)]);
70    }
71
72    /**
73     * Determine if the key and cipher combination is valid
74     *
75     * @param  string $key
76     * @param  string $cipher
77     * @param  bool   $raw
78     * @return bool
79     */
80    public static function isValid(string $key, string $cipher, bool $raw = true): bool
81    {
82        $cipher = strtolower($cipher);
83        if (!isset(static::$ciphers[$cipher])) {
84            return false;
85        }
86        if (!$raw) {
87            $key = base64_decode($key);
88        }
89        return (mb_strlen($key, '8bit') === static::$ciphers[$cipher]['size']);
90    }
91
92    /**
93     * Generate encryption key
94     *
95     * @param  string $cipher
96     * @param  bool   $raw
97     * @return string
98     */
99    public static function generateKey(string $cipher, bool $raw = true): string
100    {
101        $key = random_bytes((static::$ciphers[strtolower($cipher)]['size'] ?? 32));
102        return ($raw) ? $key : base64_encode($key);
103    }
104
105    /**
106     * Encrypt value
107     *
108     * @param  mixed $value
109     * @return string
110     */
111    public function encrypt(#[\SensitiveParameter] mixed $value): string
112    {
113        $iv    = random_bytes(openssl_cipher_iv_length(strtolower($this->cipher)));
114        $tag   = '';
115        $value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv, $tag);
116        $iv    = base64_encode($iv);
117        $tag   = base64_encode(($tag ?? ''));
118        $mac   = (!static::$ciphers[$this->cipher]['aead']) ?
119            hash_hmac('sha256', $iv . $value, $this->key) : '';
120
121        $json = json_encode([
122            'iv'    => $iv,
123            'value' => $value,
124            'mac'   => $mac,
125            'tag'   => $tag,
126        ], JSON_UNESCAPED_SLASHES);
127
128        return base64_encode($json);
129    }
130
131    /**
132     * Decrypt value
133     *
134     * @param  string $payload
135     * @throws Exception
136     * @return mixed
137     */
138    public function decrypt(string $payload): mixed
139    {
140        $payload = json_decode(base64_decode($payload), true);
141
142        if (!is_array($payload) || (!isset($payload['iv']) || !isset($payload['value']))) {
143            throw new Exception('Error: The payload is not valid data.');
144        }
145
146        $iv        = base64_decode($payload['iv']);
147        $tag       = (!empty($payload['tag'])) ? base64_decode($payload['tag']) : '';
148        $decrypted = false;
149        $validMac  = null;
150
151        foreach ($this->getAllKeys() as $key) {
152            $decrypted = openssl_decrypt($payload['value'], $this->cipher, $key, 0, $iv, $tag);
153
154            if (!static::$ciphers[$this->cipher]['aead']) {
155                $validMac = hash_equals(hash_hmac('sha256', $payload['iv'] . $payload['value'], $key), $payload['mac']);
156            }
157
158            if ($decrypted !== false) {
159                break;
160            }
161        }
162
163        if ($validMac === false) {
164            throw new Exception('Error: Invalid MAC value.');
165        }
166        if ($decrypted === false) {
167            throw new Exception('Error: Unable to decrypt the data.');
168        }
169
170        return $decrypted;
171    }
172
173}