catacomb/pwsafe.py: Hack around the difference in octal literal syntax.
[catacomb-python] / pwsafe
diff --git a/pwsafe b/pwsafe
index 043db43..e622e53 100644 (file)
--- a/pwsafe
+++ b/pwsafe
@@ -27,7 +27,8 @@
 ###---------------------------------------------------------------------------
 ### Imported modules.
 
 ###---------------------------------------------------------------------------
 ### Imported modules.
 
-import gdbm as G
+from __future__ import with_statement
+
 from os import environ
 from sys import argv, exit, stdin, stdout, stderr
 from getopt import getopt, GetoptError
 from os import environ
 from sys import argv, exit, stdin, stdout, stderr
 from getopt import getopt, GetoptError
@@ -38,6 +39,14 @@ import catacomb as C
 from catacomb.pwsafe import *
 
 ###--------------------------------------------------------------------------
 from catacomb.pwsafe import *
 
 ###--------------------------------------------------------------------------
+### Python version portability.
+
+def _text(bin): return bin
+def _bin(text): return text
+
+def _excval(): return SYS.exc_info()[1]
+
+###--------------------------------------------------------------------------
 ### Utilities.
 
 ## The program name.
 ### Utilities.
 
 ## The program name.
@@ -45,162 +54,130 @@ prog = re.sub(r'^.*[/\\]', '', argv[0])
 
 def moan(msg):
   """Issue a warning message MSG."""
 
 def moan(msg):
   """Issue a warning message MSG."""
-  print >>stderr, '%s: %s' % (prog, msg)
+  stderr.write('%s: %s\n' % (prog, msg))
 
 def die(msg):
   """Report MSG as a fatal error, and exit."""
   moan(msg)
   exit(1)
 
 
 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.
 ###--------------------------------------------------------------------------
 ### Subcommand implementations.
 
 def cmd_create(av):
 
   ## Default crypto-primitive selections.
-  cipher = 'blowfish-cbc'
-  hash = 'rmd160'
+  cipher = 'rijndael-cbc'
+  hash = 'sha256'
   mac = None
 
   ## Parse the options.
   try:
   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
   except GetoptError:
     return 1
+  dbty = 'flat'
   for o, a in opts:
   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'
+    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 Exception("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'
 
   ## Set up the database.
   if mac is None: mac = hash + '-hmac'
-  PW.create(file, C.gcciphers[cipher], C.gchashes[hash], C.gcmacs[mac], tag)
+  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):
 
 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):
 
 def cmd_find(av):
-  if len(av) != 1:
-    return 1
-  pw = PW(file)
-  try:
-    print pw[av[0]]
-  except KeyError, exc:
-    die('Password `%s\' not found.' % exc.args[0])
+  if len(av) != 1: return 1
+  with PW(file) as pw:
+    try: print(_text(pw[_bin(av[0])]))
+    except KeyError: die("Password `%s' not found" % _excval().args[0])
 
 def cmd_store(av):
 
 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]
   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]] = chomp(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 = _bin(stdin.readline().rstrip('\n'))
+    else:
+      pp = _bin(av[1])
+    pw[_bin(av[0])] = pp
 
 def cmd_copy(av):
 
 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:
+        ktext = _text(k)
+        if pat is None or fnmatch(ktext, pat): pw_out[k] = pw_in[k]
 
 def cmd_list(av):
 
 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:
+      ktext = _text(k)
+      if pat is None or fnmatch(ktext, pat): print(ktext)
 
 def cmd_topixie(av):
 
 def cmd_topixie(av):
-  if len(av) > 2:
-    return 1
-  pw = PW(file)
-  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]
+  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[_bin(tag)])
     else:
     else:
-      pptag = av[0]
-    pix.set(pptag, pw[tag])
+      tag = _bin(av[0])
+      if len(av) >= 2: pptag = av[1]
+      else: pptag = av[0]
+      pix.set(pptag, pw[tag])
 
 def cmd_del(av):
 
 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 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 = _bin(av[0])
+    try: del pw[tag]
+    except KeyError: die("Password `%s' not found" % _excval().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 Exception("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,
 
 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]'],
              'find' : [cmd_find, 'LABEL'],
              'store' : [cmd_store, 'LABEL [VALUE]'],
              'list' : [cmd_list, '[GLOB-PATTERN]'],
@@ -208,22 +185,24 @@ commands = { 'create': [cmd_create,
              'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
              'to-pixie' : [cmd_topixie, '[TAG [PIXIE-TAG]]'],
              'delete' : [cmd_del, 'TAG'],
              'copy' : [cmd_copy, 'DEST-FILE [GLOB-PATTERN]'],
              '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():
 
 ###--------------------------------------------------------------------------
 ### Command-line handling and dispatch.
 
 def version():
-  print '%s 1.0.0' % prog
+  print('%s 1.0.0' % prog)
+  print('Backend types: %s' %
+        ' '.join([c.NAME for c in StorageBackend.classes()]))
 
 def usage(fp):
 
 def usage(fp):
-  print >>fp, 'Usage: %s COMMAND [ARGS...]' % prog
+  fp.write('Usage: %s COMMAND [ARGS...]\n' % prog)
 
 def help():
   version()
 
 def help():
   version()
-  print
+  print('')
   usage(stdout)
   usage(stdout)
-  print '''
+  print('''
 Maintains passwords or other short secrets securely.
 
 Options:
 Maintains passwords or other short secrets securely.
 
 Options:
@@ -235,9 +214,9 @@ Options:
 -f, --file=FILE                Where to find the password-safe file.
 
 Commands provided:
 -f, --file=FILE                Where to find the password-safe file.
 
 Commands provided:
-'''
-  for c in commands:
-    print '%s %s' % (c, commands[c][1])
+''')
+  for c in sorted(commands):
+    print('%s %s' % (c, commands[c][1]))
 
 ## Choose a default database file.
 if 'PWSAFE' in environ:
 
 ## Choose a default database file.
 if 'PWSAFE' in environ:
@@ -265,7 +244,7 @@ for o, a in opts:
   elif o in ('-f', '--file'):
     file = a
   else:
   elif o in ('-f', '--file'):
     file = a
   else:
-    raise 'Barf!'
+    raise Exception("barf")
 if len(argv) < 1:
   usage(stderr)
   exit(1)
 if len(argv) < 1:
   usage(stderr)
   exit(1)
@@ -278,7 +257,7 @@ else:
   c = 'find'
 try:
   if commands[c][0](argv):
   c = 'find'
 try:
   if commands[c][0](argv):
-    print >>stderr, 'Usage: %s %s %s' % (prog, c, commands[c][1])
+    stderr.write('Usage: %s %s %s\n' % (prog, c, commands[c][1]))
     exit(1)
 except DecryptError:
   die("decryption failure")
     exit(1)
 except DecryptError:
   die("decryption failure")