4f134a4f0a97c1b69f3335c0fdf2c5664f27bc78
[debianmemberportfolio.git] / debianmemberportfolio / model / keyringanalyzer.py
1 # -*- python -*-
2 # -*- coding: utf-8 -*-
3 #
4 # Debian Member Portfolio service application key ring analyzer tool
5 #
6 # Copyright © 2009-2014 Jan Dittberner <jan@dittberner.info>
7 #
8 # This file is part of the Debian Member Portfolio service.
9 #
10 # Debian Member Portfolio service is free software: you can redistribute it
11 # and/or modify it under the terms of the GNU Affero General Public License as
12 # published by the Free Software Foundation, either version 3 of
13 # the License, or (at your option) any later version.
14 #
15 # Debian Member Portfolio service is distributed in the hope that it will be
16 # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
18 # General Public License for more details.
19 #
20 # You should have received a copy of the GNU Affero General Public
21 # License along with this program.  If not, see
22 # <http://www.gnu.org/licenses/>.
23 #
24 """
25 This is a tool that analyzes GPG and PGP keyrings and stores the
26 retrieved data in a file database. The tool was inspired by Debian
27 qa's carnivore.
28 """
29
30 import anydbm
31 import pkg_resources
32 import glob
33 import ConfigParser
34 import os
35 import os.path
36 import logging
37 import subprocess
38 import sys
39 import email.utils
40
41
42 CONFIG = ConfigParser.SafeConfigParser()
43
44
45 def _get_keyrings():
46     """
47     Gets the available keyring files from the keyring directory
48     configured in ddportfolio.ini.
49     """
50     keyringdir = os.path.expanduser(CONFIG.get('DEFAULT', 'keyring.dir'))
51     logging.debug("keyring dir is %s", keyringdir)
52     keyrings = glob.glob(os.path.join(keyringdir, '*.gpg'))
53     keyrings.extend(glob.glob(os.path.join(keyringdir, '*.pgp')))
54     keyrings.sort()
55     return keyrings
56
57
58 def _parse_uid(uid):
59     """
60     Parse a uid of the form 'Real Name <email@example.com>' into email
61     and realname parts.
62     """
63
64     # First try with the Python library, but it doesn't always catch everything
65     (name, mail) = email.utils.parseaddr(uid)
66     if (not name) and (not mail):
67         logging.warning("malformed uid %s", uid)
68     if (not name) or (not mail):
69         logging.debug("strange uid %s: '%s' - <%s>", uid, name, mail)
70         # Try and do better than the python library
71         if not '@' in mail:
72             uid = uid.strip()
73             # First, strip comment
74             s = uid.find('(')
75             e = uid.find(')')
76             if s >= 0 and e >= 0:
77                 uid = uid[:s] + uid[e + 1:]
78             s = uid.find('<')
79             e = uid.find('>')
80             mail = None
81             if s >= 0 and e >= 0:
82                 mail = uid[s + 1:e]
83                 uid = uid[:s] + uid[e + 1:]
84             uid = uid.strip()
85             if not mail and uid.find('@') >= 0:
86                 mail, uid = uid, mail
87
88             name = uid
89             logging.debug("corrected: '%s' - <%s>", name, mail)
90     return (name, mail)
91
92 resultdict = {}
93
94
95 def _get_canonical(key):
96     if not key in resultdict:
97         resultdict[key] = []
98     return key
99
100
101 def _add_to_result(key, newvalue):
102     logging.debug("adding %s: %s", key, newvalue)
103     thekey = _get_canonical(key)
104     if newvalue not in resultdict[thekey]:
105         resultdict[thekey].append(newvalue)
106
107
108 def _handle_mail(mail, fpr):
109     if mail.endswith('@debian.org'):
110         login = mail[0:-len('@debian.org')]
111         _add_to_result('login:email:%s' % mail, login)
112         _add_to_result('login:fpr:%s' % fpr, login)
113         _add_to_result('fpr:login:%s' % login, fpr)
114     _add_to_result('fpr:email:%s' % mail, fpr)
115     _add_to_result('email:fpr:%s' % fpr, mail)
116
117
118 def _handle_uid(uid, fpr):
119     # Do stuff with 'uid'
120     if uid:
121         (uid, mail) = _parse_uid(uid)
122         if mail:
123             _handle_mail(mail, fpr)
124     if uid:
125         _add_to_result('name:fpr:%s' % fpr, uid)
126         if mail:
127             _add_to_result('name:email:%s' % mail, uid)
128     return fpr
129
130
131 def process_gpg_list_keys_line(line, fpr):
132     """
133     Process a line of gpg --list-keys --with-colon output.
134     """
135     items = line.split(':')
136     if items[0] == 'pub':
137         return None
138     if items[0] == 'fpr':
139         return items[9].strip()
140     if items[0] == 'uid':
141         if items[1] == 'r':
142             return fpr
143         return _handle_uid(items[9].strip(), fpr)
144     else:
145         return fpr
146
147
148 def process_keyrings():
149     """Process the keyrings and store the extracted data in an anydbm
150     file."""
151     for keyring in _get_keyrings():
152         logging.debug("get data from %s", keyring)
153         proc = subprocess.Popen([
154             "gpg", "--no-options", "--no-default-keyring",
155             "--homedir", os.path.expanduser(
156                 CONFIG.get('DEFAULT', 'gnupghome')),
157             "--no-expensive-trust-checks",
158             "--keyring", keyring, "--list-keys",
159             "--with-colons", "--fixed-list-mode", "--with-fingerprint",
160             "--with-fingerprint"],
161             stdout=subprocess.PIPE)
162         fpr = None
163         for line in proc.stdout.readlines():
164             fpr = process_gpg_list_keys_line(line, fpr)
165         retcode = proc.wait()
166         if retcode != 0:
167             logging.error("subprocess ended with return code %d", retcode)
168     db = anydbm.open(pkg_resources.resource_filename(__name__,
169                                                      'keyringcache'), 'c')
170     for key in resultdict:
171         db[key] = ":".join(resultdict[key])
172     db.close()
173
174
175 if __name__ == '__main__':
176     logging.basicConfig(stream=sys.stderr, level=logging.WARNING)
177     CONFIG.readfp(pkg_resources.resource_stream(
178         __name__, 'ddportfolio.ini'))
179     gpghome = os.path.expanduser(CONFIG.get('DEFAULT', 'gnupghome'))
180     if not os.path.isdir(gpghome):
181         os.makedirs(gpghome, 0700)
182     process_keyrings()