Add PGPPacket._serialize_generic_public_key_packet
[gpg-migrate.git] / gpg-migrate.py
index 47ea115904569695299a043a8a47af1421645ee2..8b0f1a22023388245564ab78886adb73d581a0bf 100755 (executable)
@@ -1,6 +1,7 @@
 #!/usr/bin/python
 
 import hashlib as _hashlib
+import math as _math
 import re as _re
 import subprocess as _subprocess
 import struct as _struct
@@ -240,6 +241,15 @@ class PGPPacket (dict):
             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)
@@ -295,6 +305,9 @@ class PGPPacket (dict):
     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(
@@ -583,6 +596,10 @@ class PGPPacket (dict):
     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'] = [
@@ -637,7 +654,121 @@ class PGPPacket (dict):
         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):
@@ -649,6 +780,41 @@ 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
@@ -683,6 +849,10 @@ class PGPKey (object):
             ['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')