Add PGPPacket._str_generic_key_packet and fingerprint calculation
[gpg-migrate.git] / gpg-migrate.py
1 #!/usr/bin/python
2
3 import hashlib as _hashlib
4 import re as _re
5 import subprocess as _subprocess
6 import struct as _struct
7
8
9 def _get_stdout(args, stdin=None):
10     stdin_pipe = None
11     if stdin is not None:
12         stdin_pipe = _subprocess.PIPE
13     p = _subprocess.Popen(args, stdin=stdin_pipe, stdout=_subprocess.PIPE)
14     stdout, stderr = p.communicate(stdin)
15     status = p.wait()
16     if status != 0:
17         raise RuntimeError(status)
18     return stdout
19
20
21 class PGPPacket (dict):
22     # http://tools.ietf.org/search/rfc4880
23     _old_format_packet_length_type = {  # type: (bytes, struct type)
24         0: (1, 'B'),  # 1-byte unsigned integer
25         1: (2, 'H'),  # 2-byte unsigned integer
26         2: (4, 'I'),  # 4-byte unsigned integer
27         3: (None, None),
28         }
29
30     _packet_types = {
31         0: 'reserved',
32         1: 'public-key encrypted session key packet',
33         2: 'signature packet',
34         3: 'symmetric-key encrypted session key packet',
35         4: 'one-pass signature packet',
36         5: 'secret-key packet',
37         6: 'public-key packet',
38         7: 'secret-subkey packet',
39         8: 'compressed data packet',
40         9: 'symmetrically encrypted data packet',
41         10: 'marker packet',
42         11: 'literal data packet',
43         12: 'trust packet',
44         13: 'user id packet',
45         14: 'public-subkey packet',
46         17: 'user attribute packet',
47         18: 'sym. encrypted and integrity protected data packet',
48         19: 'modification detection code packet',
49         60: 'private',
50         61: 'private',
51         62: 'private',
52         63: 'private',
53         }
54
55     _public_key_algorithms = {
56         1: 'rsa (encrypt or sign)',
57         2: 'rsa encrypt-only',
58         3: 'rsa sign-only',
59         16: 'elgamal (encrypt-only)',
60         17: 'dsa (digital signature algorithm)',
61         18: 'reserved for elliptic curve',
62         19: 'reserved for ecdsa',
63         20: 'reserved (formerly elgamal encrypt or sign)',
64         21: 'reserved for diffie-hellman',
65         100: 'private',
66         101: 'private',
67         102: 'private',
68         103: 'private',
69         104: 'private',
70         105: 'private',
71         106: 'private',
72         107: 'private',
73         108: 'private',
74         109: 'private',
75         110: 'private',
76         }
77
78     _symmetric_key_algorithms = {
79         0: 'plaintext or unencrypted data',
80         1: 'idea',
81         2: 'tripledes',
82         3: 'cast5',
83         4: 'blowfish',
84         5: 'reserved',
85         6: 'reserved',
86         7: 'aes with 128-bit key',
87         8: 'aes with 192-bit key',
88         9: 'aes with 256-bit key',
89         10: 'twofish',
90         100: 'private',
91         101: 'private',
92         102: 'private',
93         103: 'private',
94         104: 'private',
95         105: 'private',
96         106: 'private',
97         107: 'private',
98         108: 'private',
99         109: 'private',
100         110: 'private',
101         }
102
103     _cipher_block_size = {  # in bits
104         'aes with 128-bit key': 128,
105         'aes with 192-bit key': 128,
106         'aes with 256-bit key': 128,
107         'cast5': 64,
108         }
109
110     _compression_algorithms = {
111         0: 'uncompressed',
112         1: 'zip',
113         2: 'zlib',
114         3: 'bzip2',
115         100: 'private',
116         101: 'private',
117         102: 'private',
118         103: 'private',
119         104: 'private',
120         105: 'private',
121         106: 'private',
122         107: 'private',
123         108: 'private',
124         109: 'private',
125         110: 'private',
126         }
127
128     _hash_algorithms = {
129         1: 'md5',
130         2: 'sha-1',
131         3: 'ripe-md/160',
132         4: 'reserved',
133         5: 'reserved',
134         6: 'reserved',
135         7: 'reserved',
136         8: 'sha256',
137         9: 'sha384',
138         10: 'sha512',
139         11: 'sha224',
140         100: 'private',
141         101: 'private',
142         102: 'private',
143         103: 'private',
144         104: 'private',
145         105: 'private',
146         106: 'private',
147         107: 'private',
148         108: 'private',
149         109: 'private',
150         110: 'private',
151         }
152
153     _string_to_key_types = {
154         0: 'simple',
155         1: 'salted',
156         2: 'reserved',
157         3: 'iterated and salted',
158         100: 'private',
159         101: 'private',
160         102: 'private',
161         103: 'private',
162         104: 'private',
163         105: 'private',
164         106: 'private',
165         107: 'private',
166         108: 'private',
167         109: 'private',
168         110: 'private',
169         }
170
171     _signature_types = {
172         0x00: 'binary document',
173         0x01: 'canonical text document',
174         0x02: 'standalone',
175         0x10: 'generic user id and public-key packet',
176         0x11: 'persona user id and public-key packet',
177         0x12: 'casual user id and public-key packet',
178         0x13: 'postitive user id and public-key packet',
179         0x18: 'subkey binding',
180         0x19: 'primary key binding',
181         0x1F: 'direct key',
182         0x20: 'key revocation',
183         0x28: 'subkey revocation',
184         0x30: 'certification revocation',
185         0x40: 'timestamp',
186         0x50: 'third-party confirmation',
187         }
188
189     _clean_type_regex = _re.compile('\W+')
190
191     def _clean_type(self):
192         return self._clean_type_regex.sub('_', self['type'])
193
194     def __str__(self):
195         method_name = '_str_{}'.format(self._clean_type())
196         method = getattr(self, method_name, None)
197         if not method:
198             return self['type']
199         details = method()
200         return '{}: {}'.format(self['type'], details)
201
202     def _str_public_key_packet(self):
203         return self._str_generic_key_packet()
204
205     def _str_public_subkey_packet(self):
206         return self._str_generic_key_packet()
207
208     def _str_secret_key_packet(self):
209         return self._str_generic_key_packet()
210
211     def _str_secret_subkey_packet(self):
212         return self._str_generic_key_packet()
213
214     def _str_generic_key_packet(self):
215         return self['fingerprint'][-8:].upper()
216
217     def _str_user_id_packet(self):
218         return self['user']
219
220     def from_bytes(self, data):
221         offset = self._parse_header(data=data)
222         packet = data[offset:offset + self['length']]
223         if len(packet) < self['length']:
224             raise ValueError('packet too short ({} < {})'.format(
225                 len(packet), self['length']))
226         offset += self['length']
227         method_name = '_parse_{}'.format(self._clean_type())
228         method = getattr(self, method_name, None)
229         if not method:
230             raise NotImplementedError(
231                 'cannot parse packet type {!r}'.format(self['type']))
232         method(data=packet)
233         return offset
234
235     def _parse_header(self, data):
236         packet_tag = data[0]
237         offset = 1
238         always_one = packet_tag & 1 << 7
239         if not always_one:
240             raise ValueError('most significant packet tag bit not set')
241         self['new-format'] = packet_tag & 1 << 6
242         if self['new-format']:
243             type_code = packet_tag & 0b111111
244             raise NotImplementedError('new-format packet length')
245         else:
246             type_code = packet_tag >> 2 & 0b1111
247             self['length-type'] = packet_tag & 0b11
248             length_bytes, length_type = self._old_format_packet_length_type[
249                 self['length-type']]
250             if not length_bytes:
251                 raise NotImplementedError(
252                     'old-format packet of indeterminate length')
253             length_format = '>{}'.format(length_type)
254             length_data = data[offset: offset + length_bytes]
255             offset += length_bytes
256             self['length'] = _struct.unpack(length_format, length_data)[0]
257         self['type'] = self._packet_types[type_code]
258         return offset
259
260     @staticmethod
261     def _parse_multiprecision_integer(data):
262         r"""Parse RFC 4880's multiprecision integers
263
264         >>> PGPPacket._parse_multiprecision_integer(b'\x00\x01\x01')
265         (3, 1)
266         >>> PGPPacket._parse_multiprecision_integer(b'\x00\x09\x01\xff')
267         (4, 511)
268         """
269         bits = _struct.unpack('>H', data[:2])[0]
270         offset = 2
271         length = (bits + 7) // 8
272         value = 0
273         for i in range(length):
274             value += data[offset + i] * 1 << (8 * (length - i - 1))
275         offset += length
276         return (offset, value)
277
278     def _parse_string_to_key_specifier(self, data):
279         self['string-to-key-type'] = self._string_to_key_types[data[0]]
280         offset = 1
281         if self['string-to-key-type'] == 'simple':
282             self['string-to-key-hash-algorithm'] = self._hash_algorithms[
283                 data[offset]]
284             offset += 1
285         elif self['string-to-key-type'] == 'salted':
286             self['string-to-key-hash-algorithm'] = self._hash_algorithms[
287                 data[offset]]
288             offset += 1
289             self['string-to-key-salt'] = data[offset: offset + 8]
290             offset += 8
291         elif self['string-to-key-type'] == 'iterated and salted':
292             self['string-to-key-hash-algorithm'] = self._hash_algorithms[
293                 data[offset]]
294             offset += 1
295             self['string-to-key-salt'] = data[offset: offset + 8]
296             offset += 8
297             self['string-to-key-coded-count'] = data[offset]
298             offset += 1
299         else:
300             raise NotImplementedError(
301                 'string-to-key type {}'.format(self['string-to-key-type']))
302         return offset
303
304     def _parse_public_key_packet(self, data):
305         self._parse_generic_public_key_packet(data=data)
306
307     def _parse_public_subkey_packet(self, data):
308         self._parse_generic_public_key_packet(data=data)
309
310     def _parse_generic_public_key_packet(self, data):
311         self['key-version'] = data[0]
312         offset = 1
313         if self['key-version'] != 4:
314             raise NotImplementedError(
315                 'public (sub)key packet version {}'.format(
316                     self['key-version']))
317         length = 5
318         self['creation-time'], algorithm = _struct.unpack(
319             '>IB', data[offset: offset + length])
320         offset += length
321         self['public-key-algorithm'] = self._public_key_algorithms[algorithm]
322         if self['public-key-algorithm'].startswith('rsa '):
323             o, self['public-modulus'] = self._parse_multiprecision_integer(
324                 data[offset:])
325             offset += o
326             o, self['public-exponent'] = self._parse_multiprecision_integer(
327                 data[offset:])
328             offset += o
329         elif self['public-key-algorithm'].startswith('dsa '):
330             o, self['prime'] = self._parse_multiprecision_integer(
331                 data[offset:])
332             offset += o
333             o, self['group-order'] = self._parse_multiprecision_integer(
334                 data[offset:])
335             offset += o
336             o, self['group-generator'] = self._parse_multiprecision_integer(
337                 data[offset:])
338             offset += o
339             o, self['public-key'] = self._parse_multiprecision_integer(
340                 data[offset:])
341             offset += o
342         elif self['public-key-algorithm'].startswith('elgamal '):
343             o, self['prime'] = self._parse_multiprecision_integer(
344                 data[offset:])
345             offset += o
346             o, self['group-generator'] = self._parse_multiprecision_integer(
347                 data[offset:])
348             offset += o
349             o, self['public-key'] = self._parse_multiprecision_integer(
350                 data[offset:])
351             offset += o
352         else:
353             raise NotImplementedError(
354                 'algorithm-specific key fields for {}'.format(
355                     self['public-key-algorithm']))
356         fingerprint = _hashlib.sha1()
357         fingerprint.update(b'\x99')
358         fingerprint.update(_struct.pack('>H', len(data)))
359         fingerprint.update(data)
360         self['fingerprint'] = fingerprint.hexdigest()
361         return offset
362
363     def _parse_secret_key_packet(self, data):
364         self._parse_generic_secret_key_packet(data=data)
365
366     def _parse_secret_subkey_packet(self, data):
367         self._parse_generic_secret_key_packet(data=data)
368
369     def _parse_generic_secret_key_packet(self, data):
370         offset = self._parse_generic_public_key_packet(data=data)
371         string_to_key_usage = data[offset]
372         offset += 1
373         if string_to_key_usage in [255, 254]:
374             self['symmetric-encryption-algorithm'] = (
375                 self._symmetric_key_algorithms[data[offset]])
376             offset += 1
377             offset += self._parse_string_to_key_specifier(data=data[offset:])
378         else:
379             self['symmetric-encryption-algorithm'] = (
380                 self._symmetric_key_algorithms[string_to_key_usage])
381         if string_to_key_usage:
382             block_size_bits = self._cipher_block_size.get(
383                 self['symmetric-encryption-algorithm'], None)
384             if block_size_bits % 8:
385                 raise NotImplementedError(
386                     ('{}-bit block size for {} is not an integer number of bytes'
387                      ).format(
388                          block_size_bits, self['symmetric-encryption-algorithm']))
389             block_size = block_size_bits // 8
390             if not block_size:
391                 raise NotImplementedError(
392                     'unknown block size for {}'.format(
393                         self['symmetric-encryption-algorithm']))
394             self['initial-vector'] = data[offset: offset + block_size]
395             offset += block_size
396         if string_to_key_usage in [0, 255]:
397             key_end = -2
398         else:
399             key_end = 0
400         self['secret-key'] = data[offset:key_end]
401         if key_end:
402             self['secret-key-checksum'] = data[key_end:]
403
404     def _parse_signature_packet(self, data):
405         self['signature-version'] = data[0]
406         offset = 1
407         if self['signature-version'] != 4:
408             raise NotImplementedError(
409                 'signature packet version {}'.format(
410                     self['signature-version']))
411         self['signature-type'] = self._signature_types[data[offset]]
412         offset += 1
413         self['public-key-algorithm'] = self._public_key_algorithms[
414             data[offset]]
415         offset += 1
416         self['hash-algorithm'] = self._hash_algorithms[data[offset]]
417         offset += 1
418         hashed_count = _struct.unpack('>H', data[offset: offset + 2])[0]
419         offset += 2
420         self['hashed-subpackets'] = data[offset: offset + hashed_count]
421         offset += hashed_count
422         unhashed_count = _struct.unpack('>H', data[offset: offset + 2])[0]
423         offset += 2
424         self['unhashed-subpackets'] = data[offset: offset + unhashed_count]
425         offset += unhashed_count
426         self['signed-hash-word'] = data[offset: offset + 2]
427         offset += 2
428         self['signature'] = data[offset:]
429
430     def _parse_user_id_packet(self, data):
431         self['user'] = str(data, 'utf-8')
432
433     def to_bytes(self):
434         pass
435
436
437 def packets_from_bytes(data):
438     offset = 0
439     while offset < len(data):
440         packet = PGPPacket()
441         offset += packet.from_bytes(data=data[offset:])
442         yield packet
443
444
445 class PGPKey (object):
446     def __init__(self, fingerprint):
447         self.fingerprint = fingerprint
448         self.public_packets = None
449         self.secret_packets = None
450
451     def __str__(self):
452         lines = ['key: {}'.format(self.fingerprint)]
453         if self.public_packets:
454             lines.append('  public:')
455             for packet in self.public_packets:
456                 lines.append('    {}'.format(packet))
457         if self.secret_packets:
458             lines.append('  secret:')
459             for packet in self.secret_packets:
460                 lines.append('    {}'.format(packet))
461         return '\n'.join(lines)
462
463     def import_from_gpg(self):
464         key_export = _get_stdout(
465             ['gpg', '--export', self.fingerprint])
466         self.public_packets = list(
467             packets_from_bytes(data=key_export))
468         if self.public_packets[0]['type'] != 'public-key packet':
469             raise ValueError(
470                 '{} does not start with a public-key packet'.format(
471                     self.fingerprint))
472         key_secret_export = _get_stdout(
473             ['gpg', '--export-secret-keys', self.fingerprint])
474         self.secret_packets = list(
475             packets_from_bytes(data=key_secret_export))
476
477     def export_to_gpg(self):
478         raise NotImplemetedError('export to gpg')
479
480
481 def migrate(old_key, new_key):
482     """Add the old key and sub-keys to the new key
483
484     For example, to upgrade your master key, while preserving old
485     signatures you'd made.  You will lose signature *on* your old key
486     though, since sub-keys can't be signed (I don't think).
487     """
488     old_key = PGPKey(fingerprint=old_key)
489     old_key.import_from_gpg()
490     new_key = PGPKey(fingerprint=new_key)
491     new_key.import_from_gpg()
492
493     print(old_key)
494     print(new_key)
495
496
497 if __name__ == '__main__':
498     import sys as _sys
499
500     old_key, new_key = _sys.argv[1:3]
501     migrate(old_key=old_key, new_key=new_key)