#!/usr/bin/env python import fuse # fusepy import gnupg # python-gnupg import zlib from errno import ENOENT, EACCES, ENOSYS, ENODATA import stat from binascii import hexlify import os import sys import logging import struct import time from cStringIO import StringIO magic = 'GPGFS1\n' def decrypt(gpg, path): data = file(path).read() if not data: return data res = gpg.decrypt(data) if not res.ok: raise IOError, "decryption failed, %s" % path data = zlib.decompress(res.data) return data def encrypt(gpg, keyid, path, data): data = zlib.compress(data, 1) res = gpg.encrypt(data, keyid, armor=False) if not res.ok: raise IOError, "encryption failed, keyid %s, path %s" % (keyid, path) with file(path, 'w') as fd: fd.write(res.data) class Entry: ''' Filesystem object, either file or directory. ''' def __init__(self, **kwargs): for k,v in kwargs.iteritems(): setattr(self, k, v) # entry types: ENT_FILE = 0 ENT_DIR = 1 def read_index(gpg, path): data = decrypt(gpg, path) buf = StringIO(data) if buf.read(len(magic)) != magic: raise IOError, 'index parse error: %s' % path read_atom(buf) root = Entry(**read_dict(buf)) return root def write_index(gpg, keyid, path, root): buf = StringIO() buf.write(magic) header = '' write_atom(buf, header) write_dict(buf, root) encrypt(gpg, keyid, path, buf.getvalue()) def write_dict(fd, dct): # breadth-first children = [] buf = StringIO() if not isinstance(dct, dict): dct = dct.__dict__ for key in dct: write_atom(buf, key.encode('utf8')) val = dct[key] if isinstance(val, dict): buf.write('D') children.append(val) elif isinstance(val, Entry): buf.write('E') children.append(val) elif isinstance(val, int): buf.write('I') buf.write(struct.pack(' %s %s %s', op, path, atxt) ret = '[Unhandled Exception]' try: ret = getattr(self, op)(path, *args) return ret except OSError, e: ret = str(e) raise finally: rtxt = repr(ret) if op=='read': rtxt = rtxt[:10] self.log.debug('<- %s %s', op, rtxt) #class GpgFs(LoggingMixIn, fuse.Operations): class GpgFs(fuse.Operations): def __init__(self, encroot, keyid): ''' :param encroot: Encrypted root directory ''' self.encroot = encroot self.keyid = keyid #self.cache = cache self.gpg = gnupg.GPG() self.index_path = encroot + '/index' if os.path.exists(self.index_path): self.root = read_index(self.gpg, self.index_path) else: self.root = Entry(type=ENT_DIR, children={}, st_mtime=int(time.time()), st_ctime=int(time.time())) self._write_index() logging.info('created %s', self.index_path) self.fd = 0 self.write_path = None self.write_buf = [] self.write_pos = 0 self.write_dirty = False def _write_index(self): write_index(self.gpg, self.keyid, self.index_path, self.root) def _find(self, path, parent=False): assert path.startswith('/') if path == '/': return self.root node = self.root path = path[1:].split('/') if parent: path = path[:-1] for name in path: node = node.children[name] return node def chmod(self, path, mode): raise fuse.FuseOSError(ENOSYS) def chown(self, path, uid, gid): raise fuse.FuseOSError(ENOSYS) def create(self, path, mode): encpath = hexlify(os.urandom(20)) encpath = encpath[:2] + '/' + encpath[2:] dir = self._find(path, parent=True) path = path.rsplit('/', 1)[-1] assert path not in dir.children dir.children[path] = Entry(type=ENT_FILE, path=encpath, st_size=0) logging.debug('new path %s => %s', path, encpath) encdir = self.encroot + '/' + encpath[:2] if not os.path.exists(encdir): os.mkdir(encdir, 0755) with file(self.encroot + '/' + encpath, 'w'): pass self._write_index() self.fd += 1 return self.fd def flush(self, path, fh): logging.debug('flush %s', path) if not (path is not None and path == self.write_path and self.write_dirty): return 0 ent = self._find(path) encpath = self.encroot + '/' + ent.path buf = ''.join(self.write_buf) encrypt(self.gpg, self.keyid, encpath, buf) ent.st_size = len(buf) self._write_index() self.write_buf = [buf] self.write_dirty = False return 0 def getattr(self, path, fh = None): try: ent = self._find(path) except KeyError: raise fuse.FuseOSError(ENOENT) if ent.type == ENT_DIR: return dict(st_mode = stat.S_IFDIR | 0755, st_size = 0, st_ctime = ent.st_ctime, st_mtime = ent.st_mtime, st_atime = 0, st_nlink = 3) encpath = self.encroot + '/' + ent.path s = os.stat(encpath) return dict(st_mode = s.st_mode, st_size = ent.st_size, st_atime = s.st_atime, st_mtime = s.st_mtime, st_ctime = s.st_ctime, st_nlink = s.st_nlink) def getxattr(self, path, name, position = 0): raise fuse.FuseOSError(ENODATA) # ENOATTR def listxattr(self, path): return [] def mkdir(self, path, mode): dir = self._find(path, parent=True) path = path.rsplit('/', 1)[-1] assert path not in dir.children dir.children[path] = Entry(type=ENT_DIR, children={}, st_mtime=int(time.time()), st_ctime=int(time.time())) self._write_index() def open(self, path, flags): return 0 def read(self, path, size, offset, fh): ent = self._find(path) assert ent.type == ENT_FILE encpath = self.encroot + '/' + ent.path data = decrypt(self.gpg, encpath) return data[offset:offset + size] def readdir(self, path, fh): dir = self._find(path) return ['.', '..'] + list(dir.children) def readlink(self, path): raise fuse.FuseOSError(ENOSYS) def removexattr(self, path, name): raise fuse.FuseOSError(ENOSYS) def rename(self, old, new): raise fuse.FuseOSError(ENOSYS) def rmdir(self, path): raise fuse.FuseOSError(ENOSYS) def setxattr(self, path, name, value, options, position = 0): raise fuse.FuseOSError(ENOSYS) def statfs(self, path): raise fuse.FuseOSError(ENOSYS) def symlink(self, target, source): raise fuse.FuseOSError(ENOSYS) def truncate(self, path, length, fh = None): raise fuse.FuseOSError(ENOSYS) def unlink(self, path, times = None): raise fuse.FuseOSError(ENOSYS) def utimens(self, path, times = None): raise fuse.FuseOSError(ENOSYS) def write(self, path, data, offset, fh): ent = self._find(path) encpath = self.encroot + '/' + ent.path if path != self.write_path: self.flush(self.write_path, None) buf = decrypt(self.gpg, encpath) self.write_buf = [buf] self.write_pos = len(buf) self.write_path = path if offset == self.write_pos: self.write_buf.append(data) self.write_pos += len(data) else: buf = ''.join(self.write_buf) buf = buf[:offset] + data + buf[offset + len(data):] self.write_buf = [buf] self.write_pos = len(buf) self.write_dirty = True return len(data) if __name__ == '__main__': if len(sys.argv) != 4: sys.stderr.write('Usage: gpgfs \n') sys.exit(1) logging.basicConfig(level=logging.DEBUG) logging.getLogger('gnupg').setLevel(logging.INFO) fs = GpgFs(sys.argv[2], sys.argv[1]) fuse.FUSE(fs, sys.argv[3], foreground=True)