chiark / gitweb /
New option to list a user's (or all) extant tokens.
[cryptomail] / bin / cryptomail
index 27beea296f557688db5dc7921d69bd18ab944bb6..9559acc49a194e1e96f29415f66fc8b17e465023 100755 (executable)
@@ -221,12 +221,22 @@ class CMDB (AttrDB):
                            (SELECT attrset FROM expiry WHERE time < ?)''',
                 [now])
     cur.execute('DELETE FROM expiry WHERE time < ?', [now])
+    cur.execute('''DELETE FROM expiry WHERE attrset IN
+                           (SELECT attrset
+                            FROM expiry LEFT JOIN attrset
+                            ON expiry.attrset = attrset.id
+                            WHERE attrset.id ISNULL)''')
     AttrDB.cleanup(me)
-  def expiredp(me, id):
+  def expiry(me, id):
     for t, in me.select('SELECT time FROM expiry WHERE attrset = ?', [id]):
-      if t < time_format():
-        return True
-    return False
+      return t
+    return None
+  def expiredp(me, id):
+    t = me.expiry(id)
+    if t is not None and t < time_format():
+      return True
+    else:
+      return False
   def setexpire(me, id, when):
     if when != C.KEXP_FOREVER:
       cur = me.db.cursor()
@@ -369,6 +379,7 @@ def check(db, id, sender = None, msgfile = None):
 keyfile = 'db/keyring'
 tag = 'cryptomail'
 dbfile = 'db/cryptomail.db'
+user = None
 commands = {}
 
 def timecmp(x, y):
@@ -381,10 +392,14 @@ def timecmp(x, y):
   else:
     return cmp(x, y)
 
+def token(c, id):
+  return M.base32_encode(c.encrypt(id)).strip('=').lower()
+
 def cmd_generate(argv):
   try:
-    opts, argv = getopt(argv, 't:c:f:',
-                        ['expire=', 'timeout=', 'constraint=', 'format='])
+    opts, argv = getopt(argv, 't:c:f:i:',
+                        ['expire=', 'timeout=', 'constraint=',
+                         'info=', 'format='])
   except GetoptError:
     return 1
   kr = C.KeyFile(keyfile, C.KOPEN_WRITE)
@@ -406,6 +421,8 @@ def cmd_generate(argv):
       map.setdefault(c, []).append(v)
     elif o in ('-f', '--format'):
       format = a
+    elif o in ('-i', '--info'):
+      map['info'] = [a]
     else:
       raise 'Barf!'
   if timecmp(expwhen, k.deltime) > 0:
@@ -416,10 +433,10 @@ def cmd_generate(argv):
   a = AttrMultiMap(db)
   a.update(map)
   a['addr'] = [addr]
-  c = Crypto(k).encrypt(a.id)
+  if user is not None:
+    a['user'] = [user]
   db.setexpire(a.id, expwhen)
-  print format.replace('%', M.base32_encode(Crypto(k).encrypt(a.id)).
-                       strip('=').lower())
+  print format.replace('%', token(Crypto(k), a.id))
   db.commit()
   kr.save()
 commands['generate'] = \
@@ -449,18 +466,22 @@ commands['initdb'] = \
   (cmd_initdb, '', """
 Initialize an attribute database.""")
 
+def getid(local):
+  k = C.KeyFile(keyfile, C.KOPEN_READ)[tag]
+  id = Crypto(k).decrypt(M.base32_decode(local))
+  if id is None:
+    raise Reject, 'decrypt failed'
+  return id
+
 def cmd_addrcheck(argv):
   try:
     opts, argv = getopt(argv, '', [])
   except GetoptError:
     return 1
   local, sender = (lambda addr, sender = None, *hunoz: (addr, sender))(*argv)
-  k = C.KeyFile(keyfile, C.KOPEN_READ)[tag]
   db = CMDB(dbfile)
   try:
-    id = Crypto(k).decrypt(M.base32_decode(local))
-    if id is None:
-      raise Reject, 'decrypt failed'
+    id = getid(local)
     addr = check(db, id, sender)
   except Reject, msg:
     print '-%s' % msg
@@ -476,11 +497,12 @@ def cmd_fwaddr(argv):
     opts, argv = getopt(argv, '', [])
   except GetoptError:
     return 1
-  local, sender = (lambda addr, sender = None, *hunoz: (addr, sender))(*argv)
-  k = C.KeyFile(keyfile, C.KOPEN_READ)[tag]
+  if len(argv) not in (1, 2):
+    return 1
+  local, sender = (lambda addr, sender = None: (addr, sender))(*argv)
   db = CMDB(dbfile)
   try:
-    id = Crypto(k).decrypt(M.base32_decode(local))
+    id = getid(local)
     if id is None:
       raise Reject, 'decrypt failed'
     addr = check(db, id, sender, stdin)
@@ -490,11 +512,103 @@ def cmd_fwaddr(argv):
   stdin.seek(0)
   print addr
 commands['fwaddr'] = \
-  (cmd_fwaddr, 'LOCAL [SENDER [IGNORED ...]]', """
+  (cmd_fwaddr, 'LOCAL [SENDER]', """
 Check address token LOCAL.  On failure, report reason to stderr and exit
 111.  On success, write forwarding address to stdout and exit 0.  Expects
 the message on standard input, as a seekable file.""")
 
+ignore = {'user': 1, 'addr': 1}
+def show(db, a):
+  keys = a.keys()
+  keys.sort()
+  for k in keys:
+    if k in ignore:
+      continue
+    for v in a[k]:
+      print '\t%s: %s' % (k, v)
+  expwhen = db.expiry(a.id)
+  if expwhen:
+    print '\texpires: %s' % expwhen
+  else:
+    print '\tno-expiry'
+
+def cmd_info(argv):
+  try:
+    opts, argv = getopt(argv, '', [])
+  except GetoptError:
+    return 1
+  if len(argv) != 1:
+    return 1
+  local = argv[0]
+  db = CMDB(dbfile)
+  try:
+    id = getid(local)
+    a = AttrMultiMap(db, id)
+    if user is not None and user != a.get('user', [None])[0]:
+      raise Reject, 'not your token'
+    if 'addr' not in a:
+      die('unknown token (expired?)')
+    print 'addr: %s' % a['addr'][0]
+    show(db, a)
+  except Reject, msg:
+    die('invalid token')
+commands['info'] = \
+  (cmd_info, 'LOCAL', """
+Exaimne the address token LOCAL, and print information about it to standard
+output.""")
+
+def cmd_revoke(argv):
+  try:
+    opts, argv = getopt(argv, '', [])
+  except GetoptError:
+    return 1
+  if len(argv) != 1:
+    return 1
+  local = argv[0]
+  db = CMDB(dbfile)
+  try:
+    id = getid(local)
+    a = AttrMultiMap(db, id)
+    if user is not None and user != a.get('user', [None])[0]:
+      raise Reject, 'not your token'
+    if 'addr' not in a:
+      die('unknown token (expired?)')
+    a.clear()
+    db.cleanup()
+    db.commit()
+  except Reject, msg:
+    die('invalid token')
+commands['revoke'] = \
+  (cmd_revoke, 'LOCAL', """
+Revoke the token LOCAL.""")
+
+def cmd_list(argv):
+  try:
+    opts, argv = getopt(argv, '', [])
+  except GetoptError:
+    return 1
+  if argv:
+    return 1
+  c = Crypto(C.KeyFile(keyfile, C.KOPEN_READ)[tag])
+  db = CMDB(dbfile)
+  if not user:
+    gen = db.select('SELECT DISTINCT id FROM attrset')
+  else:
+    gen = db.select('''SELECT DISTINCT attrset.id
+                               FROM attrset, attr ON attrset.attr = attr.id
+                               WHERE attr.key = 'user' AND attr.value = ?''',
+                    [user])
+  for id, in gen:
+    a = AttrMultiMap(db, id)    
+    print '%s %s%s' % \
+          (token(c, id),
+           a.get('addr', '<no-address>')[0],
+           (not user and ' [%s]' % a.get('user', ['<no-user>'])[0] or ''))
+    show(db, a)
+commands['list'] = \
+  (cmd_list, '', """
+List the user's tokens and information about them.""")
+
 def cmd_cleanup(argv):
   try:
     opts, argv = getopt(argv, '', [])
@@ -544,6 +658,7 @@ Global options:
   -d, --database=FILE          Use FILE as the attribute database.
   -k, --keyring=KEYRING                Use KEYRING as the keyring.
   -t, --tag=TAG                        Use TAG as the key tag.
+  -U, --user=USER              Claim to be USER.
 """
     cmds = commands.keys()
     cmds.sort()
@@ -566,12 +681,12 @@ def help():
   cmd_help()  
 
 def main():
-  global argv
+  global argv, user, keyfile, dbfile, tag
   try:
     opts, argv = getopt(argv[1:],
-                        'hvud:k:t:',
+                        'hvud:k:t:U:',
                         ['help', 'version', 'usage',
-                         'database=', 'keyring=', 'tag='])
+                         'database=', 'keyring=', 'tag=', 'user='])
   except GetoptError:
     usage(stderr)
     exit(111)
@@ -591,6 +706,8 @@ def main():
       keyfile = a
     elif o in ('-t', '--tag'):
       tag = a
+    elif o in ('-U', '--user'):
+      user = a
     else:
       raise 'Barf!'
   if len(argv) < 1: