#! /bin/bash ROOT_DIR=~/thc THC_RFS_VERSION="1.4" [ -z "$THC_RFS_SERVER" ] && THC_RFS_SERVER=rfs.thc.org [ -z "$THC_RFS_PORT" ] && THC_RFS_PORT=22 FUSE_OPT="" if [[ x"$OSTYPE" == "xdarwin"* ]]; then # Setting 'noapplexattr' prevents 'finder' from copying. FUSE_OPT="${FUSE_OPT} -o iosize=65536,noappledouble" fi # Turn of compression. All transfered data is encrypted and thus cant be compressed. SSHFS_OPT="-o ServerAliveInterval=30,reconnect,attr_timeout=60,auto_cache,compression=no,default_permissions" SSHFS_OPT="${SSHFS_OPT} ${FUSE_OPT}" # l(58^44)/l(2) == 257 Bit ENC_B58_LEN=44 # l(58^32)/l(2) == 187 Bit ENC_B58_LEN=32 # l(58^24)/l(2) == 140 Bit (ought to be enough) ENC_B58_LEN=24 SSH_KEY_FILE="${ROOT_DIR}/.id_rsa-rfs" SSH_PARAM="-q -i ${SSH_KEY_FILE}" SSH_PRIV_KEY="-----BEGIN OPENSSH PRIVATE KEY----- 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 Fb9mr1O9fL8zkAAAALdHBAdGVyZWtub3I= -----END OPENSSH PRIVATE KEY-----" debug() { #echo $@ : } usage() { echo "\ ERFS Version: $THC_RFS_VERSION $0 [] [] CMD: init - Create a NEW secure file system mount - Mount a secure file system umount - Unmount one or all secure file systems clean - Unmount all and purge runtime directories (wont delete data). Options: -x - Prompt for SHARE-SECRET Create a NEW and empty secure remote file system: $ $0 init Mount an EXISTING secure remote file system to ~/test: $ $0 mount ~/test Unmount a secure remote file system: $ $0 umount Unmount all secure remote file systems: $ $0 umount" exit 1 } # Do a dirty base58 from input data (likely to be base64) b58sanitize() { local CLEAR CLEAR=$(echo "$1" | sed 's/[^a-zA-Z0-9]//g' | sed 's/[0OIl]//g'| head -n1 | cut -c 1-"$2") if [ ${#CLEAR} -lt "$2" ]; then # This is fatal and should never happen echo "ERROR base58: ${#CLEAR} but length is $2" exit 1 fi echo "$CLEAR" } # Converts (user-)input into likely output. # 0 -> o # O -> o # l -> 1 # I -> 1 b58convert() { # shellcheck disable=SC2020 echo "$1" | tr 0OlI oo11 } # Sanitize mount point mp_sanitize() { local CLEAR CLEAR="${1}" # If you like to have awkward characters in your mount point # (mkdir "~.../;id|-") then you can remove the next line at your # own risk, fool. You have been warned... CLEAR="${1//[^a-zA-Z0-9_- /]/}" echo "$CLEAR" } # Deterministic secret from Master-Secret derive_secrets() { # Derive all keys from the master key # RSEC is the username # RID a local id (short) # EPAS is the encryptino password # SPAS is the ssh login password RSEC=$(b58sanitize "$(echo s1"${1}" | openssl sha256 -binary | openssl base64)" 16) RID=$(b58sanitize "$(echo i2"${1}" | openssl sha256 -binary | openssl base64)" 4) EPAS=$(b58sanitize "$(echo s3"${1}" | openssl sha256 -binary | openssl base64)" "$ENC_B58_LEN") SPAS=$(b58sanitize "$(echo p4"${1}" | openssl sha256 -binary | openssl base64)" 16) } master_pwd_new() { # Create a new Master Password MPASS=$(b58sanitize "$(dd if=/dev/urandom bs=1 count=48 2>/dev/null | openssl base64)" "$ENC_B58_LEN") } unmount_cmd() { # Linux does not accept sym-link as unmount points. REAL_RSEC="${1}" [ -L "${1}" ] && REAL_RSEC=$(readlink "${1}") if [ x"${HAS_FUSERMOUNT}" = x1 ]; then fusermount -uz "${REAL_RSEC}" ret=$? else # Try MacOS way..(fusermount does not exist) umount -f "${REAL_RSEC}" ret=$? fi } unmount() { # Three cases to consider: # 1. Unmount fails because mount-point is busy. # -> Do not clear runtime files # 2. Unmount fails because mount-point directory does not exist # -> Clear runtime files # 3. Unmount fails because mount-point is already unmounted # -> Clear runtime files # Do not return if this fails. We like to unmount # rfs regardless if we managed to unmount rsec. unmount_cmd "${ROOT_DIR}/.run/id-${1}/rsec" if [ $ret -ne 0 ]; then #echo "unmount rsec failed" : fi [ -L "${ROOT_DIR}/.run/id-${1}/rsec" ] && rm -f "${ROOT_DIR}/.run/id-${1}/rsec" [ -d "${ROOT_DIR}/.run/id-${1}/rsec" ] && rmdir "${ROOT_DIR}/.run/id-${1}/rsec" unmount_cmd "${ROOT_DIR}/.run/id-${1}/rfs" if [ $ret -ne 0 ]; then # unmount rfs failed. : fi # Clear runtime files if 'rsec' has been unmounted (but only then) [ -d "${ROOT_DIR}/.run/id-${1}/rfs" ] && rmdir "${ROOT_DIR}/.run/id-${1}/rfs" rmdir "${ROOT_DIR}/.run/id-${1}" 2>/dev/null rm -f "${ROOT_DIR}/sec-${1}" } unmount_all() { for x in "${ROOT_DIR}/.run/id-"*; do if [ ! -d "${x}" ]; then break fi id=$(echo "$x" | cut -f2 -d-) debug "Working on '${id}'" unmount "$id" done } get_mpass() { # read master password from file,device or sym-link if exists # Grugq's idea to have -x to read password from stdin: # shellcheck disable=SC2153 # We mean SEC, not RSEC if [ x"${1}" = "x-x" ] || [ x"${1}" = "xstdin" ]; then echo -n "Enter SHARE-SECRET: " read -r mpass elif [ -e "${1}" ]; then read -r mpass <"${1}" elif [ x"${1}" != x ]; then mpass="${1}" elif [ x"${SEC}" != x ]; then # Try to get password from environment variable mpass="${SEC}" else echo "ERROR: Need to specify password" exit 1 fi mpass=$(b58convert "$mpass") if [ ${#mpass} -ne "$ENC_B58_LEN" ]; then echo "ERROR. Illegal master password" exit 1 fi } # Replace string in file replace() { if ! grep "$2" "$1" >/dev/null; then # Pattern found; replace in file sed -i-old "s/$2/$3/g" "$1" >/dev/null fi } if [ $# -lt 1 ]; then usage fi command -v sshfs >/dev/null 2>&1 || { echo >&2 "sshfs not found. Try 'apt-get install sshfs'"; exit 1; } command -v encfs >/dev/null 2>&1 || { echo >&2 "EncFS not found. Try 'apt-get install encfs'"; exit 1; } # MacOS does not have fusermount and uses 'umount' instead. HAS_FUSERMOUNT=1 command -v fusermount >/dev/null 2>&1 || HAS_FUSERMOUNT=0 if [[ x"$OSTYPE" == "xdarwin"* ]]; then VER=$(sshfs --version 2>&1 | grep ^SSHFS | cut -f3 -d" ") if [ x"$VER" = "x2.9" ]; then echo " Buggy SSHFS dedected. Your read performance might be slow. Please read https://github.com/osxfuse/sshfs/issues/57 for details. To fix this please executed: $ sudo curl -sSL https://tiny.cc/apewqz >\"$(which sshfs)\"" fi fi mkdir -p "${ROOT_DIR}" if [ ! -f "${SSH_KEY_FILE}" ]; then echo "${SSH_PRIV_KEY}" >"${SSH_KEY_FILE}" chmod 600 "${SSH_KEY_FILE}" fi if [ x"$1" = xinit ] || [ x"$1" = xnew ] || [ x"$1" = xi ]; then if [ x"$2" != x ]; then usage fi master_pwd_new derive_secrets "${MPASS}" export THC_RFS_SECRET="${RSEC}" export THC_SSH_PASSWORD="${SPAS}" export THC_RFS_VERSION="1.1" # Bump the server to create a new file share # shellcheck disable=SC2086 # Allow SSH_PARAM to split into words ssh -p ${THC_RFS_PORT} ${SSH_PARAM} -o SendEnv="THC_RFS_VERSION THC_RFS_TOKEN THC_RFS_SECRET THC_SSH_PASSWORD" rfs-init@${THC_RFS_SERVER} ret=$? if [ $ret -ne 0 ]; then echo "Server problem. Aborting..." exit 1 fi echo " --> You MUST remember this SHARE-SECRET. Access to the data is lost <-- --> *FOREVER* if the SHARE-SECRET is lost. KEEP IT SAFE. <-- ############################################## ## ## ## SHARE-SECRET: $MPASS ## ## ## ############################################## SUCCESS! Now mount your newly created file system like so: $ $0 mount -x or $ $0 mount ${MPASS} or $ SEC=${MPASS} $0 m " exit 0 elif [ x"$1" = xstop ] || [ x"$1" = xu ] || [ x"$1" = xumount ] || [ x"$1" = xunmount ]; then # Unmount all if no share-secret if [ ${#2} -eq 0 ]; then unmount_all exit 0 fi # Unmount by mount point if [ -d "${2}" ]; then MPOINT=$(mp_sanitize "${2}") if [ ! -f "${MPOINT}/.thc.id" ]; then echo "ERROR: Please specify the SHARE-SECRET instead of the mount point" echo " $0 umount " echo "or unmount all devices:" echo " $0 unmount" exit 1 fi RID=$(cat "${MPOINT}"/.thc.id) unset MPOINT else # Unmount by share-secret get_mpass "${2}" derive_secrets "${mpass}" fi unmount "${RID}" exit 0 elif [ x"$1" = xclean ] || [ x"$1" = xc ]; then # unmount all directories and clean all runtime directories unmount_all [ -d "${ROOT_DIR}/.run" ] && rmdir "${ROOT_DIR}/.run" for x in "${ROOT_DIR}/sec-"*; do if [ ! -L "${x}" ]; then break fi rm -f "${x}" done rm -f "${ROOT_DIR}/.id_rsa-rfs" rmdir "${ROOT_DIR}" exit 0 fi # HERE: Command is 'mount' or similar. if [ x"$1" != xmount ] && [ x"$1" != xm ]; then echo "ERROR: Unknown command or illegal share-secret." usage fi get_mpass "${2}" derive_secrets "${mpass}" # Create state/runtime directories mkdir -p "${ROOT_DIR}/.run/id-${RID}" debug "DEBUG master $MPASS (r $RSEC e $EPAS rid $RID s $SPAS)" mkdir -p "${ROOT_DIR}/.run/id-${RID}/rfs" # Linux non-root does not allow 'allow_other' # shellcheck disable=SC2086 # Allow SSHFS_OPT to split into words sshfs -p "${THC_RFS_PORT}" ${SSHFS_OPT} -o ssh_command='ssh -o StrictHostKeyChecking=accept-new' -o password_stdin,idmap=user,uid="${UID}",gid="$(id -g)" rfs-"${RSEC}@${THC_RFS_SERVER}:rw" "${ROOT_DIR}/.run/id-${RID}/rfs" <<<"${SPAS}" ret=$? unset SPAS if [ $ret -ne 0 ]; then # sshfs failed. echo "ERROR: sshfs failed." echo " 1. The volume is already mounted." echo " 2. Wrong SHARE-SECRET." echo " 3. The server can not be reached." exit 1 fi # Check if the 'encrypted' subdirectory exists if [ ! -d "${ROOT_DIR}/.run/id-${RID}/rfs/encrypted" ]; then # File system exists (sshfs worked) but no file found. # This should never happen unless someone delete the 'encrypted' # directory manually. echo "ERROR: File System does not exist. Wrong share-secret?" exit 1 fi # Increase block-size to 4k to increase EncFS performance (3x on MacOS) FILE="${ROOT_DIR}/.run/id-${RID}/rfs/encrypted/.encfs6.xml" if [ -f "$FILE" ] && [ ! -f "$FILE"-old ]; then replace "${ROOT_DIR}/.run/id-${RID}/rfs/encrypted/.encfs6.xml" "blockSize>1024" "blockSize>4096" fi # Display MOTD and wait...Only way we can notiy a user.. if [ -f "${ROOT_DIR}/.run/id-${RID}/rfs/motd" ]; then # Discard any nasty characters. Limit to 24 lines. tail -n24 <"${ROOT_DIR}/.run/id-${RID}/rfs/motd" | sed 's/[^a-zA-Z0-9#!. ]//g' echo "Waiting 10 seconds before continuing..." sleep 10 fi # Cleanup previous state USER_MOUNT_POINT="${ROOT_DIR}/sec-${RID}" ENCFS_MOUNT_POINT="${ROOT_DIR}/.run/id-${RID}/rsec" [ -L "${ENCFS_MOUNT_POINT}" ] && rm -f "${ENCFS_MOUNT_POINT}" rmdir "${ROOT_DIR}/.run/id-${RID}/rsec" 2>/dev/null if [ ${#3} -ne 0 ]; then MPOINT=$(mp_sanitize "${3}") if [ ! -d "${MPOINT}" ]; then echo "Directory ${MPOINT} does not exist." unmount "${RID}" exit 1 fi # Link from the default mount point to the user specified mount point ln -sf "${MPOINT}" "${ENCFS_MOUNT_POINT}" ENCFS_MOUNT_POINT="${MPOINT}" USER_MOUNT_POINT="${ENCFS_MOUNT_POINT}" unset MPOINT else mkdir -p "${ROOT_DIR}/.run/id-${RID}/rsec" fi ENCFS_OPT="-S --standard --no-default-flags" FUSE_OPT="${FUSE_OPT} -o use_ino,default_permissions" if [[ x"$OSTYPE" == "xdarwin"* ]]; then CLEAR="${USER_MOUNT_POINT//[^a-zA-Z0-9_\/-]/}" VOLNAME="$(basename "${CLEAR}")" FUSE_OPT="${FUSE_OPT} -o volname=${VOLNAME},async,local" fi # Securely pass password to encfs...well, as secure as this shit can be. # # shellcheck disable=SC2086 # Allow ENCFS_OPT to split into words encfs ${ENCFS_OPT} "${ROOT_DIR}/.run/id-${RID}/rfs/encrypted" "${ENCFS_MOUNT_POINT}" -- ${FUSE_OPT} <<<"${EPAS}" >/dev/null ret=$? unset EPAS if [ $ret -ne 0 ]; then echo "EncFS failed. Aborting..." unmount "${RID}" exit 1 fi ln -sf "${ENCFS_MOUNT_POINT}" "${ROOT_DIR}/sec-${RID}" # Store the ID. We need this to 'erfs u ' echo "${RID}" >"${USER_MOUNT_POINT}/.thc.id" echo "Encrypted partition mounted to ${USER_MOUNT_POINT}"