X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/catacomb-python/blobdiff_plain/3aa33042dc760937bb9da54c09f7c668f00eb241..278e43d0c27875a1355ebaf3bef6d0f5df739626:/pwsafe diff --git a/pwsafe b/pwsafe old mode 100755 new mode 100644 index 6b36bf4..c12f856 --- a/pwsafe +++ b/pwsafe @@ -1,282 +1,197 @@ -#! /usr/bin/python2.2 -# -*-python-*- +#! /usr/bin/python +### -*-python-*- +### +### Tool for maintaining a secure-ish password database +### +### (c) 2005 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to Catacomb. +### +### Catacomb/Python 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 2 of the License, or +### (at your option) any later version. +### +### Catacomb/Python 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 Catacomb/Python; if not, write to the Free Software Foundation, +### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +###--------------------------------------------------------------------------- +### Imported modules. + +from __future__ import with_statement -import catacomb as C -import gdbm, struct +from os import environ from sys import argv, exit, stdin, stdout, stderr from getopt import getopt, GetoptError -from os import environ from fnmatch import fnmatch +import re -if 'PWSAFE' in environ: - file = environ['PWSAFE'] -else: - file = '%s/.pwsafe' % environ['HOME'] +import catacomb as C +from catacomb.pwsafe import * -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) +###-------------------------------------------------------------------------- +### Utilities. + +## The program name. +prog = re.sub(r'^.*[/\\]', '', argv[0]) + +def moan(msg): + """Issue a warning message MSG.""" + print >>stderr, '%s: %s' % (prog, msg) + +def die(msg): + """Report MSG as a fatal error, and exit.""" + moan(msg) + exit(1) + +###-------------------------------------------------------------------------- +### Subcommand implementations. def cmd_create(av): + + ## Default crypto-primitive selections. cipher = 'blowfish-cbc' hash = 'rmd160' mac = None + + ## Parse the options. try: - opts, args = getopt(av, 'c:h:m:', ['cipher=', 'mac=', 'hash=']) + opts, args = getopt(av, 'c:d:h:m:', + ['cipher=', 'database=', 'mac=', 'hash=']) except GetoptError: return 1 + dbty = 'flat' 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)) + if o in ('-c', '--cipher'): cipher = a + elif o in ('-m', '--mac'): mac = a + elif o in ('-h', '--hash'): hash = a + elif o in ('-d', '--database'): dbty = a + else: raise 'Barf!' + if len(args) > 2: return 1 + if len(args) >= 1: tag = args[0] + else: tag = 'pwsafe' + + ## Set up the database. + if mac is None: mac = hash + '-hmac' + try: dbcls = StorageBackend.byname(dbty) + except KeyError: die("Unknown database backend `%s'" % dbty) + PW.create(dbcls, file, tag, + C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac]) def cmd_changepp(av): - if len(av) != 0: - return 1 - pw = PW(file, 'w') - pw.changepp() + if len(av) != 0: return 1 + with PW(file, writep = True) as pw: pw.changepp() def cmd_find(av): - if len(av) != 1: - return 1 - pw = PW(file) - print pw[av[0]] + if len(av) != 1: return 1 + with PW(file) as pw: + try: print pw[av[0]] + except KeyError, exc: die("Password `%s' not found" % exc.args[0]) def cmd_store(av): - if len(av) < 1 or len(av) > 2: - return 1 + 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 + with PW(file, writep = True) as pw: + if len(av) < 2: + pp = C.getpass("Enter passphrase `%s': " % tag) + vpp = C.getpass("Confirm passphrase `%s': " % tag) + if pp != vpp: die("passphrases don't match") + elif av[1] == '-': + pp = stdin.readline().rstrip('\n') + else: + pp = av[1] + 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] + if len(av) < 1 or len(av) > 2: return 1 + with PW(file) as pw_in: + with PW(av[0], writep = True) as pw_out: + 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 + if len(av) > 1: return 1 + with PW(file) as pw: + 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]) + if len(av) > 2: return 1 + with PW(file) as pw: + pix = C.Pixie() + if len(av) == 0: + for tag in pw: pix.set(tag, pw[tag]) + else: + tag = av[0] + if len(av) >= 2: pptag = av[1] + else: pptag = av[0] + pix.set(pptag, pw[tag]) def cmd_del(av): - if len(av) != 1: - return 1 - pw = PW(file, 'w') - tag = av[0] - del 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) + if len(av) != 1: return 1 + with PW(file, writep = True) as pw: + tag = av[0] + try: del pw[tag] + except KeyError, exc: die("Password `%s' not found" % exc.args[0]) + +def cmd_xfer(av): + + ## Parse the command line. + try: opts, args = getopt(av, 'd:', ['database=']) + except GetoptError: return 1 + dbty = 'flat' + for o, a in opts: + if o in ('-d', '--database'): dbty = a + else: raise 'Barf!' + if len(args) != 1: return 1 + try: dbcls = StorageBackend.byname(dbty) + except KeyError: die("Unknown database backend `%s'" % dbty) + + ## Create the target database. + with StorageBackend.open(file) as db_in: + with dbcls.create(args[0]) as db_out: + for k, v in db_in.iter_meta(): db_out.put_meta(k, v) + for k, v in db_in.iter_passwds(): db_out.put_passwd(k, v) commands = { 'create': [cmd_create, - '[-c CIPHER] [-h HASH] [-m MAC] [PP-TAG]'], + '[-c CIPHER] [-d DBTYPE] [-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]'], + 'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'], 'delete' : [cmd_del, 'TAG'], - 'dump' : [cmd_dump, '']} + 'xfer': [cmd_xfer, '[-d DBTYPE] DEST-FILE'] } + +###-------------------------------------------------------------------------- +### Command-line handling and dispatch. def version(): - print 'pwsafe 1.0.0' + print '%s 1.0.0' % prog + print 'Backend types: %s' % \ + ' '.join([c.NAME for c in StorageBackend.classes()]) + def usage(fp): - print >>fp, 'Usage: pwsafe COMMAND [ARGS...]' + print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog + def help(): version() print - usage(stdout) + usage(stdout) print ''' Maintains passwords or other short secrets securely. @@ -286,14 +201,22 @@ Options: -v, --version Show program version number. -u, --usage Show short usage message. +-f, --file=FILE Where to find the password-safe file. + Commands provided: ''' - for c in commands: + for c in sorted(commands): print '%s %s' % (c, commands[c][1]) +## Choose a default database file. +if 'PWSAFE' in environ: + file = environ['PWSAFE'] +else: + file = '%s/.pwsafe' % environ['HOME'] + +## Parse the command-line options. try: - opts, argv = getopt(argv[1:], - 'hvuf:', + opts, argv = getopt(argv[1:], 'hvuf:', ['help', 'version', 'usage', 'file=']) except GetoptError: usage(stderr) @@ -316,11 +239,17 @@ if len(argv) < 1: usage(stderr) exit(1) +## Dispatch to a command handler. 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) +try: + if commands[c][0](argv): + print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1]) + exit(1) +except DecryptError: + die("decryption failure") + +###----- That's all, folks --------------------------------------------------