[HTC Sensation] mirfatif:
I used these scripts without any problems on:

Android 4.4:

#!/system/bin/sh

#####################################################################################################
# DISABLE INTERNAL MEMORY (/data/media/0) AND USE EXTERNAL SD CARD AS INTERNAL STORAGE #
# ENCRYPT FILES ON EXTERNAL SD CARD FOR SECURITY #
# #
# STEPS: #
# 1. DIM i.e disable internal memory (KitKat, Lollipop), not explained here #
# #
# 2. change in boot.img > ramdisk > init.qcom.rc: #
# service fuse_sdcard0 /system/bin/sdcard -u 1023 -g 1023 -d /mnt/media_rw/sdcard0 /storage/sdcard0 #
# to #
# service fuse_sdcard1 /system/bin/sdcard -u 1023 -g 1023 -d /mnt/media_rw/sdcard0 /storage/sdcard1 #
# #
# ensure both lines exist in init.qcom.rc: #
# "mkdir /storage/sdcard1 0700 root root" and "mkdir /storage/sdcard0 0700 root root" #
# #
# 3. rename /system/bin/sdcard to sdcard_orig #
# 4. replace /system/bin/sdcard with this script (named "sdcard") #
# 5. copy encfs bianry to /system/bin #
# 6. set chmod +x permissions on all 3 files #
# #
# 7. encrypt internal storage (from settings) and save encfs password: #
# echo <your_password> | tee /data/local/pswd #
# 8. run this as root to mount encrypted directory: #
# /system/bin/encfs -o allow_other,umask=0 /mnt/media_rw/sdcard0/.encfs /data/local/SDCardTemp #
# copy all data on internal storage (/data/media/0/) to /data/local/SDCardTemp #
# reboot #
#####################################################################################################

export PATH=/system/bin:/system/xbin

# save log
mkdir -p /data/local/tmp
exec &>/data/local/tmp/encfs.log

date
echo "sdcard $*"

# mount encfs if not mounted already
if ! mountpoint /storage/sdcard0
then
cat /data/local/pswd | encfs -S /mnt/media_rw/sdcard0/.encfs /storage/sdcard0 -- -o allow_other,umask=0
# or may use uid=0,gid=1028,umask=006
fi

# pass original init command to sdcard daemon
sdcard_orig "$@"

Android 5.0:

#!/system/bin/sh

# add following lines to boot.img > ramdisk > init.qcom.rc before sdcard service:
## service encfs /system/bin/encfs.sh
## oneshot
## class late_start
# save this script as /system/bin/encfs.sh

# copy encfs bianry to /system/bin
# chmod a+x /system/bin/encfs*
# echo <password> | tee /data/local/pswd
# /system/bin/encfs -o allow_other,umask=0 /data/media/encfs_crypted /data/local/SDCardTemp
# rsync -a --progress --exclude=encfs_crypted /data/media/ /data/local/SDCardTemp/

export PATH=/system/bin

# wait to avoid "PROTO_EXEC|PROTO_WRITE failed" error
sleep 5

# save log
exec &>/data/local/encfs.log
date

# mount encfs
exec cat /data/local/pswd | encfs -S /data/media_encfs_crypted /data/media -- -o uid=1023,gid=1023,umask=006,allow_other,nonempty &&
echo Mounted.

Mount options "nosuid" and "nodev" are set by default as a security measure. Mostly applications making use of FUSE (in kernel space) and optionally libfuse (in userspace) also set (or give a commandline parameters to set) "allow_other" and "default_permissions" options by default.

UMASK, UID, GID, ALLOW_OTHER

"umask" (or "dmask" and "fmask") needs to be provided explicitly to make files world accessible. Default mode set by "encfs" is 0600 (umask=177) and only mount owner (root) can access files unless every newly created file is manually chmod-ed.

Similarly, "uid" and "gid" options are needed to set desired owner/group. Default owner:group is the mount owner (0:0) unless every newly created file is manually chown-ed.

Actually mount owner and files/directories owner can be different, which was the source of my confusion. That's how FUSE is implemented to let non-privileged mounts work. Have a look at this:

Since all "init" processes (including "sdcard" daemon) run as root (uid=0 and gid=0), 'mount' command always show "user_id=0" and "group_id=0". However it should be noted that provided "uid" and "gid" appear when we view files' ownership:

