From daf2a1185552b0c40687c6118eaf5c7174000052 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 09:10:00 -0700 Subject: [PATCH 01/18] Update README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5a3494a..92f14c9 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,10 @@ SECRET_KEY="souper_seekret_key" As early as possible in your application bootstrap process, load .env: ```php -use DotenvVault\DotenvVault; require 'vendor/autoload.php'; -$dotenv = DotenvVault::createImmutable(__DIR__, '.env.vault'); -$dotenv->load(); # take environment variables from .env.vault +$dotenv = DotenvVault\DotenvVault::createImmutable(__DIR__); +$dotenv->load(); ``` When your application loads, these variables will be available in `$_ENV` or `$_SERVER`: From 29f9084cb48a129148be92218ea6cbfa6db51e03 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 09:15:27 -0700 Subject: [PATCH 02/18] Remove var_dump --- src/DotenvVault.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 1e12aa4..0dc2f06 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -51,9 +51,6 @@ public function load() else { $entries = $this->parser->parse($this->store->read()); - var_dump($entries[0]->getName()); - var_dump($entries[0]->getValue()); - return $this->loader->load($this->repository, $entries); } } From a9aa7723934b15ff801ce7cba1d361235bf48e0d Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 09:16:38 -0700 Subject: [PATCH 03/18] Update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a55858..09f38a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [Unreleased](https://github.com/dotenv-org/phpdotenv-vault/compare/v0.1.2...master) +## [Unreleased](https://github.com/dotenv-org/phpdotenv-vault/compare/v0.1.3...master) + +## 0.1.3 + +### Removed + +- Remove `var_dump` when falling back to `.env` file ## 0.1.2 From e27d65dfbd40d6997894b5917dd8e11b690e5e40 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 09:55:15 -0700 Subject: [PATCH 04/18] Add phpunit --- composer.json | 2 +- src/DotenvVault.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index d05786e..e208daf 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,6 @@ "vlucas/phpdotenv": "^5.5" }, "require-dev": { - "orchestra/testbench": "^3.8" + "phpunit/phpunit": "^10.4" } } diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 0dc2f06..343fea9 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -45,8 +45,8 @@ public function load() $plaintext = $this->parse_vault(); // parsing plaintext and loading to getenv - $test_entries = $this->parser->parse($plaintext); - $this->loader->load($this->repository, $test_entries); + $vault_entries = $this->parser->parse($plaintext); + $this->loader->load($this->repository, $vault_entries); } else { $entries = $this->parser->parse($this->store->read()); From 372e370a653d9df3847dd1de03205a833ecf73c5 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 09:58:05 -0700 Subject: [PATCH 05/18] Set up composer with test autoloads --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index e208daf..2cb677b 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,11 @@ "DotenvVault\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "DotenvVault\\Tests\\": "tests/" + } + }, "authors": [ { "name": "dotenv", From 33c7f44d24bc1db1e5f22b3707e6c4abcd5f369f Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 10:10:33 -0700 Subject: [PATCH 06/18] Adds functional testing --- DEVELOPMENT.md | 8 ++++++ phpunit.xml.dist | 29 +++++++++++++++++++++ tests/DotenvVault/DotenvVaultTest.php | 37 +++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 phpunit.xml.dist create mode 100644 tests/DotenvVault/DotenvVaultTest.php diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a4b9307..b84ca90 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,5 +1,13 @@ # DEVELOPMENT +## Running tests + +Tests use PHPUnit. + +``` +./vendor/bin/phpunit +``` + ## Publishing Published at [packagist](https://packagist.org/packages/dotenv-org/phpdotenv-vault) diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..b57bcc9 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php new file mode 100644 index 0000000..4eb3905 --- /dev/null +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -0,0 +1,37 @@ +expectException(InvalidPathException::class); + $this->expectExceptionMessage('Unable to read any of the environment file(s) at'); + + $dotenv->load(); + } +} +?> From 256558f8f6dcdf6fe4de612e0d5b40ad2a48a568 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 10:14:10 -0700 Subject: [PATCH 07/18] Passing test --- .gitignore | 4 ++- phpunit.xml.dist | 39 +++++++++------------------ tests/DotenvVault/DotenvVaultTest.php | 7 ++++- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 979d356..290276a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ phpstan.tests.neon phpunit.xml vendor -.vscode \ No newline at end of file +.vscode + +.phpunit.cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b57bcc9..020141b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,14 @@ - - - - ./tests - - - - - ./src - - + + + + + ./tests + + + + + ./src + + diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php index 4eb3905..67953e5 100644 --- a/tests/DotenvVault/DotenvVaultTest.php +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -4,7 +4,12 @@ namespace DotenvVault\Tests; +// phpdotenv-vault libs use DotenvVault\DotenvVault; + +// phpdotenv libs +use Dotenv; + use PHPUnit\Framework\TestCase; final class DotenvVaultTest extends TestCase @@ -28,7 +33,7 @@ public function testDotenvThrowsExceptionIfUnableToLoadFile() { $dotenv = DotenvVault::createMutable(__DIR__); - $this->expectException(InvalidPathException::class); + $this->expectException(Dotenv\Exception\InvalidPathException::class); $this->expectExceptionMessage('Unable to read any of the environment file(s) at'); $dotenv->load(); From a81fdb115088ca2462fff513e51f98a920d0c136 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 10:18:38 -0700 Subject: [PATCH 08/18] Add GitHub workflow ci --- ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 ci.yml diff --git a/ci.yml b/ci.yml new file mode 100644 index 0000000..584bb1c --- /dev/null +++ b/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + php: [7.x, 8.x] + + steps: + - uses: actions/checkout@v3 + - name: Use PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: none + - name: Setup problem matchers + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install latest dependencies + uses: nick-invision/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --no-interaction --no-progress + - run: vendor/bin/phpunit From 2e210b42610e9ab797a7a51633aa46d90036c0a6 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 10:20:34 -0700 Subject: [PATCH 09/18] DotenvVault loads .env file --- tests/DotenvVault/DotenvVaultTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php index 67953e5..0b1807b 100644 --- a/tests/DotenvVault/DotenvVaultTest.php +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -38,5 +38,11 @@ public function testDotenvThrowsExceptionIfUnableToLoadFile() $dotenv->load(); } + + public function testDotenvTriesPathsToLoad() + { + $dotenv = DotenvVault::createMutable([__DIR__, self::$folder]); + self::assertCount(4, $dotenv->load()); + } } ?> From 757cdaa397c518b6138a1295c4d1a4636f72e5c5 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 14:19:08 -0700 Subject: [PATCH 10/18] move things around. create decrypter in separate class with separate specs --- ci.yml => .github/workflows/ci.yml | 2 +- src/Decrypter/Decrypter.php | 62 +++++ src/Decrypter/DecrypterInterface.php | 21 ++ src/DotenvVault.php | 211 ++++++------------ src/DotenvVaultBackup.php | 193 ++++++++++++++++ tests/DotenvVault/Decrypter/DecrypterTest.php | 64 ++++++ tests/DotenvVault/DotenvVaultTest.php | 22 +- 7 files changed, 431 insertions(+), 144 deletions(-) rename ci.yml => .github/workflows/ci.yml (94%) create mode 100644 src/Decrypter/Decrypter.php create mode 100644 src/Decrypter/DecrypterInterface.php create mode 100644 src/DotenvVaultBackup.php create mode 100644 tests/DotenvVault/Decrypter/DecrypterTest.php diff --git a/ci.yml b/.github/workflows/ci.yml similarity index 94% rename from ci.yml rename to .github/workflows/ci.yml index 584bb1c..1169991 100644 --- a/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,4 @@ jobs: timeout_minutes: 5 max_attempts: 5 command: composer update --no-interaction --no-progress - - run: vendor/bin/phpunit + - run: vendor/bin/phpunit --testdox diff --git a/src/Decrypter/Decrypter.php b/src/Decrypter/Decrypter.php new file mode 100644 index 0000000..60e2eac --- /dev/null +++ b/src/Decrypter/Decrypter.php @@ -0,0 +1,62 @@ +store = $store; - $this->parser = $parser; - $this->loader = $loader; - $this->repository = $repository; - } - - public function load() - { - $this->dotenv_key = getenv("DOTENV_KEY"); - if ($this->dotenv_key !== false){ - - $entries = $this->parser->parse($this->store->read()); - $this->loader->load($this->repository, $entries); - - $plaintext = $this->parse_vault(); - - // parsing plaintext and loading to getenv - $vault_entries = $this->parser->parse($plaintext); - $this->loader->load($this->repository, $vault_entries); - } - else { - $entries = $this->parser->parse($this->store->read()); - - return $this->loader->load($this->repository, $entries); - } - } - - public function parse_vault() - { - $dotenv_keys = explode(',', $this->dotenv_key); - $keys = array(); - foreach($dotenv_keys as $key) - { - // parse DOTENV_KEY, format is a URI. - $uri = parse_url(trim($key)); - - // get encrypted key - $pass = $uri['pass']; - - // Get environment from query params. - parse_str($uri['query'], $params); - $vault_environment = $params['environment'] or throw new DotEnvVaultError('INVALID_DOTENV_KEY: Missing environment part.'); - - # Getting ciphertext from correct environment in .env.vault - $vault_environment = strtoupper($vault_environment); - $environment_key = "DOTENV_VAULT_{$vault_environment}"; - - $ciphertext = getenv("{$environment_key}") or throw new DotEnvVaultError("NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment {$environment_key} in your .env.vault file. Run 'npx dotenv-vault build' to include it."); - - array_push($keys, array('encrypted_key' => $pass, 'ciphertext' => $ciphertext)); - } - return $this->key_rotation($keys); +class DotenvVault { + public static function __callStatic($name, $arguments) { + // Forward the call to the Dotenv\Dotenv class + return call_user_func_array(['Dotenv\Dotenv', $name], $arguments); } - private function key_rotation($keys){ - $count = count($keys); - foreach($keys as $index=>$value) { - $decrypt = $this->decrypt($value['ciphertext'], $value['encrypted_key']); - - if ($decrypt == false && $index + 1 >= $count){ - throw new DotEnvVaultError('INVALID_DOTENV_KEY: Key must be valid.'); - } - elseif($decrypt == false){ - continue; - } - else{ - return $decrypt; - } - } - } - - private function decrypt($data, $secret) - { - $secret = hex2bin(substr($secret, 4, strlen($secret))); - $data = base64_decode($data, true); - $nonce = substr($data, 0, 12); - $tag = substr($data, -16); - $ciphertext = substr($data, 12, -16); - - try { - return openssl_decrypt( - $ciphertext, - 'aes-256-gcm', - $secret, - OPENSSL_RAW_DATA, - $nonce, - $tag - ); - } catch (Exception $e) { - return false; - } - } - - public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); - - foreach ((array) $paths as $path) { - $builder = $builder->addPath($path); - } - - foreach ((array) $names as $name) { - $builder = $builder->addName($name); - } - - if ($shortCircuit) { - $builder = $builder->shortCircuit(); - } - - return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); - } + // public function load() + // { + // $this->dotenv_key = getenv("DOTENV_KEY"); + // if ($this->dotenv_key !== false){ + + // $entries = $this->parser->parse($this->store->read()); + // $this->loader->load($this->repository, $entries); + + // $plaintext = $this->parse_vault(); + + // // parsing plaintext and loading to getenv + // $vault_entries = $this->parser->parse($plaintext); + // return $this->loader->load($this->repository, $vault_entries); + // } + // else { + // $entries = $this->parser->parse($this->store->read()); + + // return $this->loader->load($this->repository, $entries); + // } + // } + + // public function parse_vault() + // { + // $dotenv_keys = explode(',', $this->dotenv_key); + // $keys = array(); + // foreach($dotenv_keys as $key) + // { + // // parse DOTENV_KEY, format is a URI. + // $uri = parse_url(trim($key)); + + // // get encrypted key + // $pass = $uri['pass']; + // + // // Get environment from query params. + // parse_str($uri['query'], $params); + // $vault_environment = $params['environment'] or throw new Exception('INVALID_DOTENV_KEY: Missing environment part.'); + + // # Getting ciphertext from correct environment in .env.vault + // $vault_environment = strtoupper($vault_environment); + // $environment_key = "DOTENV_VAULT_{$vault_environment}"; + + // $ciphertext = getenv("{$environment_key}") or throw new Exception("NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment {$environment_key} in your .env.vault file. Run 'npx dotenv-vault build' to include it."); + // + // array_push($keys, array('encrypted_key' => $pass, 'ciphertext' => $ciphertext)); + // } + // return $this->key_rotation($keys); + // } + + // private function key_rotation($keys){ + // $count = count($keys); + // foreach($keys as $index=>$value) { + // $decrypt = $this->decrypt($value['ciphertext'], $value['encrypted_key']); + + // if ($decrypt == false && $index + 1 >= $count){ + // throw new Exception('INVALID_DOTENV_KEY: Key must be valid.'); + // } + // elseif($decrypt == false){ + // continue; + // } + // else{ + // return $decrypt; + // } + // } + // } } diff --git a/src/DotenvVaultBackup.php b/src/DotenvVaultBackup.php new file mode 100644 index 0000000..68ebbfb --- /dev/null +++ b/src/DotenvVaultBackup.php @@ -0,0 +1,193 @@ +store = $store; + $this->parser = $parser; + $this->loader = $loader; + $this->repository = $repository; + } + + public function load() + { + $this->dotenv_key = getenv("DOTENV_KEY"); + if ($this->dotenv_key !== false){ + + $entries = $this->parser->parse($this->store->read()); + $this->loader->load($this->repository, $entries); + + $plaintext = $this->parse_vault(); + + // parsing plaintext and loading to getenv + $vault_entries = $this->parser->parse($plaintext); + return $this->loader->load($this->repository, $vault_entries); + } + else { + $entries = $this->parser->parse($this->store->read()); + + return $this->loader->load($this->repository, $entries); + } + } + + public function parse_vault() + { + $dotenv_keys = explode(',', $this->dotenv_key); + $keys = array(); + foreach($dotenv_keys as $key) + { + // parse DOTENV_KEY, format is a URI. + $uri = parse_url(trim($key)); + + // get encrypted key + $pass = $uri['pass']; + + // Get environment from query params. + parse_str($uri['query'], $params); + $vault_environment = $params['environment'] or throw new DotEnvVaultError('INVALID_DOTENV_KEY: Missing environment part.'); + + # Getting ciphertext from correct environment in .env.vault + $vault_environment = strtoupper($vault_environment); + $environment_key = "DOTENV_VAULT_{$vault_environment}"; + + $ciphertext = getenv("{$environment_key}") or throw new DotEnvVaultError("NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment {$environment_key} in your .env.vault file. Run 'npx dotenv-vault build' to include it."); + + array_push($keys, array('encrypted_key' => $pass, 'ciphertext' => $ciphertext)); + } + return $this->key_rotation($keys); + } + + private function key_rotation($keys){ + $count = count($keys); + foreach($keys as $index=>$value) { + $decrypt = $this->decrypt($value['ciphertext'], $value['encrypted_key']); + + if ($decrypt == false && $index + 1 >= $count){ + throw new DotEnvVaultError('INVALID_DOTENV_KEY: Key must be valid.'); + } + elseif($decrypt == false){ + continue; + } + else{ + return $decrypt; + } + } + } + + public static function decrypt($encrypted, $keyStr) + { + // grab last 64 to permit keys like vlt_64 or custom_64 + $last64 = substr($keyStr, -64); + + if (strlen($last64) !== 64) { + $msg = 'INVALID_DOTENV_KEY: It must be 64 characters long (or more)'; + throw new Exception($msg); + } + + // check key length is good INVALID_DOTENV_KEY: It must be 64 characters long (or more) + $key = hex2bin($last64); + + // base64 decode + $decoded = base64_decode($encrypted, true); + + // determine cipher and pull out nonce and tag + $ciphertext = substr($decoded, 12, -16); + $nonce = substr($decoded, 0, 12); + $tag = substr($decoded, -16); + + try { + $plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag); + + if ($plaintext === false) { + $msg = 'DECRYPTION_FAILED: Please check your DOTENV_KEY'; + throw new Exception($msg); + } else { + return $plaintext; + } + } catch (ExceptionType $e) { + $msg = 'DECRYPTION_FAILED: Please check your DOTENV_KEY'; + throw new Exception($msg); + } + } + + /** + * Create a new immutable dotenvVault instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \DotenvVault\DotenvVault + */ + public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenvVault instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \DotenvVault\DotenvVault + */ + public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->immutable() + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); + + foreach ((array) $paths as $path) { + $builder = $builder->addPath($path); + } + + foreach ((array) $names as $name) { + $builder = $builder->addName($name); + } + + if ($shortCircuit) { + $builder = $builder->shortCircuit(); + } + + return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); + } +} diff --git a/tests/DotenvVault/Decrypter/DecrypterTest.php b/tests/DotenvVault/Decrypter/DecrypterTest.php new file mode 100644 index 0000000..c3a1fc9 --- /dev/null +++ b/tests/DotenvVault/Decrypter/DecrypterTest.php @@ -0,0 +1,64 @@ +encrypted = 's7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R'; + } + + public function testDecrypterInstanceOf() + { + self::assertInstanceOf(DecrypterInterface::class, new Decrypter()); + } + + public function testFullDecrypt() + { + $keyStr = 'ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00'; + + $result = (new Decrypter())->decrypt($this->encrypted, $keyStr); + + self::assertEquals($result, "# development@v6\nALPHA=\"zeta\""); + } + + public function testDecryptWhenKeyIsTooShort() + { + $keyStr = 'vlt_tooshort'; + + $this->expectExceptionMessage('INVALID_DOTENV_KEY: It must be 64 characters long (or more)'); + + $result = (new Decrypter())->decrypt($this->encrypted, $keyStr); + + self::assertEquals($result, "# development@v6\nALPHA=\"zeta\""); + } + + public function testDecryptWhenKeyIsWrong() + { + $keyStr = 'AAcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00'; + + $this->expectExceptionMessage('DECRYPTION_FAILED: Please check your DOTENV_KEY'); + + $result = (new Decrypter())->decrypt($this->encrypted, $keyStr); + + self::assertEquals($result, "# development@v6\nALPHA=\"zeta\""); + } + + public function testDecryptEmptyEncryptedText() + { + $keyStr = 'ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00'; + + $this->expectExceptionMessage('MISSING_CIPHERTEXT: It must be a non-empty string'); + + $result = (new Decrypter())->decrypt('', $keyStr); + } +} diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php index 0b1807b..340d6e9 100644 --- a/tests/DotenvVault/DotenvVaultTest.php +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -29,7 +29,7 @@ public static function setFolder() self::$folder = \dirname(__DIR__).'/fixtures/env'; } - public function testDotenvThrowsExceptionIfUnableToLoadFile() + public function testThrowsExceptionIfUnableToLoadFile() { $dotenv = DotenvVault::createMutable(__DIR__); @@ -39,10 +39,26 @@ public function testDotenvThrowsExceptionIfUnableToLoadFile() $dotenv->load(); } - public function testDotenvTriesPathsToLoad() + public function testTriesPathsToLoad() { $dotenv = DotenvVault::createMutable([__DIR__, self::$folder]); self::assertCount(4, $dotenv->load()); } + + public function testLoadsFromEnvFile() + { + DotenvVault::createMutable([__DIR__, self::$folder]); + + self::assertEquals($_ENV['FOO'], 'bar'); + self::assertEquals($_ENV['BAR'], 'baz'); + } + + public function testLoadsFromEnvVaultFileWhenDotenvKeyPresent() + { + $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; + + DotenvVault::createMutable([__DIR__, self::$folder]); + + self::assertEquals($_ENV['ALPA'], 'zeta'); + } } -?> From 3ea6a92f4466428f89dfb39cb91efbe1ffffc1d8 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:08:36 -0700 Subject: [PATCH 11/18] Cleaned up --- .github/workflows/ci.yml | 2 +- DEVELOPMENT.md | 2 +- README.md | 2 +- src/DotenvVault.php | 275 +++++++++++++++++++++++++- src/DotenvVaultBackup.php | 193 ------------------ tests/DotenvVault/DotenvVaultTest.php | 17 +- 6 files changed, 283 insertions(+), 208 deletions(-) delete mode 100644 src/DotenvVaultBackup.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1169991..fcbc29b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,4 @@ jobs: timeout_minutes: 5 max_attempts: 5 command: composer update --no-interaction --no-progress - - run: vendor/bin/phpunit --testdox + - run: vendor/bin/phpunit --display-deprecations diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b84ca90..e67b365 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -5,7 +5,7 @@ Tests use PHPUnit. ``` -./vendor/bin/phpunit +./vendor/bin/phpunit --testdox --display-deprecations ``` ## Publishing diff --git a/README.md b/README.md index 92f14c9..31bbede 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ As early as possible in your application bootstrap process, load .env: require 'vendor/autoload.php'; $dotenv = DotenvVault\DotenvVault::createImmutable(__DIR__); -$dotenv->load(); +$dotenv->safeLoad(); ``` When your application loads, these variables will be available in `$_ENV` or `$_SERVER`: diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 4b9f9ba..49372d6 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -5,12 +5,279 @@ namespace DotenvVault; use Dotenv\Dotenv; +use Dotenv\Loader\Loader; +use Dotenv\Loader\LoaderInterface; +use Dotenv\Parser\Parser; +use Dotenv\Parser\ParserInterface; +use Dotenv\Repository\RepositoryBuilder; +use Dotenv\Repository\RepositoryInterface; +use Dotenv\Store\StoreBuilder; +use Dotenv\Store\StoreInterface; +use Dotenv\Util\Str; +use PhpOption\Option; use Exception; -class DotenvVault { - public static function __callStatic($name, $arguments) { - // Forward the call to the Dotenv\Dotenv class - return call_user_func_array(['Dotenv\Dotenv', $name], $arguments); +use DotenvVault\Decrypter\Decrypter; +use DotenvVault\Decrypter\DecrypterInterface; + +class DotenvVault extends Dotenv { + /** + * Keep track of the original paths + * $paths string|string[] + */ + private $paths; + + private $store; + private $parser; + private $loader; + private $repository; + + public function __construct( + StoreInterface $store, + ParserInterface $parser, + LoaderInterface $loader, + RepositoryInterface $repository, + $paths = null + ) + { + $this->store = $store; + $this->parser = $parser; + $this->loader = $loader; + $this->repository = $repository; + $this->paths = $paths; + } + + /** + * Create a new dotenv instance. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \Dotenv\Dotenv + */ + public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); + + foreach ((array) $paths as $path) { + $builder = $builder->addPath($path); + } + + foreach ((array) $names as $name) { + $builder = $builder->addName($name); + } + + if ($shortCircuit) { + $builder = $builder->shortCircuit(); + } + + return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository, $paths); + } + + /** + * Create a new immutable dotenvVault instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \DotenvVault\DotenvVault + */ + public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenvVault instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \DotenvVault\DotenvVault + */ + public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->immutable() + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + public function _loadDotenv() + { + $entries = $this->parser->parse($this->store->read()); + + return $this->loader->load($this->repository, $entries); + } + + public function load() + { + $vaultPath = $this->_vaultPath(); + + // fallback to original dotenv if DOTENV_KEY is not set + if (strlen($this->_dotenvKey()) === 0) { + return $this->_loadDotenv(); + } + + // // dotenvKey exists but .env.vault file does not exist + if (!$vaultPath || !file_exists($vaultPath)) { + trigger_error('You set DOTENV_KEY but you are missing a .env.vault file at ' . $vaultPath . '. Did you forget to build it?', E_USER_WARNING); + + return $this->_loadDotenv(); + } + + $this->_loadVault(); + } + + public function _loadVault() { + // _log('Loading env from encrypted .env.vault') + + $decrypted = $this->_decryptVault(); + + $vaultEntries = $this->parser->parse($decrypted); + + return $this->loader->load($this->repository, $vaultEntries); + } + + public function _decryptVault() + { + $vaultPath = $this->_vaultPath(); + + // .env.vault as string + $content = file_get_contents($vaultPath); + + // Parse .env.vault file with Dotenv parser + $entries = (new Parser())->parse($content); + + // built DOTENV_${ENVIRONMENT} lookups + $lookups = []; + foreach ($entries as $entry) { + $key = $entry->getName(); + $value = $entry->getValue()->get()->getChars(); // really ugly api here from the original phpdotenv lib. + + $lookups[$key] = $value; + } + + // handle scenario for comma separated keys - for use with key rotation + // example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod" + $keys = explode(',', $this->_dotenvKey()); + + $decrypted = null; + + for ($i = 0; $i < count($keys); $i++) { + try { + // Get full key + $key = trim($keys[$i]); + + // Get instructions for decrypt + $attrs = $this->_instructions($lookups, $key); + + // Decrypt + $decrypted = (new Decrypter())->decrypt($attrs['ciphertext'], $attrs['key']); + + // If successful, break the loop + break; + } catch (Exception $error) { + // if last key + if ($i + 1 >= count($keys)) { + // rethrow the exception + throw $error; + } + // Otherwise, the loop will continue to the next key + } + + } + + return $decrypted; + + // Parse contents of decrypted DOTENV_VAULT_${ENVIRONMENT} + // $vaultEntries = (new Parser())->parse($decrypted); + + // return $vaultEntries; + } + + public function _instructions($lookups, $dotenvKey) { + // Parse DOTENV_KEY. Format is a URI + $uri = parse_url($dotenvKey); + if ($uri === false) { + throw new Exception('INVALID_DOTENV_KEY: Wrong format. Must be in valid URI format like dotenv://key_1234@dotenv.org/vault/.env.vault?environment=development'); + } + + // Get decrypt key + $key = $uri['pass'] ?? null; + if (!$key) { + throw new Exception('INVALID_DOTENV_KEY: Missing key part'); + } + + // Get environment + parse_str($uri['query'], $queryParams); + $environment = $queryParams['environment'] ?? null; + if (!$environment) { + throw new Exception('INVALID_DOTENV_KEY: Missing environment part'); + } + + // Get ciphertext payload + $environmentKey = 'DOTENV_VAULT_' . strtoupper($environment); + $ciphertext = $lookups[$environmentKey] ?? null; + if (!$ciphertext) { + throw new Exception("NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment {$environmentKey} in your .env.vault file."); + } + + return ['ciphertext' => $ciphertext, 'key' => $key]; + } + + public function _dotenvKey() + { + $dotenv_key = $_ENV['DOTENV_KEY'] ?? $_SERVER['DOTENV_KEY'] ?? getenv('DOTENV_KEY'); + + // infra already contains a DOTENV_KEY environment variable + if ($dotenv_key && strlen($dotenv_key) > 0) { + return $dotenv_key; + } + + // fallback to empty string + return ''; + } + + public function _vaultPath() + { + $dotenvVaultPath = null; + + foreach ($this->paths as $dotenvPath) { + // Check if the path ends with '.vault'. If not, append '.vault' to the path. + $dotenvPath .= '/.env.vault'; + + // check if .env.vault exists + if (file_exists($dotenvPath)) { + $dotenvVaultPath = $dotenvPath; + break; + } + } + + return $dotenvVaultPath; + } + + public function _readFromFile(string $path, string $encoding = null) + { + /** @var Option */ + $content = Option::fromValue(@\file_get_contents($path), false); + + return $content->flatMap(static function (string $content) use ($encoding) { + return Str::utf8($content, $encoding)->mapError(static function (string $error) { + throw new Exception($error); + })->success(); + }); } // public function load() diff --git a/src/DotenvVaultBackup.php b/src/DotenvVaultBackup.php deleted file mode 100644 index 68ebbfb..0000000 --- a/src/DotenvVaultBackup.php +++ /dev/null @@ -1,193 +0,0 @@ -store = $store; - $this->parser = $parser; - $this->loader = $loader; - $this->repository = $repository; - } - - public function load() - { - $this->dotenv_key = getenv("DOTENV_KEY"); - if ($this->dotenv_key !== false){ - - $entries = $this->parser->parse($this->store->read()); - $this->loader->load($this->repository, $entries); - - $plaintext = $this->parse_vault(); - - // parsing plaintext and loading to getenv - $vault_entries = $this->parser->parse($plaintext); - return $this->loader->load($this->repository, $vault_entries); - } - else { - $entries = $this->parser->parse($this->store->read()); - - return $this->loader->load($this->repository, $entries); - } - } - - public function parse_vault() - { - $dotenv_keys = explode(',', $this->dotenv_key); - $keys = array(); - foreach($dotenv_keys as $key) - { - // parse DOTENV_KEY, format is a URI. - $uri = parse_url(trim($key)); - - // get encrypted key - $pass = $uri['pass']; - - // Get environment from query params. - parse_str($uri['query'], $params); - $vault_environment = $params['environment'] or throw new DotEnvVaultError('INVALID_DOTENV_KEY: Missing environment part.'); - - # Getting ciphertext from correct environment in .env.vault - $vault_environment = strtoupper($vault_environment); - $environment_key = "DOTENV_VAULT_{$vault_environment}"; - - $ciphertext = getenv("{$environment_key}") or throw new DotEnvVaultError("NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment {$environment_key} in your .env.vault file. Run 'npx dotenv-vault build' to include it."); - - array_push($keys, array('encrypted_key' => $pass, 'ciphertext' => $ciphertext)); - } - return $this->key_rotation($keys); - } - - private function key_rotation($keys){ - $count = count($keys); - foreach($keys as $index=>$value) { - $decrypt = $this->decrypt($value['ciphertext'], $value['encrypted_key']); - - if ($decrypt == false && $index + 1 >= $count){ - throw new DotEnvVaultError('INVALID_DOTENV_KEY: Key must be valid.'); - } - elseif($decrypt == false){ - continue; - } - else{ - return $decrypt; - } - } - } - - public static function decrypt($encrypted, $keyStr) - { - // grab last 64 to permit keys like vlt_64 or custom_64 - $last64 = substr($keyStr, -64); - - if (strlen($last64) !== 64) { - $msg = 'INVALID_DOTENV_KEY: It must be 64 characters long (or more)'; - throw new Exception($msg); - } - - // check key length is good INVALID_DOTENV_KEY: It must be 64 characters long (or more) - $key = hex2bin($last64); - - // base64 decode - $decoded = base64_decode($encrypted, true); - - // determine cipher and pull out nonce and tag - $ciphertext = substr($decoded, 12, -16); - $nonce = substr($decoded, 0, 12); - $tag = substr($decoded, -16); - - try { - $plaintext = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag); - - if ($plaintext === false) { - $msg = 'DECRYPTION_FAILED: Please check your DOTENV_KEY'; - throw new Exception($msg); - } else { - return $plaintext; - } - } catch (ExceptionType $e) { - $msg = 'DECRYPTION_FAILED: Please check your DOTENV_KEY'; - throw new Exception($msg); - } - } - - /** - * Create a new immutable dotenvVault instance with default repository. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \DotenvVault\DotenvVault - */ - public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - /** - * Create a new immutable dotenvVault instance with default repository with the putenv adapter. - * - * @param string|string[] $paths - * @param string|string[]|null $names - * @param bool $shortCircuit - * @param string|null $fileEncoding - * - * @return \DotenvVault\DotenvVault - */ - public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters() - ->addAdapter(PutenvAdapter::class) - ->immutable() - ->make(); - - return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); - } - - public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); - - foreach ((array) $paths as $path) { - $builder = $builder->addPath($path); - } - - foreach ((array) $names as $name) { - $builder = $builder->addName($name); - } - - if ($shortCircuit) { - $builder = $builder->shortCircuit(); - } - - return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); - } -} diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php index 340d6e9..052574b 100644 --- a/tests/DotenvVault/DotenvVaultTest.php +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -31,23 +31,23 @@ public static function setFolder() public function testThrowsExceptionIfUnableToLoadFile() { - $dotenv = DotenvVault::createMutable(__DIR__); + $dotenvVault = DotenvVault::createMutable(__DIR__); - $this->expectException(Dotenv\Exception\InvalidPathException::class); $this->expectExceptionMessage('Unable to read any of the environment file(s) at'); - $dotenv->load(); + $dotenvVault->load(); } public function testTriesPathsToLoad() { - $dotenv = DotenvVault::createMutable([__DIR__, self::$folder]); - self::assertCount(4, $dotenv->load()); + $dotenvVault = DotenvVault::createMutable([__DIR__, self::$folder]); + self::assertCount(4, $dotenvVault->load()); } public function testLoadsFromEnvFile() { - DotenvVault::createMutable([__DIR__, self::$folder]); + $dotenvVault = DotenvVault::createImmutable([__DIR__, self::$folder]); + $dotenvVault->load(); self::assertEquals($_ENV['FOO'], 'bar'); self::assertEquals($_ENV['BAR'], 'baz'); @@ -57,8 +57,9 @@ public function testLoadsFromEnvVaultFileWhenDotenvKeyPresent() { $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; - DotenvVault::createMutable([__DIR__, self::$folder]); + $dotenvVault = DotenvVault::createImmutable([__DIR__, self::$folder]); + $dotenvVault->load(); - self::assertEquals($_ENV['ALPA'], 'zeta'); + self::assertEquals($_ENV['ALPHA'], 'zeta'); } } From bf21fd5b0804bb66b541795e548f5d2792be29b0 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:12:32 -0700 Subject: [PATCH 12/18] Include .env files for tests --- .gitignore | 8 +++----- tests/fixtures/env/.env | 5 +++++ tests/fixtures/env/.env.vault | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/env/.env create mode 100644 tests/fixtures/env/.env.vault diff --git a/.gitignore b/.gitignore index 290276a..e811ed4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,7 @@ - -# Environments -.env +.env* .venv -.env.vault -.env.me +!.env.vault +!tests/**/.env* .phpunit.result.cache composer.lock diff --git a/tests/fixtures/env/.env b/tests/fixtures/env/.env new file mode 100644 index 0000000..3a006e1 --- /dev/null +++ b/tests/fixtures/env/.env @@ -0,0 +1,5 @@ +FOO=bar +BAR=baz +SPACED="with spaces" + +NULL= diff --git a/tests/fixtures/env/.env.vault b/tests/fixtures/env/.env.vault new file mode 100644 index 0000000..19bdd46 --- /dev/null +++ b/tests/fixtures/env/.env.vault @@ -0,0 +1 @@ +DOTENV_VAULT_DEVELOPMENT='s7NYXa809k/bVSPwIAmJhPJmEGTtU0hG58hOZy7I0ix6y5HP8LsHBsZCYC/gw5DDFy5DgOcyd18R' From 87d2365da715e2b46c80a9e904f57255ed92eba5 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:17:26 -0700 Subject: [PATCH 13/18] Modify tests --- .github/workflows/ci.yml | 2 +- composer.json | 2 +- phpunit.xml.dist | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcbc29b..584bb1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,4 +30,4 @@ jobs: timeout_minutes: 5 max_attempts: 5 command: composer update --no-interaction --no-progress - - run: vendor/bin/phpunit --display-deprecations + - run: vendor/bin/phpunit diff --git a/composer.json b/composer.json index 2cb677b..35f6655 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,6 @@ "vlucas/phpdotenv": "^5.5" }, "require-dev": { - "phpunit/phpunit": "^10.4" + "phpunit/phpunit": "^9.0|^8.0|^7.0|6.0" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 020141b..4237315 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,13 @@ - - + + + + ./src + + ./tests - - - ./src - - From 7295a304928ca2de66c124c5c14e36d21c9fc4d0 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:21:14 -0700 Subject: [PATCH 14/18] Update changelog --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09f38a1..1ebdf83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [Unreleased](https://github.com/dotenv-org/phpdotenv-vault/compare/v0.1.3...master) +## [Unreleased](https://github.com/dotenv-org/phpdotenv-vault/compare/v0.2.0...master) + +## 0.2.0 + +### Added + +- Moved decryption to its own class for better testing and ease of usage + +### Fixed + +- DOTENV_KEY was not respected if set in the infrastructure. Fixed. +- Decryptiong could fail related to some misconfigured logic. Fixed. ## 0.1.3 From 88146524fe99994ea45ee716e4a121c83d7a2cda Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:27:12 -0700 Subject: [PATCH 15/18] Remove cruft --- .gitignore | 1 + src/DotenvVault.php | 65 --------------------------------------------- 2 files changed, 1 insertion(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index e811ed4..d6af236 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ vendor .vscode .phpunit.cache + diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 49372d6..5d02db0 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -279,69 +279,4 @@ public function _readFromFile(string $path, string $encoding = null) })->success(); }); } - - // public function load() - // { - // $this->dotenv_key = getenv("DOTENV_KEY"); - // if ($this->dotenv_key !== false){ - - // $entries = $this->parser->parse($this->store->read()); - // $this->loader->load($this->repository, $entries); - - // $plaintext = $this->parse_vault(); - - // // parsing plaintext and loading to getenv - // $vault_entries = $this->parser->parse($plaintext); - // return $this->loader->load($this->repository, $vault_entries); - // } - // else { - // $entries = $this->parser->parse($this->store->read()); - - // return $this->loader->load($this->repository, $entries); - // } - // } - - // public function parse_vault() - // { - // $dotenv_keys = explode(',', $this->dotenv_key); - // $keys = array(); - // foreach($dotenv_keys as $key) - // { - // // parse DOTENV_KEY, format is a URI. - // $uri = parse_url(trim($key)); - - // // get encrypted key - // $pass = $uri['pass']; - // - // // Get environment from query params. - // parse_str($uri['query'], $params); - // $vault_environment = $params['environment'] or throw new Exception('INVALID_DOTENV_KEY: Missing environment part.'); - - // # Getting ciphertext from correct environment in .env.vault - // $vault_environment = strtoupper($vault_environment); - // $environment_key = "DOTENV_VAULT_{$vault_environment}"; - - // $ciphertext = getenv("{$environment_key}") or throw new Exception("NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment {$environment_key} in your .env.vault file. Run 'npx dotenv-vault build' to include it."); - // - // array_push($keys, array('encrypted_key' => $pass, 'ciphertext' => $ciphertext)); - // } - // return $this->key_rotation($keys); - // } - - // private function key_rotation($keys){ - // $count = count($keys); - // foreach($keys as $index=>$value) { - // $decrypt = $this->decrypt($value['ciphertext'], $value['encrypted_key']); - - // if ($decrypt == false && $index + 1 >= $count){ - // throw new Exception('INVALID_DOTENV_KEY: Key must be valid.'); - // } - // elseif($decrypt == false){ - // continue; - // } - // else{ - // return $decrypt; - // } - // } - // } } From e0ac09945f41099ba633d47496da3ef5441d01b4 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:32:28 -0700 Subject: [PATCH 16/18] Update README to pass as an array --- README.md | 2 +- src/DotenvVault.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31bbede..03a381d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ As early as possible in your application bootstrap process, load .env: ```php require 'vendor/autoload.php'; -$dotenv = DotenvVault\DotenvVault::createImmutable(__DIR__); +$dotenv = DotenvVault\DotenvVault::createImmutable([__DIR__]); $dotenv->safeLoad(); ``` diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 5d02db0..04c943a 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -62,6 +62,7 @@ public static function create(RepositoryInterface $repository, $paths, $names = { $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); + $passedPaths = [] foreach ((array) $paths as $path) { $builder = $builder->addPath($path); } @@ -132,7 +133,7 @@ public function load() // // dotenvKey exists but .env.vault file does not exist if (!$vaultPath || !file_exists($vaultPath)) { - trigger_error('You set DOTENV_KEY but you are missing a .env.vault file at ' . $vaultPath . '. Did you forget to build it?', E_USER_WARNING); + trigger_error("You set DOTENV_KEY but you are missing a .env.vault file at '{$vaultPath}'. Did you forget to build it?", E_USER_WARNING); return $this->_loadDotenv(); } From e0f35cc9576dc2d357376be00482711c54282209 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 21:38:55 -0700 Subject: [PATCH 17/18] Support passing string directly --- CHANGELOG.md | 6 ++++++ src/DotenvVault.php | 3 +-- tests/DotenvVault/DotenvVaultTest.php | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ebdf83..10da428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. See [standa ## [Unreleased](https://github.com/dotenv-org/phpdotenv-vault/compare/v0.2.0...master) +## 0.2.1 + +### Changed + +- Added support for passing string to paths argument. + ## 0.2.0 ### Added diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 04c943a..8d2aadf 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -62,7 +62,6 @@ public static function create(RepositoryInterface $repository, $paths, $names = { $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); - $passedPaths = [] foreach ((array) $paths as $path) { $builder = $builder->addPath($path); } @@ -255,7 +254,7 @@ public function _vaultPath() { $dotenvVaultPath = null; - foreach ($this->paths as $dotenvPath) { + foreach ((array) $this->paths as $dotenvPath) { // Check if the path ends with '.vault'. If not, append '.vault' to the path. $dotenvPath .= '/.env.vault'; diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php index 052574b..3f65557 100644 --- a/tests/DotenvVault/DotenvVaultTest.php +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -53,6 +53,15 @@ public function testLoadsFromEnvFile() self::assertEquals($_ENV['BAR'], 'baz'); } + public function testLoadsFromEnvFileAndPathsPassedAsString() + { + $dotenvVault = DotenvVault::createImmutable(self::$folder); + $dotenvVault->load(); + + self::assertEquals($_ENV['FOO'], 'bar'); + self::assertEquals($_ENV['BAR'], 'baz'); + } + public function testLoadsFromEnvVaultFileWhenDotenvKeyPresent() { $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; @@ -62,4 +71,14 @@ public function testLoadsFromEnvVaultFileWhenDotenvKeyPresent() self::assertEquals($_ENV['ALPHA'], 'zeta'); } + + public function testLoadsFromEnvVaultFileWhenDotenvKeyPresentAndPathsPassedAsString() + { + $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; + + $dotenvVault = DotenvVault::createImmutable(self::$folder); + $dotenvVault->load(); + + self::assertEquals($_ENV['ALPHA'], 'zeta'); + } } From 471fe7425ec6887907e67619ab6c9064c6c5b02b Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Fri, 3 Nov 2023 22:06:14 -0700 Subject: [PATCH 18/18] Add additional methods --- src/DotenvVault.php | 80 ++++++++++++++++++--------- tests/DotenvVault/DotenvVaultTest.php | 76 +++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 32 deletions(-) diff --git a/src/DotenvVault.php b/src/DotenvVault.php index 8d2aadf..d28e3e5 100644 --- a/src/DotenvVault.php +++ b/src/DotenvVault.php @@ -9,6 +9,8 @@ use Dotenv\Loader\LoaderInterface; use Dotenv\Parser\Parser; use Dotenv\Parser\ParserInterface; +use Dotenv\Repository\Adapter\ArrayAdapter; +use Dotenv\Repository\Adapter\PutenvAdapter; use Dotenv\Repository\RepositoryBuilder; use Dotenv\Repository\RepositoryInterface; use Dotenv\Store\StoreBuilder; @@ -77,8 +79,9 @@ public static function create(RepositoryInterface $repository, $paths, $names = return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository, $paths); } + /** - * Create a new immutable dotenvVault instance with default repository. + * Create a new mutable dotenv instance with default repository. * * @param string|string[] $paths * @param string|string[]|null $names @@ -87,15 +90,15 @@ public static function create(RepositoryInterface $repository, $paths, $names = * * @return \DotenvVault\DotenvVault */ - public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) - { - $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); + public static function createMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } /** - * Create a new immutable dotenvVault instance with default repository with the putenv adapter. + * Create a new mutable dotenv instance with default repository with the putenv adapter. * * @param string|string[] $paths * @param string|string[]|null $names @@ -104,21 +107,50 @@ public static function createImmutable($paths, $names = null, bool $shortCircuit * * @return \DotenvVault\DotenvVault */ - public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { $repository = RepositoryBuilder::createWithDefaultAdapters() ->addAdapter(PutenvAdapter::class) - ->immutable() ->make(); return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } - public function _loadDotenv() + /** + * Create a new immutable dotenv instance with default repository. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \DotenvVault\DotenvVault + */ + public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) { - $entries = $this->parser->parse($this->store->read()); + $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); - return $this->loader->load($this->repository, $entries); + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); + } + + /** + * Create a new immutable dotenv instance with default repository with the putenv adapter. + * + * @param string|string[] $paths + * @param string|string[]|null $names + * @param bool $shortCircuit + * @param string|null $fileEncoding + * + * @return \DotenvVault\DotenvVault + */ + public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null) + { + $repository = RepositoryBuilder::createWithDefaultAdapters() + ->addAdapter(PutenvAdapter::class) + ->immutable() + ->make(); + + return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); } public function load() @@ -140,6 +172,17 @@ public function load() $this->_loadVault(); } + // + // public functions treated like private functions. + // exposed publicly for convenience of your consumption. + // + public function _loadDotenv() + { + $entries = $this->parser->parse($this->store->read()); + + return $this->loader->load($this->repository, $entries); + } + public function _loadVault() { // _log('Loading env from encrypted .env.vault') @@ -200,11 +243,6 @@ public function _decryptVault() } return $decrypted; - - // Parse contents of decrypted DOTENV_VAULT_${ENVIRONMENT} - // $vaultEntries = (new Parser())->parse($decrypted); - - // return $vaultEntries; } public function _instructions($lookups, $dotenvKey) { @@ -267,16 +305,4 @@ public function _vaultPath() return $dotenvVaultPath; } - - public function _readFromFile(string $path, string $encoding = null) - { - /** @var Option */ - $content = Option::fromValue(@\file_get_contents($path), false); - - return $content->flatMap(static function (string $content) use ($encoding) { - return Str::utf8($content, $encoding)->mapError(static function (string $error) { - throw new Exception($error); - })->success(); - }); - } } diff --git a/tests/DotenvVault/DotenvVaultTest.php b/tests/DotenvVault/DotenvVaultTest.php index 3f65557..a59d218 100644 --- a/tests/DotenvVault/DotenvVaultTest.php +++ b/tests/DotenvVault/DotenvVaultTest.php @@ -40,11 +40,11 @@ public function testThrowsExceptionIfUnableToLoadFile() public function testTriesPathsToLoad() { - $dotenvVault = DotenvVault::createMutable([__DIR__, self::$folder]); + $dotenvVault = DotenvVault::createImmutable([__DIR__, self::$folder]); self::assertCount(4, $dotenvVault->load()); } - public function testLoadsFromEnvFile() + public function testLoadFromEnvFile() { $dotenvVault = DotenvVault::createImmutable([__DIR__, self::$folder]); $dotenvVault->load(); @@ -53,7 +53,43 @@ public function testLoadsFromEnvFile() self::assertEquals($_ENV['BAR'], 'baz'); } - public function testLoadsFromEnvFileAndPathsPassedAsString() + public function testLoadFromEnvFileMutable() + { + $dotenvVault = DotenvVault::createMutable([__DIR__, self::$folder]); + $dotenvVault->load(); + + self::assertEquals($_ENV['FOO'], 'bar'); + self::assertEquals($_ENV['BAR'], 'baz'); + } + + public function testLoadFromEnvFileUnsafeMutable() + { + $dotenvVault = DotenvVault::createUnsafeMutable([__DIR__, self::$folder]); + $dotenvVault->load(); + + self::assertEquals(getenv('FOO'), 'bar'); + self::assertEquals(getenv('BAR'), 'baz'); + } + + public function testLoadFromEnvFileUnsafeImmutable() + { + $dotenvVault = DotenvVault::createUnsafeImmutable([__DIR__, self::$folder]); + $dotenvVault->load(); + + self::assertEquals(getenv('FOO'), 'bar'); + self::assertEquals(getenv('BAR'), 'baz'); + } + + public function testSafeLoadFromEnvFile() + { + $dotenvVault = DotenvVault::createImmutable([__DIR__, self::$folder]); + $dotenvVault->safeLoad(); + + self::assertEquals($_ENV['FOO'], 'bar'); + self::assertEquals($_ENV['BAR'], 'baz'); + } + + public function testLoadFromEnvFileAndPathsPassedAsString() { $dotenvVault = DotenvVault::createImmutable(self::$folder); $dotenvVault->load(); @@ -62,7 +98,7 @@ public function testLoadsFromEnvFileAndPathsPassedAsString() self::assertEquals($_ENV['BAR'], 'baz'); } - public function testLoadsFromEnvVaultFileWhenDotenvKeyPresent() + public function testLoadFromEnvVaultFileWhenDotenvKeyPresent() { $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; @@ -72,7 +108,7 @@ public function testLoadsFromEnvVaultFileWhenDotenvKeyPresent() self::assertEquals($_ENV['ALPHA'], 'zeta'); } - public function testLoadsFromEnvVaultFileWhenDotenvKeyPresentAndPathsPassedAsString() + public function testLoadFromEnvVaultFileWhenDotenvKeyPresentAndPathsPassedAsString() { $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; @@ -81,4 +117,34 @@ public function testLoadsFromEnvVaultFileWhenDotenvKeyPresentAndPathsPassedAsStr self::assertEquals($_ENV['ALPHA'], 'zeta'); } + + public function testLoadFromEnvVaultFileWhenDotenvKeyPresentMutable() + { + $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; + + $dotenvVault = DotenvVault::createMutable([__DIR__, self::$folder]); + $dotenvVault->load(); + + self::assertEquals($_ENV['ALPHA'], 'zeta'); + } + + public function testLoadFromEnvVaultFileWhenDotenvKeyPresentUnsafeImmutable() + { + $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; + + $dotenvVault = DotenvVault::createUnsafeImmutable([__DIR__, self::$folder]); + $dotenvVault->load(); + + self::assertEquals(getenv('ALPHA'), 'zeta'); + } + + public function testLoadFromEnvVaultFileWhenDotenvKeyPresentUnsafeMutable() + { + $_ENV["DOTENV_KEY"] = 'dotenv://:key_ddcaa26504cd70a6fef9801901c3981538563a1767c297cb8416e8a38c62fe00@dotenv.org/vault/.env.vault?environment=development'; + + $dotenvVault = DotenvVault::createUnsafeMutable([__DIR__, self::$folder]); + $dotenvVault->load(); + + self::assertEquals(getenv('ALPHA'), 'zeta'); + } }