Commit | Line | Data |
---|---|---|
71074336 MW |
1 | #! /usr/bin/python |
2 | ### -*-python-*- | |
3 | ### | |
4 | ### Encrypted email address handling | |
5 | ### | |
6 | ### (c) 2006 Mark Wooding | |
7 | ### | |
8 | ||
9 | ###----- Licensing notice --------------------------------------------------- | |
10 | ### | |
11 | ### This program is free software; you can redistribute it and/or modify | |
12 | ### it under the terms of the GNU General Public License as published by | |
13 | ### the Free Software Foundation; either version 2 of the License, or | |
14 | ### (at your option) any later version. | |
15 | ### | |
16 | ### This program is distributed in the hope that it will be useful, | |
17 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 | ### GNU General Public License for more details. | |
20 | ### | |
21 | ### You should have received a copy of the GNU General Public License | |
22 | ### along with this program; if not, write to the Free Software Foundation, | |
23 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
24 | ||
25 | ###----- External dependencies ---------------------------------------------- | |
26 | ||
27 | import catacomb as C | |
28 | import mLib as M | |
29 | from pysqlite2 import dbapi2 as sqlite | |
30 | from UserDict import DictMixin | |
31 | from getopt import getopt, GetoptError | |
32 | from getdate import getdate | |
33 | from sys import stdin, stdout, stderr, exit, argv, exc_info | |
34 | from email import Parser as EP | |
35 | import os as OS | |
36 | import time as T | |
37 | import sre as RX | |
38 | import traceback as TB | |
39 | ||
40 | ###----- Database messing --------------------------------------------------- | |
41 | ||
42 | class AttrDB (object): | |
43 | def __init__(me, dbfile): | |
44 | me.db = sqlite.connect(dbfile) | |
45 | def setup(me): | |
46 | cur = me.db.cursor() | |
47 | cur.execute('''CREATE TABLE attr | |
48 | (id INTEGER PRIMARY KEY, | |
49 | key VARCHAR(64) NOT NULL, | |
50 | value VARCHAR(256) NOT NULL)''') | |
51 | cur.execute('''CREATE TABLE attrset | |
52 | (id INTEGER NOT NULL, | |
53 | attr INTEGER NOT NULL)''') | |
54 | cur.execute('''CREATE TABLE uniq | |
55 | (id INTEGER PRIMARY KEY AUTOINCREMENT, | |
56 | dummy INTEGER NOT NULL)''') | |
57 | cur.execute('CREATE UNIQUE INDEX attr_bykv ON attr (key, value)') | |
58 | cur.execute('CREATE INDEX attrset_byid ON attrset (id)') | |
59 | cur.execute('CREATE INDEX attrset_byattr ON attrset (attr)') | |
60 | cur.execute('CREATE UNIQUE INDEX attrset_all ON attrset (id, attr)') | |
61 | def uniqueid(me): | |
62 | cur = me.db.cursor() | |
63 | cur.execute('INSERT INTO uniq (dummy) VALUES (0)') | |
64 | cur.execute('SELECT MAX(id) FROM uniq') | |
65 | id = cur.fetchone()[0] | |
66 | cur.execute('DELETE FROM uniq') | |
67 | me.commit() | |
68 | return id | |
69 | def select(me, expr, args = [], cur = None): | |
70 | if cur is None: cur = me.db.cursor() | |
71 | cur.execute(expr, args) | |
72 | while True: | |
73 | r = cur.fetchone() | |
74 | if r is None: break | |
75 | yield r | |
76 | def cleanup(me): | |
77 | cur = me.db.cursor() | |
78 | cur.execute('''DELETE FROM attr WHERE id IN | |
79 | (SELECT attr.id | |
80 | FROM attr LEFT JOIN attrset | |
81 | ON attr.id = attrset.attr | |
82 | WHERE attrset.id ISNULL)''') | |
83 | def check(me, cleanp = False): | |
84 | toclean = {} | |
85 | cur = me.db.cursor() | |
86 | for set, attr in me.select('''SELECT attrset.id, attrset.attr | |
87 | FROM attrset LEFT JOIN attr | |
88 | ON attrset.attr = attr.id | |
89 | WHERE attr.id ISNULL''', | |
90 | [], cur): | |
91 | print "attrset %d missing attr %d" % (set, attr) | |
92 | toclean[set] = True | |
93 | if cleanp: | |
94 | for set in toclean: | |
95 | cur.execute('DELETE FROM attrset WHERE id = ?', [set]) | |
96 | me.cleanup() | |
97 | def commit(me): | |
98 | me.db.commit() | |
99 | ||
100 | class AttrSet (object): | |
101 | def __init__(me, db, id = None): | |
102 | if id is None: id = db.uniqueid() | |
103 | me.id = id | |
104 | me.db = db | |
105 | def insert(me, key, value): | |
106 | cur = me.db.db.cursor() | |
107 | try: | |
108 | cur.execute('INSERT INTO attr (key, value) VALUES (?, ?)', | |
109 | [key, value]) | |
110 | except sqlite.OperationalError: | |
111 | pass | |
112 | cur.execute('SELECT id FROM attr WHERE key = ? AND value = ?', | |
113 | [key, value]) | |
114 | r = cur.fetchone() | |
115 | attr = r[0] | |
116 | try: | |
117 | cur.execute('INSERT INTO attrset VALUES (?, ?)', | |
118 | [me.id, attr]) | |
119 | except sqlite.OperationalError: | |
120 | pass | |
121 | def fetch(me): | |
122 | for r in me.db.select('''SELECT attr.key, attr.value | |
123 | FROM attr, attrset ON attr.id = attrset.attr | |
124 | WHERE attrset.id = ?''', | |
125 | [me.id]): | |
126 | yield r | |
127 | def delete(me): | |
128 | cur = me.db.db.cursor() | |
129 | cur.execute('DELETE FROM attrset WHERE id = ?', [me.id]) | |
130 | me.db.cleanup() | |
131 | ||
132 | class AttrMap (AttrSet, DictMixin): | |
133 | def __getitem__(me, key): | |
134 | it = None | |
135 | for v, in me.db.select('''SELECT attr.value | |
136 | FROM attr, attrset ON attr.id = attrset.attr | |
137 | WHERE attrset.id = ? AND attr.key = ?''', | |
138 | [me.id, key]): | |
139 | if it is None: | |
140 | it = v | |
141 | else: | |
142 | raise ValueError, 'multiple values for key %s' % key | |
143 | if it is None: | |
144 | raise KeyError, key | |
145 | return it | |
146 | def __delitem__(me, key): | |
147 | cur = me.db.db.cursor() | |
148 | cur.execute('''DELETE FROM attrset | |
149 | WHERE id = ? AND | |
150 | attr in | |
151 | (SELECT id FROM attr WHERE key = ?)''', | |
152 | [me.id, key]) | |
153 | me.db.cleanup() | |
154 | def __setitem__(me, key, value): | |
155 | me.__delitem__(key) | |
156 | me.insert(key, value) | |
157 | def __iter__(me): | |
158 | set = {} | |
159 | for k, v in me.fetch(): | |
160 | if k in set: | |
161 | continue | |
162 | set[k] = True | |
163 | yield k | |
164 | def keys(me): | |
165 | return [k for k in me] | |
166 | ||
167 | class AttrMultiMap (AttrMap): | |
168 | def __getitem__(me, key): | |
169 | them = [] | |
170 | for v, in me.db.select('''SELECT attr.value | |
171 | FROM attr, attrset ON attr.id = attrset.attr | |
172 | WHERE attrset.id = ? AND attr.key = ?''', | |
173 | [me.id, key]): | |
174 | them.append(v) | |
175 | if not them: | |
176 | raise KeyError, key | |
177 | return them | |
178 | def __setitem__(me, key, values): | |
179 | me.__delitem__(key) | |
180 | for it in values: | |
181 | me.insert(key, it) | |
182 | ||
183 | ###----- Miscellaneous utilities -------------------------------------------- | |
184 | ||
185 | def time_format(t = None): | |
186 | if t is None: | |
187 | t = T.time() | |
188 | tm = T.gmtime(t) | |
189 | return T.strftime('%Y-%m-%d %H:%M:%S', tm) | |
190 | ||
191 | def any(pred, list): | |
192 | for i in list: | |
193 | if pred(i): return True | |
194 | return False | |
195 | def every(pred, list): | |
196 | for i in list: | |
197 | if not pred(i): return False | |
198 | return True | |
199 | ||
200 | prog = RX.sub(r'^.*[/\\]', '', argv[0]) | |
201 | def moan(msg): | |
202 | print >>stderr, '%s: %s' % (prog, msg) | |
203 | def die(msg): | |
204 | moan(msg) | |
205 | exit(111) | |
206 | ||
207 | ###----- My actual database ------------------------------------------------- | |
208 | ||
209 | class CMDB (AttrDB): | |
210 | def setup(me): | |
211 | AttrDB.setup(me) | |
212 | cur = me.db.cursor() | |
213 | cur.execute('''CREATE TABLE expiry | |
214 | (attrset INTEGER PRIMARY KEY, | |
215 | time CHAR(20) NOT NULL)''') | |
216 | cur.execute('CREATE INDEX expiry_bytime ON expiry (time)') | |
217 | def cleanup(me): | |
218 | cur = me.db.cursor() | |
219 | now = time_format() | |
220 | cur.execute('''DELETE FROM attrset WHERE id IN | |
221 | (SELECT attrset FROM expiry WHERE time < ?)''', | |
222 | [now]) | |
223 | cur.execute('DELETE FROM expiry WHERE time < ?', [now]) | |
e345b51f MW |
224 | cur.execute('''DELETE FROM expiry WHERE attrset IN |
225 | (SELECT attrset | |
226 | FROM expiry LEFT JOIN attrset | |
227 | ON expiry.attrset = attrset.id | |
228 | WHERE attrset.id ISNULL)''') | |
71074336 | 229 | AttrDB.cleanup(me) |
e345b51f | 230 | def expiry(me, id): |
71074336 | 231 | for t, in me.select('SELECT time FROM expiry WHERE attrset = ?', [id]): |
e345b51f MW |
232 | return t |
233 | return None | |
234 | def expiredp(me, id): | |
235 | t = me.expiry(id) | |
236 | if t is not None and t < time_format(): | |
237 | return True | |
238 | else: | |
239 | return False | |
71074336 MW |
240 | def setexpire(me, id, when): |
241 | if when != C.KEXP_FOREVER: | |
242 | cur = me.db.cursor() | |
243 | cur.execute('INSERT INTO expiry VALUES (?, ?)', | |
244 | [id, time_format(when)]) | |
245 | ||
246 | ###----- Crypto messing about ----------------------------------------------- | |
247 | ||
248 | ## Very vague security arguments... | |
249 | ## | |
250 | ## If the block size n of the PRP is large enough (128 bits) then we encrypt | |
251 | ## id || 0^{n - 64}. Decryption checks we have the right thing. The | |
252 | ## security proofs for secrecy and integrity are trivial. | |
253 | ## | |
254 | ## If the block size is small, then we encrypt two blocks: | |
255 | ## C_0 = E_K(0^{n - 64} || id) | |
256 | ## C_1 = E_K(C_0) | |
257 | ## The proofs are a little more complicated, but essentially work like this. | |
258 | ## If no 0^{n - 64} || id is ever seen as a C_0 then an adversary can't tell | |
259 | ## the difference between this and a similar construction using independent | |
260 | ## keys. This other construction must provide secrecy (pushing a | |
261 | ## nonrepeating thing through a PRF) and integrity (PRF on noncolliding | |
262 | ## inputs). So we win, give or take a birthday term. | |
263 | class Crypto (object): | |
264 | def __init__(me, key): | |
265 | me.prp = C.gcprps[key.attr.get('prp', 'blowfish')](key.data.bin) | |
266 | def encrypt(me, id): | |
267 | blksz = type(me.prp).blksz | |
268 | p = C.MP(id).storeb(blksz) | |
269 | c = me.prp.encrypt(p) | |
270 | if blksz < 16: | |
271 | c += me.prp.encrypt(c) | |
272 | return c | |
273 | def decrypt(me, c): | |
274 | bad = False | |
275 | blksz = type(me.prp).blksz | |
276 | if blksz < 16: | |
277 | if len(c) != blksz * 2: | |
278 | return None | |
279 | c, c1 = c[:blksz], c[blksz:] | |
280 | if c1 != me.prp.encrypt(c): | |
281 | bad = True | |
282 | else: | |
283 | if len(c) != blksz: | |
284 | return None | |
285 | p = me.prp.decrypt(c) | |
286 | id = C.MP.loadb(p) | |
287 | if id >> 64: | |
288 | bad = True | |
289 | if bad: | |
290 | return None | |
291 | return long(id) | |
292 | ||
293 | ###----- Canonification ----------------------------------------------------- | |
294 | ||
295 | rx_prefix = RX.compile(r'''(?x) ^ ( | |
296 | \[ \S+ \] \s* | | |
297 | \S{,4} : \s* | | |
298 | \s+ | |
299 | ) | |
300 | ''') | |
301 | rx_suffix = RX.compile(r'''(?ix) ( | |
302 | \( \s* was \s* : .* \) \s* | | |
303 | \s+ | |
304 | ) $''') | |
305 | rx_punct = RX.compile(r'(?x) [^\w]+ ') | |
306 | ||
307 | def canon_sender(addr): | |
308 | return addr.lower() | |
309 | ||
310 | def canon_subject(subject): | |
311 | subject = subject.lower() | |
312 | while True: | |
313 | m = rx_prefix.match(subject) | |
314 | if not m: break | |
315 | subject = subject[m.end():] | |
316 | while True: | |
317 | m = rx_suffix.search(subject) | |
318 | if not m: break | |
319 | subject = subject[:m.start()] | |
320 | subject = rx_punct.sub('', subject) | |
321 | return subject | |
322 | ||
323 | ###----- Checking a message for validity ------------------------------------ | |
324 | ||
325 | class Reject (Exception): pass | |
326 | ||
327 | class MessageInfo (object): | |
328 | __slots__ = ''' | |
329 | sender msg | |
330 | '''.split() | |
331 | ||
332 | constraints = {} | |
333 | ||
334 | def check_sender(mi, vv): | |
335 | if mi.sender is None: | |
336 | raise Reject, 'no sender' | |
337 | sender = canon_sender(mi.sender) | |
338 | if not any(lambda pat: M.match(pat.lower(), sender), vv): | |
339 | raise Reject, 'unmatched sender' | |
340 | constraints['sender'] = check_sender | |
341 | ||
342 | def check_subject(mi, vv): | |
343 | if mi.msg is None: | |
344 | return | |
345 | subj = mi.msg['subject'] | |
346 | if subj is None: | |
347 | raise Reject, 'no subject' | |
348 | subj = canon_subject(subj) | |
349 | if not any(lambda pat: M.match(pat.lower(), subj), vv): | |
350 | raise Reject, 'unmatched subject' | |
351 | constraints['subject'] = check_subject | |
352 | ||
353 | def check_nothing(me, vv): | |
354 | pass | |
355 | ||
356 | def check(db, id, sender = None, msgfile = None): | |
357 | mi = MessageInfo() | |
358 | a = AttrMultiMap(db, id) | |
359 | try: | |
360 | addr = a['addr'][0] | |
361 | except KeyError: | |
362 | raise Reject, 'unknown id' | |
363 | if db.expiredp(id): | |
364 | raise Reject, 'expired' | |
365 | if msgfile is None: | |
366 | mi.msg = None | |
367 | else: | |
368 | try: | |
369 | mi.msg = EP.HeaderParser().parse(msgfile) | |
370 | except EP.Errors.HeaderParseError: | |
371 | raise Reject, 'unparseable header' | |
372 | mi.sender = sender | |
373 | for k, vv in a.iteritems(): | |
374 | constraints.get(k, check_nothing)(mi, vv) | |
375 | return a['addr'][0] | |
376 | ||
377 | ###----- Commands ----------------------------------------------------------- | |
378 | ||
379 | keyfile = 'db/keyring' | |
380 | tag = 'cryptomail' | |
381 | dbfile = 'db/cryptomail.db' | |
e345b51f | 382 | user = None |
71074336 MW |
383 | commands = {} |
384 | ||
385 | def timecmp(x, y): | |
386 | if x == y: | |
387 | return 0 | |
388 | elif x == C.KEXP_FOREVER or y == C.KEXP_EXPIRE: | |
389 | return +1 | |
390 | elif y == C.KEXP_FOREVER or x == C.KEXP_EXPIRE: | |
391 | return +1 | |
392 | else: | |
393 | return cmp(x, y) | |
394 | ||
6253ed06 MW |
395 | def token(c, id): |
396 | return M.base32_encode(c.encrypt(id)).strip('=').lower() | |
397 | ||
71074336 MW |
398 | def cmd_generate(argv): |
399 | try: | |
e345b51f MW |
400 | opts, argv = getopt(argv, 't:c:f:i:', |
401 | ['expire=', 'timeout=', 'constraint=', | |
402 | 'info=', 'format=']) | |
71074336 MW |
403 | except GetoptError: |
404 | return 1 | |
405 | kr = C.KeyFile(keyfile, C.KOPEN_WRITE) | |
406 | k = kr[tag] | |
407 | db = CMDB(dbfile) | |
408 | map = {} | |
409 | expwhen = C.KEXP_FOREVER | |
410 | format = '%' | |
411 | for o, a in opts: | |
412 | if o in ('-t', '--expire', '--timeout'): | |
413 | if a == 'forever': | |
414 | expwhen = C.KEXP_FOREVER | |
415 | else: | |
416 | expwhen = getdate(a) | |
417 | elif o in ('-c', '--constraint'): | |
418 | c, v = a.split('=', 1) | |
419 | if c not in constraints: | |
420 | die("unknown constraint `%s'", c) | |
421 | map.setdefault(c, []).append(v) | |
010ac9cf | 422 | elif o in ('-f', '--format'): |
71074336 | 423 | format = a |
e345b51f MW |
424 | elif o in ('-i', '--info'): |
425 | map['info'] = [a] | |
71074336 MW |
426 | else: |
427 | raise 'Barf!' | |
428 | if timecmp(expwhen, k.deltime) > 0: | |
429 | k.deltime = expwhen | |
430 | if len(argv) != 1: | |
431 | return 1 | |
432 | addr = argv[0] | |
433 | a = AttrMultiMap(db) | |
434 | a.update(map) | |
435 | a['addr'] = [addr] | |
e345b51f MW |
436 | if user is not None: |
437 | a['user'] = [user] | |
71074336 | 438 | db.setexpire(a.id, expwhen) |
6253ed06 | 439 | print format.replace('%', token(Crypto(k), a.id)) |
71074336 MW |
440 | db.commit() |
441 | kr.save() | |
442 | commands['generate'] = \ | |
443 | (cmd_generate, '[-t TIME] [-c TYPE=VALUE] ADDR', """ | |
444 | Generate a new encrypted email address token forwarding to ADDR. | |
445 | ||
446 | Subcommand options: | |
447 | -t, --timeout=TIME Address should expire at TIME. | |
448 | -c, --constraint=TYPE=VALUE Apply constraint on the use of the address. | |
449 | -f, --format=STRING Substitute token for `%' in STRING. | |
450 | ||
451 | Constraint types: | |
452 | sender Envelope sender must match glob pattern. | |
453 | subject Message subject must match glob pattern.""") | |
454 | ||
455 | def cmd_initdb(argv): | |
456 | try: | |
457 | opts, argv = getopt(argv, '', []) | |
458 | except GetoptError: | |
459 | return 1 | |
460 | try: | |
461 | OS.unlink(dbfile) | |
462 | except OSError: | |
463 | pass | |
464 | CMDB(dbfile).setup() | |
465 | commands['initdb'] = \ | |
466 | (cmd_initdb, '', """ | |
467 | Initialize an attribute database.""") | |
468 | ||
e345b51f MW |
469 | def getid(local): |
470 | k = C.KeyFile(keyfile, C.KOPEN_READ)[tag] | |
471 | id = Crypto(k).decrypt(M.base32_decode(local)) | |
472 | if id is None: | |
473 | raise Reject, 'decrypt failed' | |
474 | return id | |
475 | ||
71074336 MW |
476 | def cmd_addrcheck(argv): |
477 | try: | |
478 | opts, argv = getopt(argv, '', []) | |
479 | except GetoptError: | |
480 | return 1 | |
481 | local, sender = (lambda addr, sender = None, *hunoz: (addr, sender))(*argv) | |
71074336 MW |
482 | db = CMDB(dbfile) |
483 | try: | |
e345b51f | 484 | id = getid(local) |
71074336 MW |
485 | addr = check(db, id, sender) |
486 | except Reject, msg: | |
487 | print '-%s' % msg | |
488 | return | |
489 | print '+%s' % addr | |
490 | commands['addrcheck'] = \ | |
491 | (cmd_addrcheck, 'LOCAL [SENDER [IGNORED ...]]', """ | |
492 | Check address token LOCAL, and report `-REASON' for failure or `+ADDR' for | |
493 | success.""") | |
494 | ||
495 | def cmd_fwaddr(argv): | |
496 | try: | |
497 | opts, argv = getopt(argv, '', []) | |
498 | except GetoptError: | |
499 | return 1 | |
e345b51f MW |
500 | if len(argv) not in (1, 2): |
501 | return 1 | |
502 | local, sender = (lambda addr, sender = None: (addr, sender))(*argv) | |
71074336 MW |
503 | db = CMDB(dbfile) |
504 | try: | |
e345b51f | 505 | id = getid(local) |
71074336 MW |
506 | if id is None: |
507 | raise Reject, 'decrypt failed' | |
508 | addr = check(db, id, sender, stdin) | |
509 | except Reject, msg: | |
510 | print >>stderr, '%s rejected message: %s' % (prog, msg) | |
511 | exit(100) | |
512 | stdin.seek(0) | |
513 | print addr | |
514 | commands['fwaddr'] = \ | |
e345b51f | 515 | (cmd_fwaddr, 'LOCAL [SENDER]', """ |
71074336 MW |
516 | Check address token LOCAL. On failure, report reason to stderr and exit |
517 | 111. On success, write forwarding address to stdout and exit 0. Expects | |
518 | the message on standard input, as a seekable file.""") | |
519 | ||
6253ed06 MW |
520 | ignore = {'user': 1, 'addr': 1} |
521 | def show(db, a): | |
522 | keys = a.keys() | |
523 | keys.sort() | |
524 | for k in keys: | |
525 | if k in ignore: | |
526 | continue | |
527 | for v in a[k]: | |
528 | print '\t%s: %s' % (k, v) | |
529 | expwhen = db.expiry(a.id) | |
530 | if expwhen: | |
531 | print '\texpires: %s' % expwhen | |
532 | else: | |
533 | print '\tno-expiry' | |
534 | ||
e345b51f MW |
535 | def cmd_info(argv): |
536 | try: | |
537 | opts, argv = getopt(argv, '', []) | |
538 | except GetoptError: | |
539 | return 1 | |
540 | if len(argv) != 1: | |
541 | return 1 | |
542 | local = argv[0] | |
543 | db = CMDB(dbfile) | |
544 | try: | |
545 | id = getid(local) | |
546 | a = AttrMultiMap(db, id) | |
547 | if user is not None and user != a.get('user', [None])[0]: | |
548 | raise Reject, 'not your token' | |
549 | if 'addr' not in a: | |
550 | die('unknown token (expired?)') | |
6253ed06 MW |
551 | print 'addr: %s' % a['addr'][0] |
552 | show(db, a) | |
e345b51f MW |
553 | except Reject, msg: |
554 | die('invalid token') | |
555 | commands['info'] = \ | |
556 | (cmd_info, 'LOCAL', """ | |
557 | Exaimne the address token LOCAL, and print information about it to standard | |
558 | output.""") | |
559 | ||
560 | def cmd_revoke(argv): | |
561 | try: | |
562 | opts, argv = getopt(argv, '', []) | |
563 | except GetoptError: | |
564 | return 1 | |
565 | if len(argv) != 1: | |
566 | return 1 | |
567 | local = argv[0] | |
568 | db = CMDB(dbfile) | |
569 | try: | |
570 | id = getid(local) | |
571 | a = AttrMultiMap(db, id) | |
572 | if user is not None and user != a.get('user', [None])[0]: | |
573 | raise Reject, 'not your token' | |
574 | if 'addr' not in a: | |
575 | die('unknown token (expired?)') | |
576 | a.clear() | |
577 | db.cleanup() | |
578 | db.commit() | |
579 | except Reject, msg: | |
580 | die('invalid token') | |
581 | commands['revoke'] = \ | |
582 | (cmd_revoke, 'LOCAL', """ | |
583 | Revoke the token LOCAL.""") | |
584 | ||
6253ed06 MW |
585 | def cmd_list(argv): |
586 | try: | |
587 | opts, argv = getopt(argv, '', []) | |
588 | except GetoptError: | |
589 | return 1 | |
590 | if argv: | |
591 | return 1 | |
592 | c = Crypto(C.KeyFile(keyfile, C.KOPEN_READ)[tag]) | |
593 | db = CMDB(dbfile) | |
594 | if not user: | |
595 | gen = db.select('SELECT DISTINCT id FROM attrset') | |
596 | else: | |
597 | gen = db.select('''SELECT DISTINCT attrset.id | |
598 | FROM attrset, attr ON attrset.attr = attr.id | |
599 | WHERE attr.key = 'user' AND attr.value = ?''', | |
600 | [user]) | |
601 | for id, in gen: | |
602 | a = AttrMultiMap(db, id) | |
603 | print '%s %s%s' % \ | |
604 | (token(c, id), | |
605 | a.get('addr', '<no-address>')[0], | |
606 | (not user and ' [%s]' % a.get('user', ['<no-user>'])[0] or '')) | |
607 | show(db, a) | |
608 | commands['list'] = \ | |
609 | (cmd_list, '', """ | |
610 | List the user's tokens and information about them.""") | |
611 | ||
71074336 MW |
612 | def cmd_cleanup(argv): |
613 | try: | |
614 | opts, argv = getopt(argv, '', []) | |
615 | except GetoptError: | |
616 | return 1 | |
617 | db = CMDB(dbfile) | |
618 | db.cleanup() | |
619 | cur = db.db.cursor() | |
620 | cur.execute('VACUUM') | |
621 | db.commit() | |
622 | commands['cleanup'] = \ | |
623 | (cmd_cleanup, '', """ | |
624 | Cleans up the attribute database, disposing of old records and compatifying | |
625 | the file.""") | |
626 | ||
627 | def cmd_help(argv): | |
628 | try: | |
629 | opts, argv = getopt(argv, '', []) | |
630 | except GetoptError: | |
631 | return 1 | |
632 | if len(argv) == 0: | |
633 | cmd = None | |
634 | elif len(argv) == 1: | |
635 | try: | |
636 | cmd = argv[0] | |
637 | ci = commands[cmd] | |
638 | except KeyError: | |
639 | die("unknown command `%s'" % cmd) | |
640 | else: | |
641 | return 1 | |
642 | version() | |
643 | ||
644 | if cmd: | |
645 | print 'Usage: %s [-OPTIONS] %s %s' % (prog, cmd, ci[1]) | |
646 | print ci[2] | |
647 | else: | |
648 | usage(stdout) | |
649 | print """ | |
650 | Handle encrypted email addresses. | |
651 | ||
652 | Help options: | |
653 | -h, --help Show this help text. | |
654 | -v, --version Show version number. | |
655 | -u, --usage Show a usage message. | |
656 | ||
657 | Global options: | |
658 | -d, --database=FILE Use FILE as the attribute database. | |
659 | -k, --keyring=KEYRING Use KEYRING as the keyring. | |
660 | -t, --tag=TAG Use TAG as the key tag. | |
e345b51f | 661 | -U, --user=USER Claim to be USER. |
71074336 MW |
662 | """ |
663 | cmds = commands.keys() | |
664 | cmds.sort() | |
665 | print 'Subcommands:' | |
666 | for c in cmds: | |
667 | print ' %s %s' % (c, commands[c][1]) | |
668 | commands['help'] = \ | |
669 | (cmd_help, '[COMMAND]', """ | |
670 | Show help for subcommand COMMAND. | |
671 | """) | |
672 | ||
673 | ###----- Main program ------------------------------------------------------- | |
674 | ||
675 | def usage(file): | |
676 | print >>file, \ | |
677 | 'Usage: %s [-d FILE] [-k KEYRING] [-t TAG] COMMAND [ARGS...]' % prog | |
678 | def version(): | |
679 | print '%s version 1.0.0' % prog | |
680 | def help(): | |
681 | cmd_help() | |
682 | ||
683 | def main(): | |
e345b51f | 684 | global argv, user, keyfile, dbfile, tag |
71074336 MW |
685 | try: |
686 | opts, argv = getopt(argv[1:], | |
e345b51f | 687 | 'hvud:k:t:U:', |
71074336 | 688 | ['help', 'version', 'usage', |
e345b51f | 689 | 'database=', 'keyring=', 'tag=', 'user=']) |
71074336 MW |
690 | except GetoptError: |
691 | usage(stderr) | |
692 | exit(111) | |
693 | for o, a in opts: | |
694 | if o in ('-h', '--help'): | |
695 | help() | |
696 | exit(0) | |
697 | elif o in ('-v', '--version'): | |
698 | version() | |
699 | exit(0) | |
700 | elif o in ('-u', '--usage'): | |
701 | usage(stdout) | |
702 | exit(0) | |
703 | elif o in ('-d', '--database'): | |
704 | dbfile = a | |
705 | elif o in ('-k', '--keyring'): | |
706 | keyfile = a | |
707 | elif o in ('-t', '--tag'): | |
708 | tag = a | |
e345b51f MW |
709 | elif o in ('-U', '--user'): |
710 | user = a | |
71074336 MW |
711 | else: |
712 | raise 'Barf!' | |
713 | if len(argv) < 1: | |
714 | usage(stderr) | |
715 | exit(111) | |
716 | ||
717 | if argv[0] in commands: | |
718 | c = argv[0] | |
719 | argv = argv[1:] | |
720 | else: | |
721 | usage(stderr) | |
722 | exit(111) | |
723 | cmd = commands[c] | |
724 | if cmd[0](argv): | |
725 | print >>stderr, 'Usage: %s %s %s' % (prog, c, cmd[1]) | |
726 | exit(111) | |
727 | ||
728 | try: | |
729 | main() | |
aeec1a4e MW |
730 | except SystemExit: |
731 | raise | |
732 | except: | |
71074336 MW |
733 | ty, exc, tb = exc_info() |
734 | moan('unhandled %s exception' % ty.__name__) | |
735 | for file, line, func, text in TB.extract_tb(tb): | |
736 | print >>stderr, \ | |
737 | ' %-35s -- %.38s' % ('%s:%d (%s)' % (file, line, func), text) | |
738 | die('%s: %s' % (ty.__name__, exc[0])) |