#! /usr/bin/python2.2 import catacomb as C import gdbm, struct from sys import argv, exit, stdin, stdout, stderr from getopt import getopt, GetoptError from os import environ from fnmatch import fnmatch file = '%s/.pwsafe' % environ['HOME'] class DecryptError (Exception): pass class Crypto (object): def __init__(me, c, h, m, ck, mk): me.c = c(ck) me.m = m(mk) me.h = h def encrypt(me, pt): if me.c.__class__.blksz: iv = C.rand.block(me.c.__class__.blksz) me.c.setiv(iv) else: iv = '' y = iv + me.c.encrypt(pt) t = me.m().hash(y).done() return t + y def decrypt(me, ct): t = ct[:me.m.__class__.tagsz] y = ct[me.m.__class__.tagsz:] if t != me.m().hash(y).done(): raise DecryptError iv = y[:me.c.__class__.blksz] if me.c.__class__.blksz: me.c.setiv(iv) return me.c.decrypt(y[me.c.__class__.blksz:]) class PPK (Crypto): def __init__(me, pp, c, h, m, salt = None): if not salt: salt = C.rand.block(h.hashsz) tag = '%s\0%s' % (pp, salt) Crypto.__init__(me, c, h, m, h().hash('cipher:' + tag).done(), h().hash('mac:' + tag).done()) me.salt = salt class Buffer (object): def __init__(me, s): me.str = s me.i = 0 def get(me, n): i = me.i if n + i > len(me.str): raise IndexError, 'buffer underflow' me.i += n return me.str[i:i + n] def getbyte(me): return ord(me.get(1)) def unpack(me, fmt): return struct.unpack(fmt, me.get(struct.calcsize(fmt))) def getstring(me): return me.get(me.unpack('>H')[0]) def checkend(me): if me.i != len(me.str): raise ValueError, 'junk at end of buffer' def wrapstr(s): return struct.pack('>H', len(s)) + s class PWIter (object): def __init__(me, pw): me.pw = pw me.k = me.pw.db.firstkey() def next(me): k = me.k while True: if k is None: raise StopIteration if k[0] == '$': break k = me.pw.db.nextkey(k) me.k = me.pw.db.nextkey(k) return me.pw.unpack(me.pw.db[k])[0] class PW (object): def __init__(me, file, mode = 'r'): me.db = gdbm.open(file, mode) c = C.gcciphers[me.db['cipher']] h = C.gchashes[me.db['hash']] m = C.gcmacs[me.db['mac']] tag = me.db['tag'] ppk = PPK(C.ppread(tag), c, h, m, me.db['salt']) try: buf = Buffer(ppk.decrypt(me.db['key'])) except DecryptError: C.ppcancel(tag) raise me.ck = buf.getstring() me.mk = buf.getstring() buf.checkend() me.k = Crypto(c, h, m, me.ck, me.mk) me.magic = me.k.decrypt(me.db['magic']) def keyxform(me, key): return '$' + me.k.h().hash(me.magic).hash(key).done() def changepp(me): tag = me.db['tag'] C.ppcancel(tag) ppk = PPK(C.ppread(tag, C.PMODE_VERIFY), me.k.c.__class__, me.k.h, me.k.m.__class__) me.db['key'] = ppk.encrypt(wrapstr(me.ck) + wrapstr(me.mk)) me.db['salt'] = ppk.salt def pack(me, key, value): w = wrapstr(key) + wrapstr(value) pl = (len(w) + 255) & ~255 w += '\0' * (pl - len(w)) return me.k.encrypt(w) def unpack(me, p): buf = Buffer(me.k.decrypt(p)) key = buf.getstring() value = buf.getstring() return key, value def __getitem__(me, key): return me.unpack(me.db[me.keyxform(key)])[1] def __setitem__(me, key, value): me.db[me.keyxform(key)] = me.pack(key, value) def __delitem__(me, key): del me.db[me.keyxform(key)] def __iter__(me): return PWIter(me) def cmd_create(av): cipher = 'blowfish-cbc' hash = 'rmd160' mac = None try: opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash=']) except GetoptError: return 1 for o, a in opts: if o in ('-c', '--cipher'): cipher = a elif o in ('-m', '--mac'): mac = a elif o in ('-h', '--hash'): hash = a else: raise 'Barf!' if len(args) > 2: return 1 if len(args) >= 1: tag = args[0] else: tag = 'pwsafe' db = gdbm.open(file, 'n', 0600) pp = C.ppread(tag, C.PMODE_VERIFY) if not mac: mac = hash + '-hmac' c = C.gcciphers[cipher] h = C.gchashes[hash] m = C.gcmacs[mac] ppk = PPK(pp, c, h, m) ck = C.rand.block(c.keysz.default) mk = C.rand.block(m.keysz.default) k = Crypto(c, h, m, ck, mk) db['tag'] = tag db['salt'] = ppk.salt db['cipher'] = cipher db['hash'] = hash db['mac'] = mac db['key'] = ppk.encrypt(wrapstr(ck) + wrapstr(mk)) db['magic'] = k.encrypt(C.rand.block(h.hashsz)) def cmd_changepp(av): if len(av) != 0: return 1 pw = PW(file, 'w') pw.changepp() def cmd_find(av): if len(av) != 1: return 1 pw = PW(file) print pw[av[0]] def cmd_store(av): if len(av) < 1 or len(av) > 2: return 1 tag = av[0] if len(av) < 2: pp = C.getpass("Enter passphrase `%s': " % tag) vpp = C.getpass("Confirm passphrase `%s': " % tag) if pp != vpp: raise ValueError, "passphrases don't match" elif av[1] == '-': pp = stdin.readline() else: pp = av[1] pw = PW(file, 'w') pw[av[0]] = pp def cmd_copy(av): if len(av) < 1 or len(av) > 2: return 1 pw_in = PW(file) pw_out = PW(av[0], 'w') if len(av) >= 3: pat = av[1] else: pat = None for k in pw_in: if pat is None or fnmatch(k, pat): pw_out[k] = pw_in[k] def cmd_list(av): if len(av) > 1: return 1 pw = PW(file) if len(av) >= 1: pat = av[0] else: pat = None for k in pw: if pat is None or fnmatch(k, pat): print k def cmd_topixie(av): if len(av) < 1 or len(av) > 2: return 1 pw = PW(file) tag = av[0] if len(av) >= 2: pptag = av[1] else: pptag = av[0] C.Pixie().set(pptag, pw[tag]) def asciip(s): for ch in s: if ch < ' ' or ch > '~': return False return True def present(s): if asciip(s): return s return C.ByteString(s) def cmd_dump(av): db = gdbm.open(file, 'r') k = db.firstkey() while True: if k is None: break print '%r: %r' % (present(k), present(db[k])) k = db.nextkey(k) commands = { 'create': [cmd_create, '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'], 'find' : [cmd_find, 'LABEL'], 'store' : [cmd_store, 'LABEL [VALUE]'], 'list' : [cmd_list, '[GLOB-PATTERN]'], 'changepp' : [cmd_changepp, ''], 'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'], 'to-pixie' : [cmd_topixie, 'TAG [PIXIE-TAG]'], 'dump' : [cmd_dump, '']} def version(): print 'pwsafe 1.0.0' def usage(fp): print >>fp, 'Usage: pwsafe COMMAND [ARGS...]' def help(): version() print usage(stdout) print ''' Maintains passwords or other short secrets securely. Options: -h, --help Show this help text. -v, --version Show program version number. -u, --usage Show short usage message. Commands provided: ''' for c in commands: print '%s %s' % (c, commands[c][1]) try: opts, argv = getopt(argv[1:], 'hvuf:', ['help', 'version', 'usage', 'file=']) except GetoptError: usage(stderr) exit(1) for o, a in opts: if o in ('-h', '--help'): help() exit(0) elif o in ('-v', '--version'): version() exit(0) elif o in ('-u', '--usage'): usage(stdout) exit(0) elif o in ('-f', '--file'): file = a else: raise 'Barf!' if len(argv) < 1: usage(stderr) exit(1) if argv[0] in commands: c = argv[0] argv = argv[1:] else: c = 'find' if commands[c][0](argv): print >>stderr, 'Usage: pwsafe %s %s' % (c, commands[c][1]) exit(1)