[B]/~#[/B] encfs -o allow_other,[COLOR="Red"]umask=7,uid=1023,gid=0[/COLOR] /.enc /enc
EncFS Password:
[B]/~#[/B] mount | grep enc
encfs on /encfs type fuse.encfs (rw,nosuid,nodev,relatime,[COLOR="red"]user_id=0,group_id=0[/COLOR],default_permissions,allow_other)
[B]/~#[/B] touch /enc/test
[B]/~#[/B] ls -l /.enc /enc
/.enc:
[COLOR="red"]-rw-rw-rw-[/COLOR] 1 [COLOR="Red"]0 0[/COLOR] 0 Mar 3 17:58 CoXOLL0fc0RaxZjq2HaSj9en

/enc:
[COLOR="YellowGreen"]-rwxrwx---[/COLOR] 1 [COLOR="YellowGreen"]1023 0[/COLOR] 0 Mar 3 17:58 test
[B]/~#[/B] chmod 0777 /enc/test
[B]/~#[/B] chown 0:1023 /enc/test
[B]/~#[/B] ls -l /.enc /enc
/.enc:
[COLOR="red"]-rwxrwxrwx[/COLOR] 1 [COLOR="red"]0 1023[/COLOR] 0 Mar 3 17:58 CoXOLL0fc0RaxZjq2HaSj9en

/enc:
[COLOR="YellowGreen"]-rwxrwx---[/COLOR] 1 [COLOR="YellowGreen"]1023 0[/COLOR] 0 Mar 3 17:58 test
[B]/~#[/B]

Setting "uid"/"gid" isn't necessarily required if universal access is granted using "umask=0" and "allow_other" mount options. Since every app is treated as a user on Android, so even the file explorer (same user who mounted filesystem by getting root privileges) can't access EncFS mounted directory without "allow_other" (if not running as root). So "allow_other" is necessarily required.

Mounting with "gid=9997,dmask=0007,fmask=0117,allow_other" at "/mnt/runtime/write/emulated/0/<mount_point>" on Android 6+ grants write access to apps with android.permission.WRITE_EXTERNAL_STORAGE granted. See this answer for details.

MOUNT OWNER

On Linux non-root users who are member of "fuse" group can mount and un-mount FUSE filesystems using "fusermount(3)" which is always run as root by set-UID-root or "setcap cap_sys_admin+ep". "fuse" group is the GID of "/dev/fuse". But non-privileged users can't set "allow_other" unless "user_allow_other" is set in FUSE configuration file /etc/fuse.conf. "/etc/fuse.conf" is used by "libfuse", and hence by all program depending on or linked to this library like "bindfs", "encfs", "sshfs", "rclone" and so on. So a non-root user can be the mount owner but other users (including root) can't access the mounted filesystem.

However on Android non-privileged user can't (u)mount FUSE filesystem because:

