e75cab84803dfd9c8866871246c16ea5e33d6531
[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     _compression_algorithms = {
103         0: 'uncompressed',
104         1: 'zip',
105         2: 'zlib',
106         3: 'bzip2',
107         100: 'private',
108         101: 'private',
109         102: 'private',
110         103: 'private',
111         104: 'private',
112         105: 'private',
113         106: 'private',
114         107: 'private',
115         108: 'private',
116         109: 'private',
117         110: 'private',
118         }
119
120     _hash_algorithms = {
121         1: 'md5',
122         2: 'sha-1',
123         3: 'ripe-md/160',
124         4: 'reserved',
125         5: 'reserved',
126         6: 'reserved',
127         7: 'reserved',
128         8: 'sha256',
129         9: 'sha384',
130         10: 'sha512',
131         11: 'sha224',
132         100: 'private',
133         101: 'private',
134         102: 'private',
135         103: 'private',
136         104: 'private',
137         105: 'private',
138         106: 'private',
139         107: 'private',
140         108: 'private',
141         109: 'private',
142         110: 'private',
143         }
144
145     _clean_type_regex = _re.compile('\W+')
146
147     def _clean_type(self):
148         return self._clean_type_regex.sub('_', self['type'])
149
150     def from_bytes(self, data):
151         offset = self._parse_header(data=data)
152         packet = data[offset:offset + self['length']]
153         if len(packet) < self['length']:
154             raise ValueError('packet too short ({} < {})'.format(
155                 len(packet), self['length']))
156         offset += self['length']
157         method_name = '_parse_{}'.format(self._clean_type())
158         method = getattr(self, method_name, None)
159         if not method:
160             raise NotImplementedError(
161                 'cannot parse packet type {!r}'.format(self['type']))
162         method(data=packet)
163         return offset
164
165     def _parse_header(self, data):
166         packet_tag = data[0]
167         offset = 1
168         always_one = packet_tag & 1 << 7
169         if not always_one:
170             raise ValueError('most significant packet tag bit not set')
171         self['new-format'] = packet_tag & 1 << 6
172         if self['new-format']:
173             type_code = packet_tag & 0b111111
174             raise NotImplementedError('new-format packet length')
175         else:
176             type_code = packet_tag >> 2 & 0b1111
177             self['length-type'] = packet_tag & 0b11
178             length_bytes, length_type = self._old_format_packet_length_type[
179                 self['length-type']]
180             if not length_bytes:
181                 raise NotImplementedError(
182                     'old-format packet of indeterminate length')
183             length_format = '>{}'.format(length_type)
184             length_data = data[offset: offset + length_bytes]
185             offset += length_bytes
186             self['length'] = _struct.unpack(length_format, length_data)[0]
187         self['type'] = self._packet_types[type_code]
188         return offset
189
190     def _parse_public_key_packet(self, data):
191         self._parse_generic_public_key_packet(data=data)
192
193     def _parse_public_subkey_packet(self, data):
194         self._parse_generic_public_key_packet(data=data)
195
196     def _parse_generic_public_key_packet(self, data):
197         self['key-version'] = data[0]
198         offset = 1
199         if self['key-version'] != 4:
200             raise NotImplementedError(
201                 'public (sub)key packet version {}'.format(
202                     self['key-version']))
203         length = 5
204         self['creation_time'], self['public-key-algorithm'] = _struct.unpack(
205             '>IB', data[offset: offset + length])
206         offset += length
207         self['key'] = data[offset:]
208
209     def to_bytes(self):
210         pass
211
212
213 def packets_from_bytes(data):
214     offset = 0
215     while offset < len(data):
216         packet = PGPPacket()
217         offset += packet.from_bytes(data=data[offset:])
218         yield packet
219
220
221 def migrate(old_key, new_key):
222     """Add the old key and sub-keys to the new key
223
224     For example, to upgrade your master key, while preserving old
225     signatures you'd made.  You will lose signature *on* your old key
226     though, since sub-keys can't be signed (I don't think).
227     """
228     old_key_export = _get_stdout(
229         ['gpg', '--export', old_key])
230     old_key_packets = list(
231         packets_from_bytes(data=old_key_export))
232     if old_key_packets[0]['type'] != 'public-key packet':
233         raise ValueError(
234             '{} does not start with a public-key packet'.format(
235                 old_key))
236     old_key_secret_export = _get_stdout(
237         ['gpg', '--export-secret-keys', old_key])
238     old_key_secret_packets = list(
239         packets_from_bytes(data=old_key_secret_export))
240
241     import pprint
242     pprint.pprint(old_key_packets)
243     pprint.pprint(old_key_secret_packets)
244
245
246 if __name__ == '__main__':
247     import sys as _sys
248
249     old_key, new_key = _sys.argv[1:3]
250     migrate(old_key=old_key, new_key=new_key)