Jump to content
mgutt

filecryptor: AES script to encrypt/decrypt files

3 posts in this topic Last Reply

Recommended Posts

Posted (edited)

Description
filecryptor is a shell script that reads all files in a source directory and copies them encrypted through AES-256 in a target directory. The passphrase must be added through a file named "filecryptor.key" that has to be located in the root of the source directory. Your password is salted with the file's timestamp and hashed with PBKDF2 10.000x times. This is the default value of filecryptor. You can change it, but you need to remind the exact value or you are not able to decrypt the files anymore!

 

Feel free to upload your files to a cloud after encryption has been finished!

 

You can execute this script by using the CA User Scripts plugin.

 

Features

- encrypt files of a source to a target directory with AES-256 by your password

- encryption is hardened by iterating your password through PBKDF2 10.000x times which slows down bruteforce attacks

- encryption is hardened by salting your password through the file's timestamp leaving no chance to rainbow table attacks

- decrypt files of a source folder

- resume if last script execution has been interrupted (delete filecryptor.last in target dir to fully restart)

- skip_files_last_seconds allows skipping files that are (partially) written at the moment

- dry_run allows testing filecryptor

- already existing files with a newer or same timestamp will be skipped

#!/bin/sh
# #####################################
# filecryptor
# Version: 0.3
# Author: Marc Gutt
# 
# Description:
# Copies and encrypts files from a source to a target dir. This scripts uses 
# AES encryption with the file modification time as salt. By that re-encrypting 
# produces identical files making it easier for rsync/rclone to skip already 
# transfered files.
# 
# How-to encrypt:
# 1.) Add a text file with the name "filecryptor.key" and your encryption password as content in the source directory.
# 2.) Set your source and target directories
# 3.) Execute this script
# 
# How-to decrypt:
# 1.) Backup your encrypted files
# 2.) Add a text file with the name "filecryptor.key" and your encryption password as content in the source directory.
# 3.) Set ONLY the source directory (encrypted files will be overwritten through their decrypted version!)
# 4.) Execute this script
# 
# Notes:
# - A public salt should be safe (https://crypto.stackexchange.com/a/59180/41422 & https://stackoverflow.com/a/3850335/318765)
# - After encrypting you could use rsync with "--remove-source-files" to move the encrypted files to a final target directory
# - rclone has a "move" mode to move the encrypted files to a cloud
# - You can set the PBKDF2 iterations through "iter". A higher number of iterations adds more security but is slower 
#   (https://en.wikipedia.org/wiki/PBKDF2#Purpose_and_operation & https://security.stackexchange.com/a/3993/2296)
# 
# Changelog:
# 0.3
# - set your own PBKDF2 iteration count through "iter" setting
# - salt is padded with zeros to avoid "hex string is too short, padding with zero bytes to length" message on openssl output
# - decryption now supports resume
# - empty openssl files are deleted on decryption fails
# - encryption of already existing files in the target will be skipped as long the source files aren't newer
# 0.2
# - skip dirs on resume (not only files)
# - bug fix: endless looping empty dirs
# 0.1
# - first release
# 
# Todo:
# - optional filename encryption
# - use a different filename if decrypted filename "${file}.filecryptor" already exists
# - add "overwrite=true" setting and check file existence before executing openssl
# #####################################

# settings
source="/mnt/disks/THOTH_Photo"
target="/mnt/user/photo"
skip_files_last_seconds=false
dry_run=false
iter=10000 # remind this number or you are not able to decrypt your files anymore!

# check settings
source=$([[ "${source: -1}" == "/" ]] && echo "${source%?}" || echo "$source")
target=$([[ "${target: -1}" == "/" ]] && echo "${target%?}" || echo "$target")
target=$([[ $target == 0 ]] && echo "false" || echo "$target")
target=$([[ $target == "" ]] && echo "false" || echo "$target")
skip_files_last_seconds=$([[ $skip_files_last_seconds == 0 ]] && echo "false" || echo "$skip_files_last_seconds")
dry_run=$([[ $dry_run == 0 ]] && echo "false" || echo "$dry_run")
dry_run=$([[ $dry_run == 1 ]] && echo "true" || echo "$dry_run")

# defaults
pwfile="${source}/filecryptor.key"
resume="${target}/filecryptor.last"

# check if passphrase exists
if [[ ! -f $pwfile ]]; then
    echo "Error! filecryptor did not found ${source}/filecryptor.key"
    exit 1
fi

# check if we have a starting point
if [[ -f $resume ]]; then
    last_file=$( < $resume)
fi

