#!/usr/bin/python
import hashlib as _hashlib
+import math as _math
import re as _re
import subprocess as _subprocess
import struct as _struct
type = self['type']
return self._clean_type_regex.sub('_', type)
+ @staticmethod
+ def _reverse(dict, value):
+ """Reverse lookups in dictionaries
+
+ >>> PGPPacket._reverse(PGPPacket._packet_types, 'public-key packet')
+ 6
+ """
+ return [k for k,v in dict.items() if v == value][0]
+
def __str__(self):
method_name = '_str_{}'.format(self._clean_type())
method = getattr(self, method_name, None)
def _str_issuer_signature_subpacket(self, subpacket):
return subpacket['issuer'][-8:].upper()
+ def _str_key_expiration_time_signature_subpacket(self, subpacket):
+ return str(subpacket['key-expiration-time'])
+
def _str_preferred_symmetric_algorithms_signature_subpacket(
self, subpacket):
return ', '.join(
def _parse_issuer_signature_subpacket(self, data, subpacket):
subpacket['issuer'] = ''.join('{:02x}'.format(byte) for byte in data)
+ def _parse_key_expiration_time_signature_subpacket(
+ self, data, subpacket):
+ subpacket['key-expiration-time'] = _struct.unpack('>I', data)[0]
+
def _parse_preferred_symmetric_algorithms_signature_subpacket(
self, data, subpacket):
subpacket['preferred-symmetric-algorithms'] = [
self['user'] = str(data, 'utf-8')
def to_bytes(self):
- pass
+ method_name = '_serialize_{}'.format(self._clean_type())
+ method = getattr(self, method_name, None)
+ if not method:
+ raise NotImplementedError(
+ 'cannot serialize packet type {!r}'.format(self['type']))
+ body = method()
+ self['length'] = len(body)
+ return b''.join([
+ self._serialize_header(),
+ body,
+ ])
+
+ def _serialize_header(self):
+ always_one = 1
+ new_format = 0
+ type_code = self._reverse(self._packet_types, self['type'])
+ packet_tag = (
+ always_one * (1 << 7) |
+ new_format * (1 << 6) |
+ type_code * (1 << 2) |
+ self['length-type']
+ )
+ length_bytes, length_type = self._old_format_packet_length_type[
+ self['length-type']]
+ length_format = '>{}'.format(length_type)
+ length_data = _struct.pack(length_format, self['length'])
+ return b''.join([
+ bytes([packet_tag]),
+ length_data,
+ ])
+
+ @staticmethod
+ def _serialize_multiprecision_integer(integer):
+ r"""Serialize RFC 4880's multipricision integers
+
+ >>> PGPPacket._serialize_multiprecision_integer(1)
+ b'\x00\x01\x01'
+ >>> PGPPacket._serialize_multiprecision_integer(511)
+ b'\x00\t\x01\xff'
+ """
+ bit_length = int(_math.log(integer, 2)) + 1
+ chunks = [
+ _struct.pack('>H', bit_length),
+ ]
+ while integer > 0:
+ chunks.insert(1, bytes([integer & 0xff]))
+ integer = integer >> 8
+ return b''.join(chunks)
+
+ def _serialize_string_to_key_specifier(self):
+ string_to_key_type = bytes([
+ self._reverse(
+ self._string_to_key_types, self['string-to-key-type']),
+ ])
+ chunks = [string_to_key_type]
+ if self['string-to-key-type'] == 'simple':
+ chunks.append(bytes([self._reverse(
+ self._hash_algorithms, self['string-to-key-hash-algorithm'])]))
+ elif self['string-to-key-type'] == 'salted':
+ chunks.append(bytes([self._reverse(
+ self._hash_algorithms, self['string-to-key-hash-algorithm'])]))
+ chunks.append(self['string-to-key-salt'])
+ elif self['string-to-key-type'] == 'iterated and salted':
+ chunks.append(bytes([self._reverse(
+ self._hash_algorithms, self['string-to-key-hash-algorithm'])]))
+ chunks.append(self['string-to-key-salt'])
+ chunks.append(bytes([self['string-to-key-coded-count']]))
+ else:
+ raise NotImplementedError(
+ 'string-to-key type {}'.format(self['string-to-key-type']))
+ return offset
+ return b''.join(chunks)
+
+ def _serialize_public_key_packet(self):
+ return self._serialize_generic_public_key_packet()
+
+ def _serialize_public_subkey_packet(self):
+ return self._serialize_generic_public_key_packet()
+
+ def _serialize_generic_public_key_packet(self):
+ key_version = bytes([self['key-version']])
+ chunks = [key_version]
+ if self['key-version'] != 4:
+ raise NotImplementedError(
+ 'public (sub)key packet version {}'.format(
+ self['key-version']))
+ chunks.append(_struct.pack('>I', self['creation-time']))
+ chunks.append(bytes([self._reverse(
+ self._public_key_algorithms, self['public-key-algorithm'])]))
+ if self['public-key-algorithm'].startswith('rsa '):
+ chunks.append(self._serialize_multiprecision_integer(
+ self['public-modulus']))
+ chunks.append(self._serialize_multiprecision_integer(
+ self['public-exponent']))
+ elif self['public-key-algorithm'].startswith('dsa '):
+ chunks.append(self._serialize_multiprecision_integer(
+ self['prime']))
+ chunks.append(self._serialize_multiprecision_integer(
+ self['group-order']))
+ chunks.append(self._serialize_multiprecision_integer(
+ self['group-generator']))
+ chunks.append(self._serialize_multiprecision_integer(
+ self['public-key']))
+ elif self['public-key-algorithm'].startswith('elgamal '):
+ chunks.append(self._serialize_multiprecision_integer(
+ self['prime']))
+ chunks.append(self._serialize_multiprecision_integer(
+ self['group-generator']))
+ chunks.append(self._serialize_multiprecision_integer(
+ self['public-key']))
+ else:
+ raise NotImplementedError(
+ 'algorithm-specific key fields for {}'.format(
+ self['public-key-algorithm']))
+ return b''.join(chunks)
def packets_from_bytes(data):
class PGPKey (object):
+ """An OpenPGP key with public and private parts.
+
+ From RFC 4880 [1]:
+
+ OpenPGP users may transfer public keys. The essential elements
+ of a transferable public key are as follows:
+
+ - One Public-Key packet
+ - Zero or more revocation signatures
+ - One or more User ID packets
+ - After each User ID packet, zero or more Signature packets
+ (certifications)
+ - Zero or more User Attribute packets
+ - After each User Attribute packet, zero or more Signature
+ packets (certifications)
+ - Zero or more Subkey packets
+ - After each Subkey packet, one Signature packet, plus
+ optionally a revocation
+
+ Secret keys have a similar packet stream [2]:
+
+ OpenPGP users may transfer secret keys. The format of a
+ transferable secret key is the same as a transferable public key
+ except that secret-key and secret-subkey packets are used
+ instead of the public key and public-subkey packets.
+ Implementations SHOULD include self-signatures on any user IDs
+ and subkeys, as this allows for a complete public key to be
+ automatically extracted from the transferable secret key.
+ Implementations MAY choose to omit the self-signatures,
+ especially if a transferable public key accompanies the
+ transferable secret key.
+
+ [1]: http://tools.ietf.org/search/rfc4880#section-11.1
+ [2]: http://tools.ietf.org/search/rfc4880#section-11.2
+ """
def __init__(self, fingerprint):
self.fingerprint = fingerprint
self.public_packets = None
['gpg', '--export-secret-keys', self.fingerprint])
self.secret_packets = list(
packets_from_bytes(data=key_secret_export))
+ if self.secret_packets[0]['type'] != 'secret-key packet':
+ raise ValueError(
+ '{} does not start with a secret-key packet'.format(
+ self.fingerprint))
def export_to_gpg(self):
raise NotImplemetedError('export to gpg')