FUSE support is fully enabled from kernel side (on Linux it's usually a loadable module), can be checked by:

grep -E 'fuse|fuseblk' /proc/filesystems

But there are limitations in userspace.

There's no "fusermount" binary available on Android and the libfuse implementation doesn't use FUSE configuration because it's meant only for root users. But if we try to use binaries statically linked to standard Linux "libfuse" (like EncFS), they do read "/etc/fuse.conf" and may depend on "fusermount".

Apps on Android are run with all privileges dropped, so they can't make "mount" syscall even if "fusermount" is present with SUID bit set.

"/dev/fuse" is readable and writable only by root (0600, 0:0).

Non-root users on Android may face problem accessing filesystems (e.g. executing EncFS binary and reading "/dev/fuse") due to DAC or MAC permission checks. SELinux might need to be set "permissive" for the same reason, or the policy needs to be patched. SELinux also restricts Linux capabilities of processes.

So in short, EncFS and all other programs that make use of FUSE, can only be mounted by root on Android.

MOUNT NAMESPACE

Next important thing to be considered is "mount namespace" where even "allow_other" and root access won't help you. Mount namespaces enable apps to be exposed to an individual view of mounted filesystems. It means that "/proc/mounts" (which actually is "/proc/self/mounts") may look different to different processes. FUSE was implemented since KitKat to impose permissions derived from directory structure by emulating internal and/or external SD cards instead of direct mounting them. Mount namespaces are implemented since Marshmallow to serve the purpose of apps isolation in better way enforced by "zygote" at runtime. On rooted devices when an app requests a root shell, SU manager starts the shell in a new isolated namespace. While FUSE is implemented through UID's, namespaces are implemented through PID's. Super user rights with namespace separation are granted through

unshare -m sh

i.e. providing the app (process id) an independent view of mount points, not visible to the rest of apps.

Namespaces only affect (1) emulated SD card mounts handled by zygote or (2) any other filesystem mounted from a shell which was initiated by SuperSU with mount NS separation. All root processes started by Android ("init" on boot) run in global namespace. For example all mounts from adb shell will have no namespace separation because "adbd" is run in global namespace. Similarly, all shared mounts done at boot by init are propagated as a single unit from default/global/root namespace to all new zygote-forked per process namespaces. If we un-mount a filesystem from an app with root access, it will not affect the system and other apps. However if mount namespace separation is disabled, un-mounting a filesystem from any app will un-mount it globally. So isolated namespace feature provided by SU solutions is a security measure.

Say, you execute the following command from Termux (or any other terminal emulator app):

/system/bin/encfs -o allow_other /data/media/0/.encfs /data/media/0/encfs

Now, if mount namespace separation is enabled (by default), you will be able to open /data/media/0/encfs only from Termux app and from no other file explorer etc. For any other app, EncFS is not mounted at all. However Android su binaries provide a mount master option to always mount in global namespace:

[COLOR="Red"][B]su -mm[/B][/COLOR] /system/bin/encfs -o allow_other /data/media/0/.encfs /data/media/0/encfs

nsenter command can also be used to enter an existing isolated mount namespace. But note that namespaces are killed as soon as they have zero process running.

Other option is to disable "Mount Namespace Separation" in SuperSU app settings. However, do not disable this if you want to use apps like Titanium Backup or "Hide Root" feature as in Magisk etc. In addition to Isolated and Global, Magisk also provides a third option: Inherited i.e. run the root shell in same isolated namespace in which app is already running.

Related: Partition gets unmounted automatically in Android Oreo

DROP DAEMON PRIVILEGES

What I quoted earlier:

This "drop daemon privilege" thing is specific for "sdcard" daemon only as stated here and neither true for FUSE in general, nor related to filesystem "user_id" or "uid" options:

snprintf(opts, sizeof(opts),
"fd=%i,rootmode=40000,default_permissions,allow_other,user_id=%d,group_id=%d",
fd, uid, gid);

res = mount("/dev/fuse", dest_path, "fuse", MS_NOSUID | MS_NODEV, opts);
if (res < 0) {
ERROR("cannot mount fuse filesystem: %s\n", strerror(errno));
goto error;
}

/* Drop privs */
res = setgroups(sizeof(kGroups) / sizeof(kGroups[0]), kGroups);
if (res < 0) {
ERROR("cannot setgroups: %s\n", strerror(errno));
goto error;
}

res = setgid(gid);
if (res < 0) {
ERROR("cannot setgid: %s\n", strerror(errno));
goto error;
}

res = setuid(uid);
if (res < 0) {
ERROR("cannot setuid: %s\n", strerror(errno));
goto error;
}

fuse_init(&fuse, fd, source_path, write_gid, derive, split_perms);

umask(0);
res = ignite_fuse(&fuse, num_threads);

/* we do not attempt to umount the file system here because we are no longer
* running as the root user */

Process should call "setuid" itself to drop/elevate privileges which isn't the case for EncFS/FUSE by default. We can't use "su" or "setuidgid" or "chpst" commands to drop privileges later on but this isn't needed either as explained earlier. Only "uid=" and "gid=" options are enough.

Same source code file also explains somewhat how "legacy" and "derived permissions" work particularly on Android 4.4, 5 and 6 using FUSE filesystems for emulating SD card.

SUMMARY

On Android, only root can mount EncFS (FUSE) filesystem

Do mention "allow_other" option if you want to access EncFS mounted directory from a non-root app

Do mention "umask=" option if you want to read/write encrypted files from a non-root app

"uid" and "gid" need to be set if you want to give some specific non-root app write access to the filesystem. Otherwise "allow_other" and "umask" can suffice.

Disable "Mount Namespace Separation" option in SuperSU app if:

EncFS is to be mounted feom CLI (Terminal Emulator) app and not by some boot script or from adb shell, and

You want to access EncFS mounted directory from some app other than the root shell in CLI app

Latest EncFS binary is available here. Thanks to [MENTION=4293606]DualJoe[/MENTION]. It works with Android Oreo as well. However, overlapping with already mounted FUSE or sdcardfs (a FUSE-like filesystem implemented in Android 8+ that works in kernel space) should be avoided.

This is my understanding of EncFS and FUSE. Correct me if I'm wrong.