-#! /usr/bin/python2.2
+#! /usr/bin/python
+### -*-python-*-
+###
+### Tool for maintaining a secure-ish password database
+###
+### (c) 2005 Straylight/Edgeware
+###
-import catacomb as C
-import gdbm, struct
+###----- 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.
+
+import gdbm as G
+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
-file = '%s/.pwsafe' % environ['HOME']
+import catacomb as C
+from catacomb.pwsafe import *
-class DecryptError (Exception):
- pass
+###--------------------------------------------------------------------------
+### Utilities.
-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)
+## 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)
+
+def chomp(pp):
+ """Return the string PP, without its trailing newline if it has one."""
+ if len(pp) > 0 and pp[-1] == '\n':
+ pp = pp[:-1]
+ return pp
+
+def asciip(s):
+ """Answer whether all of the characters of S are plain ASCII."""
+ for ch in s:
+ if ch < ' ' or ch > '~': return False
+ return True
+
+def present(s):
+ """
+ Return a presentation form of the string S.
+
+ If S is plain ASCII, then return S unchanged; otherwise return it as one of
+ Catacomb's ByteString objects.
+ """
+ if asciip(s): return s
+ return C.ByteString(s)
+
+###--------------------------------------------------------------------------
+### 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='])
except GetoptError:
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))
+
+ ## Set up the database.
+ if mac is None: mac = hash + '-hmac'
+ PW.create(file, C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac], tag)
def cmd_changepp(av):
if len(av) != 0:
if len(av) != 1:
return 1
pw = PW(file)
- print pw[av[0]]
+ 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:
else:
pp = av[1]
pw = PW(file, 'w')
- pw[av[0]] = pp
+ pw[av[0]] = chomp(pp)
def cmd_copy(av):
if len(av) < 1 or len(av) > 2:
print k
def cmd_topixie(av):
- if len(av) < 1 or len(av) > 2:
+ if len(av) > 2:
return 1
pw = PW(file)
- tag = av[0]
- if len(av) >= 2:
- pptag = av[1]
+ pix = C.Pixie()
+ if len(av) == 0:
+ for tag in pw:
+ pix.set(tag, pw[tag])
else:
- pptag = av[0]
- C.Pixie().set(pptag, pw[tag])
+ 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]
+ try:
+ del pw[tag]
+ except KeyError, exc:
+ die('Password `%s\' not found.' % exc.args[0])
-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()
'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, '']}
+###--------------------------------------------------------------------------
+### Command-line handling and dispatch.
+
def version():
- print 'pwsafe 1.0.0'
+ print '%s 1.0.0' % prog
+
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.
-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:
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)
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])
+ print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
exit(1)
+
+###----- That's all, folks --------------------------------------------------