├── .gitignore ├── .travis.yml ├── README.md ├── bin └── encrypt ├── composer.json ├── phpunit.xml ├── src ├── Crypto.php ├── File.php ├── KeySource.php ├── KeySource │ ├── KeyFile.php │ └── KeyString.php └── Parser.php └── tests ├── .env ├── CryptoTest.php ├── KeySource ├── KeyFileTest.php └── KeyStringTest.php ├── ParserTest.php └── test-encryption-key.txt /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | before_script: 3 | - composer self-update 4 | - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi; 5 | - composer install 6 | php: 7 | - 7.1 8 | - 7.2 9 | - 7.3 10 | script: phpunit 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secure_dotenv 2 | 3 | The `secure_dotenv` library provides an easy way to handle the encryption and decryption of the information in your `.env` file. 4 | 5 | One of the generally accepted security best practices is preventing the use of hard-coded, plain-text credentials of any kind. This library allows you to store the values in your `.env` as encrypted strings but still be able to access them transparently without worrying about implementing your own encryption method. 6 | 7 | [![Travis-CI Build Status](https://secure.travis-ci.org/psecio/secure_dotenv.png?branch=master)](http://travis-ci.org/psecio/secure_dotenv) 8 | 9 | ## Installation 10 | 11 | ### Download Composer package 12 | 13 | You can install the library easily with a Composer `require` call on the command line: 14 | 15 | ``` 16 | composer require psecio/secure_dotenv 17 | ``` 18 | 19 | ### Generate the key 20 | 21 | First, you'll need to generate your encryption key. The library makes use of the [defuse/php-encryption](https://github.com/defuse/php-encryption) library for it's encryption handling. 22 | 23 | ``` 24 | php vendor/bin/generate-defuse-key 25 | ``` 26 | 27 | This will result in a randomized string to use with the `php-encryption` library's default encryption. This string should be placed in a file where the script can access it. 28 | 29 | > **NOT:** According to security best practices, this key file should remain outside of the document root (not web accessible) but should be readable by the web server user (or executing user). 30 | 31 | ### Create the `.env` file 32 | 33 | You'll then need to make the `.env` file you're wanting to place the values in: 34 | 35 | ``` 36 | touch /project/root/dir/.env 37 | ``` 38 | 39 | ### Loading the values 40 | 41 | With the key file and .env created, you can now create a new instance that can be used to read the encrypted values: 42 | 43 | ```php 44 | getContent()); 54 | ?> 55 | ``` 56 | 57 | You don't have to use a file as a source for the key either - you can use a string (potentially something fron an `$_ENV` variable or some other source): 58 | 59 | ```php 60 | 69 | ``` 70 | 71 | This can be useful to help prevent the key from being read by a [local file inclusion](https://en.wikipedia.org/wiki/File_inclusion_vulnerability#Local_File_Inclusion) attack. 72 | 73 | 74 | If there are values currently in your `.env` file that are unencrypted, the library will pass them over and just return the plain-text version as pulled directly from the `.env` configuration. 75 | 76 | ## Setting values 77 | 78 | You can also dynamically set values into your `.env` file using the `save()` method on the `Parser` class: 79 | 80 | ```php 81 | save($keyName, $keyValue)) { 93 | echo 'Save successful'; 94 | } else { 95 | echo 'There was an error while saving the value.'; 96 | } 97 | ``` 98 | 99 | There's no need to worry about encrypting the value as the library takes care of that for you and outputs the encrypted result to the `.env` file. 100 | 101 | ## Encrypting values via CLI 102 | 103 | This library also comes with a handy way to encrypt values and write them out to the `.env` configuration automatically: 104 | 105 | ``` 106 | vendor/psecio/secure_dotenv/bin/encrypt --keyfile=/path/to/keyfile 107 | ``` 108 | 109 | This tool will ask a few questions about the location of the `.env` file and the key/value pair to set. When it completes it will write the new, encrypted, value to the `.env` file. If a value is already set in the configuration and you want to overwrite it, call the `encrypt` script with the `--override` command line flag. 110 | -------------------------------------------------------------------------------- /bin/encrypt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['keyfile'] 12 | ]; 13 | $args = $cmd->execute($_SERVER['argv'], $config); 14 | } catch (\Exception $e) { 15 | die($e->getMessage()."\n\n"); 16 | } 17 | 18 | $keyPath = $args['keyfile']; 19 | if (!is_file($keyPath)) { 20 | $climate->to('error')->red('Invalid key file location: '.$keyPath); 21 | die(); 22 | } 23 | 24 | $input = $climate->input('Location of the .env file:'); 25 | $envFile = $input->prompt(); 26 | 27 | if (empty($envFile)) { 28 | $envFile = getcwd().'/.env'; 29 | } 30 | 31 | // Step 1: Set up the location to the .env file 32 | if (substr($envFile, 0, '1') !== '/') { 33 | $envFile = getcwd().'/'.$envFile; 34 | } 35 | $envFile = realpath($envFile); 36 | 37 | if ($envFile == false || !is_file($envFile)) { 38 | $climate->to('error')->red('Invalid .env location: '.$envFile."\n"); 39 | die(); 40 | } 41 | $climate->out('Env path: '.$envFile); 42 | 43 | // Step 2: Take in the key name for the encrypted value 44 | $input = $climate->input('Keyname:'); 45 | $envName = $input->prompt(); 46 | 47 | if (empty($envName)) { 48 | $climate->to('error')->red('Invalid key name: '.$envName."\n"); 49 | die(); 50 | } 51 | 52 | // Step 3: Take in the value to encrypt 53 | $input = $climate->input('Value:'); 54 | $envValue = $input->prompt(); 55 | 56 | if (empty($envValue)) { 57 | $climate->to('error')->red('Invalid value: '.$envValue."\n"); 58 | die(); 59 | } 60 | 61 | // Step 4: Try to write the encrypted value to the .env file 62 | try { 63 | $overwrite = false; 64 | if (isset($args['overwrite']) && $args['overwrite'] == true) { 65 | $overwrite = true; 66 | } 67 | 68 | $d = new \Psecio\SecureDotenv\Parser($keyPath, $envFile); 69 | 70 | if ($d->save($envName, $envValue, $overwrite)) { 71 | $climate->out('Encrypted value for "'.$envName.'" has been added to '.$envFile); 72 | } else { 73 | $climate->to('error')->red('Something went wrong writing the value to '.$envFile); 74 | } 75 | 76 | } catch (\Exception $e) { 77 | $climate->to('error')->red('ERROR: '.$e->getMessage()); 78 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"psecio/secure_dotenv", 3 | "type":"library", 4 | "description":"An encrypted environment configuration handler", 5 | "keywords":["encryption", "configuration", "environment"], 6 | "homepage":"https://github.com/psecio/secure_dotenv.git", 7 | "license":"MIT", 8 | "authors":[ 9 | { 10 | "name":"Chris Cornutt", 11 | "email":"ccornutt@phpdeveloper.org", 12 | "homepage":"http://www.phpdeveloper.org/" 13 | } 14 | ], 15 | "autoload":{ 16 | "psr-4":{ 17 | "Psecio\\SecureDotenv\\":"src/" 18 | } 19 | }, 20 | "autoload-dev":{ 21 | "psr-4":{ 22 | "Psecio\\SecureDotenv\\":"tests/" 23 | } 24 | }, 25 | "require":{ 26 | "php": ">=7.1", 27 | "defuse/php-encryption": "^2.2", 28 | "enygma/cmd": "^0.4.0", 29 | "league/climate": "^3.4" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^7.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | ./tests 12 | 13 | 14 | 15 | 16 | src 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Crypto.php: -------------------------------------------------------------------------------- 1 | setKey($this->createKey($key)); 25 | } 26 | 27 | /** 28 | * Create the key instance based on either a string or file path 29 | * 30 | * @param string $key The "key" value, either a string or a file path 31 | * @return \Psecio\SecureDotenv\KeySource instance 32 | */ 33 | public function createKey($key) 34 | { 35 | if (is_file($key)) { 36 | $key = new KeySource\KeyFile($key); 37 | } elseif (is_string($key)) { 38 | $key = new KeySource\KeyString($key); 39 | } else { 40 | throw new \InvalidArgumentException('Could not create key from value provided.'); 41 | } 42 | 43 | return $key; 44 | } 45 | 46 | /** 47 | * Set the currekt key instance 48 | * 49 | * @param KeySource $key instance 50 | */ 51 | public function setKey(KeySource $key) 52 | { 53 | $this->key = $key; 54 | } 55 | 56 | /** 57 | * Return the current key instance 58 | * 59 | * @return \Psecio\SecureDotenv\KeySource instance 60 | */ 61 | public function getKey() 62 | { 63 | return $this->key; 64 | } 65 | 66 | /** 67 | * Encrypt the value provided with the current key and the Defuse library 68 | * 69 | * @param string $value Value to encrypt 70 | * @return string Ciphertext (encrypted) value 71 | */ 72 | public function encrypt($value) 73 | { 74 | // Get the key contents, no sense in keeping it in memory for too long 75 | $keyAscii = trim($this->key->getContent()); 76 | return DefuseCrypto::encrypt($value, DefuseKey::loadFromAsciiSafeString($keyAscii)); 77 | } 78 | 79 | /** 80 | * Decrypt the ciphertext value provided 81 | * This method also catches values that may not be encrypted 82 | * and returns them normally 83 | * 84 | * @param string $value Ciphertext (encrypted) string 85 | * @return mixed The value if it could be decrypted, otherwse null 86 | */ 87 | public function decrypt($value) 88 | { 89 | try { 90 | $keyAscii = trim($this->key->getContent()); 91 | $value = DefuseCrypto::decrypt($value, DefuseKey::loadFromAsciiSafeString($keyAscii)); 92 | 93 | return $value; 94 | } catch (\Defuse\Crypto\Exception\CryptoException $e) { 95 | // The value probably wasn't encrypted, move along... 96 | return null; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/File.php: -------------------------------------------------------------------------------- 1 | $data) { 22 | // See if it's a section 23 | if (is_array($data)) { 24 | $output .= '['.$index.']'; 25 | foreach ($data as $i => $d) { 26 | $output .= $i.'='.$d."\n"; 27 | } 28 | $output .= "\n"; 29 | } else { 30 | $output .= $index.'='.$data."\n"; 31 | } 32 | } 33 | 34 | if ($path !== null) { 35 | return file_put_contents($path, $output); 36 | } else { 37 | echo $output; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/KeySource.php: -------------------------------------------------------------------------------- 1 | content; 22 | } 23 | 24 | /** 25 | * Set the current key content 26 | * 27 | * @param string $content 28 | */ 29 | public function setContent($content) 30 | { 31 | $this->content = $content; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/KeySource/KeyFile.php: -------------------------------------------------------------------------------- 1 | setContent(file_get_contents($source)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/KeySource/KeyString.php: -------------------------------------------------------------------------------- 1 | setContent($source); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | setCrypto(new Crypto($key)); 32 | 33 | if ($configPath == null) { 34 | $configPath = __DIR__.'/.env'; 35 | } 36 | $this->setConfigPath($configPath); 37 | } 38 | 39 | /** 40 | * Decrypt the values provided 41 | * Supports sections 42 | * 43 | * @param array $values 44 | * @return array Decrypted values 45 | */ 46 | public function decryptValues(array $values) : array 47 | { 48 | foreach ($values as $index => $value) { 49 | if (is_array($value)) { 50 | foreach ($value as $i => $v) { 51 | $de = $this->crypto->decrypt(trim($v)); 52 | $values[$index][$i] = ($de == null) ? $v : $de; 53 | } 54 | } else { 55 | $de = $this->crypto->decrypt(trim($value)); 56 | $values[$index] = ($de == null) ? $value : $de; 57 | } 58 | } 59 | return $values; 60 | } 61 | 62 | /** 63 | * Read in the configuration file 64 | * 65 | * @param string $configPath Configuration file path 66 | * @return string 67 | */ 68 | public function loadFile($configPath) 69 | { 70 | $contents = $this->decryptValues(File::read($configPath)); 71 | return $contents; 72 | } 73 | 74 | /** 75 | * Set the current instance of the Crypto class 76 | * 77 | * @param \Psecio\SecureDotenv\Crypto $crypto 78 | */ 79 | public function setCrypto(Crypto $crypto) 80 | { 81 | $this->crypto = $crypto; 82 | } 83 | 84 | /** 85 | * Set the path for the current configuration file 86 | * 87 | * @param string $configPath 88 | * @throws \InvalidArgumentException If the path is invalid 89 | */ 90 | public function setConfigPath($configPath) 91 | { 92 | if (empty($configPath) || !is_file($configPath)) { 93 | throw new \InvalidArgumentException('Invalid config file path: '.$configPath); 94 | } 95 | $this->configPath = $configPath; 96 | } 97 | 98 | /** 99 | * Save a new encrypted value to the .env file 100 | * 101 | * @param string $keyName Key name (plain-text) 102 | * @param string $keyValue Value to set for the key (plain-text) 103 | * @param boolean $overwrite Flag to either overwrite the value that exists or leave it 104 | * @return boolean Success/fail of the write 105 | */ 106 | public function save($keyName, $keyValue, $overwrite = false) 107 | { 108 | return $this->writeEnv($keyName, $keyValue, $overwrite); 109 | } 110 | 111 | /** 112 | * Write the contents out to the .env configuration file 113 | * 114 | * @param string $keyName Key name (plain-text) 115 | * @param string $ciphertext Encrypted value 116 | * @param boolean $overwrite Flag to either overwrite the value that exists or leave it 117 | * @throws \Exception If the key name already exists and the overwrite flag isn't true 118 | * @return boolean Success/fail of file write 119 | */ 120 | public function writeEnv($keyName, $keyValue, $overwrite = false) 121 | { 122 | $contents = $this->loadFile($this->configPath); 123 | 124 | // read from the .env file, update any that need it or add a new one 125 | if (isset($contents[$keyName]) && $overwrite == false) { 126 | throw new \Exception('Key name "'.$keyName.'" already exists!'); 127 | } 128 | 129 | // If it's not already set (or overwrite is true), write it out 130 | $contents[$keyName] = $keyValue; 131 | 132 | foreach ($contents as $index => $value) { 133 | if (is_array($value)) { 134 | foreach ($value as $i => $v) { 135 | $contents[$index][$i] = $this->crypto->encrypt($v); 136 | } 137 | } else { 138 | $contents[$index] = $this->crypto->encrypt($value); 139 | } 140 | } 141 | 142 | return File::write($contents, $this->configPath); 143 | } 144 | 145 | /** 146 | * Get the contents of the current configuration file 147 | * 148 | * @param string $keyName Name of key to locate [optional] 149 | * @return array|string 150 | */ 151 | public function getContent($keyName = null) 152 | { 153 | $contents = $this->loadFile($this->configPath); 154 | 155 | if ($keyName !== null && isset($contents[$keyName])) { 156 | return $contents[$keyName]; 157 | } 158 | return $contents; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/.env: -------------------------------------------------------------------------------- 1 | env1=def5020020024d4c49cd92b82363643afcb04455b8e0bd6fe47c4f6283d942f3037306b40ae6dc752a2819e638c10a021ab4380d1d798d1d785d847559275dff51f0b735b397c79b3ef718e3a77aa7bf8ae3b7b7f23af6a60bc75620 2 | env2=value2 3 | -------------------------------------------------------------------------------- /tests/CryptoTest.php: -------------------------------------------------------------------------------- 1 | getKey(); 14 | 15 | $this->assertInstanceOf(KeyString::class, $k); 16 | $this->assertEquals($k->getContent(), $keyString); 17 | } 18 | 19 | public function testGetSetKey() 20 | { 21 | $keyString = '123456'; 22 | $c = new Crypto($keyString); 23 | 24 | $this->assertEquals($c->getKey()->getContent(), $keyString); 25 | 26 | // Reset it 27 | $key = new KeySource\KeyString('test123'); 28 | $c->setKey($key); 29 | 30 | $this->assertEquals($c->getKey()->getContent(), 'test123'); 31 | } 32 | 33 | public function testEncryptDecrypt() 34 | { 35 | $value = 'test1234'; 36 | $c = new Crypto(__DIR__.'/test-encryption-key.txt'); 37 | $encrypted = $c->encrypt($value); 38 | 39 | $this->assertNotEquals($value, $encrypted); 40 | $this->assertEquals($value, $c->decrypt($encrypted)); 41 | 42 | } 43 | 44 | public function testDecryptWithInvalidValue() 45 | { 46 | $c = new Crypto(__DIR__.'/test-encryption-key.txt'); 47 | 48 | $this->assertNull($c->decrypt('invalid_value')); 49 | } 50 | 51 | public function testCreateKeyWithInalvidKey() 52 | { 53 | $c = new Crypto(__DIR__.'/test-encryption-key.txt'); 54 | 55 | $this->expectException(\InvalidArgumentException::class); 56 | $this->expectExceptionMessage('Could not create key from value provided.'); 57 | $c->createKey(1000); 58 | } 59 | } -------------------------------------------------------------------------------- /tests/KeySource/KeyFileTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 12 | $this->expectExceptionMessage('Invalid source: invalid_source'); 13 | 14 | new KeyFile('invalid_source'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/KeySource/KeyStringTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($keyString, $key->getContent()); 14 | } 15 | 16 | public function testInvalidInit() 17 | { 18 | $this->expectException(\InvalidArgumentException::class); 19 | 20 | $keyString = new \stdClass(); 21 | new KeyString($keyString); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/ParserTest.php: -------------------------------------------------------------------------------- 1 | keyPath = __DIR__.'/test-encryption-key.txt'; 14 | $this->envPath = __DIR__.'/.env'; 15 | 16 | // Clear out the config 17 | file_put_contents($this->envPath, ''); 18 | } 19 | 20 | public function testConstructor() 21 | { 22 | $parser = new Parser($this->keyPath, $this->envPath); 23 | $this->assertInstanceOf(Parser::class, $parser); 24 | } 25 | 26 | public function testSetConfigPathWithInvalidPath() 27 | { 28 | $parser = new Parser($this->keyPath, $this->envPath); 29 | 30 | $this->expectException(\InvalidArgumentException::class); 31 | $this->expectExceptionMessage('Invalid config file path: invalid_config_file'); 32 | $parser->setConfigPath('invalid_config_file'); 33 | } 34 | 35 | public function testSave() 36 | { 37 | $c = new Crypto(file_get_contents($this->keyPath)); 38 | $parser = new Parser($this->keyPath, $this->envPath); 39 | $parser->setCrypto($c); 40 | 41 | $this->assertEquals(190, $parser->save('env1', 'test1234', true)); 42 | } 43 | 44 | public function testWriteEnvWithDuplicatedEnv() 45 | { 46 | $c = new Crypto(file_get_contents($this->keyPath)); 47 | $parser = new Parser($this->keyPath, $this->envPath); 48 | $parser->setCrypto($c); 49 | 50 | $parser->save('env1', '123456', true); 51 | 52 | $this->expectException(\Exception::class); 53 | $this->expectExceptionMessage('Key name "env1" already exists!'); 54 | $parser->writeEnv('env1', 'overwrite_value1'); 55 | } 56 | 57 | public function testReadWrite() 58 | { 59 | $content = 'test1234'; 60 | 61 | $parser = new Parser($this->keyPath, $this->envPath); 62 | $parser->save('readwrite1', $content, true); 63 | 64 | // Now reparse the file 65 | $parser = new Parser($this->keyPath, $this->envPath); 66 | $this->assertEquals($content, $parser->getContent('readwrite1')); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/test-encryption-key.txt: -------------------------------------------------------------------------------- 1 | def00000bf948d35d29e0ca25ccaac3c67bab348486feada515a8804013651d3a3a64302d94e5c1f4a6be3e4ccbfbdc3dc16eafc679a68f2ea3325fca5af6bdaf45d3145 --------------------------------------------------------------------------------