|
1 | | -# Encryption |
2 | | -PHP OpenSSL/Sodium Encryption and Decryption |
| 1 | +# initphp/encryption |
3 | 2 |
|
4 | | -[](https://packagist.org/packages/initphp/encryption) [](https://packagist.org/packages/initphp/encryption) [](https://packagist.org/packages/initphp/encryption) [](https://packagist.org/packages/initphp/encryption) [](https://packagist.org/packages/initphp/encryption) |
| 3 | +Secure, modern symmetric encryption for PHP on top of OpenSSL and libsodium. |
| 4 | + |
| 5 | +[](https://packagist.org/packages/initphp/encryption) |
| 6 | +[](https://packagist.org/packages/initphp/encryption) |
| 7 | +[](https://packagist.org/packages/initphp/encryption) |
| 8 | +[](https://packagist.org/packages/initphp/encryption) |
| 9 | + |
| 10 | +## Why |
| 11 | + |
| 12 | +PHP's encryption primitives are powerful but unforgiving. Pick the wrong cipher, |
| 13 | +mix up IV and HMAC ordering, forget constant-time comparison, or hand libsodium |
| 14 | +a 14-byte "key" and you ship a vulnerability — or, more often, a silent failure |
| 15 | +that "works on my machine". |
| 16 | + |
| 17 | +This package wraps `ext-openssl` and `ext-sodium` behind a small, opinionated |
| 18 | +API: |
| 19 | + |
| 20 | +- Authenticated by default. OpenSSL uses encrypt-then-MAC; Sodium uses the |
| 21 | + built-in AEAD construction. |
| 22 | +- Keys of any non-empty length are accepted and derived to the size the |
| 23 | + primitive actually requires. |
| 24 | +- Ciphertexts are self-describing: a 2-byte header (version + serializer flag) |
| 25 | + lets the library reject malformed or out-of-date input with a clear error |
| 26 | + instead of returning garbage. |
| 27 | +- A single `EncryptionException` covers every failure mode, so a `try` / |
| 28 | + `catch` is enough to handle all error paths. |
| 29 | +- JSON is the default payload serializer, so the historical |
| 30 | + `unserialize()`-on-attacker-controlled-bytes pitfall is closed by default. |
5 | 31 |
|
6 | 32 | ## Requirements |
7 | 33 |
|
8 | | -- PHP 7.4 or higher |
9 | | -- MB_String extension |
10 | | -- Depending on usage: |
11 | | - - OpenSSL extesion |
12 | | - - Sodium extension |
| 34 | +- PHP **8.1** or higher |
| 35 | +- `ext-openssl` for the OpenSSL handler |
| 36 | +- `ext-sodium` for the Sodium handler |
13 | 37 |
|
| 38 | +Both extensions ship with mainstream PHP distributions, but the package only |
| 39 | +loads the one you actually instantiate — you can use one handler without the |
| 40 | +other being available. |
14 | 41 |
|
15 | 42 | ## Installation |
16 | 43 |
|
17 | | -``` |
| 44 | +```bash |
18 | 45 | composer require initphp/encryption |
19 | 46 | ``` |
20 | 47 |
|
21 | | -## Configuration |
| 48 | +## Quickstart |
22 | 49 |
|
23 | | -```php |
24 | | -$options = [ |
25 | | - 'algo' => 'SHA256', |
26 | | - 'cipher' => 'AES-256-CTR', |
27 | | - 'key' => null, |
28 | | - 'blocksize' => 16, |
29 | | -]; |
30 | | -``` |
| 50 | +```php |
| 51 | +<?php |
31 | 52 |
|
32 | | -- `algo` : Used by OpenSSL handler only. The algorithm to use to sign the data. |
33 | | -- `cipher` : Used by OpenSSL handler only. The encryption algorithm that will be used to encrypt the data. |
34 | | -- `key` : The top secret key string to use for encryption. |
35 | | -- `blocksize` : It is used for sodium handler only. It is used in the `sodium_pad()` and `sodium_unpad()` functions. |
| 53 | +require __DIR__ . '/vendor/autoload.php'; |
36 | 54 |
|
| 55 | +use InitPHP\Encryption\Encrypt; |
| 56 | +use InitPHP\Encryption\OpenSSL; |
37 | 57 |
|
38 | | -## Usage |
| 58 | +$handler = Encrypt::use(OpenSSL::class, [ |
| 59 | + 'key' => getenv('APP_ENCRYPTION_KEY'), |
| 60 | +]); |
39 | 61 |
|
40 | | -```php |
41 | | -require_once "vendor/autoload.php"; |
42 | | -use \InitPHP\Encryption\Encrypt; |
| 62 | +$ciphertext = $handler->encrypt(['user_id' => 42, 'role' => 'admin']); |
| 63 | +// → "02006f1c…": hex string, safe to store in cookies / DBs / URL params |
43 | 64 |
|
44 | | -// OpenSSL Handler |
45 | | -/** @var $openssl \InitPHP\Encryption\HandlerInterface */ |
46 | | -$openssl = Encrypt::use(\InitPHP\Encryption\OpenSSL::class, [ |
47 | | - 'algo' => 'SHA256', |
48 | | - 'cipher' => 'AES-256-CTR', |
49 | | - 'key' => 'TOP_Secret_Key', |
50 | | -]); |
| 65 | +$plaintext = $handler->decrypt($ciphertext); |
| 66 | +// → ['user_id' => 42, 'role' => 'admin'] |
| 67 | +``` |
| 68 | + |
| 69 | +The Sodium handler has the same surface: |
| 70 | + |
| 71 | +```php |
| 72 | +use InitPHP\Encryption\Encrypt; |
| 73 | +use InitPHP\Encryption\Sodium; |
51 | 74 |
|
52 | | -// Sodium Handler |
53 | | -/** @var $sodium \InitPHP\Encryption\HandlerInterface */ |
54 | | -$sodium = Encrypt::use(\InitPHP\Encryption\Sodium::class, [ |
55 | | - 'key' => 'TOP_Secret_Key', |
56 | | - 'blocksize' => 16, |
| 75 | +$handler = Encrypt::use(Sodium::class, [ |
| 76 | + 'key' => getenv('APP_ENCRYPTION_KEY'), |
57 | 77 | ]); |
| 78 | + |
| 79 | +$ciphertext = $handler->encrypt('a secret message'); |
| 80 | +$plaintext = $handler->decrypt($ciphertext); |
58 | 81 | ``` |
59 | 82 |
|
60 | | -### Methods |
| 83 | +## Configuration |
61 | 84 |
|
62 | | -#### `encrypt()` |
| 85 | +Every option is optional except `key`. Unknown keys are ignored. Keys are |
| 86 | +case-insensitive on input (`'CIPHER'` and `'cipher'` are the same option). |
63 | 87 |
|
64 | | -```php |
65 | | -public function encrypt(mixed $data, array $options = []): string; |
66 | | -``` |
| 88 | +| Option | Used by | Default | Description | |
| 89 | +| ------------ | --------- | -------------- | ----------- | |
| 90 | +| `key` | both | _required_ | The user-supplied secret. Any non-empty string; the handler derives a key of the correct length internally. | |
| 91 | +| `cipher` | OpenSSL | `AES-256-CTR` | Any algorithm from `openssl_get_cipher_methods()`. | |
| 92 | +| `algo` | OpenSSL | `SHA256` | Any algorithm from `hash_hmac_algos()`. Used both for HKDF key derivation and for the HMAC tag. | |
| 93 | +| `blocksize` | Sodium | `16` | Block size for `sodium_pad()` / `sodium_unpad()`. Must be a positive integer. | |
| 94 | +| `serializer` | both | `'json'` | One of `'json'`, `'php_serialize'`, `'php'`, `'serialize'`. See [Serialization](#serialization). | |
| 95 | + |
| 96 | +Options can be set in three places, in order of precedence (highest wins): |
67 | 97 |
|
68 | | -#### `decrypt()` |
| 98 | +```php |
| 99 | +// 1) Per-call override |
| 100 | +$handler->encrypt($data, ['cipher' => 'AES-256-GCM']); |
69 | 101 |
|
70 | | -```php |
71 | | -public function decrypt(string $data, array $options = []): mixed; |
| 102 | +// 2) Mutated on the handler |
| 103 | +$handler->setOption('cipher', 'AES-256-GCM'); |
| 104 | +$handler->setOptions(['cipher' => 'AES-256-GCM', 'algo' => 'SHA512']); |
| 105 | + |
| 106 | +// 3) Constructor / factory |
| 107 | +$handler = Encrypt::use(OpenSSL::class, ['cipher' => 'AES-256-GCM']); |
72 | 108 | ``` |
73 | 109 |
|
74 | | -## Writing Your Own Handler |
| 110 | +Per-call options do **not** mutate the handler — they are merged into a fresh |
| 111 | +array for that single call only. |
75 | 112 |
|
76 | | -```php |
77 | | -namespace App; |
| 113 | +## Serialization |
78 | 114 |
|
79 | | -use \InitPHP\Encryption\{HandlerInterface, BaseHandler}; |
| 115 | +`encrypt()` accepts `mixed` and round-trips the value through a serializer |
| 116 | +chosen via the `serializer` option. The flag is embedded in the ciphertext, so |
| 117 | +`decrypt()` always restores the original type without you having to track the |
| 118 | +choice yourself. |
80 | 119 |
|
81 | | -class MyHandler extends BaseHandler implements HandlerInterface |
82 | | -{ |
83 | | - public function encrypt($data, array $options = []): string |
84 | | - { |
85 | | - $options = $this->options($options); |
86 | | - // ... process |
87 | | - } |
| 120 | +| `serializer` value | On-the-wire flag | Behaviour | |
| 121 | +| --- | --- | --- | |
| 122 | +| `'json'` (default) | `0x00` | Uses `json_encode`/`json_decode` with `JSON_THROW_ON_ERROR`. Safe: no PHP class is ever instantiated during decoding. Cannot carry raw binary bytes — use `php_serialize` if you need that. | |
| 123 | +| `'php_serialize'`, `'php'`, `'serialize'` | `0x01` | Uses `serialize()`/`unserialize()` with `['allowed_classes' => false]`. Round-trips scalars, arrays and binary strings; custom objects degrade to `__PHP_Incomplete_Class` on decode. | |
88 | 124 |
|
89 | | - public function decrypt($data, array $options = []) |
90 | | - { |
91 | | - $options = $this->options($options); |
92 | | - // ... process |
93 | | - } |
94 | | -} |
95 | | -``` |
| 125 | +The PHP serializer is opt-in for one reason only: even though we always pass |
| 126 | +`allowed_classes:false`, the safer default lets you not have to think about |
| 127 | +object-injection vectors at all. |
96 | 128 |
|
97 | | -```php |
98 | | -use \InitPHP\Encryption\Encrypt; |
| 129 | +## Writing a Custom Handler |
99 | 130 |
|
100 | | -$myhandler = Encrypt::use(\App\MyHandler::class); |
101 | | -``` |
| 131 | +Extend `BaseHandler` (not `OpenSSL` / `Sodium` — those are `final`) and |
| 132 | +implement `encrypt()` and `decrypt()`: |
102 | 133 |
|
103 | | -## Getting Help |
| 134 | +```php |
| 135 | +namespace App\Crypto; |
104 | 136 |
|
105 | | -If you have questions, concerns, bug reports, etc, please file an issue in this repository's Issue Tracker. |
| 137 | +use InitPHP\Encryption\BaseHandler; |
| 138 | +use InitPHP\Encryption\Exceptions\EncryptionException; |
| 139 | + |
| 140 | +final class MyHandler extends BaseHandler |
| 141 | +{ |
| 142 | + public function encrypt(mixed $data, array $options = []): string |
| 143 | + { |
| 144 | + $options = $this->resolveOptions($options); |
| 145 | + $key = $this->requireKey($options); |
| 146 | + $serializerFlag = $this->serializerFlag($options); |
106 | 147 |
|
107 | | -## Getting Involved |
| 148 | + $payload = $this->serializePayload($data, $serializerFlag); |
| 149 | + // ... apply your primitive of choice, return a hex-encoded string ... |
| 150 | + } |
108 | 151 |
|
109 | | -> All contributions to this project will be published under the MIT License. By submitting a pull request or filing a bug, issue, or feature request, you are agreeing to comply with this waiver of copyright interest. |
| 152 | + public function decrypt(string $data, array $options = []): mixed |
| 153 | + { |
| 154 | + $options = $this->resolveOptions($options); |
| 155 | + $key = $this->requireKey($options); |
110 | 156 |
|
111 | | -There are two primary ways to help: |
| 157 | + // ... reverse the encoding, return $this->unserializePayload($plain, $flag) ... |
| 158 | + } |
| 159 | +} |
112 | 160 |
|
113 | | -- Using the issue tracker, and |
114 | | -- Changing the code-base. |
115 | | - |
116 | | -### Using the issue tracker |
| 161 | +// Use it via the factory just like the built-in handlers: |
| 162 | +$handler = \InitPHP\Encryption\Encrypt::use(\App\Crypto\MyHandler::class, [ |
| 163 | + 'key' => 'secret', |
| 164 | +]); |
| 165 | +``` |
117 | 166 |
|
118 | | -Use the issue tracker to suggest feature requests, report bugs, and ask questions. This is also a great way to connect with the developers of the project as well as others who are interested in this solution. |
| 167 | +`BaseHandler` gives you `resolveOptions()`, `requireKey()`, |
| 168 | +`serializerFlag()`, `serializePayload()` and `unserializePayload()` for free, |
| 169 | +so you only write the cryptographic glue. |
119 | 170 |
|
120 | | -Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in the issue that you will take on that effort, then follow the Changing the code-base guidance below. |
| 171 | +## Error Handling |
121 | 172 |
|
122 | | -### Changing the code-base |
| 173 | +Every failure path raises `InitPHP\Encryption\Exceptions\EncryptionException` |
| 174 | +(which extends `\RuntimeException`). A single `catch` covers everything: |
123 | 175 |
|
124 | | -Generally speaking, you should fork this repository, make changes in your own fork, and then submit a pull request. All new code should have associated unit tests that validate implemented features and the presence or lack of defects. Additionally, the code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of such guidelines, mimic the styles and patterns in the existing code-base. |
| 176 | +```php |
| 177 | +use InitPHP\Encryption\Exceptions\EncryptionException; |
125 | 178 |
|
126 | | -## Credits |
| 179 | +try { |
| 180 | + $plaintext = $handler->decrypt($incoming); |
| 181 | +} catch (EncryptionException $e) { |
| 182 | + // Bad input, tampered ciphertext, missing key, unsupported format |
| 183 | + // version, unknown cipher or hash algorithm, … |
| 184 | + $logger->warning('decrypt failed', ['reason' => $e->getMessage()]); |
| 185 | +} |
| 186 | +``` |
127 | 187 |
|
128 | | -- [Muhammet ŞAFAK](https://www.muhammetsafak.com.tr) |
| 188 | +Notable messages you may see: |
| 189 | + |
| 190 | +- `Unsupported ciphertext format version 0x01; expected 0x02. Ciphertexts produced by 1.x are not readable by 2.x.` |
| 191 | +- `HMAC verification failed; ciphertext is corrupted or has been tampered with.` |
| 192 | +- `Sodium decryption failed; ciphertext is corrupted or has been tampered with.` |
| 193 | +- `The "key" option is required and must be a non-empty string.` |
| 194 | +- `Unknown OpenSSL cipher "…".` |
| 195 | + |
| 196 | +## Security Notes |
| 197 | + |
| 198 | +- **Key management is your job.** Store the key outside the code repository — |
| 199 | + environment variable, secret manager, KMS, etc. — and rotate it like any |
| 200 | + other secret. |
| 201 | +- **Key strength matters.** The handler accepts any non-empty user key and |
| 202 | + derives one of the right length, but it cannot add entropy that the input |
| 203 | + does not contain. Use a random 256-bit string (`bin2hex(random_bytes(32))`) |
| 204 | + in production rather than a passphrase. |
| 205 | +- **Authentication is always on.** OpenSSL ciphertexts include an HMAC of the |
| 206 | + header, IV, and ciphertext; Sodium uses its built-in AEAD. There is no |
| 207 | + "encrypt without authenticate" mode. |
| 208 | +- **Format is versioned.** The first byte of every ciphertext identifies the |
| 209 | + format. A future major release that changes the layout will bump this byte |
| 210 | + and reject older ciphertexts with a clear error. |
| 211 | +- **Found something concerning?** See [SECURITY.md](./SECURITY.md) — please do |
| 212 | + not open a public issue for vulnerabilities. |
| 213 | + |
| 214 | +## Upgrading from 1.x |
| 215 | + |
| 216 | +Version 2.0 is a hard reset of the public surface and the on-wire format: |
| 217 | + |
| 218 | +- Minimum PHP version is now **8.1**. |
| 219 | +- Ciphertexts produced by 1.x **cannot be decrypted by 2.x.** Plan a |
| 220 | + re-encryption migration before upgrading. |
| 221 | +- The default payload serializer is JSON (was `serialize`). Pass |
| 222 | + `'serializer' => 'php_serialize'` to keep the old behaviour. |
| 223 | +- `Encrypt::create()` has been removed; use `Encrypt::use()`. |
| 224 | +- The Sodium handler no longer requires a 32-byte key — any non-empty string |
| 225 | + is now accepted and derived internally. |
| 226 | +- `ext-mbstring` is no longer required. |
| 227 | + |
| 228 | +A full migration walk-through lives in |
| 229 | +[`docs/08-migration-v1-to-v2.md`](./docs/08-migration-v1-to-v2.md) (shipped |
| 230 | +with the package; see also the [docs/](./docs/) index). |
| 231 | + |
| 232 | +## Contributing |
| 233 | + |
| 234 | +PRs are welcome. Please read [CONTRIBUTING.md](./CONTRIBUTING.md) first — it |
| 235 | +covers the local quality gates (`composer test`, `composer phpstan`, |
| 236 | +`composer cs-check`) and the security-review process for changes touching the |
| 237 | +cryptographic primitives. |
129 | 238 |
|
130 | 239 | ## License |
131 | 240 |
|
132 | | -Copyright © 2022 [MIT License](./LICENSE) |
| 241 | +MIT — see [LICENSE](./LICENSE). |
0 commit comments