From 126f8043a780d18159bf9cdd8cef571d52d99ef6 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 7 Dec 2024 21:38:51 +0100 Subject: [PATCH] added support for age encryption for automatic encryption against ssh or age keys after upload --- README.md | 2 +- config/example.config.inc.php | 6 ++++++ docker/Dockerfile | 2 +- docker/rootfs/start.sh | 3 +++ rtfm/encryption.md | 22 ++++++++++++++++++++- web/index.php | 13 +++++++++--- web/lib/encryption.php | 37 +++++++++++++++++++++++++++++++++++ 7 files changed, 79 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3a9689f..fd903a8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
-![](https://img.shields.io/badge/php-7.4%2B-brightgreen.svg) +![](https://img.shields.io/badge/php-8.3%2B-brightgreen.svg) [![Apache License](https://img.shields.io/badge/license-Apache-brightgreen.svg?style=flat)](https://github.com/hascheksolutions/backupdrop/blob/master/LICENSE) ![HitCount](http://hits.dwyl.com/hascheksolutions/backupdrop.svg) [![](https://img.shields.io/github/stars/hascheksolutions/backupdrop.svg?label=Stars&style=social)](https://github.com/hascheksolutions/backupdrop) diff --git a/config/example.config.inc.php b/config/example.config.inc.php index 775f19d..52b5ef5 100644 --- a/config/example.config.inc.php +++ b/config/example.config.inc.php @@ -3,6 +3,12 @@ // copy this file to config.inc.php // and edit to your needs + +// AGE encryption settings +// More info on age encryption: https://github.com/FiloSottile/age +define('ENCRYPTION_AGE_SSH_PUBKEY',''); // Enter your SSH public key here to automatically encrypt all uploads +define('ENCRYPTION_AGE_PUBKEY',''); // Enter an "age public key" created with `age-keygen -o key.txt` here to automatically encrypt all uploads with this key + // global settings for retention and version control // 0 means unlimited define('KEEP_N_BACKUPS',0); // How many uploads will be saved. Oldest one will be deleted if this number is surpassed diff --git a/docker/Dockerfile b/docker/Dockerfile index 8ad975c..88754c5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ FROM alpine:3.20 RUN apk add --no-cache pwgen sudo socat wget curl git nginx \ -php83-ctype php83-ftp php83-simplexml php83 php83-phar php83-curl php83-openssl php83-mbstring php83-json php83-dom php83-fpm +php83-ctype php83-ftp php83-simplexml php83 php83-phar php83-curl php83-openssl php83-mbstring php83-json php83-dom php83-fpm age RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer RUN mkdir -p /var/www/backupdrop diff --git a/docker/rootfs/start.sh b/docker/rootfs/start.sh index 3e32ec2..023c070 100644 --- a/docker/rootfs/start.sh +++ b/docker/rootfs/start.sh @@ -20,6 +20,9 @@ nginx _buildConfig() { echo " secrets.txt` + +Read more about age and SSH key based encryption [here](https://github.com/FiloSottile/age?tab=readme-ov-file#ssh-keys) + +### age public key encryption +You can generate an age public and private key by running `age-keygen -o key.txt` which will generate a key file and print out the public key. You can put this public key in the config option `ENCRYPTION_AGE_PUBKEY` and all uploads will automatically be encrypted against your key. + +For example if you upload `secrets.txt`, it will be stored as `secrets.txt.age` in the data directory. To decrypt you can run `age --decrypt -i key.txt secrets.txt.age > secrets.txt` ## Method 1: Encrypt using Public Key This method should only be used on **smaller files**. Because of the nature of the algorithm we can only encrypt 245 characters at a time which means encrypting of large files will be painfully slow. diff --git a/web/index.php b/web/index.php index 28b990c..17f27d5 100644 --- a/web/index.php +++ b/web/index.php @@ -35,7 +35,7 @@ //let's filter out the hostname and get rid of every special char except for: . _ - $hostname = preg_replace("/[^a-zA-Z0-9\.\-_]+/", "", $hostname); - echo json_encode(handleUpload($hostname)); + echo json_encode(handleUpload($hostname)).PHP_EOL; } @@ -47,11 +47,11 @@ function handleUpload($hostname) if(isset($_FILES["file"]) && $_FILES["file"]["error"] == 0) { //target name of the backup is the date and the original extension - $backupname = date("Y-m-d H.i").'.'.pathinfo($_FILES["file"]["name"], PATHINFO_EXTENSION); + $backupname = date("Y-m-d_H.i").'.'.pathinfo($_FILES["file"]["name"], PATHINFO_EXTENSION); $path = ROOT.DS.'..'.DS.'data'.DS.$hostname.DS; if(!is_dir($path)) mkdir($path); //if the path doesn't exist yet, create it - // if the user wants to encrypt it + // if the user wants to encrypt it using custom key if($_REQUEST['enc_key'] || $_REQUEST['pub_key']) { $backupname.='.enc'; @@ -59,6 +59,13 @@ function handleUpload($hostname) if(!$e->encryptFile($_FILES["file"]["tmp_name"], ($_REQUEST['enc_key']?:$_REQUEST['pub_key']), $path.$backupname,($_REQUEST['pub_key']?true:false))) return ['status'=>'error','reason'=>'Failed to encrypt. Is the Key valid?']; } + else if(defined('ENCRYPTION_AGE_SSH_PUBKEY') || defined('ENCRYPTION_AGE_PUBKEY') && (new Encryption)->checkAge()) //if the user wants to encrypt it using the predefined key + { + $backupname.='.age'; + $e = new Encryption; + if(!$e->encryptAge($_FILES["file"]["tmp_name"], $path.$backupname)) + return ['status'=>'error','reason'=>'Failed to encrypt. Is the Key valid?']; + } else move_uploaded_file($_FILES["file"]["tmp_name"], $path.$backupname); diff --git a/web/lib/encryption.php b/web/lib/encryption.php index db2cc17..52f9759 100644 --- a/web/lib/encryption.php +++ b/web/lib/encryption.php @@ -110,4 +110,41 @@ function decryptFile($source, $key, $dest,$pubkey=false) return $error ? false : $dest; } + + function checkAge() + { + $age = shell_exec('which age'); + if($age) + return true; + return false; + } + + function encryptAge($source, $dest){ + if(!$this->checkAge()) + throw new Exception('age not found'); + $pubkey = defined('ENCRYPTION_AGE_SSH_PUBKEY') && ENCRYPTION_AGE_SSH_PUBKEY != '' ? ENCRYPTION_AGE_SSH_PUBKEY : false; + $sshpubkey = defined('ENCRYPTION_AGE_PUBKEY') && ENCRYPTION_AGE_PUBKEY != '' ? ENCRYPTION_AGE_PUBKEY : false; + + if(!$pubkey && !$sshpubkey) + throw new Exception('No pubkeys configured'); + + $cmd = ['age']; + + if($pubkey) + $cmd[] = '-r '.escapeshellarg(ENCRYPTION_AGE_SSH_PUBKEY); + if($sshpubkey) + $cmd[] = '-r '.escapeshellarg(ENCRYPTION_AGE_PUBKEY); + + $cmd[] = '-o '.escapeshellarg($dest); + $cmd[] = escapeshellarg($source); + + + $cmd = implode(' ',$cmd); + + shell_exec($cmd); + + if(file_exists($dest) && filesize($dest) > 0) + return true; + return false; + } }