From b1a7bf454696142e4d8effb4204793cf7d91cd6b Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Thu, 24 Aug 2023 12:47:10 +0200 Subject: [PATCH] feat: add key chaining for encrypt and decrypt --- src/FileEncryption.php | 131 ++++++++++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 42 deletions(-) diff --git a/src/FileEncryption.php b/src/FileEncryption.php index c49e467..ddda4ea 100644 --- a/src/FileEncryption.php +++ b/src/FileEncryption.php @@ -10,7 +10,8 @@ class FileEncryption { private string $input; - // Enable or disable --armor flag + private ?string $public_key = null; + private ?string $private_key = null; private bool $armor = false; public function __construct(mixed $input) { @@ -28,10 +29,18 @@ /* ---- */ // Execute a shell command from enum of allowed commands - private static function exec(ShellCommands $cmd, string $args = ""): string|null { + private static function exec(ShellCommands $cmd, string $args = ""): ?string { return shell_exec("{$cmd->value} {$args} 2>&1"); } + // File is readable or throw + private static function is_readable_or_throw(string $path): true { + if (!is_file($path) || !is_readable($path)) { + throw new \Exception("Input file '{$path}' is not readable for current user"); + } + return true; + } + // Parent directory of $output is writeable or throw private static function is_writable_or_throw(string $path): true { if (!is_writable(dirname($path))) { @@ -40,20 +49,23 @@ return true; } - // Generate asymmetric key pair - private static function keygen(): array { - // Generate age key pair - $keygen = explode(PHP_EOL, self::exec(ShellCommands::KEYGEN)); - // Remove first line of output - array_shift($keygen); + // Parse and extract public key from age-keygen output + private static function parse_public_key(string|array $input): string { + // Split age-keygen output by line + $lines = !is_array($input) ? explode(PHP_EOL, $input, 3) : $input; - // Return asymmetric key pair strings as assoc array - return [ - // Extract public key from second line of age-keygen output - "public" => substr($keygen[1], 14), - // Return generated key pair - "private" => implode(PHP_EOL, $keygen) - ]; + // Public key will be on second line if we received an age-keygen string + // Otherwise we will assume we got the public key string directly + $public_key = $lines[count($lines) > 0 ? 1 : 0]; + // Strip prefix if present + return str_replace("# public key: ", "", $public_key); + } + + private function parse_private_key(string|array $input): string { + // Split age-keygen output by line + $lines = !is_array($input) ? explode(PHP_EOL, $input, 3) : $input; + // Private key will be on the 3rd line of an age-keygen output + return $lines[2]; } /* ---- */ @@ -64,17 +76,63 @@ return $this; } - // Decrypt a file using a provided private key string and output file name - public function decrypt(string $key_file, string $output): true { - $this->is_writable_or_throw($output); + public function public_key(string $input): self { + // Get public key from age-keygen file + if (is_file($input)) { + $this->is_readable_or_throw($input); - // Throw if private key file is not readable - if (!is_readable($key_file)) { - throw new \Exception("Failed to open private key file '{$key_file}'"); + $input = self::parse_public_key(file_get_contents($input)); } + + $this->public_key = $input; + return $this; + } + + public function private_key(string $input): self { + $this->is_readable_or_throw($input); + + $this->private_key = $input; + return $this; + } + + // Generate asymmetric key pair and optionally write to file + public function keygen(?string $output = null): self { + // Generate age key pair + $keygen = explode(PHP_EOL, self::exec(ShellCommands::KEYGEN)); + // Remove first line of output + array_shift($keygen); + + // Set global key properties + $this->public_key(self::parse_public_key($keygen)); + $this->private_key = implode(PHP_EOL, $keygen); + + // Write generated key pair to file + if ($output) { + $this->is_writable_or_throw($output); + + file_put_contents($output, implode(PHP_EOL, $keygen)); + } + + return $this; + } + + /* ---- */ + + // Return key pair as assoc array + public function get_keypair(): array { + return [ + "public" => $this->public_key, + "private" => $this->private_key + ]; + } + + // Decrypt a file using a provided private key string and output file name + public function decrypt(string $output): true { + $this->is_writable_or_throw($output); - // Attempt to decrypt file using private key file - $decrypt = $this->exec(ShellCommands::AGE, "--decrypt -i {$key_file} -o {$output} {$this->input}"); + // Decrypt file using private key file + $cmd = "--decrypt -i {$this->private_key} -o {$output} {$this->input}"; + $decrypt = $this->exec(ShellCommands::AGE, $cmd); // Decryption failed if (!is_null($decrypt)) { @@ -85,31 +143,20 @@ } // Encrypt a file and return its private key string - public function encrypt(string $output, string|null $output_key = null): string|false { + public function encrypt(string $output): array { $this->is_writable_or_throw($output); - // Generate asymmetric keypair - $key = $this->keygen(); - // Add --armor string if PEM encoding is enabled + // Add --armor flag if PEM encoding is enabled $armor = $this->armor ? "--armor" : ""; - // Encrypt file to output using age - $encrypt = $this->exec(ShellCommands::AGE, "--encrypt -r {$key["public"]} {$armor} -o {$output} {$this->input}"); + $cmd = "--encrypt -r {$this->public_key} {$armor} -o {$output} {$this->input}"; + $encrypt = $this->exec(ShellCommands::AGE, $cmd); - // Write private key to file - if (is_null($encrypt) && $output_key) { - $this->is_writable_or_throw($output_key); - - // Write private key to file - file_put_contents($output_key, $key["private"]); - - // Throw if private key file could not be created - if (!is_file($output_key)) { - throw new \Exception("Failed to write private key file to '{$output_key}'"); - } + if (!is_null($encrypt)) { + throw new \Exception("Failed to encrypt '{$this->input}' using public key '{$this->public_key}'"); } - // Return private key if $encrypt returns null - return is_null($encrypt) ? $key["private"] : false; + // Return keypair + return $this->get_keypair(); } } \ No newline at end of file