#!/usr/bin/python # ============================================================================= # # gnome-encfs - GNOME keyring and auto-mount integration of EncFS folders. # Copyright (C) 2010 Oben Sonne # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ============================================================================= import getpass import os import os.path import optparse import subprocess import sys from xdg.DesktopEntry import DesktopEntry as xdg_de from xdg.BaseDirectory import xdg_config_home as xdg_ch import gtk # automatically set application name import gnomekeyring as gk # ============================================================================= # test mode related # ============================================================================= TEST = "GNOME_ENCFS_TEST" in os.environ class preset: """Preset user input. Used for non-interactive testing mode.""" proceed = None password = None epath = None mpoint = None amount = None # ============================================================================= # constants # ============================================================================= # attributes to detect gnome-encfs items in keyring GENCFS_ATTR = {"gnome-encfs": TEST and "test" or "" } KEYRING = gk.get_default_keyring_sync() ITYPE = gk.ITEM_GENERIC_SECRET MSG_NO_MATCH = ("No matching EncFS items in keyring.\n" "Use option --list to show available items or " "option --add to add items.") USAGE = """Usage: %prog --list %prog --mount [ENCFS-PATH-or-MOUNT-POINT] %prog --add ENCFS-PATH MOUNT-POINT %prog --edit MOUNT-POINT %prog --remove MOUNT-POINT""" DESCRIPTION = """Painlessly mount and manage EncFS folders using GNOME's keyring.""" EPILOG = """This tool stores EncFS paths, corresponding mount points as well as passwords in the GNOME keyring and optionally mounts EncFS paths automatically on login to a GNOME session (works only if gnome-encfs is installed in the system path, at /usr/bin or /usr/local/bin). """ VERSION="0.1" # ============================================================================= # helper # ============================================================================= def _options(): """Parse command line options.""" op = optparse.OptionParser(usage=USAGE, version=VERSION, description=DESCRIPTION, epilog=EPILOG) op.add_option("-l", "--list", action="store_true", default=False, help="list all EncFS items stored in keyring") op.add_option("-m", "--mount", action="store_true", default=False, help="mount all or selected EncFS paths stored in keyring") op.add_option("-a", "--add", action="store_true", default=False, help="add a new EncFS item to keyring") op.add_option("-e", "--edit", action="store_true", default=False, help="edit an EncFS item in keyring") op.add_option("-r", "--remove", action="store_true", default=False, help="remove an EncFS item from keyring") og = optparse.OptionGroup(op, "Non-interactive") og.add_option("", "--proceed", default=None, help="Input for proceed question") og.add_option("", "--password", default=None, metavar="PW", help="Input for password prompt") og.add_option("", "--epath", default=None, help="Input for EncFS path edit") og.add_option("", "--mpoint", default=None, help="Input for mount point edit") og.add_option("", "--amount", default=None, help="Input for auto mount question") op.add_option_group(og) opts, args = op.parse_args() try: args.remove("autostart") opts.autostart = True opts.mount = True except ValueError: opts.autostart = False if opts.list + opts.mount + opts.add + opts.edit + opts.remove != 1: op.print_help() op.exit(1) # normalize paths args = [_pathify(a) for a in args] opts.p1 = args and args.pop(0) or None opts.p2 = args and args.pop(0) or None if opts.add and not (opts.p1 and opts.p2): op.error("add needs an EncFS path and a moint point") op.print_help() op.exit(1) if opts.remove and not opts.p1: op.error("remove needs a moint point") op.print_help() op.exit(1) preset.proceed = opts.proceed preset.password = opts.password preset.epath = opts.epath preset.mpoint = opts.mpoint preset.amount = opts.amount return opts def _exit(ec): """Exit with additional check if autostart file is still needed.""" _autostart(_get_items(amount="y")) sys.exit(ec) def _proceed(msg): print("Warning: %s" % msg) proceed = preset.proceed or raw_input("Proceed [y/N]: ") if proceed.strip()[0].lower() != "y": _exit(2) def _pathify(path): path = os.path.expanduser(path) path = os.path.expandvars(path) path = os.path.abspath(path) path = os.path.realpath(path) return path def _is_mounted(mpoint): """Check of something is mounted at given mount point.""" p = subprocess.Popen(["mount"], stdout=subprocess.PIPE) mount = p.communicate()[0] lines = mount.strip('\n').split('\n') points = map(lambda line: line.split()[2], lines) points = [os.path.abspath(p) for p in points] return os.path.abspath(mpoint) in points def _is_encfs(epath): """Check if 'epath' points to an EncFS directory.""" p = subprocess.Popen(["encfsctl", "info", epath], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() return p.returncode == 0 _items_cached = None def _get_items(mpoint=None, epath=None, anypath=None, amount=None): """Get all EncFS items or those matching given attributes.""" global _items_cached if _items_cached is None: try: _items_cached = gk.find_items_sync(ITYPE, GENCFS_ATTR) except gk.NoMatchError: _items_cached = [] match = [] for item in _items_cached: if mpoint and item.attributes["mount-point"] != mpoint: continue if epath and item.attributes["encfs-path"] != epath: continue if anypath and (item.attributes["mount-point"] != anypath and item.attributes["encfs-path"] != anypath): continue if amount and item.attributes["auto-mount"] != amount: continue match.append(item) return match def _autostart(enable): """Set up XDG autostart file.""" if TEST: fname = os.path.join(os.path.curdir, "autostart.desktop") else: fname = os.path.join(xdg_ch, "autostart", "gnome-encfs.desktop") if not enable: if os.path.exists(fname): os.remove(fname) return content = { "Exec": "gnome-encfs autostart", "Name": "EncFS", "Comment": "Mount EncFS folders configured in GNOME's keyring", "Icon": "folder", "Version": "1.0", "X-GNOME-Autostart-enabled": "true" } de = xdg_de(filename=fname) for key, value in content.items(): de.set(key, value) de.validate() de.write() # ============================================================================= # actions # ============================================================================= def list_items(path=None): """List EncFS items in keyring.""" items = _get_items(anypath=path) if not items and path: print(MSG_NO_MATCH) return False for item in items: epath = item.attributes["encfs-path"] mpoint = item.attributes["mount-point"] amount = item.attributes["auto-mount"] print("* encfs path : %s" % epath) print(" mount point : %s" % mpoint) print(" mount at login : %s" % (amount == "y" and "yes" or "no")) return True def add_item(epath, mpoint): """Add new EncFS item to keyring.""" if not _is_encfs(epath): _proceed("no EncFS at given path") if not os.path.isdir(mpoint): _proceed("mount point is not a directory") if _get_items(mpoint=mpoint): _proceed("mount point already in keyring") secret = preset.password or getpass.getpass("EncFS password: ") amount = preset.amount or raw_input("Mount at login [Y/n]: ") or "y" amount = amount.strip()[0].lower() == "y" and "y" or "n" attr = {"encfs-path": epath, "mount-point": mpoint, "auto-mount": amount} attr.update(GENCFS_ATTR) name = "EncFS mount at %s" % mpoint gk.item_create_sync(KEYRING, ITYPE, name, attr, secret, False) global _items_cached _items_cached = None return True def edit_item(mpoint): """Edit EncFS item in keyring.""" items = _get_items() edits = _get_items(mpoint=mpoint) if not edits: print(MSG_NO_MATCH) return False for item in edits: # get item data epath = item.attributes["encfs-path"] mpoint = item.attributes["mount-point"] amount = item.attributes["auto-mount"] epath = preset.epath or raw_input("EncFS path [%s]: " % epath) or epath mpoint = preset.mpoint or raw_input("Mount point [%s]: " % mpoint) or mpoint secret = preset.password or getpass.getpass("Password [**current**]: ") or item.secret hint = amount == "y" and "Y/n" or "y/N" amount = preset.amount or raw_input("Mount at login [%s]: " % hint) or amount amount = amount.strip()[0].lower() == "y" and "y" or "n" mpoint = _pathify(mpoint) epath = _pathify(epath) # check item data for other in [i for i in items if i.item_id != item.item_id]: if other.attributes["mount-point"] == mpoint: _proceed("mount point already in use") if not _is_encfs(epath): _proceed("no EncFS at given path") if not os.path.isdir(mpoint): _proceed("mount point is not a directory") # update item data attributes = GENCFS_ATTR.copy() attributes["encfs-path"] = epath attributes["mount-point"] = mpoint attributes["auto-mount"] = amount gk.item_set_attributes_sync(KEYRING, item.item_id, attributes) info = gk.item_get_info_sync(KEYRING, item.item_id) info.set_secret(secret) gk.item_set_info_sync(KEYRING, item.item_id, info) global _items_cached _items_cached = None return True def remove_item(mpoint): """Remove EncFS item from keyring.""" items = _get_items(mpoint=mpoint) if not items: print(MSG_NO_MATCH) return False for item in items: gk.item_delete_sync(KEYRING, item.item_id) global _items_cached _items_cached = None return True def mount_items(path, autostart): """Mount selected items. If `path` is set, mount only those items where the EncFS path or mount point equals `path`. If `autostart` is True, mount only those items where auto-mount is set to 'y'. """ items = _get_items(anypath=path, amount=(autostart and "y" or None)) if not items and path: print(MSG_NO_MATCH) return False rc = True for item in items: epath = item.attributes["encfs-path"] mpoint = item.attributes["mount-point"] msg = "Mounting %s at %s: " % (epath, mpoint) if _is_mounted(mpoint): msg += "mount point already in use" elif not os.path.isdir(mpoint): msg += "mount point does not exist or is not a directory" rc = False else: cmd = ["encfs", "-o", "nonempty", "-S", epath, mpoint] p = subprocess.Popen(cmd, stdin=subprocess.PIPE) p.communicate(input="%s\n" % item.secret) msg += p.returncode and "FAILED" or "OK" rc &= not p.returncode print(msg) return rc # ============================================================================= # main # ============================================================================= def main(): opts = _options() if opts.add: ok = add_item(opts.p1, opts.p2) elif opts.list: ok = list_items(opts.p1) elif opts.mount: ok = mount_items(opts.p1, opts.autostart) elif opts.edit: ok = edit_item(opts.p1) elif opts.remove: ok = remove_item(opts.p1) else: assert False return ok if __name__ == '__main__': ret = main() _exit(int(not ret))