3 import hashlib as _hashlib
5 import subprocess as _subprocess
6 import struct as _struct
9 def _get_stdout(args, stdin=None):
12 stdin_pipe = _subprocess.PIPE
13 p = _subprocess.Popen(args, stdin=stdin_pipe, stdout=_subprocess.PIPE)
14 stdout, stderr = p.communicate(stdin)
17 raise RuntimeError(status)
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
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',
42 11: 'literal data 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',
55 _public_key_algorithms = {
56 1: 'rsa (encrypt or sign)',
57 2: 'rsa encrypt-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',
78 _symmetric_key_algorithms = {
79 0: 'plaintext or unencrypted data',
86 7: 'aes with 128-bit key',
87 8: 'aes with 192-bit key',
88 9: 'aes with 256-bit key',
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,
110 _compression_algorithms = {
153 _string_to_key_types = {
157 3: 'iterated and salted',
172 0x00: 'binary document',
173 0x01: 'canonical text document',
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',
182 0x20: 'key revocation',
183 0x28: 'subkey revocation',
184 0x30: 'certification revocation',
186 0x50: 'third-party confirmation',
189 _clean_type_regex = _re.compile('\W+')
191 def _clean_type(self):
192 return self._clean_type_regex.sub('_', self['type'])
195 method_name = '_str_{}'.format(self._clean_type())
196 method = getattr(self, method_name, None)
200 return '{}: {}'.format(self['type'], details)
202 def _str_public_key_packet(self):
203 return self._str_generic_key_packet()
205 def _str_public_subkey_packet(self):
206 return self._str_generic_key_packet()
208 def _str_secret_key_packet(self):
209 return self._str_generic_key_packet()
211 def _str_secret_subkey_packet(self):
212 return self._str_generic_key_packet()
214 def _str_generic_key_packet(self):
215 return self['fingerprint'][-8:].upper()
217 def _str_user_id_packet(self):
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)
230 raise NotImplementedError(
231 'cannot parse packet type {!r}'.format(self['type']))
235 def _parse_header(self, data):
238 always_one = packet_tag & 1 << 7
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')
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[
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]
261 def _parse_multiprecision_integer(data):
262 r"""Parse RFC 4880's multiprecision integers
264 >>> PGPPacket._parse_multiprecision_integer(b'\x00\x01\x01')
266 >>> PGPPacket._parse_multiprecision_integer(b'\x00\x09\x01\xff')
269 bits = _struct.unpack('>H', data[:2])[0]
271 length = (bits + 7) // 8
273 for i in range(length):
274 value += data[offset + i] * 1 << (8 * (length - i - 1))
276 return (offset, value)
278 def _parse_string_to_key_specifier(self, data):
279 self['string-to-key-type'] = self._string_to_key_types[data[0]]
281 if self['string-to-key-type'] == 'simple':
282 self['string-to-key-hash-algorithm'] = self._hash_algorithms[
285 elif self['string-to-key-type'] == 'salted':
286 self['string-to-key-hash-algorithm'] = self._hash_algorithms[
289 self['string-to-key-salt'] = data[offset: offset + 8]
291 elif self['string-to-key-type'] == 'iterated and salted':
292 self['string-to-key-hash-algorithm'] = self._hash_algorithms[
295 self['string-to-key-salt'] = data[offset: offset + 8]
297 self['string-to-key-coded-count'] = data[offset]
300 raise NotImplementedError(
301 'string-to-key type {}'.format(self['string-to-key-type']))
304 def _parse_public_key_packet(self, data):
305 self._parse_generic_public_key_packet(data=data)
307 def _parse_public_subkey_packet(self, data):
308 self._parse_generic_public_key_packet(data=data)
310 def _parse_generic_public_key_packet(self, data):
311 self['key-version'] = data[0]
313 if self['key-version'] != 4:
314 raise NotImplementedError(
315 'public (sub)key packet version {}'.format(
316 self['key-version']))
318 self['creation-time'], algorithm = _struct.unpack(
319 '>IB', data[offset: 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(
326 o, self['public-exponent'] = self._parse_multiprecision_integer(
329 elif self['public-key-algorithm'].startswith('dsa '):
330 o, self['prime'] = self._parse_multiprecision_integer(
333 o, self['group-order'] = self._parse_multiprecision_integer(
336 o, self['group-generator'] = self._parse_multiprecision_integer(
339 o, self['public-key'] = self._parse_multiprecision_integer(
342 elif self['public-key-algorithm'].startswith('elgamal '):
343 o, self['prime'] = self._parse_multiprecision_integer(
346 o, self['group-generator'] = self._parse_multiprecision_integer(
349 o, self['public-key'] = self._parse_multiprecision_integer(
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()
363 def _parse_secret_key_packet(self, data):
364 self._parse_generic_secret_key_packet(data=data)
366 def _parse_secret_subkey_packet(self, data):
367 self._parse_generic_secret_key_packet(data=data)
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]
373 if string_to_key_usage in [255, 254]:
374 self['symmetric-encryption-algorithm'] = (
375 self._symmetric_key_algorithms[data[offset]])
377 offset += self._parse_string_to_key_specifier(data=data[offset:])
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'
388 block_size_bits, self['symmetric-encryption-algorithm']))
389 block_size = block_size_bits // 8
391 raise NotImplementedError(
392 'unknown block size for {}'.format(
393 self['symmetric-encryption-algorithm']))
394 self['initial-vector'] = data[offset: offset + block_size]
396 if string_to_key_usage in [0, 255]:
400 self['secret-key'] = data[offset:key_end]
402 self['secret-key-checksum'] = data[key_end:]
404 def _parse_signature_packet(self, data):
405 self['signature-version'] = data[0]
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]]
413 self['public-key-algorithm'] = self._public_key_algorithms[
416 self['hash-algorithm'] = self._hash_algorithms[data[offset]]
418 hashed_count = _struct.unpack('>H', data[offset: offset + 2])[0]
420 self['hashed-subpackets'] = data[offset: offset + hashed_count]
421 offset += hashed_count
422 unhashed_count = _struct.unpack('>H', data[offset: offset + 2])[0]
424 self['unhashed-subpackets'] = data[offset: offset + unhashed_count]
425 offset += unhashed_count
426 self['signed-hash-word'] = data[offset: offset + 2]
428 self['signature'] = data[offset:]
430 def _parse_user_id_packet(self, data):
431 self['user'] = str(data, 'utf-8')
437 def packets_from_bytes(data):
439 while offset < len(data):
441 offset += packet.from_bytes(data=data[offset:])
445 class PGPKey (object):
446 def __init__(self, fingerprint):
447 self.fingerprint = fingerprint
448 self.public_packets = None
449 self.secret_packets = None
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)
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':
470 '{} does not start with a public-key packet'.format(
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))
477 def export_to_gpg(self):
478 raise NotImplemetedError('export to gpg')
481 def migrate(old_key, new_key):
482 """Add the old key and sub-keys to the new key
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).
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()
497 if __name__ == '__main__':
500 old_key, new_key = _sys.argv[1:3]
501 migrate(old_key=old_key, new_key=new_key)