function filecryptor() {
    path=$1
    echo "Parsing $path ..."
    for file in "$path"/*; do
        # regular file
        if [ -f "$file" ]; then
            # skip passphrase file
            if [[ $file == $pwfile ]]; then
                echo "Skip $pwfile"
                continue
            fi
            # skip files until we reach our starting point
            if [[ -n $last_file ]]; then
                if [[ $file != $last_file ]]; then
                    echo "Skip $file"
                else
                    echo "Found the last processed file $last_file"
                    last_file=""
                fi
                continue
            fi
            file_time=$(stat -c %Y "$file") # file modification time
            # decrypt file
            if [[ $target == "false" ]]; then
                echo "Decrypt file ${file}"
                if [[ $dry_run != "true" ]]; then
                    if openssl aes-256-cbc -d -iter 10000 -in "$file" -out "${file}.filecryptor" -pass file:"${pwfile}"; then
                        rm "$file"
                        mv "${file}.filecryptor" "$file"
                        touch --date=@${file_time} "${file}"
                    else
                        rm "${file}.filecryptor" # cleanup on fail
                    fi
                fi
                # remember this file as starting point for the next execution (if interrupted)
                if [[ $dry_run != "true" ]]; then
                    echo "$file" > "${resume}"
                fi
                continue
            fi
            # skip new files
            if [[ "$skip_files_older_seconds" =~ ^[0-9]+$ ]]; then
                compare_time=$(($file_time + $skip_files_older_seconds))
                current_time=$(date +%s)
                # is the file old enough?
                if [[ $compare_time -gt $current_time ]]; then
                    continue
                fi
            fi
            dirname=$(dirname "$file")
            dirname="${dirname/$source/}" # remove source path from dirname
            file_basename=$(basename -- "$file")
            # skip already existing files with same timestamp
            if [ -f "${target}${dirname}/${file_basename}" ]; then
                target_file_time=$(stat -c %Y "$file") # file modification time
                if [[ $target_file_time -ge $file_time ]];then
                    echo "Skipped ${file} as it already exists in target"
                    continue
                fi
            fi
            # create parent dirs
            echo "Create parent dirs ${target}${dirname}"
            if [[ $dry_run != "true" ]]; then
                mkdir -p "${target}${dirname}"
            fi
            # encrypt file
            echo "Create encrypted file ${target}${dirname}/${file_basename}"
            if [[ $dry_run != "true" ]]; then
                salt="${file_time}0000000000000000"
                salt=${salt:0:16}
                openssl aes-256-cbc -iter $iter -in "$file" -out "${target}${dirname}/${file_basename}" -S $salt -pass file:"${pwfile}"
            fi
            # modification time
            echo "Set original file modification time"
            if [[ $dry_run != "true" ]]; then
                touch --date=@${file_time} "${target}${dirname}/${file_basename}" # https://unix.stackexchange.com/a/36765/101920
            fi
            # remember this file as starting point for the next execution (if interrupted)
            if [[ $dry_run != "true" ]]; then
                echo "$file" > "${resume}"
            fi
        # dir
        elif [ -d "$file" ]; then
            # skip dir until we reach our starting point
            if [[ -n $last_file ]]; then
                if [[ $last_file != "${file}"* ]]; then
                    echo "Skip $file"
                    continue
                else
                    echo "Found the last processed dir $file"
                fi
            fi
            filecryptor "$file"
        fi
    done
}

filecryptor "$source"

# clean up
if [[ $dry_run != "true" ]]; then
    rm "${resume}"
fi

exit

# encrypt example
if file_time=$(stat -c %Y "file.txt"); then
    openssl aes-256-cbc -iter 10000 -in "file.txt" -out "file.enc" -S $file_time -pass file:"filecryptor.key"
    touch --date=@${file_time} "file.enc"
fi

# decrypt example
if file_time=$(stat -c %Y "file.enc") && openssl aes-256-cbc -d -iter $iter -in "file.enc" -out "file.enc.filecryptor" -pass file:"filecryptor.key"; then
    rm "file.enc"
    mv "file.enc.filecryptor" "file.txt"
    touch --date=@${file_time} "file.txt"
fi

 

Edited by mgutt

Share this post


Link to post

Released Version 0.2:

Quote

# - skip dirs on resume (not only files)
# - bug fix: endless looping empty dirs

 

Share this post


Link to post

Released Version 0.3:

Quote

# - set your own PBKDF2 iteration count through "iter" setting
# - salt is padded with zeros to avoid "hex string is too short, padding with zero bytes to length" message on openssl output
# - decryption now supports resume, too
# - empty openssl files are deleted on decryption fails
# - encryption of already existing files in the target will be skipped as long the source files aren't newer

 

 

Share this post


Link to post

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.