areuz areuz - 2 months ago 6
Linux Question

Hashing a string using a passphrase in Bash

I need a way of hashing a existing filename with a passphrase (ASCII string) but being able to revert it back afterwards using the same passphrase.

I know that ciphers can do this - they are encrypting the string... But their output lenght is based on the filename lenght, which is exactly what I do not want... mainly because it sometimes doubles the file lenght, but the outputted strings are not allways compatible with the FS. Ex. "\n" in a filename.

To be clear, I did a lot of research and even wrote some scripts, but all of the solutions are either slow, or don't work for my application at all.

The Goal of this is to get a constant length filenames that can be all 'decrypted' at once using a single passphrase. Without the need of creating additional 'metadata-like' files.

Answer

I've gotten all the way around with my initial question. There seems to be only one solution to the problem above, and that is (as James suggested) Format-preserving encryption. Altough, as far as I can tell, there are no existing commands that do this.

So I did exactly what was my very first option, and that is hashing the filename, putting the hash and the filename into a plain file (one file per directory) and encrypting that file with a passphrase.

I'll post my code here. Though it's probably not the prettiest nor the most portable code, but It does the job and is (IMO) really simple.

#!/usr/bin/env bash

man="Usage:  namecrypt [ -h ] [ -e || -d ] [ -F ] [ -D ] [DIRECTORY] [PASSPHRASE]\n
    -h, --help      display this message

    -e, --encrypt       encrypt the specified directory
    -d, --decrypt       decrypt the specified directory

    -F, --files     include files
    -D, --dir       include directories

    [DIRECTORY]     relative or absolute path to a directory/symbolic link
    [PASSPHRASE]        optional - specify the user passphrase on command line";

options=();
for Arg in "$@"; do
    if [ "$Arg" == "-h" ] || [ "$Arg" == "--help" ]; then
        echo -e "$man"; exit; 
    elif [ "$Arg" == "-e" ] || [ "$Arg" == "--encrypt" ]; then
        options[0]="E";
    elif [ "$Arg" == "-d" ] || [ "$Arg" == "--decrypt" ]; then
        options[0]="D";
    elif [ "$Arg" == "-F" ] || [ "$Arg" == "--files" ]; then
        options[1]="${options[1]}F";
    elif [ "$Arg" == "-D" ] || [ "$Arg" == "--dir" ]; then
        options[1]="${options[1]}D";
    elif [ -d "$Arg" ]; then
        options[2]="$(realpath "$Arg")";
    else    
        options[3]="$Arg";      
    fi;
done;

if [ "${options[0]}" == "" ]; then echo "No Mode specified!"; exit 1; fi;
if [ "${options[1]}" == "" ]; then options[1]="F"; fi;
if [ "${options[2]}" == "" ]; then echo "No such directory!"; exit 2; fi;
if [ "${options[3]}" == "" ]; then echo "Enter a passphrase: "; read options[3]; fi;

shopt -s nullglob dotglob;

function hashTarget
{
    BASE="$(basename "$1")";
    DIR="$(dirname "$1")/";

    if [ -a "$1" ]; then
        oldName="$BASE";
        newName=$(echo "$oldName" | md5sum);
        echo "$oldName||$newName" >> "$DIR.names";
        mv "$1" "$DIR$newName";
    else echo "Skipping '$1' - No such file or directory!";
    fi;
}

function dehashTarget
{
    BASE="$(basename "$1")";
    DIR="$(dirname "$1")/";

    [ -f "$DIR.names.cpt" ] && ccdecrypt -K "${options[3]}" "$DIR.names.cpt";

    if [ -f "$DIR.names" ]; then
        oldName="$BASE";
        newName=$(grep "$oldName" "$DIR.names" | awk -F '|' '{print $1}');
        [[ ! -z "${newName// }" ]] && mv "$1" "$DIR$newName";
    else
        echo "Skipping '$1' - Hash table not found!";
    fi;
}

function mapTarget
{   
    DIR="$(dirname "$1")/";

    for Dir in "$1"/*/; do
        mapTarget "$Dir";
    done;

    for Item in "$1"/*; do
        if ([ -f "$Item" ] && [[ "${options[1]}" == *"F"* ]]) || 
           ([ -d "$Item" ] && [[ "${options[1]}" == *"D"* ]]); then

            if [ "${options[0]}" == "E" ]; then
                hashTarget "$Item";
            else
                dehashTarget "$Item";
            fi;
        fi;
    done;

    [ "${options[0]}" == "D" ] && [ -f "$DIR.names" ] && rm "$DIR.names";
    [ "${options[0]}" == "E" ] && [ -f "$DIR.names" ] && ccencrypt -K "${options[3]}" "$DIR.names";

}

mapTarget "${options[2]}";

Probably the only reason why it is so long, is because I didn't bother with any OOP, and I also did a lot of checks and steps to make sure that most of the time no names get mangled and can't be restored - user error can still cause this.

This is the command used to encrypt the hash-table files: CCrypt