4
\$\begingroup\$

Problem

When it comes to security I like to don a tin foil hat. As such I use the, off-line password manager, Keepass. Keepass allows you to to use multi-factor authentication via; a password, a keyfile and the Windows user account. I enabled only two of these, the password and the keyfile. Because I have two computers that need access to the keyfile I've stored it on a USB stick.

On Windows everything is fine, I plug the USB in, I open the database, I eject the USB from the tray, remove USB and I can use the passwords I need. This in all takes two seconds.

Solution

However on Linux this feels like a whole song and dance. Consisting of something similar to the following:

# Plug in USB
lsblk
sudo mount /dev/usb1 /path/to/folder
keypass &
sudo umount /dev/usb1
udisksctl power-off -b /dev/usb
lsblk
# Remove USB

Whilst this in theory isn't much different than Windows, it's just an added mount and unmount. Having to check lsblk and type all of that just to get a password is inconvenient. And so I decided to script this away, it's just ~6 commands, how hard can it be?

Fishy Solution

I'm a beginner with fish, I've not used functions or lists before. And the only time I've set something was to update my path. I'm such a noob that I tried to return ("" "") to return a list... 🤦 However I have some experience in other, non-shell, languages and feel I've picked it up ok.

  • pdev - finds a partition's UUID, pkname, block path and mountpoint all from the UUID. If a device with a matching UUID is not found then it will continuously inform the user and attempt to reacquire the data.

  • pmount - This takes four positional arguments for the partition; UUID, target mountpoint, block path, current mountpoint.

    • If the partition has a current mountpoint it will fail if this is not the target mountpoint.
    • Otherwise, if the block path is a value then it will mount the partition to the target path.
    • Finally if neither of these are true it does nothing.
  • umount_all - This is given a pkname and unmounts all partitions of the drive. Whilst it's unlikely that that the drive will have multiple partitions, and it's even more unlikely that they'll be mounted. I would prefer to err on the side of caution.

  • load_passwords - This is effectively the 'main' function. It takes a UUID and a target path. From here it:

    1. Get the mount information from pdev.
    2. Get the drive's path. (This would be /dev/usb rather than /dev/usb1) This uses lsblk as simply cutting off the last number wouldn't work on some storage devices. (I.e. nvme0n1p1)
    3. Display the partition's information, the block path, highlighting the drive's path, and the current mountpoint.
    4. Attempt to mount the drive using pmount.
    5. Open keypass.
    6. Ask the user if they wish to eject the drive.
    7. Exit successfully if the user does not wish to eject the drive.
    8. Unmount all the partitions on the drive with umount_all.
    9. Power off the drive.
    10. Verify the device has been powered off successfully.
  • passwords - A convenience function to pass the UUID and mountpoint that I use.

function pdev
    while true
        set mounts (lsblk -l -o UUID,PKNAME,PATH,MOUNTPOINT | grep "^$argv[1] " | grep -Po "[^ ]+")
        if set -q mounts[1]
            break
        end
        read -P "Insert key drive "
    end
    for mount in $mounts
        echo $mount
    end
end

function pmount
    if set -q argv[4]
        if test $argv[4] != $argv[2]
            echo "Mounted to wrong directory"
            return 1
        end
    else if set -q argv[3]
        sudo mount UUID=$argv[1] $argv[2]
    end
end

function umount_all
    set blocks (lsblk -l -o PKNAME,PATH,MOUNTPOINT | grep "^$argv[1] ")
    for block_ in $blocks
        set block (echo "$block_" | grep -Po "/[^ ]+")
        if set -q block[2]
            sudo umount "$block[1]"
        end
    end
end

function load_passwords
    set mounts (pdev $argv[1])
    if test $status != 0
        return 1
    end
    set drive (lsblk -o NAME,PATH | grep "^$mounts[2] " | grep -Po "/[^ ]+")
    echo "PATH : $mounts[3]" | grep "$drive"
    echo "MOUNT: $mounts[4]"
    pmount $argv[1] $argv[2] $mounts[3] $mounts[4]
    if test $status != 0
        return 1
    end
    keepass &
    read -P "Eject drive? [Y/n] " -l input
    echo "$input" | grep -Poi "(^\$)|(^y)" >> /dev/null
    if test $status = 1
        return
    end
    umount_all "$mounts[2]"
    udisksctl power-off -b $drive
    lsblk -o UUID | grep "^$argv[1]\$" >> /dev/null
    if test $status = 1
        return
    end
    echo "Failed to power off drive"
    return 1
end

function passwords
    load_passwords {redacted} /path/to/mountpoint
end

Concerns

  • If not a fan of using a for loop to echo each value in a list to 'return' a list. Is there a cleaner way to do this?

    for mount in $mounts
        echo $mount
    end
    
  • I'm not a fan of names like $argv[1] rather than $uuid as they make the code harder to understand. Is there a clean way to specify these?

  • The code feels unreadable, it's why I've written such a thorough description here. I can see myself forgetting all this nuance and coming back to this in a year and go, which idiot wrote this?! 😡

  • I'm not a fan of having all the functions be public, pmount should probably be private.

  • I'm not a fan of needing sudo when I have access to /path/to/mountpoint. There's a certain irony to needing to enter two passwords to get one...

  • I feel the code is just kinda messy and not great.

I am also happy for any other comments on my code.

\$\endgroup\$
3
\$\begingroup\$

If not a fan of using a for loop to echo each value in a list to 'return' a list. Is there a cleaner way to do this?

The printf command will reuse the format string to consume all the input:

printf "%s\n" $mounts

I'm not a fan of names like $argv[1] rather than $uuid as they make the code harder to understand. Is there a clean way to specify these?

Use the -a option to function

function load_passwords -a uuid -a mountpath
    set mounts (pdev $uuid)
    ...

The code feels unreadable, it's why I've written such a thorough description here. I can see myself forgetting all this nuance and coming back to this in a year and go, which idiot wrote this?! 😡

Sorry, can't help with that. Looks reasonable to me. If you're worried about forgetting the nuance, add some comments including the URL for this question.

I'm not a fan of having all the functions be public, pmount should probably be private.

Why? There's no sensitive info in it.

The only way you can achieve privacy is to chmod the source files so no other user can read them.

I'm not a fan of needing sudo when I have access to /path/to/mountpoint. There's a certain irony to needing to enter two passwords to get one...

Assuming you have the permission to do so, you could visudo so that your user does not need to enter a password for sudo mount and sudo umount

I feel the code is just kinda messy and not great.

That's pretty much the nature of the beast with shell scripting. At least fish has cleaner (if perhaps more verbose) syntax than bash.


I'm a Lastpass user, and have made similar efforts to access passwords via the lpass command line tool.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you, this is really helpful. -a has helped clean up a lot of the confusion, and I can probably use -d to add a description to make the rest of it more readable. It seems there's no tuple unpacking equivalent, so I have to deal with some "$mounts[2]" noise, but it's so much better. Thanks! \$\endgroup\$ – user226435 Jun 30 at 15:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.