├── .github └── workflows │ └── ci-php82.yaml ├── .gitignore ├── LICENSE.md ├── README.md ├── build └── report.junit.xml ├── composer.json ├── config └── env-security.php ├── phpunit.xml ├── src ├── Console │ ├── Concerns │ │ └── HandlesEnvFiles.php │ ├── Decrypt.php │ ├── Edit.php │ └── Encrypt.php ├── Drivers │ ├── GoogleKmsDriver.php │ └── KmsDriver.php ├── EnvSecurityFacade.php ├── EnvSecurityManager.php └── EnvSecurityServiceProvider.php └── tests ├── CompressionTest.php ├── DecryptDouble.php ├── DecryptTest.php ├── EditDouble.php ├── EditTest.php ├── EncryptTest.php ├── ManagerTest.php ├── ServiceProviderDouble.php └── TestCase.php /.github/workflows/ci-php82.yaml: -------------------------------------------------------------------------------- 1 | name: CI PHP 8.2 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - uses: php-actions/composer@v6 13 | with: 14 | php_version: "8.2" 15 | 16 | - name: PHPUnit Tests 17 | uses: php-actions/phpunit@v3 18 | env: 19 | APP_ENV: testing 20 | APP_KEY: zbcmF4pUB5v4ShHiq6kJbWTQyiT8CUyw 21 | with: 22 | bootstrap: vendor/autoload.php 23 | configuration: phpunit.xml 24 | args: --coverage-text 25 | php_version: "8.2" 26 | version: "9" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /output/ 3 | /tests/store/ 4 | .idea 5 | .env 6 | .env.txt.enc 7 | .env.txt 8 | .php_cs.cache 9 | .php_cs_laravel 10 | .phpunit* 11 | composer.lock 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Signature Tech Studio, Inc 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 13 | > all 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 21 | > THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Securely manage Laravel .env files 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/stechstudio/laravel-env-security.svg?style=flat-square)](https://packagist.org/packages/stechstudio/laravel-env-security) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/stechstudio/laravel-env-security.svg?style=flat-square)](https://packagist.org/packages/stechstudio/laravel-env-security) 6 | ![Build](https://img.shields.io/github/actions/workflow/status/stechstudio/laravel-env-security/ci-php82.yaml?style=flat-square) 7 | 8 | This package helps you manage .env files for different deployment environments. Each .env file is securely encrypted and kept in your app's version control. When you deploy your app the appropriate environment-specific .env file is decrypted and moved into place. 9 | 10 | This was partly inspired by [Marcel's excellent credentials package](https://github.com/beyondcode/laravel-credentials). If you want to manage your credentials in a json file, with Laravel handling the encryption/decryption, and only have one deployment environment, that package may be a better fit for your needs. 11 | 12 | Our package is different in the following ways: 13 | 14 | 1) We wanted to work with .env files directly and have the decrypted .env file end up where Laravel already expects it. The app makes use of the .env variables just like it normally would. 15 | 2) We need to manage .env files for multiple environments (like qa, uat, production). This package allows you to manage any number of environment-specific .env files. 16 | 3) We wanted to leverage services like AWS Key Management Service to handle encryption/decryption, with the option to add other encryption drivers (like ansible) or secrets management services (like AWS Secrets Manager) in the future. 17 | 18 | > ### Note 19 | > Laravel v9.32.0 introduced new `env:encrypt` and `env:decrypt` commands, which conflicted with the commands in this package. We have moved our commands in v2 to `env:store` and `env:fetch`. 20 | 21 | ## Installation and Setup 22 | ### Prerequisites 23 | If you intend to enable compression you must have the [Zlib Compression Extension](https://www.php.net/manual/en/book.zlib.php) installed and enabled. 24 | 25 | ### Install the package 26 | 27 | `composer require stechstudio/laravel-env-security` 28 | 29 | ### Add the composer hook 30 | 31 | In your composer.json file add `php artisan env:fetch` as a post-install hook. You likely already have a `post-install-cmd` section, add the new command so it looks like this: 32 | 33 | ``` 34 | "scripts": { 35 | "post-install-cmd": [ 36 | ... 37 | "php artisan env:fetch" 38 | ] 39 | ``` 40 | 41 | ### Generate configuration (optional) 42 | 43 | Default configuration is based on environment variables (e.g. driver, store env, destination file, aws, gcp). If you need to customize it you can publish the config by running: 44 | 45 | `php artisan vendor:publish --provider="STS\EnvSecurity\EnvSecurityServiceProvider" --tag="config"` 46 | 47 | ### Setup service provider (older versions of Laravel) 48 | 49 | If you are using a version of Laravel earlier than 5.5, you will need to manually add the service provider to your `config/app.php` file: 50 | 51 | ```php 52 | 'providers' => [ 53 | ... 54 | STS\EnvSecurity\EnvSecurityServiceProvider::class, 55 | ] 56 | ``` 57 | ### Configuration Settings 58 | | Configuration Key | Environment Variable | Default | Description | 59 | |--------------------------------------------|--------------------------------------------------|----------------|-------------------------------------------------------------| 60 | | `env-security.default` | **ENV_DRIVER** | `kms` | The default driver. | 61 | | `env-security.editor` | **EDITOR** | `vi` | Preferred text editor. | 62 | | `env-security.store` | **ENV_STORAGE_PATH** | `env` | The directory where should we keep the encrypted .env files | 63 | | `env-security.destination` | **ENV_DESTINATION_FILE** | `.env` | This is where we will put the decrypted .env file | 64 | | `env-security.enable_compression` | **ENV_COMPRESSION** | `false` | Should data be compressed prior to encrypting it? | 65 | | `env-security.divers.kms.key_id` | **AWS_KMS_KEY** | `null` | An AWS Key ID or Alias | 66 | | `env-security.drivers.kms.region` | **AWS_KMS_REGION** | `null` | An AWS Region the key is in. | 67 | | `env-security.drivers.google_kms.project` | **GOOGLE_KMS_PROJECT**, **GOOGLE_CLOUD_PROJECT** | `null`, `null` | A Google CLoud Project Identifier | 68 | | `env-security.drivers.google_kms.location` | **GOOGLE_KMS_LOCATION** | `global` | A Google Cloud KMS Location | 69 | | `env-security.drivers.google_kms.key_ring` | **GOOGLE_KMS_KEY_RING** | `null` | A Google Cloud KMS Key Ring | 70 | | `env-security.drivers.google_kms.key_id` | **GOOGLE_KMS_KEY** | `null` | A Google CLoud KMS Key | 71 | 72 | ## Environment resolutionss 73 | In order for this package to decrypt the correct .env file when you deploy, you need to tell it how to figure out the environment. 74 | 75 | By default it will look for a `APP_ENV` environment variable. However you can provide your own custom resolver with a callback function. Put this in your `AppServiceProvider`'s `boot` method: 76 | 77 | ```php 78 | \EnvSecurity::resolveEnvironmentUsing(function() { 79 | // return the environment name 80 | }); 81 | ``` 82 | 83 | This way you can resolve out the environment based on hostname, an EC2 instance tag, etc. This will then decrypt the correct .env file based on the environment name you return. 84 | 85 | ## Key name resolution 86 | 87 | Normally we expect your key name to be specified in the .env file. However you may want to specify _different_ keys depending on the environment. If you have, say, different restricted AWS IAM credentials setup for your environments, this would allow you to ensure each .env file can only be decrypted in the appropriate environment. 88 | 89 | Of course your own local IAM credentials would still need full access to all keys, so that you can edit each .env file locally. 90 | 91 | To resolve the key name, provide a callback function like this: 92 | 93 | ```php 94 | \EnvSecurity::resolveKeyUsing(function($environment) { 95 | return "myapp-$environment"; 96 | }); 97 | ``` 98 | 99 | Notice that your resolver will receive the already-resolved environment name, so you can use this to help figure out which key name to return. 100 | 101 | ## Drivers 102 | 103 | ### AWS Key Management Service 104 | 105 | AWS KMS is a managed service that makes it easy for you to create and control the encryption keys used to encrypt your data, and uses FIPS 140-2 validated hardware security modules to protect the security of your keys. 106 | 107 | To use this driver set `ENV_DRIVER=kms` in your .env file. 108 | 109 | In the [AWS Console](https://console.aws.amazon.com/iam/home?#/encryptionKeys) create your encryption key. Make sure your AWS IAM user has `kms:Encrypt` and `kms:Decrypt` permissions on this key. 110 | 111 | Copy the Key ID and store it as `AWS_KMS_KEY` in your local .env file. As you setup environment-specific .env files, make sure to include this `AWS_KMS_KEY` in each .env file. 112 | 113 | ### Google Cloud Key Management Service 114 | 115 | Google KMS securely manages encryption keys and secrets on Google Cloud Platform. The Google KMS integration with Google HSM makes it simple to create a key protected by a FIPS 140-2 Level 3 device. 116 | 117 | To use this driver set `ENV_DRIVER=google_kms` in your .env file. 118 | 119 | In the [Google Cloud Console](https://console.cloud.google.com/security/kms) create your key ring and key. Make sure your Google IAM user has the `Cloud KMS CryptoKey Encrypter/Decrypter` role for this key. 120 | 121 | Copy the Project, Key Ring and Key storing them as `GOOGLE_KMS_PROJECT`, `GOOGLE_KMS_KEY_RING` and `GOOGLE_KMS_KEY` in your local .env file. As you setup environment-specific .env files, make sure to include these keys in each .env file. 122 | 123 | ## Usage 124 | 125 | #### Create/edit a .env file 126 | Run `php artisan env:edit [name]` where `[name]` is the environment you wish to create or edit. This will open the file in `vi` for you to edit. Modify something 127 | in the file, save, and quit. 128 | 129 | _Use the `EDITOR` environment variable to set your preferred editor._ 130 | 131 | #### Decrypt your .env 132 | Now you can run `php artisan env:fetch [name]` which will decrypt the ciphertext file you edited, and write the 133 | plaintext to your `.env`, replacing anything that was in it. Now if you look at your `.env` you should see your edit. 134 | 135 | If no environment `[name]` is provided, the environment will be determined by your own custom resolution callback or the `APP_ENV` environment variable. 136 | 137 | #### Encrypt and save your current .env 138 | Sometimes you may want to take your current .env file and encrypt exactly as-is. 139 | 140 | Run `php artisan env:store [name]` to do this, where `name` is the name of the encrypted environment file you wish to create. If you don't provide a name, your current `APP_ENV` environment name will be used. 141 | 142 | ## First deploy 143 | 144 | As you're reading through this, you're probably wondering how that *first initial* deploy is going to work. In order for this package to decrypt your .env config where all your sensitive credentials are stored, it needs account credentials with permission to your KMS key. 145 | 146 | Yep, it's [turtles all the way down](https://en.wikipedia.org/wiki/Turtles_all_the_way_down). 147 | 148 | There are a number of ways to handle this, all dependent on the environment and deployment process. 149 | 150 | 1. If you are using AWS EC2, you can [assign IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html#roles-usingrole-ec2instance-roles) to grant the instance access to your KMS key. 151 | 2. For AWS, you can always put a `~/.aws/credentials` file on the server to provide necessary AWS permissions, regardless of your host. 152 | 3. For GCP your project ID and credentials are [discovered automatically](https://github.com/googleapis/google-cloud-php/blob/master/AUTHENTICATION.md#google-cloud-platform-environments). 153 | 3. Many deployment services like [Laravel Forge](https://forge.laravel.com/) or [Laravel Envoyer](https://envoyer.io/) provide ways to specify environment variables which you can use to provide credentials. 154 | 4. And of course, you can always just ssh in manually to a fresh new server and put the necessary environment variables in a temporary .env file as well, which will get overwritten on the first deploy. 155 | -------------------------------------------------------------------------------- /build/report.junit.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stechstudio/laravel-env-security/31d9932e1dc7d0938e3959642f953ac3981ea8e6/build/report.junit.xml -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stechstudio/laravel-env-security", 3 | "description": "Securely manage .env files for different deployment environments", 4 | "type": "library", 5 | "require": { 6 | "php": "^5.6|^7.0|^8.0", 7 | "aws/aws-sdk-php": "^3.0", 8 | "illuminate/support": "^5.6|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 9 | "google/cloud-kms": "^1.7" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^9.0", 13 | "orchestra/testbench": "^7.0|^8.0|^9.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "STS\\EnvSecurity\\": "src" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Tests\\": "tests" 23 | } 24 | }, 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "STS\\EnvSecurity\\EnvSecurityServiceProvider" 29 | ], 30 | "aliases": { 31 | "EnvSecurity": "STS\\EnvSecurity\\EnvSecurityFacade" 32 | } 33 | } 34 | }, 35 | "license": "MIT", 36 | "authors": [ 37 | { 38 | "name": "Bubba", 39 | "email": "rob@stechstudio.com" 40 | }, 41 | { 42 | "name": "Joseph Szobody", 43 | "email": "joseph@stechstudio.com" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /config/env-security.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE.md 8 | * file that was distributed with this source code. 9 | */ 10 | return [ 11 | /** 12 | * This is the default driver we'll use to manage encryption/decryption 13 | */ 14 | 'default' => env('ENV_DRIVER', 'kms'), 15 | 16 | /** 17 | * Specify the preferred text editor on your system 18 | */ 19 | 'editor' => env('EDITOR', 'vi'), 20 | 21 | /** 22 | * The directory where should we keep the encrypted .env files 23 | */ 24 | 'store' => base_path(env('ENV_STORAGE_PATH', 'env')), 25 | 26 | /** 27 | * This is where we will put the decrypted .env file 28 | */ 29 | 'destination' => base_path(env('ENV_DESTINATION_FILE', '.env')), 30 | 31 | /** 32 | * Should data be compressed prior to encrypting it? 33 | */ 34 | 'enable_compression' => env('ENV_COMPRESSION', false), 35 | 36 | 'drivers' => [ 37 | 'kms' => [ 38 | 'version' => 'latest', 39 | 'key_id' => env('AWS_KMS_KEY'), 40 | 'region' => env('AWS_KMS_REGION', env('AWS_REGION', 'us-east-1')), 41 | ], 42 | 43 | 'google_kms' => [ 44 | 'project' => env('GOOGLE_KMS_PROJECT', env('GOOGLE_CLOUD_PROJECT')), 45 | 'location' => env('GOOGLE_KMS_LOCATION', 'global'), 46 | 'key_ring' => env('GOOGLE_KMS_KEY_RING'), 47 | 'key_id' => env('GOOGLE_KMS_KEY'), 48 | ], 49 | ] 50 | ]; 51 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Console/Concerns/HandlesEnvFiles.php: -------------------------------------------------------------------------------- 1 | getFilePathForEnvironment($environment); 19 | 20 | if (file_exists($path) && is_readable($path)) { 21 | return file_get_contents($path); 22 | } 23 | 24 | return null; 25 | } 26 | 27 | /** 28 | * @param $ciphertext 29 | * @param $environment 30 | * 31 | * @return bool 32 | */ 33 | protected function saveEncrypted($ciphertext, $environment) 34 | { 35 | $path = $this->getFilePathForEnvironment($environment); 36 | 37 | return file_put_contents($path, $ciphertext) !== false; 38 | } 39 | 40 | /** 41 | * @param $plaintext 42 | * @param null $output 43 | * 44 | * @return bool 45 | */ 46 | protected function saveDecrypted($plaintext, $output = null) 47 | { 48 | if (!$output) { 49 | $output = config('env-security.destination'); 50 | } 51 | 52 | return file_put_contents($output, $plaintext) !== false; 53 | } 54 | 55 | /** 56 | * @param $environment 57 | * 58 | * @return string 59 | */ 60 | protected function getFilePathForEnvironment($environment) 61 | { 62 | return config('env-security.store') . '/' . $environment . '.env.enc'; 63 | } 64 | } -------------------------------------------------------------------------------- /src/Console/Decrypt.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE.md 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace STS\EnvSecurity\Console; 12 | 13 | use STS\EnvSecurity\Console\Concerns\HandlesEnvFiles; 14 | use Illuminate\Console\Command; 15 | use STS\EnvSecurity\EnvSecurityManager; 16 | 17 | 18 | /** 19 | * Class Decrypt 20 | * @package STS\EnvSecurity\Console 21 | */ 22 | class Decrypt extends Command 23 | { 24 | use HandlesEnvFiles; 25 | 26 | /** 27 | * The name and signature of the console command. 28 | * 29 | * @var string 30 | */ 31 | protected $signature = 'env:fetch 32 | {environment? : Which environment file you wish to decrypt} 33 | {--o|out= : Saves the decrypted file to an alternate location}'; 34 | 35 | /** 36 | * The console command description. 37 | * 38 | * @var string 39 | */ 40 | protected $description = 'Retrieve and decrypt a .env file. Tries to deduce the environment if none provided.'; 41 | 42 | /** 43 | * @var EnvSecurityManager 44 | */ 45 | protected $envSecurity; 46 | 47 | public function __construct(EnvSecurityManager $envSecurity) 48 | { 49 | $this->envSecurity = $envSecurity; 50 | 51 | parent::__construct(); 52 | } 53 | 54 | /** 55 | * Execute the console command. 56 | * 57 | * @return mixed 58 | */ 59 | public function handle() 60 | { 61 | $this->envSecurity->setEnvironment($this->environment()); 62 | 63 | if (!$environment = $this->environment()) { 64 | $this->error("No environment specified, and we couldn't resolve it on our own"); 65 | 66 | return 1; 67 | } 68 | 69 | if (!$ciphertext = $this->loadEncrypted($environment)) { 70 | $this->error("Unable to load encrypted .env file for environment [$environment]"); 71 | 72 | return 1; 73 | } 74 | 75 | $plaintext = $this->envSecurity->decrypt($ciphertext); 76 | 77 | if (!$this->saveDecrypted($plaintext, $this->option('out'))) { 78 | $this->error("Unable to save decrypted .env file"); 79 | 80 | return 1; 81 | } 82 | 83 | $this->info("Successfully decrypted .env for environment [$environment]"); 84 | } 85 | 86 | /** 87 | * @return array|string 88 | */ 89 | protected function environment() 90 | { 91 | return is_null($this->argument('environment')) 92 | ? $this->envSecurity->resolveEnvironment() 93 | : $this->argument('environment'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Console/Edit.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE.md 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace STS\EnvSecurity\Console; 12 | 13 | use Illuminate\Console\Command; 14 | use Illuminate\Support\Facades\Config; 15 | use STS\EnvSecurity\Console\Concerns\HandlesEnvFiles; 16 | use STS\EnvSecurity\EnvSecurityManager; 17 | use Symfony\Component\Process\Process; 18 | 19 | /** 20 | * Class Edit 21 | * @package STS\EnvSecurity\Console 22 | */ 23 | class Edit extends Command 24 | { 25 | use HandlesEnvFiles; 26 | 27 | /** 28 | * The name and signature of the console command. 29 | * 30 | * @var string 31 | */ 32 | protected $signature = 'env:edit 33 | {environment : Which environment file you wish to edit} 34 | {--C|compress : Override configuration and require compression.}'; 35 | 36 | /** 37 | * The console command description. 38 | * 39 | * @var string 40 | */ 41 | protected $description = 'Edit an encrypted env file'; 42 | 43 | /** 44 | * @var EnvSecurityManager 45 | */ 46 | protected $envSecurity; 47 | 48 | public function __construct(EnvSecurityManager $envSecurity) 49 | { 50 | $this->envSecurity = $envSecurity; 51 | 52 | parent::__construct(); 53 | } 54 | 55 | /** 56 | * Execute the console command. 57 | * 58 | * @return mixed 59 | */ 60 | public function handle() 61 | { 62 | if ($this->option('compress')) { 63 | Config::set('env-security.enable_compression', true); 64 | } 65 | $this->envSecurity->setEnvironment($this->environment()); 66 | 67 | $this->saveEnvContents( 68 | $this->edit( 69 | $this->loadEnvContents() 70 | ) 71 | ); 72 | 73 | $this->info("Successfully updated .env for environment [{$this->environment()}]"); 74 | } 75 | 76 | /** 77 | * @param $contents 78 | * @codeCoverageIgnore 79 | * 80 | * @return mixed 81 | */ 82 | protected function edit($contents) 83 | { 84 | $tmpFile = tmpfile(); 85 | fwrite($tmpFile, $contents); 86 | $meta = stream_get_meta_data($tmpFile); 87 | 88 | $process = new Process([config('env-security.editor'), $meta['uri']]); 89 | $process->setTimeout(null); 90 | $process->setTty(Process::isTtySupported()); 91 | $process->mustRun(); 92 | if (!Process::isTtySupported()) { 93 | while (empty(file_get_contents($meta['uri'])) || file_get_contents($meta['uri']) === $contents) { 94 | sleep(2); 95 | } 96 | } 97 | 98 | return file_get_contents($meta['uri']); 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | protected function environment() 105 | { 106 | return $this->argument('environment'); 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | protected function loadEnvContents() 113 | { 114 | if ($ciphertext = $this->loadEncrypted($this->environment())) { 115 | return $this->envSecurity->decrypt($ciphertext); 116 | } 117 | 118 | return ''; 119 | } 120 | 121 | /** 122 | * @param $plaintext 123 | */ 124 | protected function saveEnvContents($plaintext) 125 | { 126 | $ciphertext = !empty($plaintext) 127 | ? $this->envSecurity->encrypt($plaintext) 128 | : ''; 129 | 130 | $this->saveEncrypted($ciphertext, $this->environment()); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Console/Encrypt.php: -------------------------------------------------------------------------------- 1 | envSecurity = $envSecurity; 34 | 35 | parent::__construct(); 36 | } 37 | 38 | 39 | /** 40 | * Execute the console command. 41 | * 42 | * @return mixed 43 | */ 44 | public function handle() 45 | { 46 | 47 | if ($this->option('compress')) { 48 | Config::set('env-security.enable_compression', true); 49 | } 50 | if (!File::isReadable($this->path())) { 51 | return $this->error("Make sure you have a .env file in your base project path"); 52 | } 53 | 54 | $this->saveEncrypted( 55 | $this->envSecurity->encrypt(file_get_contents($this->path())), 56 | $this->environment() 57 | ); 58 | 59 | $this->info("Saved the contents of your current .env file for environment [{$this->environment()}]"); 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | protected function environment() 66 | { 67 | return $this->argument('environment') ?: config('app.env'); 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | protected function path() 74 | { 75 | return base_path('.env'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Drivers/GoogleKmsDriver.php: -------------------------------------------------------------------------------- 1 | client = $client; 36 | $this->keyName = $client::cryptoKeyName($project, $location, $keyRing, $key); 37 | } 38 | 39 | /** 40 | * @param string $value 41 | * @param bool $serialize 42 | * 43 | * @return mixed|string 44 | */ 45 | public function encrypt($value, $serialize = true) 46 | { 47 | $result = $this->client->encrypt($this->keyName, $value)->getCiphertext(); 48 | 49 | return ($serialize) 50 | ? base64_encode($result) 51 | : $result; 52 | } 53 | 54 | /** 55 | * @param string $payload 56 | * @param bool $unserialize 57 | * 58 | * @return string 59 | */ 60 | public function decrypt($payload, $unserialize = true) 61 | { 62 | if ($unserialize) { 63 | $payload = base64_decode($payload); 64 | } 65 | 66 | return $this->client->decrypt($this->keyName, $payload)->getPlaintext(); 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function getKey() 73 | { 74 | // We have no key to return. This exists purely to comply with the interface. 75 | return ''; 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function getAllKeys() 82 | { 83 | // We have no keys to return. This exists purely to comply with the interface. 84 | return []; 85 | } 86 | 87 | /** 88 | * Get the previous encryption keys. 89 | * 90 | * @return array 91 | */ 92 | public function getPreviousKeys() 93 | { 94 | // We have no keys to return. This exists purely to comply with the interface. 95 | return []; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Drivers/KmsDriver.php: -------------------------------------------------------------------------------- 1 | client = $kmsClient; 33 | $this->keyId = $keyId; 34 | } 35 | 36 | /** 37 | * @param string $value 38 | * @param bool $serialize 39 | * 40 | * @return mixed|string 41 | */ 42 | public function encrypt($value, $serialize = true) 43 | { 44 | $result = $this->client->encrypt([ 45 | 'KeyId' => $this->keyId, 46 | 'Plaintext' => $value 47 | ])->get('CiphertextBlob'); 48 | 49 | return ($serialize) 50 | ? base64_encode($result) 51 | : $result; 52 | } 53 | 54 | /** 55 | * @param string $payload 56 | * @param bool $unserialize 57 | * 58 | * @return string 59 | */ 60 | public function decrypt($payload, $unserialize = true) 61 | { 62 | if ($unserialize) { 63 | $payload = base64_decode($payload); 64 | } 65 | 66 | return $this->client->decrypt([ 67 | 'KeyId' => $this->keyId, 68 | 'CiphertextBlob' => $payload 69 | ])->get('Plaintext'); 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getKey() 76 | { 77 | // We have no key to return. This exists purely to comply with the interface. 78 | return ''; 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function getAllKeys() 85 | { 86 | // We have no keys to return. This exists purely to comply with the interface. 87 | return []; 88 | } 89 | 90 | /** 91 | * Get the previous encryption keys. 92 | * 93 | * @return array 94 | */ 95 | public function getPreviousKeys() 96 | { 97 | // We have no keys to return. This exists purely to comply with the interface. 98 | return []; 99 | } 100 | } -------------------------------------------------------------------------------- /src/EnvSecurityFacade.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE.md 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace STS\EnvSecurity; 12 | 13 | use Illuminate\Support\Facades\Facade; 14 | 15 | /** 16 | * Class Profile. 17 | * 18 | * 19 | * @method static EnvSecurityManager factory(string $keyId, array $config = []) 20 | * @method static EnvSecurityManager encrypt($plaintext, $serialize = true) 21 | * @method static EnvSecurityManager decrypt($plaintext, $serialize = true) 22 | * @method static EnvSecurityManager resolveEnvironmentUsing($callback) 23 | * @method static EnvSecurityManager resolveEnvironment() 24 | */ 25 | class EnvSecurityFacade extends Facade 26 | { 27 | protected static function getFacadeAccessor() 28 | { 29 | return 'sts.env-security'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/EnvSecurityManager.php: -------------------------------------------------------------------------------- 1 | environmentResolver = $callback; 41 | } 42 | 43 | /** 44 | * @return string|null 45 | */ 46 | public function resolveEnvironment() 47 | { 48 | if ($this->environment) { 49 | return $this->environment; 50 | } 51 | 52 | return isset($this->environmentResolver) 53 | ? call_user_func($this->environmentResolver) 54 | : config('app.env'); 55 | } 56 | 57 | /** 58 | * Setting an environment name explicitly will override any resolver and default 59 | * 60 | * @param $environment 61 | * 62 | * @return $this 63 | */ 64 | public function setEnvironment($environment) 65 | { 66 | $this->environment = $environment; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param $callback 73 | */ 74 | public function resolveKeyUsing($callback) 75 | { 76 | $this->keyResolver = $callback; 77 | } 78 | 79 | /** 80 | * @return string|null 81 | */ 82 | public function resolveKey() 83 | { 84 | return isset($this->keyResolver) 85 | ? call_user_func($this->keyResolver, $this->resolveEnvironment()) 86 | : null; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getDefaultDriver() 93 | { 94 | return config('env-security.default'); 95 | } 96 | 97 | /** 98 | * @return KmsDriver 99 | */ 100 | public function createKmsDriver() 101 | { 102 | $config = config('env-security.drivers.kms'); 103 | 104 | $key = $this->keyResolver 105 | ? $this->resolveKey() 106 | : $config['key_id']; 107 | 108 | return new KmsDriver(new KmsClient($config), $key); 109 | } 110 | 111 | /** 112 | * @return GoogleKmsDriver 113 | */ 114 | public function createGoogleKmsDriver() 115 | { 116 | $config = config('env-security.drivers.google_kms'); 117 | 118 | if ($this->keyResolver) { 119 | $config['key_id'] = $this->resolveKey(); 120 | } 121 | 122 | $options = Arr::get($config, 'options', []); 123 | 124 | return new GoogleKmsDriver( 125 | new KeyManagementServiceClient($options), 126 | Arr::get($config, 'project'), 127 | Arr::get($config, 'location'), 128 | Arr::get($config, 'key_ring'), 129 | Arr::get($config, 'key_id') 130 | ); 131 | } 132 | 133 | /** 134 | * Encrypt the value. 135 | * 136 | * @param string $value 137 | * @param bool $serialize 138 | * @return string 139 | */ 140 | public function encrypt($value, $serialize = true) 141 | { 142 | // Compress Value 143 | if (config('env-security.enable_compression')) { 144 | $this->checkZlibExtension('Laravel Env Security compression is enabled, but the zlib extension is not installed.'); 145 | if (($compressed = gzencode($value, 9)) === false) { 146 | throw new RuntimeException('Failed to compress the content.'); 147 | } 148 | 149 | $value = "gzencoded::{$compressed}"; 150 | } 151 | // Encode Value 152 | return $this->driver()->encrypt($value, $serialize); 153 | } 154 | 155 | /** 156 | * Decrypt the value. 157 | * 158 | * @param string $value 159 | * @param bool $unserialize 160 | * @return string 161 | */ 162 | public function decrypt($value, $unserialize = true) 163 | { 164 | $value = $this->driver()->decrypt($value, $unserialize); 165 | 166 | if (Str::substr($value, 0, strlen('gzencoded::')) === 'gzencoded::') { 167 | $value = $this->decompress($value); 168 | } 169 | 170 | return $value; 171 | } 172 | 173 | /** 174 | * @param $value 175 | * @return string 176 | * @throws RuntimeException 177 | */ 178 | private function decompress($value) 179 | { 180 | $this->checkZlibExtension('The environment file was compressed and can not be decompressed because the zlib extension is not installed.'); 181 | try { 182 | \set_error_handler( 183 | static function ($errno, $errstr, $errfile, $errline) { 184 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 185 | }, 186 | E_WARNING); 187 | $result = gzdecode(Str::substr($value, strlen('gzencoded::'))); 188 | } catch (ErrorException $previous) { 189 | throw new RuntimeException( 190 | 'The unencrypted data is corrupt and can not be uncompressed.', 191 | 0, 192 | $previous 193 | ); 194 | } finally { 195 | \restore_error_handler(); 196 | } 197 | 198 | return $result; 199 | } 200 | 201 | private function checkZlibExtension($message) 202 | { 203 | if (!in_array('zlib', get_loaded_extensions())) { 204 | throw new RuntimeException($message); 205 | } 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/EnvSecurityServiceProvider.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE.md 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace STS\EnvSecurity; 12 | 13 | use STS\EnvSecurity\Console\Encrypt; 14 | use Illuminate\Support\ServiceProvider; 15 | use RuntimeException; 16 | use STS\EnvSecurity\Console\Decrypt; 17 | use STS\EnvSecurity\Console\Edit; 18 | use function config; 19 | use function sprintf; 20 | 21 | class EnvSecurityServiceProvider extends ServiceProvider 22 | { 23 | /** 24 | * Indicates if loading of the provider is deferred. 25 | * 26 | * @var bool 27 | */ 28 | protected $defer = false; 29 | 30 | /** 31 | * Default path to configuration. 32 | * 33 | * @var string 34 | */ 35 | protected $configPath = __DIR__ . '/../config/env-security.php'; 36 | 37 | public function boot() 38 | { 39 | // helps deal with Lumen vs Laravel differences 40 | if (function_exists('config_path')) { 41 | $publishPath = config_path('env-security.php'); 42 | } else { 43 | $publishPath = base_path('config/env-security.php'); 44 | } 45 | $this->publishes([$this->configPath => $publishPath], 'config'); 46 | 47 | $this->verifyDirectory(); 48 | 49 | if ($this->app->runningInConsole()) { 50 | $this->commands($this->getConsoleCommands()); 51 | } 52 | } 53 | 54 | /** 55 | * Make sure our directory is setup and ready 56 | */ 57 | protected function verifyDirectory() 58 | { 59 | try { 60 | if (! is_dir(config('env-security.store'))) { 61 | if (! mkdir(config('env-security.store'))) { 62 | throw new RuntimeException( 63 | sprintf('Error creating the cipertext directory - %s', config('env-security.store')) 64 | ); 65 | } 66 | } 67 | } catch (\Throwable $e) { 68 | throw new RuntimeException( 69 | sprintf('Error creating the cipertext directory - %s', config('env-security.store')), 70 | $e->getCode(), 71 | $e 72 | ); 73 | } 74 | } 75 | 76 | /** 77 | * Register our console commands 78 | */ 79 | protected function getConsoleCommands() 80 | { 81 | return [Decrypt::class, Edit::class, Encrypt::class]; 82 | } 83 | 84 | /** 85 | * Get the services provided by the provider. 86 | * 87 | * @return array 88 | */ 89 | public function provides() 90 | { 91 | return ['sts.env-security', EnvSecurityManager::class]; 92 | } 93 | 94 | public function register() 95 | { 96 | $this->app->singleton(EnvSecurityManager::class, function () { 97 | return new EnvSecurityManager($this->app); 98 | }); 99 | $this->app->alias(EnvSecurityManager::class, 'sts.env-security'); 100 | 101 | if (is_a($this->app, 'Laravel\Lumen\Application')) { 102 | $this->app->configure('env-security'); 103 | } 104 | $this->mergeConfigFrom($this->configPath, 'env-security'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/CompressionTest.php: -------------------------------------------------------------------------------- 1 | getFilePathForEnvironment($environment_1)); 20 | @unlink($this->getFilePathForEnvironment($environment_2)); 21 | 22 | file_put_contents(base_path('.env'), $plaintext); 23 | 24 | // Encrypt without compression 25 | $this->artisan("env:store {$environment_1}") 26 | ->expectsOutput("Saved the contents of your current .env file for environment [{$environment_1}]"); 27 | $this->assertTrue(file_exists($this->getFilePathForEnvironment($environment_1))); 28 | 29 | // Encrypt the same with compression 30 | $this->artisan("env:store {$environment_2} --compress") 31 | ->expectsOutput("Saved the contents of your current .env file for environment [{$environment_2}]"); 32 | $this->assertTrue(file_exists($this->getFilePathForEnvironment($environment_2))); 33 | 34 | // Verify that the uncompressed file is larger than the compressed file. 35 | $this->assertTrue(filesize($this->getFilePathForEnvironment($environment_1)) > filesize($this->getFilePathForEnvironment($environment_2))); 36 | 37 | // Verify the decrypted uncompressed file is equal to the expected plain text. 38 | $this->assertEquals($plaintext, EnvSecurityFacade::decrypt($this->loadEncrypted($environment_1))); 39 | // Verify that the decrypted compressed file is equal to the decrypted uncompressed filesss 40 | $this->assertEquals(EnvSecurityFacade::decrypt($this->loadEncrypted($environment_1)), 41 | EnvSecurityFacade::decrypt($this->loadEncrypted($environment_2))); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /tests/DecryptDouble.php: -------------------------------------------------------------------------------- 1 | info("Used key [" . $this->envSecurity->resolveKey() . "]"); 17 | 18 | return $result; 19 | } 20 | } -------------------------------------------------------------------------------- /tests/DecryptTest.php: -------------------------------------------------------------------------------- 1 | saveEncrypted(EnvSecurity::encrypt('hello world'), 'testing'); 24 | 25 | $this->artisan('env:fetch testing') 26 | ->expectsOutput('Successfully decrypted .env for environment [testing]'); 27 | 28 | $this->assertTrue(file_exists(__DIR__ . "/.env-saved")); 29 | $this->assertEquals('hello world', file_get_contents(__DIR__ . "/.env-saved")); 30 | } 31 | 32 | public function testDecryptMissingFile() 33 | { 34 | // Make sure no file is present 35 | if(file_exists($this->getFilePathForEnvironment('testing'))) { 36 | unlink($this->getFilePathForEnvironment('testing')); 37 | } 38 | 39 | // Our test double will output the plaintext 40 | $this->artisan('env:fetch testing') 41 | ->expectsOutput('Unable to load encrypted .env file for environment [testing]'); 42 | } 43 | 44 | public function testDecryptResolveEnvironment() 45 | { 46 | EnvSecurity::resolveEnvironmentUsing(function() { 47 | return 'foobar'; 48 | }); 49 | 50 | $this->saveEncrypted(EnvSecurity::encrypt('heya'), 'foobar'); 51 | 52 | $this->artisan('env:fetch') 53 | ->expectsOutput('Successfully decrypted .env for environment [foobar]'); 54 | 55 | $this->assertTrue(file_exists(__DIR__ . "/.env-saved")); 56 | $this->assertEquals('heya', file_get_contents(__DIR__ . "/.env-saved")); 57 | } 58 | 59 | public function testDecryptResolveKey() 60 | { 61 | // This is the 62 | EnvSecurity::resolveEnvironmentUsing(function() { 63 | return 'testing'; 64 | }); 65 | 66 | EnvSecurity::resolveKeyUsing(function($environment) { 67 | return 'mykey-' . $environment; 68 | }); 69 | 70 | $this->saveEncrypted(EnvSecurity::encrypt('heya'), 'testing'); 71 | $this->saveEncrypted(EnvSecurity::encrypt('this is a separate environment file'), 'altenv'); 72 | 73 | $this->artisan('env:fetch') 74 | ->expectsOutput('Used key [mykey-testing]'); 75 | 76 | // Now specify alternate environment from CLI, ensure we use that environment's key regardless of 77 | // our previously provided resolver 78 | $this->artisan('env:fetch altenv') 79 | ->expectsOutput('Used key [mykey-altenv]'); 80 | 81 | $this->assertEquals('this is a separate environment file', file_get_contents(__DIR__ . "/.env-saved")); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/EditDouble.php: -------------------------------------------------------------------------------- 1 | info('Plaintext contents: '.$contents); 22 | 23 | if ($this->option('append')) { 24 | $contents = trim($contents." ".$this->option('append')); 25 | } 26 | 27 | return $contents; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/EditTest.php: -------------------------------------------------------------------------------- 1 | getFilePathForEnvironment('testing'))) { 25 | unlink($this->getFilePathForEnvironment('testing')); 26 | } 27 | 28 | // Setup a driver that fails if we call the encrypt method. 29 | EnvSecurity::extend('failonencrypt', function() { 30 | return new class { 31 | public function encrypt($plaintext) { 32 | throw new \Exception('Should not be here. I received: ' . $plaintext); 33 | } 34 | }; 35 | }); 36 | Config::set('env-security.default', 'failonencrypt'); 37 | 38 | // Our test double will output the plaintext 39 | $this->artisan('env:edit testing') 40 | ->expectsOutput('Plaintext contents: '); 41 | 42 | // File should be empty 43 | $this->assertEquals('', $this->loadEncrypted('testing')); 44 | } 45 | 46 | public function testEditEncryptedFile() 47 | { 48 | // Setup a testing.env.enc file 49 | $this->saveEncrypted(EnvSecurity::encrypt('hello world'), "testing"); 50 | 51 | // Our test double will output the plaintext 52 | $this->artisan('env:edit testing --append modified') 53 | ->expectsOutput('Plaintext contents: hello world'); 54 | 55 | // File will have "modified" appended and be re-encrypted 56 | $this->assertEquals(EnvSecurity::encrypt('hello world modified'), $this->loadEncrypted('testing')); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/EncryptTest.php: -------------------------------------------------------------------------------- 1 | getFilePathForEnvironment('testing')); 16 | file_put_contents(base_path('.env'), "encrypt=this"); 17 | 18 | $this->artisan('env:store testing') 19 | ->expectsOutput('Saved the contents of your current .env file for environment [testing]'); 20 | 21 | $this->assertTrue(file_exists($this->getFilePathForEnvironment('testing'))); 22 | $this->assertEquals('encrypt=this', EnvSecurity::decrypt($this->loadEncrypted("testing"))); 23 | } 24 | 25 | public function testEncryptMissingFile() 26 | { 27 | // Make sure no file is present 28 | @unlink(base_path('.env')); 29 | 30 | $this->artisan('env:store testing') 31 | ->expectsOutput('Make sure you have a .env file in your base project path'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/ManagerTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(KmsDriver::class, EnvSecurity::driver()); 16 | 17 | Config::set('env-security.default', 'google_kms'); 18 | Config::set('env-security.drivers.google_kms', [ 19 | 'project' => 'project-131708', 20 | 'location' => 'global', 21 | 'key_ring' => 'laravel', 22 | 'key_id' => 'dotenv', 23 | ]); 24 | 25 | // We have to at least pretend to have valid Google credentials 26 | file_put_contents(__DIR__ . '/store/keyfile.json', json_encode([ 27 | 'type' => 'service_account', 28 | 'project_id' => '', 29 | 'private_key_id' => '', 30 | 'private_key' => '', 31 | 'client_email' => '', 32 | 'client_id' => '', 33 | 'auth_uri' => '', 34 | 'token_uri' => '', 35 | 'auth_provider_x509_cert_url' => '', 36 | 'client_x509_cert_url' => '' 37 | ])); 38 | putenv('GOOGLE_APPLICATION_CREDENTIALS=' . __DIR__ . '/store/keyfile.json'); 39 | 40 | $this->assertInstanceOf(GoogleKmsDriver::class, EnvSecurity::driver()); 41 | 42 | Config::set('env-security.default', 'invalid'); 43 | 44 | $this->expectException(\InvalidArgumentException::class); 45 | $this->expectExceptionMessage('Driver [invalid] not supported'); 46 | EnvSecurity::driver(); 47 | } 48 | 49 | public function testResolveEnvironment() 50 | { 51 | // By default it will use our APP_ENV 52 | $this->assertEquals('testing', EnvSecurity::resolveEnvironment()); 53 | 54 | EnvSecurity::resolveEnvironmentUsing(function() { 55 | return "heya"; 56 | }); 57 | 58 | $this->assertEquals('heya', EnvSecurity::resolveEnvironment()); 59 | 60 | EnvSecurity::setEnvironment("override"); 61 | 62 | $this->assertEquals('override', EnvSecurity::resolveEnvironment()); 63 | } 64 | 65 | public function testResolveKey() 66 | { 67 | // By default it will be null 68 | $this->assertNull(EnvSecurity::resolveKey()); 69 | 70 | EnvSecurity::resolveKeyUsing(function($environment) { 71 | return "alias/myapp-$environment"; 72 | }); 73 | 74 | $this->assertEquals("alias/myapp-testing", EnvSecurity::resolveKey()); 75 | 76 | $this->assertEquals("alias/myapp-newenv", EnvSecurity::setEnvironment('newenv')->resolveKey()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/ServiceProviderDouble.php: -------------------------------------------------------------------------------- 1 | EnvSecurityFacade::class 22 | ]; 23 | } 24 | 25 | protected function setUp(): void 26 | { 27 | parent::setUp(); 28 | 29 | @mkdir(__DIR__.'/store'); 30 | 31 | EnvSecurity::extend('laravel', function () { 32 | return app('encrypter'); 33 | }); 34 | 35 | EnvSecurity::extend('test', function () { 36 | return new class { 37 | public function encrypt($plaintext) 38 | { 39 | return base64_encode($plaintext); 40 | } 41 | 42 | public function decrypt($ciphertext) 43 | { 44 | return base64_decode($ciphertext); 45 | } 46 | }; 47 | }); 48 | 49 | Config::set('env-security.default', 'test'); 50 | Config::set('env-security.store', __DIR__.'/store'); 51 | Config::set('env-security.destination', __DIR__.'/.env-saved'); 52 | Config::set('env-security.enable_compression', false); 53 | } 54 | 55 | protected function tearDown(): void 56 | { 57 | parent::tearDown(); 58 | 59 | if (file_exists(__DIR__."/.env-saved")) { 60 | unlink(__DIR__."/.env-saved"); 61 | } 62 | } 63 | } --------------------------------------------------------------------------------