mirror of
https://codeberg.org/vlw/php-age.git
synced 2025-09-13 16:03:42 +02:00
feat: add key chaining for encrypt and decrypt (#2)
* feat: add key chaining for encrypt and decrypt * feat(doc): update README
This commit is contained in:
parent
ea7396b68c
commit
1f32cb95bf
2 changed files with 131 additions and 57 deletions
57
README.md
57
README.md
|
@ -2,23 +2,29 @@
|
|||
Encrypt and decrypt files with age from PHP. This library is only a wrapper for the the command line tool, it does not implement the C2CP age specification in PHP.
|
||||
|
||||
```php
|
||||
// Encrypt a file with a generated key
|
||||
$age = new FileEncryption("hello.txt");
|
||||
$age->encrypt("hello.txt.age", "hello.txt.age.key");
|
||||
$keypair = $age->keygen("hello.key")->encrypt("hello.txt.age");
|
||||
|
||||
// Encrypt a file with a specific public key
|
||||
$age->public_key("age1mrf8uana2kan6jsrnf04ywxycvl4nnkzzk3et8rdz6fe6vg7upssclnak7")->encrypt("hello.txt.age");
|
||||
```
|
||||
```php
|
||||
// Decrypt a file with a key file
|
||||
$age = new FileEncryption("hello.txt.age");
|
||||
$age->decrypt("hello.txt.age.key", "hello-decrypted.txt");
|
||||
$age->private_key("hello.key")->decrypt("decrypted-hello.txt");
|
||||
```
|
||||
|
||||
## Installation
|
||||
# Installation
|
||||
This library requires PHP 8.1+ and the [age command line tool](https://github.com/FiloSottile/age).
|
||||
|
||||
1. [Install the age command line tool](https://github.com/FiloSottile/age#installation)
|
||||
2. Install this library with composer
|
||||
```
|
||||
composer require victorwesterlund/php-age
|
||||
```
|
||||
3. Import and use in your project
|
||||
|
||||
## How to use
|
||||
# How to use
|
||||
Import and use the library:
|
||||
```php
|
||||
require_once "vendor/autoload.php";
|
||||
|
@ -26,27 +32,48 @@ require_once "vendor/autoload.php";
|
|||
use \Age\FileEncryption;
|
||||
```
|
||||
|
||||
### Encrypt a file
|
||||
## Encrypt a file
|
||||
Encrypt a file on disk by passing it to the `FileEncryption` constructor
|
||||
```php
|
||||
// Relative or absolute path to a file that should be encrypted
|
||||
$age = new FileEncryption("hello.txt");
|
||||
// Encrypted file destination and private key destination
|
||||
// This method also returns the private key as a string
|
||||
$age->encrypt("hello.txt.age", "hello.txt.age.key");
|
||||
```
|
||||
|
||||
You can enable optional PEM encoding by chaining `armor()` before `encrypt()`
|
||||
> **Note**
|
||||
> The library will not archive a folder for you. You'll have to `tar` your folder before passing it to `FileEncryption`
|
||||
|
||||
### Generated key pair
|
||||
You can encrypt a file with a generated key pair (`age-keygen`) by chaining `keygen()`
|
||||
```php
|
||||
// Encrypt with PEM encoding
|
||||
$age->armor()->encrypt("hello.txt.age", "hello.txt.age.key");
|
||||
// encrypt() will return the generated keypair as an assoc array
|
||||
$keypair = $age->keygen()->encrypt("hello.txt.age"); // ["public" => "...", "private" => "..."]
|
||||
```
|
||||
|
||||
### Decrypt a file
|
||||
You can also save the generated key file to disk by passing an absolute or relative path to `keygen()`
|
||||
```php
|
||||
$keypair = $age->keygen("hello.key)->encrypt("hello.txt.age"); // ["public" => "...", "private" => "..."]
|
||||
```
|
||||
|
||||
### Existing public key
|
||||
Encrypt a file with an existing public key by chaining the `public_key()` method
|
||||
```php
|
||||
$keypair = $age->public_key("age1mrf8uana2kan6jsrnf04ywxycvl4nnkzzk3et8rdz6fe6vg7upssclnak7")->encrypt("hello.txt.age"); // ["public" => "age1mrf8uana2kan6jsrnf04ywxycvl4nnkzzk3et8rdz6fe6vg7upssclnak7", "private" => null]
|
||||
```
|
||||
|
||||
## Decrypt a file
|
||||
Decrypt a file on disk by passing it to the `FileEncryption` constructor
|
||||
```php
|
||||
// Relative or absolute path to a file that should be decrypted
|
||||
$age = new FileEncryption("hello.txt.age");
|
||||
// Private key file and destination of decrypted file
|
||||
$age->decrypt("hello.txt.age.key", "hello.txt");
|
||||
```
|
||||
Chain `private_key()` with an absolute or relative path to the corresponding key file
|
||||
```
|
||||
$age->private_key("hello.key")->decrypt("decrypted-hello.txt"); // true
|
||||
```
|
||||
|
||||
## Optional features
|
||||
|
||||
Enable PEM encoding by chaining the optional `armor()` method
|
||||
```
|
||||
$keypair = $age->armor()->keygen()->encrypt("hello.txt.age");
|
||||
```
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue