feat: add key chaining for encrypt and decrypt

This commit is contained in:
Victor Westerlund 2023-08-24 12:47:10 +02:00
parent ea7396b68c
commit b1a7bf4546

View file

@ -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();
}
}