├── 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](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | [![PHP Version](https://img.shields.io/badge/PHP-8.0%2B-blue)](https://www.php.net/) 5 | [![Issues](https://img.shields.io/github/issues/ramazancetinkaya/ObscuraCrypt?color=green)](https://github.com/ramazancetinkaya/ObscuraCrypt/issues) 6 | [![Stars](https://img.shields.io/github/stars/ramazancetinkaya/ObscuraCrypt?color=yellow)](https://github.com/ramazancetinkaya/ObscuraCrypt/stargazers) 7 | [![Forks](https://img.shields.io/github/forks/ramazancetinkaya/ObscuraCrypt?color=lightgrey)](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 | --------------------------------------------------------------------------------