Start Python reimplementation. Added make_backup method
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
name: backup.sh
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bash:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt update && apt -y install rsync tar openssl shellcheck
|
||||
- name: Run ShellCheck
|
||||
run: |
|
||||
shellcheck backup.sh
|
||||
shellcheck tests.sh
|
||||
- name: Install using Makefile
|
||||
run: |
|
||||
make install
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
./tests.sh I_HAVE_READ_THE_HELPER
|
||||
15
Makefile
15
Makefile
@@ -1,15 +0,0 @@
|
||||
all:
|
||||
install
|
||||
|
||||
install:
|
||||
mkdir -p /usr/local/bin /usr/local/etc
|
||||
cp -R backup.sh /usr/local/bin/backup.sh
|
||||
cp -R sources.bk /usr/local/etc/sources.bk
|
||||
cp -R backup.sh.1 /usr/share/man/man1/backup.sh.1
|
||||
chmod 755 /usr/local/bin/backup.sh
|
||||
chmod 644 /usr/local/etc/sources.bk
|
||||
|
||||
uninstall:
|
||||
rm -rf /usr/local/bin/backup.sh
|
||||
rm -ff /usr/local/etc/sources.bk
|
||||
rm -rf /usr/share/man/man1/backup.sh.1
|
||||
244
README.md
244
README.md
@@ -1,243 +1,3 @@
|
||||
# backup.sh 
|
||||
# backup.py
|
||||
|
||||
`backup.sh` is a POSIX compliant, modular and lightweight backup utility to save and encrypt your files.
|
||||
This tool is intended to be used on small scale UNIX environments such as VPS, personal servers and
|
||||
workstations. `backup.sh` uses [rsync](https://linux.die.net/man/1/rsync), [tar](https://linux.die.net/man/1/tar),
|
||||
[gpg](https://linux.die.net/man/1/gpg) and [sha256sum](https://linux.die.net/man/1/sha256sum)
|
||||
to copy, compress, encrypt the backup and verify the backup.
|
||||
|
||||
## Installation
|
||||
`backup.sh` is a single source file, to install it you can copy the script wherever you want. Alternatively, if you
|
||||
are running a DEB/RPM distribution, you can install it with the following command:
|
||||
```sh
|
||||
$> sudo apt install ./bin/backup.sh-1.0.0.x86_64.deb # Debian
|
||||
$> sudo dnf install ./bin/backup.sh-1.0.0-2.x86_64.rpm # RHEL
|
||||
```
|
||||
|
||||
For any other UNIX system, you can use the following command:
|
||||
```sh
|
||||
$> sudo make install
|
||||
```
|
||||
This will copy `backup.sh` into `/usr/local/bin/backup.sh`, `sources.bk` into `/usr/local/etc/sources.bk` and
|
||||
`backup.sh.1` into `/usr/share/man/man1/backup.sh.1`. To uninstall the program along with the sample _sources file_ and the manual page, you can issue `sudo make uninstall`.
|
||||
|
||||
At this point you still need to install the following dependencies:
|
||||
- `Bash(v>=4)`
|
||||
- `rsync`
|
||||
- `tar`
|
||||
- `gpg`
|
||||
|
||||
## Usage
|
||||
To show the available options, you can run `backup.sh --help`, which will print out the following message:
|
||||
```text
|
||||
backup.sh v1.0.0 - POSIX compliant, modular and lightweight backup utility.
|
||||
|
||||
Syntax: ./backup.sh [-b|-e|-c|-V|-h]
|
||||
options:
|
||||
-b|--backup SOURCES DEST PASS Backup folders from SOURCES file.
|
||||
-e|--extract ARCHIVE PASS Extract ARCHIVE using PASS.
|
||||
-c|--checksum Generate/check SHA256 of a backup.
|
||||
-V|--verbose Enable verbose mode.
|
||||
-h|--help Show this helper.
|
||||
|
||||
General help with the software: https://github.com/ceticamarco/backup.sh
|
||||
Report bugs to: Marco Cetica(<email@marcocetica.com>)
|
||||
```
|
||||
|
||||
As you can see, `backup.sh` supports three options: **backup creation**, **backup extraction** and **checksum** to verify the
|
||||
integrity of a backup. The first option requires
|
||||
root permissions, while the second one does not. The checksum option must be used in combination of one of the previous options.
|
||||
|
||||
### Backup creation
|
||||
To specify the directories to back up, `backup.sh` uses an associative array
|
||||
defined in a text file(called _sources file_) with the following syntax:
|
||||
|
||||
```text
|
||||
<LABEL>=<PATH>
|
||||
```
|
||||
|
||||
Where `<LABEL>` is the name of the backup and `<PATH>` is its path. For example,
|
||||
if you want to back up `/etc/nginx` and `/etc/ssh`, add the following entries to the _sources file_:
|
||||
|
||||
```text
|
||||
nginx=/etc/nginx/
|
||||
ssh=/etc/ssh/
|
||||
```
|
||||
|
||||
`backup.sh` will create two folders inside the backup archive with the following syntax:
|
||||
```text
|
||||
backup-<LABEL>-<YYYYMMDD>
|
||||
```
|
||||
|
||||
In the previous example, this would be:
|
||||
```text
|
||||
backup-nginx-<YYYYMMDD>
|
||||
backup-ssh-<YYYYMMDD>
|
||||
```
|
||||
|
||||
You can add as many entries as you want, just be sure to use the proper syntax. In particular,
|
||||
the _sources file_, **should not** include:
|
||||
- Spaces between the label and the equal sign;
|
||||
- Empty lines;
|
||||
- Comments.
|
||||
|
||||
You can find a sample _sources file_ at `sources.bk`(or at `/usr/local/etc/sources.bk`).
|
||||
|
||||
After having defined the _sources file_, you can invoke `backup.sh` using the following syntax:
|
||||
```sh
|
||||
$> sudo ./backup.sh --backup <SOURCES_FILE> <DEST> <ENCRYPTION_PASSWORD>
|
||||
```
|
||||
|
||||
Where `<SOURCES_FILE>` is the _sources file_, `<DEST>` is the absolute path of the output of the backup
|
||||
**without trailing slashes** and `<ENCRYPTION_PASSWORD>` is the password to encrypt the compressed archive.
|
||||
|
||||
In the previous example, this would be:
|
||||
```sh
|
||||
$> sudo ./backup.sh --backup sources.bk /home/john badpw1234
|
||||
```
|
||||
|
||||
You can also tell `backup.sh` to generate a SHA256 file containing the hash of each file using the `-c` option.
|
||||
In the previous example, this would be:
|
||||
```sh
|
||||
$> sudo ./backup.sh --checksum --backup sources.bk /home/john badpw1234
|
||||
```
|
||||
|
||||
The backup utility will begin to copy the files defined in the _sources file_:
|
||||
```text
|
||||
Copying nginx(1/2)
|
||||
Copying ssh(2/2)
|
||||
Compressing backup...
|
||||
Encrypting backup...
|
||||
File name: /home/john/backup-<HOSTNAME>-<YYYYMMDD>.tar.gz.enc
|
||||
Checksum file: /home/john/backup-<HOSTNAME>-<YYYYMMDD>.sha256
|
||||
File size: 7336400696(6.9G)
|
||||
Elapsed time: 259 seconds.
|
||||
```
|
||||
|
||||
After that, you will find the backup archive and the checksum file in
|
||||
`/home/john/backup-<HOSTNAME>-<YYYYMMDD>.tar.gz.enc` and `/home/john/backup-<HOSTNAME>-<YYYYMMDD>.sha256`, respectively.
|
||||
|
||||
You can also use `backup.sh` from a crontab rule:
|
||||
```sh
|
||||
$> sudo crontab -e
|
||||
30 03 * * 6 EKEY=$(cat /home/john/.ekey) bash -c '/usr/local/bin/backup.sh -b /usr/local/etc/sources.bk /home/john $EKEY' > /dev/null 2>&1
|
||||
|
||||
```
|
||||
|
||||
This will automatically run `backup.sh` every Saturday morning at 03:30 AM. In the example above, the encryption
|
||||
key is stored in a local file(with fixed permissions) to avoid password leaking in crontab logs. You can also
|
||||
adopt this practice while using the `--extract` option to avoid password leaking in shell history.
|
||||
|
||||
By default `backup.sh` is very quiet, to add some verbosity to the output, be sure to use the `-V`(`--verbose`) option.
|
||||
|
||||
### Backup extraction
|
||||
`backup.sh` can also be used to extract and to verify the encrypted backup.
|
||||
To do so, use the following commands:
|
||||
|
||||
```sh
|
||||
$> ./backup.sh --extract <ENCRYPTED_ARCHIVE> <ARCHIVE_PASSWORD>
|
||||
```
|
||||
|
||||
Where `<ENCRYPTED_ARCHIVE>` is the encrypted backup and `<ARCHIVE_PASSWORD>` is the backup password.
|
||||
|
||||
For instance:
|
||||
|
||||
```sh
|
||||
$> ./backup.sh --extract backup-<hostname>-<YYYYMMDD>.tar.gz.enc badpw1234
|
||||
```
|
||||
|
||||
This will create a new folder called `backup.sh.tmp` in your local directory with the following content:
|
||||
```text
|
||||
backup-nginx-<YYYYMMDD>
|
||||
backup-ssh-<YYYYMMDD>
|
||||
```
|
||||
|
||||
**note**: be sure to rename any directory with that name to avoid collisions.
|
||||
|
||||
|
||||
If you also want to verify the integrity of the backup data, use the following commands:
|
||||
```sh
|
||||
$> ./backup.sh --checksum --extract <ENCRYPTED_ARCHIVE> <ARCHIVE_PASSWORD> <CHECKSUM_ABSOLUTE_PATH>
|
||||
```
|
||||
|
||||
For instance:
|
||||
|
||||
```sh
|
||||
$> ./backup.sh --checksum --extract backup-<hostname>-<YYYYMMDD>.tar.gz.enc badpw1234 backup-<hostname>-<YYYYMMDD>.sha256
|
||||
```
|
||||
|
||||
## How does backup.sh work?
|
||||
**backup.sh** uses _rsync_ to copy the files, _tar_ to compress the backup, _gpg_ to encrypt it and
|
||||
_sha256sum_ to verify it.
|
||||
By default, rsync is being used with the following parameters:
|
||||
|
||||
```
|
||||
$> rsync -aPhrq --delete
|
||||
```
|
||||
|
||||
That is:
|
||||
|
||||
- a: archive mode: rsync copies files recursively while preserving as much metadata as possible;
|
||||
- P: progress/partial: allows rsync to resume interrupted transfers and to shows progress information;
|
||||
- h: human readable output, rsync shows output numbers in a more readable way;
|
||||
- r: recursive mode: forces rsync to copy directories and their content;
|
||||
- q: quiet mode: reduces the amount of information rsync produces;
|
||||
- delete: delete mode: forces rsync to delete any extraneous files at the destination dir.
|
||||
|
||||
If specified(`--checksum` option), `backup.sh` can also generate the checksum of each file of the backup.
|
||||
To do so, it uses `sha256sum(1)` to compute the hash of every single file using the SHA256 hashing algorithm.
|
||||
The checksum file contains nothing but the checksums of the files, no other information about the files stored
|
||||
on the backup archive is exposed on the unencrypted checksum file. This may be an issue if you want plausible
|
||||
deniability(see privacy section for more information).
|
||||
|
||||
|
||||
After that the backup folder is being encrypted using gpg. By default, it is used with the following parameters:
|
||||
|
||||
```
|
||||
$> gpg -a \
|
||||
--symmetric \
|
||||
--cipher-algo=AES256 \
|
||||
--no-symkey-cache \
|
||||
--pinentry-mode=loopback \
|
||||
--batch --passphrase "$PASSWORD" \
|
||||
--output "$OUTPUT" \
|
||||
"$INPUT"
|
||||
```
|
||||
|
||||
This command encrypts the backup using the AES-256 symmetric encryption algorithm with a 256bit key. Here is what each flag do:
|
||||
- `--symmetric`: Use symmetric encryption;
|
||||
- `--cipher-algo=AES256`: Use AES256 algorithm;
|
||||
- `--no-symkey-cache`: Do not save password on GPG's cache;
|
||||
- `--pinentry-mode=loopback --batch`: Do not prompt the user;
|
||||
- `--passphrase-fd 3 3<< "$PASSWORD"`: Read password without revealing it on `ps`;
|
||||
- `--output`: Specify output file;
|
||||
- `$INPUT`: Specify input file.
|
||||
|
||||
## Plausible Deniability
|
||||
While `backup.sh` provide some pretty strong security against bruteforce attack(assuming a strong passphrase is being used)
|
||||
it should by no means considered a viable tool against a cryptanalysis investigation. Many of the copying, compressing and
|
||||
encrypting operations made by `backup.sh` during the backup process can be used to invalidate plausible deniability.
|
||||
In particular, you should pay attention to the following details:
|
||||
|
||||
1. The `--checksum` option generates an **UNENCRYPTED** checksum file containing the _digests_ of **EVERY**
|
||||
file in your backup archive. If your files are known to your adversary(e.g., a banned book), they may use a rainbow table attack to
|
||||
determine whether you own a given file, voiding your plausible deniability;
|
||||
2. Since `backup.sh` is essentially a set of shell commands, an eavesdropper could monitor the whole backup process to extract
|
||||
the name of the files or the encryption password.
|
||||
|
||||
## Unit tests
|
||||
`backup.sh` provides some unit tests inside the `tests.sh` script. This script generates some dummy files inside the following
|
||||
directories:
|
||||
- /var/log
|
||||
- /var/www
|
||||
- /etc/nginx
|
||||
- /etc/ssh
|
||||
|
||||
For this reason, this script should **NOT** be used in non-testing environments. To run all tests, issue the following command:
|
||||
```sh
|
||||
$> sudo ./tests.sh I_HAVE_READ_THE_HELPER
|
||||
```
|
||||
|
||||
## License
|
||||
This software is released under GPLv3, you can obtain a copy of this license by cloning this repository or by visiting
|
||||
[this page](https://choosealicense.com/licenses/gpl-3.0/).
|
||||
Work in progress
|
||||
|
||||
488
backup.py
Executable file
488
backup.py
Executable file
@@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
# backup.py: modular and lightweight backup utility
|
||||
# Developed by Marco Cetica (c) 2018, 2023, 2024, 2026
|
||||
#
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import tarfile
|
||||
import subprocess
|
||||
import hashlib
|
||||
import getpass
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, Union, Literal, List
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Ok(Generic[T]):
|
||||
"""Sum type to represent results"""
|
||||
value: T
|
||||
success: Literal[True] = True
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Err:
|
||||
error: str
|
||||
success: Literal[False] = False
|
||||
|
||||
Result = Union[Ok[T], Err]
|
||||
|
||||
@dataclass
|
||||
class BackupSource:
|
||||
"""Struct to represent a mapping between a label and a path"""
|
||||
label: str
|
||||
path: Path
|
||||
|
||||
@dataclass
|
||||
class BackupState:
|
||||
"""Struct to represent a backup state"""
|
||||
sources: List[BackupSource]
|
||||
output_path: Path
|
||||
password: str
|
||||
checksum: bool
|
||||
verbose: bool
|
||||
|
||||
class BackupProgress:
|
||||
"""Progress indicator for backup operations"""
|
||||
def __init__(self, total: int, operation: str):
|
||||
self.total = total
|
||||
self.current = 0
|
||||
self.operation = operation
|
||||
|
||||
def update(self, message: str = ""):
|
||||
"""Update progress"""
|
||||
self.current += 1
|
||||
percentage = (self.current / self.total) * 100 if self.total > 0 else 0
|
||||
|
||||
status = f"\r{self.operation}: [{self.current}/{self.total}] {percentage: .1f}% - {message}"
|
||||
print(f"\r\033[K{status}", end='', flush=True)
|
||||
|
||||
def finish(self):
|
||||
"""Print new line"""
|
||||
print()
|
||||
|
||||
class Backup:
|
||||
@staticmethod
|
||||
def check_deps() -> Result[None]:
|
||||
"""Check whether dependencies are installed"""
|
||||
missing_deps = []
|
||||
for dep in ["gpg", "tar"]:
|
||||
if not shutil.which(dep):
|
||||
missing_deps.append(dep)
|
||||
|
||||
if missing_deps:
|
||||
return Err(f"Missing dependencies: {', '.join(missing_deps)}")
|
||||
|
||||
return Ok(None)
|
||||
|
||||
@staticmethod
|
||||
def prettify_size(byte_size: int) -> str:
|
||||
"""Convert byte_size in powers of 1024"""
|
||||
units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]
|
||||
idx = 0
|
||||
size = float(byte_size)
|
||||
|
||||
while size >= 1024.0 and idx < (len(units) - 1):
|
||||
size /= 1024.0
|
||||
idx += 1
|
||||
|
||||
if size.is_integer():
|
||||
return f"{int(size)} {units[idx]}"
|
||||
|
||||
return f"{size:.2f} {units[idx]}"
|
||||
|
||||
@staticmethod
|
||||
def parse_sources_file(sources_file: Path) -> Result[List[BackupSource]]:
|
||||
"""Parse the sources file returning a list of BackupSource elements"""
|
||||
if not sources_file.exists():
|
||||
return Err("Sources file does not exist")
|
||||
|
||||
sources: List[BackupSource] = []
|
||||
try:
|
||||
with open(sources_file, 'r') as f:
|
||||
for pos, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
if '=' not in line:
|
||||
print(f"Warning: invalid format at line {pos}: {line}")
|
||||
|
||||
label, path_str = line.split('=', 1)
|
||||
path = Path(path_str.strip())
|
||||
|
||||
if not path.exists():
|
||||
return Err(f"Path does not exist: {path}")
|
||||
|
||||
sources.append(BackupSource(label.strip(), path))
|
||||
except IOError as err:
|
||||
return Err(f"Failed to read sources file: {err}")
|
||||
|
||||
if not sources:
|
||||
return Err(f"No valid sources found in file")
|
||||
|
||||
return Ok(sources)
|
||||
|
||||
@staticmethod
|
||||
def should_ignore_file(path: Path) -> bool:
|
||||
"""Check whether a file should be ignored"""
|
||||
try:
|
||||
# Skip UNIX sockets
|
||||
if path.is_socket():
|
||||
return True
|
||||
|
||||
# Skip broken symlinks
|
||||
if path.is_symlink() and not path.exists():
|
||||
return True
|
||||
|
||||
# Skip named pipes (FIFOs)
|
||||
if path.stat().st_mode & 0o170000 == 0o010000:
|
||||
return True
|
||||
|
||||
return False
|
||||
except (OSError, IOError):
|
||||
# Skip files that can't be checked
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def ignore_special_files(directory: str, contents: List[str]) -> List[str]:
|
||||
"""Return a list of files to ignore"""
|
||||
ignored_files: List[str] = []
|
||||
dir_path = Path(directory)
|
||||
|
||||
for item in contents:
|
||||
item_path = dir_path / item
|
||||
if Backup.should_ignore_file(item_path):
|
||||
ignored_files.append(item)
|
||||
|
||||
return ignored_files
|
||||
|
||||
@staticmethod
|
||||
def copy_files(source: Path, destination: Path, verbose: bool) -> Result[None]:
|
||||
"""Copy files and directories preserving their metadata"""
|
||||
try:
|
||||
# Handle single file
|
||||
if source.is_file():
|
||||
# Parent directory might not exists, so we try to create it first
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if verbose:
|
||||
print(f"Copying file {source} -> {destination}")
|
||||
|
||||
# Copy file and its metadata
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
return Ok(None)
|
||||
|
||||
# Handle directory
|
||||
if source.is_dir():
|
||||
# If destination directory exists, we remove it
|
||||
# This approach mimics rsync's --delete option
|
||||
if destination.exists():
|
||||
shutil.rmtree(destination)
|
||||
|
||||
if verbose:
|
||||
print(f"Copying directory {source} -> {destination}")
|
||||
|
||||
# Copy directory and its metadata.
|
||||
# We also ignore special files and we preserves links instead
|
||||
# of following them.
|
||||
shutil.copytree(
|
||||
source,
|
||||
destination,
|
||||
symlinks=True, # True = preserve symlinks
|
||||
copy_function=shutil.copy2,
|
||||
ignore=Backup.ignore_special_files,
|
||||
dirs_exist_ok=False
|
||||
)
|
||||
|
||||
return Ok(None)
|
||||
|
||||
return Err(f"The following source element is neither a file nor a directory: {source}")
|
||||
|
||||
except (IOError, OSError, shutil.Error) as err:
|
||||
return Err(f"Copy failed: {err}")
|
||||
|
||||
@staticmethod
|
||||
def cleanup_files(*paths: Path) -> None:
|
||||
"""Clean up temporary files and directories"""
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
continue
|
||||
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
else:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def collect_files(directory: Path) -> List[Path]:
|
||||
"""Collect all files in a directory (recursively)"""
|
||||
files = []
|
||||
for item in directory.rglob('*'):
|
||||
if item.is_file() and not item.is_symlink():
|
||||
files.append(item)
|
||||
|
||||
return files
|
||||
|
||||
@staticmethod
|
||||
def compute_file_hash(file_path: Path) -> Result[str]:
|
||||
"""Compute SHA256 hash of a given file"""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
hashlib.sha256().update(byte_block)
|
||||
return Ok(hashlib.sha256().hexdigest())
|
||||
except IOError as e:
|
||||
return Err(f"Failed to read file {file_path}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def create_tarball(source_dir: Path, output_file: Path, verbose: bool) -> Result[None]:
|
||||
"""Create a compressed tar archive of the backup directory"""
|
||||
if verbose:
|
||||
print("Compressing backup...")
|
||||
|
||||
cmd = [
|
||||
"tar",
|
||||
"-czf",
|
||||
str(output_file),
|
||||
"-C",
|
||||
str(source_dir.parent),
|
||||
source_dir.name
|
||||
]
|
||||
|
||||
if verbose:
|
||||
cmd[1] += "-v"
|
||||
|
||||
# capture here means suppress it/holding it
|
||||
result = subprocess.run(cmd, capture_output=not verbose, text=None)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = f"tar failed: {result.stderr if result.stderr else 'Unknown error code'}"
|
||||
|
||||
return Err(error_msg)
|
||||
|
||||
return Ok(None)
|
||||
|
||||
@staticmethod
|
||||
def encrypt_file(input_file: Path, output_file: Path, password: str, verbose: bool) -> Result[None]:
|
||||
"""Encrypt a file with GPG in symmetric mode (using AES256)"""
|
||||
if verbose:
|
||||
print("Encrypting backup...")
|
||||
|
||||
cmd = [
|
||||
"gpg", "-a",
|
||||
"--symmetric",
|
||||
"--cipher-algo=AES256",
|
||||
"--no-symkey-cache",
|
||||
"--pinentry-mode=loopback",
|
||||
"--batch",
|
||||
"--passphrase-fd", "0",
|
||||
"--output", str(output_file),
|
||||
str(input_file)
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
input=password.encode(),
|
||||
capture_output=not verbose
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return Err(f"Encryption failed: {result.stderr.decode()}")
|
||||
|
||||
return Ok(None)
|
||||
|
||||
def make_backup(self, config: BackupState) -> Result[None]:
|
||||
"""Create an encrypted backup from specified sources file"""
|
||||
# Check root permissions
|
||||
if os.geteuid() != 0:
|
||||
return Err("Run this program as root!")
|
||||
|
||||
start_time = time.time()
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
hostname = os.uname().nodename
|
||||
|
||||
# Create working directory
|
||||
work_dir = config.output_path / "backup.sh.tmp"
|
||||
if not work_dir.exists():
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Format output files
|
||||
backup_archive = config.output_path / f"backup-{hostname}-{date_str}.tar.gz.enc"
|
||||
checksum_file = config.output_path / f"backup-{hostname}-{date_str}.sha256"
|
||||
temp_tarball = config.output_path / "backup.sh.tar.gz"
|
||||
|
||||
# Backup each source
|
||||
sources_count = len(config.sources)
|
||||
for idx, source in enumerate(config.sources, 1):
|
||||
print(f"Copying {source.label} ({idx}/{sources_count})")
|
||||
|
||||
# Create source subdirectory
|
||||
source_dir = work_dir / f"backup-{source.label}-{date_str}"
|
||||
if not source_dir.exists():
|
||||
source_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy files
|
||||
copy_res = self.copy_files(source.path, work_dir, config.verbose)
|
||||
match copy_res:
|
||||
case Err():
|
||||
self.cleanup_files(work_dir, temp_tarball)
|
||||
return copy_res
|
||||
case Ok(): pass
|
||||
|
||||
# Compute checksum when requested
|
||||
if config.checksum:
|
||||
files = self.collect_files(source_dir)
|
||||
|
||||
backup_progress: BackupProgress | None = None
|
||||
|
||||
if config.verbose:
|
||||
backup_progress = BackupProgress(len(files), "Computing checksums")
|
||||
|
||||
checksum_fd = open(checksum_file, 'a')
|
||||
for file in files:
|
||||
hash_result = self.compute_file_hash(file)
|
||||
match hash_result:
|
||||
case Err():
|
||||
checksum_fd.close()
|
||||
self.cleanup_files(work_dir, temp_tarball)
|
||||
return hash_result
|
||||
case Ok(value=v):
|
||||
checksum_fd.write(f"{v}\n")
|
||||
|
||||
if config.verbose and backup_progress is not None:
|
||||
backup_progress.update(str(file.name))
|
||||
|
||||
checksum_fd.close()
|
||||
|
||||
if config.verbose and backup_progress is not None:
|
||||
backup_progress.finish()
|
||||
|
||||
# Create compressed archive
|
||||
archive_res = self.create_tarball(work_dir, temp_tarball, config.verbose)
|
||||
match archive_res:
|
||||
case Err():
|
||||
self.cleanup_files(work_dir, temp_tarball)
|
||||
return archive_res
|
||||
case Ok(): pass
|
||||
|
||||
# Encrypt the archive
|
||||
encrypt_res = self.encrypt_file(temp_tarball, backup_archive, config.password, config.verbose)
|
||||
match encrypt_res:
|
||||
case Err():
|
||||
self.cleanup_files(work_dir, temp_tarball)
|
||||
return encrypt_res
|
||||
case Ok(): pass
|
||||
|
||||
# Cleanup temporary files
|
||||
self.cleanup_files(work_dir, temp_tarball)
|
||||
|
||||
# Compute file size
|
||||
if not backup_archive.exists():
|
||||
return Err("Unable to create backup archive")
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
file_size = backup_archive.stat().st_size
|
||||
file_size_hr = Backup.prettify_size(file_size)
|
||||
|
||||
print(f"\nBackup complete")
|
||||
print(f"File name: {backup_archive}")
|
||||
if config.checksum:
|
||||
print(f"Checksum file: {checksum_file}")
|
||||
print(f"File size: {file_size} bytes ({file_size_hr})")
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
|
||||
return Ok(None)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="backup.py - modular and lightweight backup utility",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Usage:
|
||||
> Create a backup
|
||||
sudo ./backup.py --backup sources.bk /home/user $PASS
|
||||
|
||||
> Create backup with checksum
|
||||
sudo ./backup.py --checksum --backup sources.bk /home/user $PASS
|
||||
|
||||
> Extract and verify checksum
|
||||
./backup.py --checksum --extract backup.tar.gz.enc $PASS hashes.sha256
|
||||
|
||||
For more information visit: https://git.marcocetica.com/marco/backup.py
|
||||
or issue `man backup.py` on your terminal.
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-b", "--backup",
|
||||
nargs=3,
|
||||
metavar=("SOURCES", "DEST", "PASS"),
|
||||
help="Backup files from SOURCES path to DEST directory with password PASS"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-e", "--extract",
|
||||
nargs="+",
|
||||
metavar="ARCHIVE",
|
||||
help="Extract ARCHIVE (optionally with PASS and SHA256 file)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-c", "--checksum",
|
||||
action="store_true",
|
||||
help="Generate or check SHA256 checksums"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-V", "--verbose",
|
||||
action="store_true",
|
||||
help="Enable verbose mode"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check whether dependencies are installed
|
||||
deps_res = Backup.check_deps()
|
||||
match deps_res:
|
||||
case Err(error=e): print(f"Error: {e}", file=sys.stderr)
|
||||
case Ok(): pass
|
||||
|
||||
backup = Backup()
|
||||
|
||||
if args.backup:
|
||||
sources_file, output_path, password = args.backup
|
||||
|
||||
sources_path = Path(sources_file)
|
||||
output_dir = Path(output_path)
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
if not output_dir.exists():
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Parse sources file
|
||||
sources_res = Backup.parse_sources_file(sources_path)
|
||||
config: BackupState
|
||||
match sources_res:
|
||||
case Err(error=e):
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
case Ok(value=v):
|
||||
# Create a backup state
|
||||
config = BackupState(
|
||||
sources=v,
|
||||
output_path=output_dir,
|
||||
password=password,
|
||||
checksum=args.checksum,
|
||||
verbose=args.verbose
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
329
backup.sh
329
backup.sh
@@ -1,329 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# backup.sh is a POSIX compliant, modular and lightweight
|
||||
# backup utility to save and encrypt your files.
|
||||
#
|
||||
# To specify the source directories to backup,
|
||||
# create a text file with the following syntax:
|
||||
#
|
||||
# <LABEL>=<PATH>
|
||||
#
|
||||
# for example(filename: 'sources.bk'):
|
||||
# nginx=/etc/nginx/
|
||||
# ssh=/etc/ssh/
|
||||
# logs=/var/log/
|
||||
#
|
||||
# After that you can launch the script with(sample usage):
|
||||
# sudo ./backup.sh --checksum --backup sources.bk /home/john badpw1234
|
||||
#
|
||||
# This will create an encrypted tar archive(password: 'badpw1234')
|
||||
# in '/home/john/backup-<hostname>-<YYYYMMDD>.tar.gz.enc' containing
|
||||
# the following three directories:
|
||||
# backup-nginx-<YYYYMMDD>
|
||||
# backup-ssh-<YYYYMMDD>
|
||||
# backup-logs-<YYYYMMDD>
|
||||
#
|
||||
# as well as a SHA256 file('/home/john/backup-<hostname>-<YYYYMMDD>.sha256')
|
||||
# containing the file hashes of the backup.
|
||||
#
|
||||
# You can then decrypt it using:
|
||||
# ./backup.sh --checksum --extract backup-<hostname>-<YYYYMMDD>.tar.gz.enc badpw1234 $PWD/backup-<hostname>-<YYYYMMDD>.sha256
|
||||
# which will also check the integrity of the backup(optional feature).
|
||||
#
|
||||
# You can read the full guide on https://github.com/ceticamarco/backup.sh
|
||||
# or on the manual page.
|
||||
# Copyright (c) 2018,2023,2024 Marco Cetica <email@marcocetica.com>
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
checkdeps() {
|
||||
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
|
||||
echo "This version of Bash is not supported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if dependencies are installed
|
||||
missing_dep=0
|
||||
deps="rsync tar gpg"
|
||||
|
||||
for dep in $deps; do
|
||||
if ! command -v "$dep" > /dev/null 2>&1; then
|
||||
printf "Cannot find '%s', please install it.\n" "$dep"
|
||||
missing_dep=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $missing_dep -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# $1: filename
|
||||
gethash() {
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
else
|
||||
sha256 -q "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# $1: sources.bk file
|
||||
# $2: output path
|
||||
# $3: password
|
||||
# $4: compute sha256(0,1)
|
||||
# $5: verbosity flag(0,1)
|
||||
make_backup() {
|
||||
BACKUP_SH_SOURCES_PATH="$1"
|
||||
BACKUP_SH_OUTPATH="$2"
|
||||
BACKUP_SH_PASS="$3"
|
||||
BACKUP_SH_SHA256="$4"
|
||||
BACKUP_SH_VERBOSE="$5"
|
||||
|
||||
if [ "$BACKUP_SH_VERBOSE" -eq 1 ]; then
|
||||
BACKUP_SH_COMMAND="rsync -aPhr --delete"
|
||||
else
|
||||
BACKUP_SH_COMMAND="rsync -aPhrq --delete"
|
||||
fi
|
||||
|
||||
BACKUP_SH_DATE="$(date +'%Y%m%d')"
|
||||
BACKUP_SH_FOLDER="backup.sh.tmp"
|
||||
BACKUP_SH_OUTPUT="$BACKUP_SH_OUTPATH/$BACKUP_SH_FOLDER"
|
||||
BACKUP_SH_START_TIME="$(date +%s)"
|
||||
BACKUP_SH_FILENAME="$BACKUP_SH_OUTPATH/backup-$(uname -n)-$BACKUP_SH_DATE.tar.gz.enc"
|
||||
BACKUP_SH_CHECKSUM_FILE="$BACKUP_SH_OUTPATH/backup-$(uname -n)-$BACKUP_SH_DATE.sha256"
|
||||
|
||||
# Check for root permissions
|
||||
[ "$(id -u)" -ne 0 ] && { echo "Run this tool as root!"; exit 1; }
|
||||
|
||||
# Create temporary directory
|
||||
mkdir -p "$BACKUP_SH_OUTPUT"
|
||||
|
||||
# For each item in the array, make a backup
|
||||
BACKUP_SH_TOTAL=$(wc -l < "$BACKUP_SH_SOURCES_PATH")
|
||||
BACKUP_SH_PROGRESS=1
|
||||
|
||||
while IFS='=' read -r label path; do
|
||||
# Check if entry path exists
|
||||
[ -e "$path" ] || { echo "$path does not exist"; exit 1; }
|
||||
|
||||
# Define a subdir for each backup entry
|
||||
BACKUP_SH_SUBDIR="$BACKUP_SH_OUTPUT/backup-$label-$BACKUP_SH_DATE"
|
||||
mkdir -p "$BACKUP_SH_SUBDIR"
|
||||
|
||||
printf "Copying %s(%s/%s)\n" "$label" "$BACKUP_SH_PROGRESS" "$BACKUP_SH_TOTAL"
|
||||
|
||||
# Copy files
|
||||
$BACKUP_SH_COMMAND "$path" "$BACKUP_SH_SUBDIR"
|
||||
|
||||
# Compute SHA256
|
||||
if [ "$BACKUP_SH_SHA256" -eq 1 ]; then
|
||||
shopt -s globstar dotglob
|
||||
for file in "$BACKUP_SH_SUBDIR"/**/*; do
|
||||
# Skip symbolic links and directories
|
||||
[ -d "$file" ] || [ -L "$file" ] && continue
|
||||
|
||||
if [ "$BACKUP_SH_VERBOSE" -eq 1 ]; then
|
||||
printf "Computing checksum for '%s'...\n" "$file"
|
||||
fi
|
||||
gethash "$file" >> "$BACKUP_SH_CHECKSUM_FILE"
|
||||
done
|
||||
shopt -u globstar dotglob
|
||||
fi
|
||||
|
||||
BACKUP_SH_PROGRESS=$((BACKUP_SH_PROGRESS+1))
|
||||
done < "$BACKUP_SH_SOURCES_PATH"
|
||||
|
||||
# Compress backup directory
|
||||
echo "Compressing backup..."
|
||||
if [ "$BACKUP_SH_VERBOSE" -eq 1 ]; then
|
||||
tar -cvzf "$BACKUP_SH_OUTPATH/backup.sh.tar.gz" \
|
||||
-C "$BACKUP_SH_OUTPATH" "$BACKUP_SH_FOLDER"
|
||||
else
|
||||
tar -czf "$BACKUP_SH_OUTPATH/backup.sh.tar.gz" \
|
||||
-C "$BACKUP_SH_OUTPATH" "$BACKUP_SH_FOLDER" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Encrypt backup directory
|
||||
echo "Encrypting backup..."
|
||||
gpg -a \
|
||||
--symmetric \
|
||||
--cipher-algo=AES256 \
|
||||
--no-symkey-cache \
|
||||
--pinentry-mode=loopback \
|
||||
--batch --passphrase-fd 3 3<<< "$BACKUP_SH_PASS" \
|
||||
--output "$BACKUP_SH_FILENAME" \
|
||||
"$BACKUP_SH_OUTPATH/backup.sh.tar.gz" > /dev/null 2>&1
|
||||
|
||||
# Remove temporary files
|
||||
rm -rf "$BACKUP_SH_OUTPUT"
|
||||
rm -rf "$BACKUP_SH_OUTPATH/backup.sh.tar.gz"
|
||||
|
||||
# Print file name, file size, file hash and elapsed time,
|
||||
BACKUP_SH_END_TIME="$(date +%s)"
|
||||
BACKUP_SH_FILE_SIZE="$(find "$BACKUP_SH_FILENAME" -exec ls -l {} \; | awk '{print $5}')"
|
||||
BACKUP_SH_FILE_SIZE_H="$(find "$BACKUP_SH_FILENAME" -exec ls -lh {} \; | awk '{print $5}')"
|
||||
|
||||
echo "File name: $BACKUP_SH_FILENAME"
|
||||
[ "$BACKUP_SH_SHA256" -eq 1 ] && { echo "Checksum file: $BACKUP_SH_CHECKSUM_FILE"; }
|
||||
echo "File size: $BACKUP_SH_FILE_SIZE($BACKUP_SH_FILE_SIZE_H)"
|
||||
printf "Elapsed time: %s seconds.\n" "$((BACKUP_SH_END_TIME - BACKUP_SH_START_TIME))"
|
||||
}
|
||||
|
||||
# $1: archive file
|
||||
# $2: archive password
|
||||
# $3: sha256 file(optional)
|
||||
# $4: verbosity flag(0,1)
|
||||
extract_backup() {
|
||||
BACKUP_SH_ARCHIVE_PATH="$1"
|
||||
BACKUP_SH_ARCHIVE_PW="$2"
|
||||
BACKUP_SH_SHA256_FILE="$3"
|
||||
BACKUP_SH_VERBOSE="$4"
|
||||
|
||||
# Decrypt the archive
|
||||
echo "Decrypting backup..."
|
||||
gpg -a \
|
||||
--quiet \
|
||||
--decrypt \
|
||||
--no-symkey-cache \
|
||||
--pinentry-mode=loopback \
|
||||
--batch --passphrase-fd 3 3<<< "$BACKUP_SH_ARCHIVE_PW" \
|
||||
--output backup.sh.tar.gz \
|
||||
"$BACKUP_SH_ARCHIVE_PATH"
|
||||
|
||||
# Extract archive
|
||||
if [ "$BACKUP_SH_VERBOSE" -eq 1 ]; then
|
||||
tar -xzvf backup.sh.tar.gz
|
||||
else
|
||||
tar -xzf backup.sh.tar.gz > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
# If specified, use SHA256 file to compute checksum of files
|
||||
if [ -n "$BACKUP_SH_SHA256_FILE" ]; then
|
||||
shopt -s globstar dotglob
|
||||
for file in "backup.sh.tmp"/**/*; do
|
||||
# Skip symbolic links and directories
|
||||
[ -d "$file" ] || [ -L "$file" ] && continue;
|
||||
# Compute sha256 for current file
|
||||
SHA256="$(gethash "$file")"
|
||||
# Check if checksum file contains hash
|
||||
if ! grep -wq "$SHA256" "$BACKUP_SH_SHA256_FILE"; then
|
||||
rm -rf backup.sh.tar.gz backup.sh.tmp
|
||||
printf "[FATAL] - integrity error for '%s'.\n" "$file"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BACKUP_SH_VERBOSE" -eq 1 ]; then
|
||||
printf "[OK] - integrity check for '%s' passed.\n" "$file"
|
||||
fi
|
||||
done
|
||||
shopt -u globstar dotglob
|
||||
fi
|
||||
|
||||
rm -rf backup.sh.tar.gz
|
||||
}
|
||||
|
||||
helper() {
|
||||
CLI_NAME="$1"
|
||||
|
||||
cat <<EOF
|
||||
backup.sh v1.0.0 - POSIX compliant, modular and lightweight backup utility.
|
||||
|
||||
Syntax: $CLI_NAME [-b|-e|-c|-V|-h]
|
||||
options:
|
||||
-b|--backup SOURCES DEST PASS Backup folders from SOURCES file.
|
||||
-e|--extract ARCHIVE PASS Extract ARCHIVE using PASS.
|
||||
-c|--checksum Generate/check SHA256 of a backup.
|
||||
-V|--verbose Enable verbose mode.
|
||||
-h|--help Show this helper.
|
||||
|
||||
General help with the software: https://github.com/ceticamarco/backup.sh
|
||||
Report bugs to: Marco Cetica(<email@marcocetica.com>)
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
# Check whether dependecies are installed
|
||||
checkdeps
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Please, specify an argument."
|
||||
echo "For more information, try --help."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHECKSUM_FLAG=0
|
||||
VERBOSE_FLAG=0
|
||||
# Parse CLI arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
-b|--backup)
|
||||
BACKUP_SH_SOURCES_PATH="$2"
|
||||
BACKUP_SH_OUTPATH="$3"
|
||||
BACKUP_SH_PASSWORD="$4"
|
||||
|
||||
if [ -z "$BACKUP_SH_SOURCES_PATH" ] || [ -z "$BACKUP_SH_OUTPATH" ] || [ -z "$BACKUP_SH_PASSWORD" ]; then
|
||||
echo "Please, specify a source file, an output path and a password."
|
||||
echo "For more informatio, try --help"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -f "$BACKUP_SH_SOURCES_PATH" ] || { echo "Sources file does not exist"; exit 1; }
|
||||
make_backup "$BACKUP_SH_SOURCES_PATH" "$BACKUP_SH_OUTPATH" "$BACKUP_SH_PASSWORD" "$CHECKSUM_FLAG" "$VERBOSE_FLAG"
|
||||
|
||||
exit 0
|
||||
;;
|
||||
-e|--extract)
|
||||
BACKUP_SH_ARCHIVE_FILE="$2"
|
||||
BACKUP_SH_ARCHIVE_PW="$3"
|
||||
BACKUP_SH_SHA256_FILE="$4"
|
||||
|
||||
if [ "$CHECKSUM_FLAG" -eq 1 ]; then
|
||||
if [ -z "$BACKUP_SH_ARCHIVE_FILE" ] || [ -z "$BACKUP_SH_ARCHIVE_PW" ] || [ -z "$BACKUP_SH_SHA256_FILE" ]; then
|
||||
echo "Please, specify an encrypted archive, a password and a SHA256 file."
|
||||
echo "For more informatio, try --help"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ -z "$BACKUP_SH_ARCHIVE_FILE" ] || [ -z "$BACKUP_SH_ARCHIVE_PW" ]; then
|
||||
echo "Please, specify an encrypted archive and a password."
|
||||
echo "For more information, try --help"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check whether backup file exists or not
|
||||
[ -f "$BACKUP_SH_ARCHIVE_FILE" ] || { echo "Backup file does not exist"; exit 1; }
|
||||
|
||||
if [ "$CHECKSUM_FLAG" -eq 1 ]; then
|
||||
[ -f "$BACKUP_SH_SHA256_FILE" ] || { echo "Checksum file does not exist"; exit 1; }
|
||||
extract_backup "$BACKUP_SH_ARCHIVE_FILE" "$BACKUP_SH_ARCHIVE_PW" "$BACKUP_SH_SHA256_FILE" "$VERBOSE_FLAG"
|
||||
else
|
||||
extract_backup "$BACKUP_SH_ARCHIVE_FILE" "$BACKUP_SH_ARCHIVE_PW" "" "$VERBOSE_FLAG"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
;;
|
||||
-c|--checksum)
|
||||
[ $# -eq 1 ] && { echo "Use '--checksum' with '--backup' or '--extract'"; exit 1; }
|
||||
CHECKSUM_FLAG=1
|
||||
shift 1
|
||||
;;
|
||||
-V|--verbose)
|
||||
[ $# -eq 1 ] && { echo "Use '--verbose' with '--backup' or '--extract'"; exit 1; }
|
||||
VERBOSE_FLAG=1
|
||||
shift 1
|
||||
;;
|
||||
-h|--help)
|
||||
helper "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1."
|
||||
echo "For more information, try --help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
# vim: ts=4 sw=4 softtabstop=4 expandtab:
|
||||
346
backup.sh.1
346
backup.sh.1
@@ -1,346 +0,0 @@
|
||||
.\" Automatically generated by Pandoc 3.5
|
||||
.\"
|
||||
.TH "backup.sh" "1" "October 21, 2024" "Marco Cetica" "General Commands Manual"
|
||||
.SH NAME
|
||||
\f[B]backup.sh\f[R] \- POSIX compliant, modular and lightweight backup
|
||||
utility.
|
||||
.SH SYNOPSIS
|
||||
.IP
|
||||
.EX
|
||||
Syntax: ./backup.sh [\-b|\-e|\-c|\-V|\-h]
|
||||
options:
|
||||
\-b|\-\-backup SOURCES DEST PASS Backup folders from SOURCES file.
|
||||
\-e|\-\-extract ARCHIVE PASS Extract ARCHIVE using PASS.
|
||||
\-c|\-\-checksum Generate/check SHA256 of a backup.
|
||||
\-V|\-\-verbose Enable verbose mode.
|
||||
\-h|\-\-help Show this helper.
|
||||
|
||||
General help with the software: https://github.com/ceticamarco/backup.sh
|
||||
Report bugs to: Marco Cetica(<email\[at]marcocetica.com>)
|
||||
.EE
|
||||
.SH DESCRIPTION
|
||||
\f[B]backup.sh\f[R] is a POSIX compliant, modular and lightweight backup
|
||||
utility to save and encrypt your files.
|
||||
This tool is intended to be used on small scale UNIX environment such as
|
||||
VPS, small servers and workstations.
|
||||
\f[B]backup.sh\f[R] uses \f[I]rsync\f[R], \f[I]tar\f[R],
|
||||
\f[I]sha256sum\f[R] and \f[I]gpg\f[R] to copy, compress, verify and
|
||||
encrypt the backup.
|
||||
.SH OPTIONS
|
||||
\f[B]backup.sh\f[R] supports three options: \f[B]backup creation\f[R],
|
||||
\f[B]backup extraction\f[R] and \f[B]checksum\f[R] to verify the
|
||||
integrity of a backup.
|
||||
The first option requires root permissions, while the second one does
|
||||
not.
|
||||
The checksum option must be used in combination of one of the previous
|
||||
options.
|
||||
.SS Backup creation
|
||||
To specify the directories to back up, \f[CR]backup.sh\f[R] uses an
|
||||
associative array defined in a text file(called \f[I]sources file\f[R])
|
||||
with the following syntax:
|
||||
.IP
|
||||
.EX
|
||||
<LABEL>=<PATH>
|
||||
.EE
|
||||
.PP
|
||||
Where \f[CR]<LABEL>\f[R] is the name of the backup and \f[CR]<PATH>\f[R]
|
||||
is its path.
|
||||
For example, if you want to back up \f[CR]/etc/nginx\f[R] and
|
||||
\f[CR]/etc/ssh\f[R], add the following entries to the \f[I]sources
|
||||
file\f[R]:
|
||||
.IP
|
||||
.EX
|
||||
nginx=/etc/nginx/
|
||||
ssh=/etc/ssh/
|
||||
.EE
|
||||
.PP
|
||||
\f[CR]backup.sh\f[R] will create two folders inside the backup archive
|
||||
with the following syntax:
|
||||
.IP
|
||||
.EX
|
||||
backup\-<LABEL>\-<YYYYMMDD>
|
||||
.EE
|
||||
.PP
|
||||
In the previous example, this would be:
|
||||
.IP
|
||||
.EX
|
||||
backup\-nginx\-<YYYYMMDD>
|
||||
backup\-ssh\-<YYYYMMDD>
|
||||
.EE
|
||||
.PP
|
||||
You can add as many entries as you want, just be sure to use the proper
|
||||
syntax.
|
||||
In particular, the \f[I]sources file\f[R], \f[B]should not\f[R] include:
|
||||
\- Spaces between the label and the equal sign;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- Empty lines;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- Comments.
|
||||
.PP
|
||||
You can find a sample \f[I]sources file\f[R] at \f[CR]sources.bk\f[R](or
|
||||
at \f[CR]/usr/local/etc/sources.bk\f[R]).
|
||||
.PP
|
||||
After having defined the \f[I]sources file\f[R], you can invoke
|
||||
\f[CR]backup.sh\f[R] using the following syntax:
|
||||
.IP
|
||||
.EX
|
||||
$> sudo ./backup.sh \-\-backup <SOURCES_FILE> <DEST> <ENCRYPTION_PASSWORD>
|
||||
.EE
|
||||
.PP
|
||||
Where \f[CR]<SOURCES_FILE>\f[R] is the \f[I]sources file\f[R],
|
||||
\f[CR]<DEST>\f[R] is the absolute path of the output of the backup
|
||||
\f[B]without trailing slashes\f[R] and \f[CR]<ENCRYPTION_PASSWORD>\f[R]
|
||||
is the password to encrypt the compressed archive.
|
||||
.PP
|
||||
In the previous example, this would be:
|
||||
.IP
|
||||
.EX
|
||||
$> sudo ./backup.sh \-\-backup sources.bk /home/john badpw1234
|
||||
.EE
|
||||
.PP
|
||||
You can also tell \f[CR]backup.sh\f[R] to generate a SHA256 file
|
||||
containing the hash of each file using the \f[CR]\-c\f[R] option.
|
||||
In the previous example, this would be:
|
||||
.IP
|
||||
.EX
|
||||
$> sudo ./backup.sh \-\-checksum \-\-backup sources.bk /home/john badpw1234
|
||||
.EE
|
||||
.PP
|
||||
The backup utility will begin to copy the files defined in the
|
||||
\f[I]sources file\f[R]:
|
||||
.IP
|
||||
.EX
|
||||
Copying nginx(1/2)
|
||||
Copying ssh(2/2)
|
||||
Compressing backup...
|
||||
Encrypting backup...
|
||||
File name: /home/john/backup\-<HOSTNAME>\-<YYYYMMDD>.tar.gz.enc
|
||||
Checksum file: /home/john/backup\-<HOSTNAME>\-<YYYYMMDD>.sha256
|
||||
File size: 7336400696(6.9G)
|
||||
Elapsed time: 259 seconds.
|
||||
.EE
|
||||
.PP
|
||||
After that, you will find the backup archive and the checksum file in
|
||||
\f[CR]/home/john/backup\-<HOSTNAME>\-<YYYYMMDD>.tar.gz.enc\f[R] and
|
||||
\f[CR]/home/john/backup\-<HOSTNAME>\-<YYYYMMDD>.sha256\f[R],
|
||||
respectively.
|
||||
.PP
|
||||
You can also use \f[CR]backup.sh\f[R] from a crontab rule:
|
||||
.IP
|
||||
.EX
|
||||
$> sudo crontab \-e
|
||||
30 03 * * 6 EKEY=$(cat /home/john/.ekey) bash \-c \[aq]/usr/local/bin/backup.sh \-b /usr/local/etc/sources.bk /home/john $EKEY\[aq] > /dev/null 2>&1
|
||||
.EE
|
||||
.PP
|
||||
This will automatically run \f[CR]backup.sh\f[R] every Saturday morning
|
||||
at 03:30 AM.
|
||||
In the example above, the encryption key is stored in a local file(with
|
||||
fixed permissions) to avoid password leaking in crontab logs.
|
||||
You can also adopt this practice while using the \f[CR]\-\-extract\f[R]
|
||||
option to avoid password leaking in shell history.
|
||||
.PP
|
||||
By default \f[CR]backup.sh\f[R] is very quiet, to add some verbosity to
|
||||
the output, be sure to use the \f[CR]\-V\f[R](\f[CR]\-\-verbose\f[R])
|
||||
option.
|
||||
.SS Backup extraction
|
||||
\f[B]backup.sh\f[R] can also be used to extract and to verify the
|
||||
encrypted backup.
|
||||
To do so, use the following commands:
|
||||
.IP
|
||||
.EX
|
||||
$> ./backup.sh \-\-extract <ENCRYPTED_ARCHIVE> <ARCHIVE_PASSWORD>
|
||||
.EE
|
||||
.PP
|
||||
Where \f[CR]<ENCRYPTED_ARCHIVE>\f[R] is the encrypted backup and
|
||||
\f[CR]<ARCHIVE_PASSWORD>\f[R] is the backup password.
|
||||
.PP
|
||||
For instance:
|
||||
.IP
|
||||
.EX
|
||||
$> ./backup.sh \-\-extract backup\-<hostname>\-<YYYYMMDD>.tar.gz.enc badpw1234
|
||||
.EE
|
||||
.PP
|
||||
This will create a new folder called \f[CR]backup.sh.tmp\f[R] in your
|
||||
local directory with the following content:
|
||||
.IP
|
||||
.EX
|
||||
backup\-nginx\-<YYYYMMDD>
|
||||
backup\-ssh\-<YYYYMMDD>
|
||||
.EE
|
||||
.PP
|
||||
\f[B]note\f[R]: be sure to rename any directory with that name to avoid
|
||||
collisions.
|
||||
.PP
|
||||
If you also want to verify the integrity of the backup data, use the
|
||||
following commands:
|
||||
.IP
|
||||
.EX
|
||||
$> ./backup.sh \-\-checksum \-\-extract <ENCRYPTED_ARCHIVE> <ARCHIVE_PASSWORD> <CHECKSUM_ABSOLUTE_PATH>
|
||||
.EE
|
||||
.PP
|
||||
For instance:
|
||||
.IP
|
||||
.EX
|
||||
$> ./backup.sh \-\-checksum \-\-extract backup\-<hostname>\-<YYYYMMDD>.tar.gz.enc badpw1234 backup\-<hostname>\-<YYYYMMDD>.sha256
|
||||
.EE
|
||||
.SS How does backup.sh work?
|
||||
\f[B]backup.sh\f[R] uses \f[I]rsync\f[R] to copy the files,
|
||||
\f[I]tar\f[R] to compress the backup, \f[I]gpg\f[R] to encrypt it and
|
||||
\f[I]sha256sum\f[R] to verify it.
|
||||
By default, rsync is being used with the following parameters:
|
||||
.IP
|
||||
.EX
|
||||
$> rsync \-aPhrq \-\-delete
|
||||
.EE
|
||||
.PP
|
||||
That is:
|
||||
.IP
|
||||
.EX
|
||||
\- a: archive mode: rsync copies files recursively while preserving as much metadata as possible;
|
||||
\- P: progress/partial: allows rsync to resume interrupted transfers and to shows progress information;
|
||||
\- h: human readable output, rsync shows output numbers in a more readable way;
|
||||
\- r: recursive mode: forces rsync to copy directories and their content;
|
||||
\- q: quiet mode: reduces the amount of information rsync produces;
|
||||
\- delete: delete mode: forces rsync to delete any extraneous files at the destination dir.
|
||||
.EE
|
||||
.PP
|
||||
If specified(\f[CR]\-\-checksum\f[R] option), \f[CR]backup.sh\f[R] can
|
||||
also generate the checksum of each file of the backup.
|
||||
To do so, it uses \f[CR]sha256sum(1)\f[R] to compute the hash of every
|
||||
single file using the SHA256 hashing algorithm.
|
||||
The checksum file contains nothing but the checksums of the files, no
|
||||
other information about the files stored on the backup archive is
|
||||
exposed on the unencrypted checksum file.
|
||||
This may be an issue if you want plausible deniability(see privacy
|
||||
section for more information).
|
||||
.PP
|
||||
After that the backup folder is being encrypted using gpg.
|
||||
By default, it is used with the following parameters:
|
||||
.IP
|
||||
.EX
|
||||
$> gpg \-a \[rs]
|
||||
\-\-symmetric \[rs]
|
||||
\-\-cipher\-algo=AES256 \[rs]
|
||||
\-\-no\-symkey\-cache \[rs]
|
||||
\-\-pinentry\-mode=loopback \[rs]
|
||||
\-\-batch \-\-passphrase \[dq]$PASSWORD\[dq] \[rs]
|
||||
\-\-output \[dq]$OUTPUT\[dq] \[rs]
|
||||
\[dq]$INPUT\[dq]
|
||||
.EE
|
||||
.PP
|
||||
This command encrypts the backup using the AES\-256 symmetric encryption
|
||||
algorithm with a 256bit key.
|
||||
Here is what each flag do: \- \f[CR]\-\-symmetric\f[R]: Use symmetric
|
||||
encryption;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- \f[CR]\-\-cipher\-algo=AES256\f[R]: Use AES256 algorithm;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- \f[CR]\-\-no\-symkey\-cache\f[R]: Do not save password on GPG\[cq]s
|
||||
cache;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- \f[CR]\-\-pinentry\-mode=loopback \-\-batch\f[R]: Do not prompt the
|
||||
user;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- \f[CR]\-\-passphrase\-fd 3 3<< \[dq]$PASSWORD\[dq]\f[R]: Read
|
||||
password without revealing it on \f[CR]ps\f[R];
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- \f[CR]\-\-output\f[R]: Specify output file;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
\- \f[CR]$INPUT\f[R]: Specify input file.
|
||||
.SS Plausible Deniability
|
||||
While \f[CR]backup.sh\f[R] provide some pretty strong security against
|
||||
bruteforce attack(assuming a strong passphrase is being used) it should
|
||||
by no means considered a viable tool against a cryptanalysis
|
||||
investigation.
|
||||
Many of the copying, compressing and encrypting operations made by
|
||||
\f[CR]backup.sh\f[R] during the backup process can be used to invalidate
|
||||
plausible deniability.
|
||||
In particular, you should pay attention to the following details:
|
||||
.IP "1." 3
|
||||
The \f[CR]\-\-checksum\f[R] option generates an \f[B]UNENCRYPTED\f[R]
|
||||
checksum file containing the \f[I]digests\f[R] of \f[B]EVERY\f[R] file
|
||||
in your backup archive.
|
||||
If your files are known to your adversary(e.g., a banned book), they may
|
||||
use a rainbow table attack to determine whether you own a given file,
|
||||
voiding your plausible deniability;
|
||||
.PD 0
|
||||
.P
|
||||
.PD
|
||||
.IP "2." 3
|
||||
Since \f[CR]backup.sh\f[R] is essentially a set of shell commands, an
|
||||
eavesdropper could monitor the whole backup process to extract the name
|
||||
of the files or the encryption password.
|
||||
.SH EXAMPLES
|
||||
Below there are some examples that demonstrate \f[B]backup.sh\f[R]\[cq]s
|
||||
usage.
|
||||
.IP "1." 3
|
||||
Create a backup of \f[CR]/etc/ssh\f[R], \f[CR]/var/www\f[R] and
|
||||
\f[CR]/var/log\f[R] inside the \f[CR]/tmp\f[R] directory using a
|
||||
password stored in \f[CR]/home/op1/.backup_pw\f[R]
|
||||
.PP
|
||||
The first thing to do is to define the source paths inside a
|
||||
\f[I]sources file\f[R]:
|
||||
.IP
|
||||
.EX
|
||||
$> cat sources.bk
|
||||
ssh=/etc/ssh/
|
||||
web_root=/var/www/
|
||||
singleFile=/home/john/file.txt
|
||||
logs=/var/log/
|
||||
.EE
|
||||
.PP
|
||||
After that we can load our encryption key from the specified file inside
|
||||
an environment variable:
|
||||
.IP
|
||||
.EX
|
||||
$> ENC_KEY=$(cat /home/op1/.backup_pw)
|
||||
.EE
|
||||
.PP
|
||||
Finally, we can start the backup process with:
|
||||
.IP
|
||||
.EX
|
||||
$> sudo backup.sh \-\-backup sources.bk /tmp $ENC_KEY
|
||||
.EE
|
||||
.IP "2." 3
|
||||
Extract the content of a backup made on 2023\-03\-14 with the password
|
||||
`Ax98f!'
|
||||
.PP
|
||||
To do this, we can simply issue the following command:
|
||||
.IP
|
||||
.EX
|
||||
$> backup.sh \-\-extract backup\-af9a8e6bfe15\-20230314.tar.gz.enc \[dq]Ax98f!\[dq]
|
||||
.EE
|
||||
.IP "3." 3
|
||||
Extract the content of a backup made on 2018\-04\-25 using the password
|
||||
in \f[CR]/home/john/.pw\f[R]
|
||||
.PP
|
||||
This example is very similar to the previous one, we just need to read
|
||||
the password from the text file:
|
||||
.IP
|
||||
.EX
|
||||
$> backup.sh \-\-extract backup\-af9a8e6bfe15\-20180425.tar.gz.enc \[dq]$(cat /home/john/.pw)\[dq]
|
||||
.EE
|
||||
.SH AUTHORS
|
||||
\f[B]backup.sh\f[R] is being developed by Marco Cetica since late 2018.
|
||||
.SH BUGS
|
||||
Submit bug reports at: \c
|
||||
.MT email@marcocetica.com
|
||||
.ME \c
|
||||
\ or open an issue on the issue tracker of the GitHub page of this
|
||||
project: https://github.com/ice\-bit/backup.sh
|
||||
Binary file not shown.
Binary file not shown.
266
man.md
266
man.md
@@ -1,266 +0,0 @@
|
||||
---
|
||||
title: backup.sh
|
||||
section: 1
|
||||
header: General Commands Manual
|
||||
footer: Marco Cetica
|
||||
date: October 21, 2024
|
||||
---
|
||||
|
||||
# NAME
|
||||
**backup.sh** - POSIX compliant, modular and lightweight backup utility.
|
||||
|
||||
# SYNOPSIS
|
||||
```
|
||||
Syntax: ./backup.sh [-b|-e|-c|-V|-h]
|
||||
options:
|
||||
-b|--backup SOURCES DEST PASS Backup folders from SOURCES file.
|
||||
-e|--extract ARCHIVE PASS Extract ARCHIVE using PASS.
|
||||
-c|--checksum Generate/check SHA256 of a backup.
|
||||
-V|--verbose Enable verbose mode.
|
||||
-h|--help Show this helper.
|
||||
|
||||
General help with the software: https://github.com/ceticamarco/backup.sh
|
||||
Report bugs to: Marco Cetica(<email@marcocetica.com>)
|
||||
```
|
||||
|
||||
# DESCRIPTION
|
||||
**backup.sh** is a POSIX compliant, modular and lightweight backup utility to save and encrypt your files.
|
||||
This tool is intended to be used on small scale UNIX environment such as VPS, small servers and workstations.
|
||||
**backup.sh** uses _rsync_, _tar_, _sha256sum_ and _gpg_ to copy, compress, verify and encrypt the backup.
|
||||
|
||||
# OPTIONS
|
||||
**backup.sh** supports three options: **backup creation**, **backup extraction** and **checksum** to verify the
|
||||
integrity of a backup. The first option requires
|
||||
root permissions, while the second one does not. The checksum option must be used in combination of one of the previous options.
|
||||
|
||||
## Backup creation
|
||||
To specify the directories to back up, `backup.sh` uses an associative array
|
||||
defined in a text file(called _sources file_) with the following syntax:
|
||||
|
||||
```
|
||||
<LABEL>=<PATH>
|
||||
```
|
||||
|
||||
Where `<LABEL>` is the name of the backup and `<PATH>` is its path. For example,
|
||||
if you want to back up `/etc/nginx` and `/etc/ssh`, add the following entries to the _sources file_:
|
||||
|
||||
```
|
||||
nginx=/etc/nginx/
|
||||
ssh=/etc/ssh/
|
||||
```
|
||||
|
||||
`backup.sh` will create two folders inside the backup archive with the following syntax:
|
||||
```
|
||||
backup-<LABEL>-<YYYYMMDD>
|
||||
```
|
||||
|
||||
In the previous example, this would be:
|
||||
```
|
||||
backup-nginx-<YYYYMMDD>
|
||||
backup-ssh-<YYYYMMDD>
|
||||
```
|
||||
|
||||
You can add as many entries as you want, just be sure to use the proper syntax. In particular,
|
||||
the _sources file_, **should not** include:
|
||||
- Spaces between the label and the equal sign;
|
||||
- Empty lines;
|
||||
- Comments.
|
||||
|
||||
You can find a sample _sources file_ at `sources.bk`(or at `/usr/local/etc/sources.bk`).
|
||||
|
||||
After having defined the _sources file_, you can invoke `backup.sh` using the following syntax:
|
||||
```
|
||||
$> sudo ./backup.sh --backup <SOURCES_FILE> <DEST> <ENCRYPTION_PASSWORD>
|
||||
```
|
||||
|
||||
Where `<SOURCES_FILE>` is the _sources file_, `<DEST>` is the absolute path of the output of the backup
|
||||
**without trailing slashes** and `<ENCRYPTION_PASSWORD>` is the password to encrypt the compressed archive.
|
||||
|
||||
In the previous example, this would be:
|
||||
```
|
||||
$> sudo ./backup.sh --backup sources.bk /home/john badpw1234
|
||||
```
|
||||
|
||||
You can also tell `backup.sh` to generate a SHA256 file containing the hash of each file using the `-c` option.
|
||||
In the previous example, this would be:
|
||||
```
|
||||
$> sudo ./backup.sh --checksum --backup sources.bk /home/john badpw1234
|
||||
```
|
||||
|
||||
The backup utility will begin to copy the files defined in the _sources file_:
|
||||
```
|
||||
Copying nginx(1/2)
|
||||
Copying ssh(2/2)
|
||||
Compressing backup...
|
||||
Encrypting backup...
|
||||
File name: /home/john/backup-<HOSTNAME>-<YYYYMMDD>.tar.gz.enc
|
||||
Checksum file: /home/john/backup-<HOSTNAME>-<YYYYMMDD>.sha256
|
||||
File size: 7336400696(6.9G)
|
||||
Elapsed time: 259 seconds.
|
||||
```
|
||||
|
||||
After that, you will find the backup archive and the checksum file in
|
||||
`/home/john/backup-<HOSTNAME>-<YYYYMMDD>.tar.gz.enc` and `/home/john/backup-<HOSTNAME>-<YYYYMMDD>.sha256`, respectively.
|
||||
|
||||
You can also use `backup.sh` from a crontab rule:
|
||||
```
|
||||
$> sudo crontab -e
|
||||
30 03 * * 6 EKEY=$(cat /home/john/.ekey) bash -c '/usr/local/bin/backup.sh -b /usr/local/etc/sources.bk /home/john $EKEY' > /dev/null 2>&1
|
||||
|
||||
```
|
||||
|
||||
This will automatically run `backup.sh` every Saturday morning at 03:30 AM. In the example above, the encryption
|
||||
key is stored in a local file(with fixed permissions) to avoid password leaking in crontab logs. You can also
|
||||
adopt this practice while using the `--extract` option to avoid password leaking in shell history.
|
||||
|
||||
By default `backup.sh` is very quiet, to add some verbosity to the output, be sure to use the `-V`(`--verbose`) option.
|
||||
|
||||
## Backup extraction
|
||||
**backup.sh** can also be used to extract and to verify the encrypted backup.
|
||||
To do so, use the following commands:
|
||||
|
||||
```
|
||||
$> ./backup.sh --extract <ENCRYPTED_ARCHIVE> <ARCHIVE_PASSWORD>
|
||||
```
|
||||
|
||||
Where `<ENCRYPTED_ARCHIVE>` is the encrypted backup and `<ARCHIVE_PASSWORD>` is the backup password.
|
||||
|
||||
For instance:
|
||||
|
||||
```
|
||||
$> ./backup.sh --extract backup-<hostname>-<YYYYMMDD>.tar.gz.enc badpw1234
|
||||
```
|
||||
|
||||
This will create a new folder called `backup.sh.tmp` in your local directory with the following content:
|
||||
```
|
||||
backup-nginx-<YYYYMMDD>
|
||||
backup-ssh-<YYYYMMDD>
|
||||
```
|
||||
|
||||
**note**: be sure to rename any directory with that name to avoid collisions.
|
||||
|
||||
|
||||
If you also want to verify the integrity of the backup data, use the following commands:
|
||||
```
|
||||
$> ./backup.sh --checksum --extract <ENCRYPTED_ARCHIVE> <ARCHIVE_PASSWORD> <CHECKSUM_ABSOLUTE_PATH>
|
||||
```
|
||||
|
||||
For instance:
|
||||
|
||||
```
|
||||
$> ./backup.sh --checksum --extract backup-<hostname>-<YYYYMMDD>.tar.gz.enc badpw1234 backup-<hostname>-<YYYYMMDD>.sha256
|
||||
```
|
||||
|
||||
|
||||
## How does backup.sh work?
|
||||
**backup.sh** uses _rsync_ to copy the files, _tar_ to compress the backup, _gpg_ to encrypt it and
|
||||
_sha256sum_ to verify it.
|
||||
By default, rsync is being used with the following parameters:
|
||||
|
||||
```
|
||||
$> rsync -aPhrq --delete
|
||||
```
|
||||
|
||||
That is:
|
||||
|
||||
- a: archive mode: rsync copies files recursively while preserving as much metadata as possible;
|
||||
- P: progress/partial: allows rsync to resume interrupted transfers and to shows progress information;
|
||||
- h: human readable output, rsync shows output numbers in a more readable way;
|
||||
- r: recursive mode: forces rsync to copy directories and their content;
|
||||
- q: quiet mode: reduces the amount of information rsync produces;
|
||||
- delete: delete mode: forces rsync to delete any extraneous files at the destination dir.
|
||||
|
||||
If specified(`--checksum` option), `backup.sh` can also generate the checksum of each file of the backup.
|
||||
To do so, it uses `sha256sum(1)` to compute the hash of every single file using the SHA256 hashing algorithm.
|
||||
The checksum file contains nothing but the checksums of the files, no other information about the files stored
|
||||
on the backup archive is exposed on the unencrypted checksum file. This may be an issue if you want plausible
|
||||
deniability(see privacy section for more information).
|
||||
|
||||
|
||||
After that the backup folder is being encrypted using gpg. By default, it is used with the following parameters:
|
||||
|
||||
```
|
||||
$> gpg -a \
|
||||
--symmetric \
|
||||
--cipher-algo=AES256 \
|
||||
--no-symkey-cache \
|
||||
--pinentry-mode=loopback \
|
||||
--batch --passphrase "$PASSWORD" \
|
||||
--output "$OUTPUT" \
|
||||
"$INPUT"
|
||||
```
|
||||
|
||||
This command encrypts the backup using the AES-256 symmetric encryption algorithm with a 256bit key. Here is what each flag do:
|
||||
- `--symmetric`: Use symmetric encryption;
|
||||
- `--cipher-algo=AES256`: Use AES256 algorithm;
|
||||
- `--no-symkey-cache`: Do not save password on GPG's cache;
|
||||
- `--pinentry-mode=loopback --batch`: Do not prompt the user;
|
||||
- `--passphrase-fd 3 3<< "$PASSWORD"`: Read password without revealing it on `ps`;
|
||||
- `--output`: Specify output file;
|
||||
- `$INPUT`: Specify input file.
|
||||
|
||||
## Plausible Deniability
|
||||
While `backup.sh` provide some pretty strong security against bruteforce attack(assuming a strong passphrase is being used)
|
||||
it should by no means considered a viable tool against a cryptanalysis investigation. Many of the copying, compressing and
|
||||
encrypting operations made by `backup.sh` during the backup process can be used to invalidate plausible deniability.
|
||||
In particular, you should pay attention to the following details:
|
||||
|
||||
1. The `--checksum` option generates an **UNENCRYPTED** checksum file containing the _digests_ of **EVERY**
|
||||
file in your backup archive. If your files are known to your adversary(e.g., a banned book), they may use a rainbow table attack to
|
||||
determine whether you own a given file, voiding your plausible deniability;
|
||||
2. Since `backup.sh` is essentially a set of shell commands, an eavesdropper could monitor the whole backup process to extract
|
||||
the name of the files or the encryption password.
|
||||
|
||||
|
||||
# EXAMPLES
|
||||
Below there are some examples that demonstrate **backup.sh**'s usage.
|
||||
|
||||
1. Create a backup of `/etc/ssh`, `/var/www` and `/var/log` inside the `/tmp` directory using a password
|
||||
stored in `/home/op1/.backup_pw`
|
||||
|
||||
The first thing to do is to define the source paths inside a _sources file_:
|
||||
|
||||
```
|
||||
$> cat sources.bk
|
||||
ssh=/etc/ssh/
|
||||
web_root=/var/www/
|
||||
singleFile=/home/john/file.txt
|
||||
logs=/var/log/
|
||||
```
|
||||
|
||||
After that we can load our encryption key from the specified file inside an environment variable:
|
||||
|
||||
```
|
||||
$> ENC_KEY=$(cat /home/op1/.backup_pw)
|
||||
```
|
||||
|
||||
Finally, we can start the backup process with:
|
||||
|
||||
```
|
||||
$> sudo backup.sh --backup sources.bk /tmp $ENC_KEY
|
||||
```
|
||||
|
||||
|
||||
2. Extract the content of a backup made on 2023-03-14 with the password 'Ax98f!'
|
||||
|
||||
To do this, we can simply issue the following command:
|
||||
|
||||
```
|
||||
$> backup.sh --extract backup-af9a8e6bfe15-20230314.tar.gz.enc "Ax98f!"
|
||||
```
|
||||
|
||||
|
||||
3. Extract the content of a backup made on 2018-04-25 using the password in `/home/john/.pw`
|
||||
|
||||
This example is very similar to the previous one, we just need to read the password from the text file:
|
||||
|
||||
```
|
||||
$> backup.sh --extract backup-af9a8e6bfe15-20180425.tar.gz.enc "$(cat /home/john/.pw)"
|
||||
```
|
||||
|
||||
# AUTHORS
|
||||
**backup.sh** is being developed by Marco Cetica since late 2018.
|
||||
|
||||
# BUGS
|
||||
Submit bug reports at: <email@marcocetica.com> or open an issue
|
||||
on the issue tracker of the GitHub page of this project: https://github.com/ice-bit/backup.sh
|
||||
@@ -1,4 +0,0 @@
|
||||
ssh=/etc/ssh/
|
||||
nginx=/etc/nginx/
|
||||
logs=/var/log/
|
||||
web_root=/var/www/
|
||||
91
tests.sh
91
tests.sh
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unit tests for backup.sh
|
||||
# This tool is NOT intended to be used outside
|
||||
# of a testing environment, please use at your own risk.
|
||||
# By Marco Cetica 2023 (<email@marcocetica.com>)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
helper() {
|
||||
cat <<EOF
|
||||
backup.sh unit testing suite.
|
||||
Do **NOT** use this tool outside of a testing environment.
|
||||
|
||||
This tool creates a lot of dummy files to emulate a real production
|
||||
environment. Any file inside the following directories will be overwritten:
|
||||
- /var/log
|
||||
- /var/www
|
||||
- /etc/nginx
|
||||
- /etc/ssh
|
||||
|
||||
Please, use at your own risk.
|
||||
To acknowledge that, run again this tool with 'I_HAVE_READ_THE_HELPER' as a parameter.
|
||||
EOF
|
||||
}
|
||||
|
||||
create_files() {
|
||||
mkdir -p /etc/{ssh,nginx}
|
||||
mkdir -p /var/{www,log}
|
||||
|
||||
touch /etc/ssh/{ssh_config,sshd_config,moduli,ssh_host_dsa_key}
|
||||
touch /etc/nginx/{nginx.conf,fastcgi.conf,mime.types}
|
||||
touch /var/www/{index.html,style.css,logic.js}
|
||||
touch /var/log/{access.log,error.log,lastlog,messages}
|
||||
|
||||
for file in ssh_config sshd_config moduli ssh_host_dsa_key ; do
|
||||
head -c 1M </dev/random > /etc/ssh/$file
|
||||
done
|
||||
|
||||
for file in index.html style.css logic.js ; do
|
||||
head -c 1M </dev/random > /var/www/$file
|
||||
done
|
||||
|
||||
for file in nginx.conf fastcgi.conf mime.types ; do
|
||||
head -c 1M </dev/random > /etc/nginx/$file
|
||||
done
|
||||
|
||||
for file in access.log error.log lastlog messages ; do
|
||||
head -c 1M </dev/random > /var/log/$file
|
||||
done
|
||||
}
|
||||
|
||||
execute_backup() {
|
||||
./backup.sh -V -c -b sources.bk "$PWD" badpw
|
||||
}
|
||||
|
||||
extract_backup() {
|
||||
host="$(uname -n)"
|
||||
date="$(date +'%Y%m%d')"
|
||||
./backup.sh -V -c -e "$PWD"/backup-"$host"-"$date".tar.gz.enc badpw "$PWD"/backup-"$host"-"$date".sha256
|
||||
}
|
||||
|
||||
test_backup() {
|
||||
for dir in "$PWD/backup.sh.tmp/"backup-*-* ; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Can't find '$dir' backup!"
|
||||
exit 1
|
||||
else
|
||||
echo "Found '$dir'"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
helper
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ "$1" = "I_HAVE_READ_THE_HELPER" ]; then
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run this tool as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
create_files
|
||||
execute_backup
|
||||
extract_backup
|
||||
test_backup
|
||||
fi
|
||||
Reference in New Issue
Block a user