├── composer.json
├── LICENSE
├── README.md
└── src
└── Obscura.php
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ramazancetinkaya/obscura",
3 | "description": "A multi-round polynomial-based encryption layer on top of AES-256 for enhanced security, without requiring GMP.",
4 | "type": "library",
5 | "keywords": [
6 | "php",
7 | "obscura",
8 | "aes",
9 | "encryption",
10 | "decryption",
11 | "polynomial",
12 | "cryptography",
13 | "data-protection"
14 | ],
15 | "license": "MIT",
16 | "authors": [
17 | {
18 | "name": "Ramazan Çetinkaya",
19 | "email": "ramazancetinkaya1337@protonmail.com",
20 | "homepage": "https://github.com/ramazancetinkaya"
21 | }
22 | ],
23 | "autoload": {
24 | "psr-4": {
25 | "ramazancetinkaya\\": "src/"
26 | }
27 | },
28 | "require": {
29 | "php": ">=8.0"
30 | },
31 | "minimum-stability": "stable",
32 | "prefer-stable": true
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ramazan Çetinkaya
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obscura - AES Cryptor
2 |
3 | [](LICENSE)
4 | [](https://www.php.net/)
5 | [](https://github.com/ramazancetinkaya/ObscuraCrypt/issues)
6 | [](https://github.com/ramazancetinkaya/ObscuraCrypt/stargazers)
7 | [](https://github.com/ramazancetinkaya/ObscuraCrypt/network)
8 |
9 | **Obscura** is an advanced PHP cryptography library that combines robust AES encryption (AES-256-CBC by default) with a multi-round polynomial-based custom encryption layer for enhanced security. It also supports URL-safe Base64 encoding when desired.
10 |
11 | Report a Bug
12 | ·
13 | New Pull Request
14 |
15 | > **No GMP extension needed** – everything is done via the **Extended Euclidean Algorithm** for modular arithmetic.
16 |
17 | > **Disclaimer:** This code is presented for educational purposes. For production environments, ensure that you have conducted a thorough security review and implemented best practices for key management, tamper detection, and environment-specific compliance requirements.
18 |
19 | ## ⭐ Support the Project
20 |
21 | If you find this library helpful, please consider giving it a star on GitHub. Your support helps improve and maintain the project. Thank you! 🌟
22 |
23 | ---
24 |
25 | ## Table of Contents
26 | - [Features](#features)
27 | - [Installation](#installation)
28 | - [Usage](#usage)
29 | - [Basic Example](#basic-example)
30 | - [URL-safe Base64 Example](#url-safe-base64-example)
31 | - [Contributing](#contributing)
32 | - [License](#license)
33 |
34 | ## Features
35 |
36 | - **AES-256-CBC**: Provides a strong, battle-tested encryption foundation via OpenSSL.
37 | - **Multi-Round Custom Layer**: Applies multiple polynomial transformations on each character, making cryptanalysis more challenging.
38 | - **No GMP Dependency**: Uses the Extended Euclidean Algorithm for modular inverse, so **GMP** is not required.
39 | - **URL-Safe Base64 Option**: Encrypt once, share anywhere. Prevents `+`, `/`, and `=` characters from breaking URLs.
40 | - **Strict Typing**: Leverages PHP 8.0+ features and enforces strict typing for reliability.
41 | - **Custom Exceptions**: Throws a `CryptoException` for all encryption/decryption errors, making error handling straightforward.
42 |
43 | ## Installation
44 |
45 | You can install the `Obscura` library using [Composer](https://getcomposer.org/). Run the following command in your terminal:
46 |
47 | ```bash
48 | composer require ramazancetinkaya/obscura
49 | ```
50 |
51 | This command adds the library to your composer.json and installs it in the vendor/ directory.
52 |
53 | ## Usage
54 |
55 | Below are quick examples demonstrating how to use Obscura in your PHP project.
56 |
57 | ### Basic Example
58 |
59 | ```php
60 | encrypt($plainText);
77 | echo "Encrypted (base64): " . $encrypted . PHP_EOL;
78 |
79 | // Decrypt
80 | $decrypted = $obscura->decrypt($encrypted);
81 | echo "Decrypted: " . $decrypted . PHP_EOL;
82 |
83 | } catch (ObscuraException $e) {
84 | echo "Error: " . $e->getMessage();
85 | }
86 | ```
87 |
88 | ### URL-safe Base64 Example
89 |
90 | If you prefer URL-safe Base64 encoding (e.g. for inclusion in GET parameters), you can enable it via the constructor:
91 |
92 | ```php
93 | encrypt($plainUrl);
110 | echo "Encrypted (URL-safe): " . $encryptedUrl . PHP_EOL;
111 |
112 | // Decrypt
113 | $decryptedUrl = $obscuraSafe->decrypt($encryptedUrl);
114 | echo "Decrypted URL: " . $decryptedUrl . PHP_EOL;
115 |
116 | } catch (ObscuraException $e) {
117 | echo "Error: " . $e->getMessage();
118 | }
119 | ```
120 |
121 | ## Contributing
122 |
123 | Contributions are welcome! Please feel free to submit a pull request or open an issue for any enhancements or bug fixes.
124 |
125 | ## License
126 |
127 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
128 |
--------------------------------------------------------------------------------
/src/Obscura.php:
--------------------------------------------------------------------------------
1 | $rounds
70 | * Each round is defined by an associative array with 'a' and 'b' coefficients.
71 | * We apply multiple passes in encryption, and reverse them in decryption.
72 | */
73 | private array $rounds = [
74 | ['a' => 5, 'b' => 11], // Round 1
75 | ['a' => 13, 'b' => 19], // Round 2
76 | ['a' => 17, 'b' => 23], // Round 3
77 | ['a' => 29, 'b' => 31], // Round 4
78 | ];
79 |
80 | /**
81 | * Constructor
82 | *
83 | * @param string $secretKey The secret key for AES encryption
84 | * @param string $cipherMethod The OpenSSL cipher method (e.g., 'aes-256-cbc')
85 | * @param bool $useUrlSafeBase64 Toggle for URL-safe base64 (default: false)
86 | *
87 | * @throws ObscuraException If the provided cipher method is invalid or not supported.
88 | */
89 | public function __construct(
90 | string $secretKey,
91 | string $cipherMethod = 'aes-256-cbc',
92 | bool $useUrlSafeBase64 = false
93 | ) {
94 | // Validate cipher method
95 | if (!in_array($cipherMethod, openssl_get_cipher_methods(), true)) {
96 | throw new ObscuraException('Invalid or unsupported cipher method provided.');
97 | }
98 |
99 | // Initialize properties
100 | $this->secretKey = $secretKey;
101 | $this->cipherMethod = $cipherMethod;
102 | $this->useUrlSafeBase64 = $useUrlSafeBase64;
103 | $this->ivLength = openssl_cipher_iv_length($this->cipherMethod);
104 |
105 | if ($this->ivLength === false) {
106 | throw new ObscuraException('Failed to get IV length for the specified cipher method.');
107 | }
108 |
109 | // Basic validation for the chosen prime
110 | if ($this->prime <= 1) {
111 | throw new ObscuraException('Prime must be greater than 1.');
112 | }
113 |
114 | // Check if all 'a' coefficients are invertible mod prime (via gcd check)
115 | foreach ($this->rounds as $round) {
116 | // Basic GCD approach without GMP
117 | $gcd = $this->basicGcd($round['a'], $this->prime);
118 | if ($gcd !== 1) {
119 | throw new ObscuraException(sprintf(
120 | "Coefficient a=%d is not invertible mod %d. Make sure gcd(a, prime) = 1.",
121 | $round['a'],
122 | $this->prime
123 | ));
124 | }
125 | }
126 | }
127 |
128 | /**
129 | * Encrypt
130 | *
131 | * @param string $plainText The plaintext to be encrypted.
132 | *
133 | * @return string Encrypted string (base64 or URL-safe base64).
134 | *
135 | * @throws ObscuraException If encryption fails at any stage.
136 | */
137 | public function encrypt(string $plainText): string
138 | {
139 | try {
140 | // 1. Multi-round custom encryption
141 | $customEncrypted = $this->customEncrypt($plainText);
142 |
143 | // 2. AES encryption
144 | $iv = random_bytes($this->ivLength);
145 | $rawCipher = openssl_encrypt(
146 | $customEncrypted,
147 | $this->cipherMethod,
148 | $this->adjustKeyLength($this->secretKey),
149 | OPENSSL_RAW_DATA,
150 | $iv
151 | );
152 |
153 | if ($rawCipher === false) {
154 | throw new ObscuraException('AES encryption failed.');
155 | }
156 |
157 | // 3. Combine IV with cipher text
158 | $combined = $iv . $rawCipher;
159 |
160 | // 4. Encode (normal base64 or URL-safe base64)
161 | $encoded = base64_encode($combined);
162 | if ($this->useUrlSafeBase64) {
163 | // Make it URL-safe by replacing +, /, = with -, _, '' respectively
164 | $encoded = str_replace(['+', '/', '='], ['-', '_', ''], $encoded);
165 | }
166 |
167 | return $encoded;
168 | } catch (\Throwable $e) {
169 | throw new ObscuraException('Encryption process failed: ' . $e->getMessage());
170 | }
171 | }
172 |
173 | /**
174 | * Decrypt
175 | *
176 | * @param string $cipherText The ciphertext to be decrypted (base64 or URL-safe base64).
177 | *
178 | * @return string Decrypted plain text.
179 | *
180 | * @throws ObscuraException If decryption fails at any stage.
181 | */
182 | public function decrypt(string $cipherText): string
183 | {
184 | try {
185 | // 1. Convert from URL-safe to normal base64 if needed
186 | if ($this->useUrlSafeBase64) {
187 | $cipherText = str_replace(['-', '_'], ['+', '/'], $cipherText);
188 | // Attempt to restore '=' padding
189 | $padding = 4 - (strlen($cipherText) % 4);
190 | if ($padding < 4) {
191 | $cipherText .= str_repeat('=', $padding);
192 | }
193 | }
194 |
195 | // 2. Base64 decode
196 | $decodedCipher = base64_decode($cipherText, true);
197 | if ($decodedCipher === false) {
198 | throw new ObscuraException('Base64 decoding of cipher text failed.');
199 | }
200 |
201 | // 3. Extract IV and raw cipher text
202 | $iv = mb_substr($decodedCipher, 0, $this->ivLength, '8bit');
203 | $rawCipher = mb_substr($decodedCipher, $this->ivLength, null, '8bit');
204 |
205 | if (strlen($iv) !== $this->ivLength) {
206 | throw new ObscuraException('Invalid IV length. Possible corruption or manipulation.');
207 | }
208 |
209 | // 4. AES decryption
210 | $decrypted = openssl_decrypt(
211 | $rawCipher,
212 | $this->cipherMethod,
213 | $this->adjustKeyLength($this->secretKey),
214 | OPENSSL_RAW_DATA,
215 | $iv
216 | );
217 |
218 | if ($decrypted === false) {
219 | throw new ObscuraException('AES decryption failed.');
220 | }
221 |
222 | // 5. Multi-round custom decryption
223 | $plainText = $this->customDecrypt($decrypted);
224 |
225 | return $plainText;
226 | } catch (\Throwable $e) {
227 | throw new ObscuraException('Decryption process failed: ' . $e->getMessage());
228 | }
229 | }
230 |
231 | /**
232 | * customEncrypt
233 | *
234 | * Applies multiple polynomial transformations in sequence to each character:
235 | * Round i: x -> (a_i*x + b_i) mod prime
236 | *
237 | * @param string $plainText The original plaintext.
238 | *
239 | * @return string The transformed string after all rounds.
240 | */
241 | private function customEncrypt(string $plainText): string
242 | {
243 | $chars = mb_str_split($plainText, 1, '8bit');
244 |
245 | $transformed = array_map(function ($char) {
246 | $x = ord($char);
247 | // Apply each round in sequence
248 | foreach ($this->rounds as $round) {
249 | $x = ($round['a'] * $x + $round['b']) % $this->prime;
250 | }
251 | return chr($x);
252 | }, $chars);
253 |
254 | return implode('', $transformed);
255 | }
256 |
257 | /**
258 | * customDecrypt
259 | *
260 | * Reverses the multiple polynomial transformations in reverse order:
261 | * Round i: x -> ( (x - b_i) * a_i^-1 ) mod prime
262 | *
263 | * @param string $encryptedText The text to be reversed from the custom transformations.
264 | *
265 | * @return string The original plaintext prior to the custom encryption.
266 | */
267 | private function customDecrypt(string $encryptedText): string
268 | {
269 | $chars = mb_str_split($encryptedText, 1, '8bit');
270 | $reversedRounds = array_reverse($this->rounds);
271 |
272 | $reversed = array_map(function ($char) use ($reversedRounds) {
273 | $x = ord($char);
274 | foreach ($reversedRounds as $round) {
275 | $aInverse = $this->modInverse($round['a'], $this->prime);
276 | $x = ($x - $round['b']) % $this->prime;
277 | if ($x < 0) {
278 | $x += $this->prime;
279 | }
280 | $x = ($x * $aInverse) % $this->prime;
281 | }
282 | return chr($x);
283 | }, $chars);
284 |
285 | return implode('', $reversed);
286 | }
287 |
288 | /**
289 | * adjustKeyLength
290 | *
291 | * Ensures the key is 32 bytes by hashing with SHA-256.
292 | *
293 | * @param string $key The user-supplied key.
294 | *
295 | * @return string 32-byte key for AES-256.
296 | */
297 | private function adjustKeyLength(string $key): string
298 | {
299 | return hash('sha256', $key, true);
300 | }
301 |
302 | /**
303 | * modInverse
304 | *
305 | * Computes modular inverse of a under modulo m using the Extended Euclidean Algorithm.
306 | *
307 | * @param int $a The integer to invert.
308 | * @param int $m The modulo.
309 | *
310 | * @return int The modular inverse of a mod m.
311 | *
312 | * @throws ObscuraException If no modular inverse exists (gcd != 1).
313 | */
314 | private function modInverse(int $a, int $m): int
315 | {
316 | // Extended Euclidean Algorithm to find x, y such that:
317 | // a*x + m*y = gcd(a, m) => a*x ≡ gcd(a, m) (mod m)
318 | // We want gcd(a, m) = 1 for invertibility, and a*x ≡ 1 (mod m).
319 |
320 | $a = $a % $m;
321 | [$gcd, $x] = $this->extendedGcd($a, $m);
322 |
323 | if ($gcd !== 1) {
324 | throw new ObscuraException("No modular inverse; gcd($a, $m) = $gcd != 1.");
325 | }
326 |
327 | // x might be negative, so normalize it in the range [0..m-1]
328 | $modInv = $x % $m;
329 | if ($modInv < 0) {
330 | $modInv += $m;
331 | }
332 |
333 | return $modInv;
334 | }
335 |
336 | /**
337 | * basicGcd
338 | *
339 | * A simple function to compute GCD without GMP.
340 | *
341 | * @param int $a
342 | * @param int $b
343 | *
344 | * @return int gcd(a, b)
345 | */
346 | private function basicGcd(int $a, int $b): int
347 | {
348 | while ($b !== 0) {
349 | $temp = $b;
350 | $b = $a % $b;
351 | $a = $temp;
352 | }
353 | return abs($a);
354 | }
355 |
356 | /**
357 | * extendedGcd
358 | *
359 | * The Extended Euclidean Algorithm. Returns [gcd, x, y] such that:
360 | * gcd(a, b) = a*x + b*y
361 | * For our usage, we'll only return [gcd, x].
362 | *
363 | * @param int $a
364 | * @param int $b
365 | *
366 | * @return array{0: int, 1: int} An array containing gcd(a,b) and the coefficient x.
367 | */
368 | private function extendedGcd(int $a, int $b): array
369 | {
370 | if ($b === 0) {
371 | return [$a, 1]; // gcd(a,0)=a => a*1 + 0*0
372 | }
373 |
374 | $x0 = 1;
375 | $x1 = 0;
376 | $y0 = 0;
377 | $y1 = 1;
378 |
379 | $aa = $a;
380 | $bb = $b;
381 |
382 | while ($bb !== 0) {
383 | $q = intdiv($aa, $bb);
384 |
385 | $temp = $bb;
386 | $bb = $aa % $bb;
387 | $aa = $temp;
388 |
389 | $temp = $x1;
390 | $x1 = $x0 - $q * $x1;
391 | $x0 = $temp;
392 |
393 | $temp = $y1;
394 | $y1 = $y0 - $q * $y1;
395 | $y0 = $temp;
396 | }
397 |
398 | // gcd is aa
399 | // x0, y0 are the final coefficients
400 | return [$aa, $x0];
401 | }
402 | }
403 |
--------------------------------------------------------------------------------