key: add a UserID.uid doctest to ensure proper XML unescaping.
[pgp-mime.git] / pgp_mime / key.py
1 # Copyright
2
3 import functools as _functools
4 import xml.etree.ElementTree as _etree
5
6 from . import LOG as _LOG
7 from . import crypt as _crypt
8
9 import pyassuan
10 import logging
11 from pyassuan import common as _common
12
13
14 @_functools.total_ordering
15 class SubKey (object):
16     """The crypographic key portion of an OpenPGP key.
17     """
18     def __init__(self, fingerprint=None):
19         self.fingerprint = fingerprint
20
21     def __str__(self):
22         return '<{} {}>'.format(type(self).__name__, self.fingerprint[-8:])
23
24     def __repr__(self):
25         return str(self)
26
27     def __eq__(self, other):
28         if self.fingerprint and hasattr(other, 'fingerprint'):
29             return self.fingerprint == other.fingerprint
30         return id(self) == id(other)
31
32     def __lt__(self, other):
33         if self.fingerprint and hasattr(other, 'fingerprint'):
34             return self.fingerprint < other.fingerprint
35         return id(self) < id(other)
36
37     def __hash__(self):
38         return int(self.fingerprint, 16)
39
40
41 @_functools.total_ordering
42 class UserID (object):
43     def __init__(self, uid=None, name=None, email=None, comment=None):
44         self.uid = uid
45         self.name = name
46         self.email = email
47         self.comment = comment
48
49     def __str__(self):
50         return '<{} {}>'.format(type(self).__name__, self.name)
51
52     def __repr__(self):
53         return str(self)
54
55     def __eq__(self, other):
56         if self.uid and hasattr(other, 'uid'):
57             return self.uid == other.uid
58         return id(self) == id(other)
59
60     def __lt__(self, other):
61         if self.uid and hasattr(other, 'uid'):
62             return self.uid < other.uid
63         return id(self) < id(other)
64
65     def __hash__(self):
66         return hash(self.uid)
67
68
69 @_functools.total_ordering
70 class Key (object):
71     def __init__(self, subkeys=None, uids=None):
72         revoked = False
73         expired = False
74         disabled = False
75         invalid = False
76         can_encrypt = False
77         can_sign = False
78         can_certify = False
79         can_authenticate = False
80         is_qualified = False
81         secret = False
82         protocol = None
83         issuer = None
84         chain_id = None
85         owner_trust = None
86         if subkeys is None:
87             subkeys = []
88         self.subkeys = subkeys
89         if uids is None:
90             uids = []
91         self.uids = uids
92
93     def __str__(self):
94         return '<{} {}>'.format(
95             type(self).__name__, self.subkeys[0].fingerprint[-8:])
96
97     def __repr__(self):
98         return str(self)
99
100     def __eq__(self, other):
101         other_subkeys = getattr(other, 'subkeys', None)
102         if self.subkeys and other_subkeys:
103             return self.subkeys[0] == other.subkeys[0]
104         return id(self) == id(other)
105
106     def __lt__(self, other):
107         other_subkeys = getattr(other, 'subkeys', None)
108         if self.subkeys and other_subkeys:
109             return self.subkeys[0] < other.subkeys[0]
110         return id(self) < id(other)
111
112     def __hash__(self):
113         return int(self.fingerprint, 16)
114
115
116 def lookup_keys(patterns=None):
117     """Lookup keys matching any patterns listed in ``patterns``.
118
119     >>> import pprint
120
121     >>> key = list(lookup_keys(['pgp-mime-test']))[0]
122     >>> key
123     <Key 4332B6E3>
124     >>> key.subkeys
125     [<SubKey 4332B6E3>, <SubKey 2F73DE2E>]
126     >>> key.uids
127     [<UserID pgp-mime-test>]
128     >>> key.uids[0].uid
129     'pgp-mime-test (http://blog.tremily.us/posts/pgp-mime/) <pgp-mime@invalid.com>'
130     >>> key.can_encrypt
131     True
132     >>> key.protocol
133     'OpenPGP'
134
135     >>> print(list(lookup_keys(['pgp-mime-test'])))
136     [<Key 4332B6E3>]
137     >>> print(list(lookup_keys(['pgp-mime@invalid.com'])))
138     [<Key 4332B6E3>]
139     >>> print(list(lookup_keys(['4332B6E3'])))
140     [<Key 4332B6E3>]
141     >>> print(list(lookup_keys(['0x2F73DE2E'])))
142     [<Key 4332B6E3>]
143     >>> print(list(lookup_keys()))  # doctest: +ELLIPSIS
144     [..., <Key 4332B6E3>, ...]
145     """
146     _LOG.debug('lookup key: {}'.format(patterns))
147     client,socket = _crypt.get_client()
148     parameters = []
149     if patterns:
150         args = [' '.join(patterns)]
151     else:
152         args = []
153     try:
154         _crypt.hello(client)
155         rs,result = client.make_request(_common.Request('KEYLIST', *args))
156     finally:
157         _crypt.disconnect(client, socket)
158     tag_mapping = {
159         }
160     tree = _etree.fromstring(result.replace(b'\x00', b''))
161     for key in tree.findall('.//key'):
162         k = Key()
163         for child in key:
164             attribute = tag_mapping.get(
165                 child.tag, child.tag.replace('-', '_'))
166             if child.tag in [
167                 'revoked', 'expired', 'disabled', 'invalid', 'can-encrypt',
168                 'can-sign', 'can-certify', 'can-authenticate', 'is-qualified',
169                 'secret', 'revoked']:
170                 # boolean values
171                 value = child.get('value')
172                 if not value.startswith('0x'):
173                     raise NotImplementedError('{} value {}'.format(
174                             child.tag, value))
175                 value = int(value, 16)
176                 value = bool(value)
177             elif child.tag in [
178                 'protocol', 'owner-trust']:
179                 value = child.text
180             elif child.tag in ['issuer', 'chain-id']:
181                 # ignore for now
182                 pass
183             elif child.tag in ['subkeys', 'uids']:
184                 parser = globals()['_parse_{}'.format(attribute)]
185                 value = parser(child)
186             else:
187                 raise NotImplementedError(child.tag)
188             setattr(k, attribute, value)
189         yield k
190
191 def _parse_subkeys(element):
192     tag_mapping = {
193         'fpr': 'fingerprint',
194         }
195     subkeys = []
196     for subkey in element:
197         s = SubKey()
198         for child in subkey.iter():
199             if child == subkey:  # iter() includes the root element
200                 continue
201             attribute = tag_mapping.get(
202                 child.tag, child.tag.replace('-', '_'))
203             if child.tag in [
204                 'fpr']:
205                 value = child.text
206             else:
207                 raise NotImplementedError(child.tag)
208             setattr(s, attribute, value)
209         subkeys.append(s)
210     return subkeys
211
212 def _parse_uids(element):
213     tag_mapping = {
214         }
215     uids = []
216     for uid in element:
217         u = UserID()
218         for child in uid.iter():
219             if child == uid:  # iter() includes the root element
220                 continue
221             attribute = tag_mapping.get(
222                 child.tag, child.tag.replace('-', '_'))
223             if child.tag in [
224                 'uid', 'name', 'email', 'comment']:
225                 value = child.text
226             else:
227                 raise NotImplementedError(child.tag)
228             setattr(u, attribute, value)
229         uids.append(u)
230     return uids