Add secret key parsing to PGPPacket
[gpg-migrate.git] / gpg-migrate.py
1 #!/usr/bin/python
2
3 import re as _re
4 import subprocess as _subprocess
5 import struct as _struct
6
7
8 def _get_stdout(args, stdin=None):
9     stdin_pipe = None
10     if stdin is not None:
11         stdin_pipe = _subprocess.PIPE
12     p = _subprocess.Popen(args, stdin=stdin_pipe, stdout=_subprocess.PIPE)
13     stdout, stderr = p.communicate(stdin)
14     status = p.wait()
15     if status != 0:
16         raise RuntimeError(status)
17     return stdout
18
19
20 class PGPPacket (dict):
21     # http://tools.ietf.org/search/rfc4880
22     _old_format_packet_length_type = {  # type: (bytes, struct type)
23         0: (1, 'B'),  # 1-byte unsigned integer
24         1: (2, 'H'),  # 2-byte unsigned integer
25         2: (4, 'I'),  # 4-byte unsigned integer
26         3: (None, None),
27         }
28
29     _packet_types = {
30         0: 'reserved',
31         1: 'public-key encrypted session key packet',
32         2: 'signature packet',
33         3: 'symmetric-key encrypted session key packet',
34         4: 'one-pass signature packet',
35         5: 'secret-key packet',
36         6: 'public-key packet',
37         7: 'secret-subkey packet',
38         8: 'compressed data packet',
39         9: 'symmetrically encrypted data packet',
40         10: 'marker packet',
41         11: 'literal data packet',
42         12: 'trust packet',
43         13: 'user id packet',
44         14: 'public-subkey packet',
45         17: 'user attribute packet',
46         18: 'sym. encrypted and integrity protected data packet',
47         19: 'modification detection code packet',
48         60: 'private',
49         61: 'private',
50         62: 'private',
51         63: 'private',
52         }
53
54     _public_key_algorithms = {
55         1: 'rsa (encrypt or sign)',
56         2: 'rsa encrypt-only',
57         3: 'rsa sign-only',
58         16: 'elgamal (encrypt-only)',
59         17: 'dsa (digital signature algorithm)',
60         18: 'reserved for elliptic curve',
61         19: 'reserved for ecdsa',
62         20: 'reserved (formerly elgamal encrypt or sign)',
63         21: 'reserved for diffie-hellman',
64         100: 'private',
65         101: 'private',
66         102: 'private',
67         103: 'private',
68         104: 'private',
69         105: 'private',
70         106: 'private',
71         107: 'private',
72         108: 'private',
73         109: 'private',
74         110: 'private',
75         }
76
77     _symmetric_key_algorithms = {
78         0: 'plaintext or unencrypted data',
79         1: 'idea',
80         2: 'tripledes',
81         3: 'cast5',
82         4: 'blowfish',
83         5: 'reserved',
84         6: 'reserved',
85         7: 'aes with 128-bit key',
86         8: 'aes with 192-bit key',
87         9: 'aes with 256-bit key',
88         10: 'twofish',
89         100: 'private',
90         101: 'private',
91         102: 'private',
92         103: 'private',
93         104: 'private',
94         105: 'private',
95         106: 'private',
96         107: 'private',
97         108: 'private',
98         109: 'private',
99         110: 'private',
100         }
101
102     _cipher_block_size = {  # in bits
103         }
104
105     _compression_algorithms = {
106         0: 'uncompressed',
107         1: 'zip',
108         2: 'zlib',
109         3: 'bzip2',
110         100: 'private',
111         101: 'private',
112         102: 'private',
113         103: 'private',
114         104: 'private',
115         105: 'private',
116         106: 'private',
117         107: 'private',
118         108: 'private',
119         109: 'private',
120         110: 'private',
121         }
122
123     _hash_algorithms = {
124         1: 'md5',
125         2: 'sha-1',
126         3: 'ripe-md/160',
127         4: 'reserved',
128         5: 'reserved',
129         6: 'reserved',
130         7: 'reserved',
131         8: 'sha256',
132         9: 'sha384',
133         10: 'sha512',
134         11: 'sha224',
135         100: 'private',
136         101: 'private',
137         102: 'private',
138         103: 'private',
139         104: 'private',
140         105: 'private',
141         106: 'private',
142         107: 'private',
143         108: 'private',
144         109: 'private',
145         110: 'private',
146         }
147
148     _string_to_key_types = {
149         0: 'simple',
150         1: 'salted',
151         2: 'reserved',
152         3: 'iterated and salted',
153         100: 'private',
154         101: 'private',
155         102: 'private',
156         103: 'private',
157         104: 'private',
158         105: 'private',
159         106: 'private',
160         107: 'private',
161         108: 'private',
162         109: 'private',
163         110: 'private',
164         }
165
166     _clean_type_regex = _re.compile('\W+')
167
168     def _clean_type(self):
169         return self._clean_type_regex.sub('_', self['type'])
170
171     def from_bytes(self, data):
172         offset = self._parse_header(data=data)
173         packet = data[offset:offset + self['length']]
174         if len(packet) < self['length']:
175             raise ValueError('packet too short ({} < {})'.format(
176                 len(packet), self['length']))
177         offset += self['length']
178         method_name = '_parse_{}'.format(self._clean_type())
179         method = getattr(self, method_name, None)
180         if not method:
181             raise NotImplementedError(
182                 'cannot parse packet type {!r}'.format(self['type']))
183         method(data=packet)
184         return offset
185
186     def _parse_header(self, data):
187         packet_tag = data[0]
188         offset = 1
189         always_one = packet_tag & 1 << 7
190         if not always_one:
191             raise ValueError('most significant packet tag bit not set')
192         self['new-format'] = packet_tag & 1 << 6
193         if self['new-format']:
194             type_code = packet_tag & 0b111111
195             raise NotImplementedError('new-format packet length')
196         else:
197             type_code = packet_tag >> 2 & 0b1111
198             self['length-type'] = packet_tag & 0b11
199             length_bytes, length_type = self._old_format_packet_length_type[
200                 self['length-type']]
201             if not length_bytes:
202                 raise NotImplementedError(
203                     'old-format packet of indeterminate length')
204             length_format = '>{}'.format(length_type)
205             length_data = data[offset: offset + length_bytes]
206             offset += length_bytes
207             self['length'] = _struct.unpack(length_format, length_data)[0]
208         self['type'] = self._packet_types[type_code]
209         return offset
210
211     @staticmethod
212     def _parse_multiprecision_integer(data):
213         r"""Parse RFC 4880's multiprecision integers
214
215         >>> PGPPacket._parse_multiprecision_integer(b'\x00\x01\x01')
216         (3, 1)
217         >>> PGPPacket._parse_multiprecision_integer(b'\x00\x09\x01\xff')
218         (4, 511)
219         """
220         bits = _struct.unpack('>H', data[:2])[0]
221         offset = 2
222         length = (bits + 7) // 8
223         value = 0
224         for i in range(length):
225             value += data[offset + i] * 1 << (8 * (length - i - 1))
226         offset += length
227         return (offset, value)
228
229     def _parse_string_to_key_specifier(self, data):
230         self['string-to-key-type'] = self._string_to_key_types[data[0]]
231         offset = 1
232         if self['string-to-key-type'] == 'simple':
233             self['string-to-key-hash-algorithm'] = self._hash_algorithms[
234                 data[offset]]
235             offset += 1
236         elif self['string-to-key-type'] == 'salted':
237             self['string-to-key-hash-algorithm'] = self._hash_algorithms[
238                 data[offset]]
239             offset += 1
240             self['string-to-key-salt'] = data[offset: offset + 8]
241             offset += 8
242         elif self['string-to-key-type'] == 'iterated and salted':
243             self['string-to-key-hash-algorithm'] = self._hash_algorithms[
244                 data[offset]]
245             offset += 1
246             self['string-to-key-salt'] = data[offset: offset + 8]
247             offset += 8
248             self['string-to-key-coded-count'] = data[offset]
249             offset += 1
250         else:
251             raise NotImplementedError(
252                 'string-to-key type {}'.format(self['string-to-key-type']))
253         return offset
254
255     def _parse_public_key_packet(self, data):
256         self._parse_generic_public_key_packet(data=data)
257
258     def _parse_public_subkey_packet(self, data):
259         self._parse_generic_public_key_packet(data=data)
260
261     def _parse_generic_public_key_packet(self, data):
262         self['key-version'] = data[0]
263         offset = 1
264         if self['key-version'] != 4:
265             raise NotImplementedError(
266                 'public (sub)key packet version {}'.format(
267                     self['key-version']))
268         length = 5
269         self['creation-time'], algorithm = _struct.unpack(
270             '>IB', data[offset: offset + length])
271         offset += length
272         self['public-key-algorithm'] = self._public_key_algorithms[algorithm]
273         if self['public-key-algorithm'].startswith('rsa '):
274             o, self['public-modulus'] = self._parse_multiprecision_integer(
275                 data[offset:])
276             offset += o
277             o, self['public-exponent'] = self._parse_multiprecision_integer(
278                 data[offset:])
279             offset += o
280         elif self['public-key-algorithm'].startswith('dsa '):
281             o, self['prime'] = self._parse_multiprecision_integer(
282                 data[offset:])
283             offset += o
284             o, self['group-order'] = self._parse_multiprecision_integer(
285                 data[offset:])
286             offset += o
287             o, self['group-generator'] = self._parse_multiprecision_integer(
288                 data[offset:])
289             offset += o
290             o, self['public-key'] = self._parse_multiprecision_integer(
291                 data[offset:])
292             offset += o
293         elif self['public-key-algorithm'].startswith('elgamal '):
294             o, self['prime'] = self._parse_multiprecision_integer(
295                 data[offset:])
296             offset += o
297             o, self['group-generator'] = self._parse_multiprecision_integer(
298                 data[offset:])
299             offset += o
300             o, self['public-key'] = self._parse_multiprecision_integer(
301                 data[offset:])
302             offset += o
303         else:
304             raise NotImplementedError(
305                 'algorithm-specific key fields for {}'.format(
306                     self['public-key-algorithm']))
307         return offset
308
309     def _parse_secret_key_packet(self, data):
310         self._parse_generic_secret_key_packet(data=data)
311
312     def _parse_secret_subkey_packet(self, data):
313         self._parse_generic_secret_key_packet(data=data)
314
315     def _parse_generic_secret_key_packet(self, data):
316         offset = self._parse_generic_public_key_packet(data=data)
317         string_to_key_usage = data[offset]
318         offset += 1
319         if string_to_key_usage in [255, 254]:
320             self['symmetric-encryption-algorithm'] = (
321                 self._symmetric_key_algorithms[data[offset]])
322             offset += 1
323             offset += self._parse_string_to_key_specifier(data=data[offset:])
324         else:
325             self['symmetric-encryption-algorithm'] = (
326                 self._symmetric_key_algorithms[string_to_key_usage])
327         if string_to_key_usage:
328             block_size_bits = self._cipher_block_size.get(
329                 self['symmetric-encryption-algorithm'], None)
330             if block_size_bits % 8:
331                 raise NotImplementedError(
332                     ('{}-bit block size for {} is not an integer number of bytes'
333                      ).format(
334                          block_size_bits, self['symmetric-encryption-algorithm']))
335             block_size = block_size_bits // 8
336             if not block_size:
337                 raise NotImplementedError(
338                     'unknown block size for {}'.format(
339                         self['symmetric-encryption-algorithm']))
340             self['initial-vector'] = data[offset: offset + block_size]
341             offset += block_size
342         if string_to_key_usage in [0, 255]:
343             key_end = -2
344         else:
345             key_end = 0
346         self['secret-key'] = data[offset:key_end]
347         if key_end:
348             self['secret-key-checksum'] = data[key_end:]
349
350     def to_bytes(self):
351         pass
352
353
354 def packets_from_bytes(data):
355     offset = 0
356     while offset < len(data):
357         packet = PGPPacket()
358         offset += packet.from_bytes(data=data[offset:])
359         yield packet
360
361
362 def migrate(old_key, new_key):
363     """Add the old key and sub-keys to the new key
364
365     For example, to upgrade your master key, while preserving old
366     signatures you'd made.  You will lose signature *on* your old key
367     though, since sub-keys can't be signed (I don't think).
368     """
369     old_key_export = _get_stdout(
370         ['gpg', '--export', old_key])
371     old_key_packets = list(
372         packets_from_bytes(data=old_key_export))
373     if old_key_packets[0]['type'] != 'public-key packet':
374         raise ValueError(
375             '{} does not start with a public-key packet'.format(
376                 old_key))
377     old_key_secret_export = _get_stdout(
378         ['gpg', '--export-secret-keys', old_key])
379     old_key_secret_packets = list(
380         packets_from_bytes(data=old_key_secret_export))
381
382     import pprint
383     pprint.pprint(old_key_packets)
384     pprint.pprint(old_key_secret_packets)
385
386
387 if __name__ == '__main__':
388     import sys as _sys
389
390     old_key, new_key = _sys.argv[1:3]
391     migrate(old_key=old_key, new_key=new_key)