Stub out signature subpacket parsing in PGPPacket
[gpg-migrate.git] / gpg-migrate.py
index 7fea58d141e630aeebbdc958da1e8d562b09228a..08ba42402e36467bf1c8d70fd37196380141ec2e 100755 (executable)
@@ -1,5 +1,6 @@
 #!/usr/bin/python
 
+import hashlib as _hashlib
 import re as _re
 import subprocess as _subprocess
 import struct as _struct
@@ -99,6 +100,13 @@ class PGPPacket (dict):
         110: 'private',
         }
 
+    _cipher_block_size = {  # in bits
+        'aes with 128-bit key': 128,
+        'aes with 192-bit key': 128,
+        'aes with 256-bit key': 128,
+        'cast5': 64,
+        }
+
     _compression_algorithms = {
         0: 'uncompressed',
         1: 'zip',
@@ -160,11 +168,110 @@ class PGPPacket (dict):
         110: 'private',
         }
 
+    _signature_types = {
+        0x00: 'binary document',
+        0x01: 'canonical text document',
+        0x02: 'standalone',
+        0x10: 'generic user id and public-key packet',
+        0x11: 'persona user id and public-key packet',
+        0x12: 'casual user id and public-key packet',
+        0x13: 'postitive user id and public-key packet',
+        0x18: 'subkey binding',
+        0x19: 'primary key binding',
+        0x1F: 'direct key',
+        0x20: 'key revocation',
+        0x28: 'subkey revocation',
+        0x30: 'certification revocation',
+        0x40: 'timestamp',
+        0x50: 'third-party confirmation',
+        }
+
+    _signature_subpacket_types = {
+        0: 'reserved',
+        1: 'reserved',
+        2: 'signature creation time',
+        3: 'signature expiration time',
+        4: 'exportable certification',
+        5: 'trust signature',
+        6: 'regular expression',
+        7: 'revocable',
+        8: 'reserved',
+        9: 'key expiration time',
+        10: 'placeholder for backward compatibility',
+        11: 'preferred symmetric algorithms',
+        12: 'revocation key',
+        13: 'reserved',
+        14: 'reserved',
+        15: 'reserved',
+        16: 'issuer',
+        17: 'reserved',
+        18: 'reserved',
+        19: 'reserved',
+        20: 'notation data',
+        21: 'preferred hash algorithms',
+        22: 'preferred compression algorithms',
+        23: 'key server preferences',
+        24: 'preferred key server',
+        25: 'primary user id',
+        26: 'policy uri',
+        27: 'key flags',
+        28: 'signer user id',
+        29: 'reason for revocation',
+        30: 'features',
+        31: 'signature target',
+        32: 'embedded signature',
+        100: 'private',
+        101: 'private',
+        102: 'private',
+        103: 'private',
+        104: 'private',
+        105: 'private',
+        106: 'private',
+        107: 'private',
+        108: 'private',
+        109: 'private',
+        110: 'private',
+        }
+
     _clean_type_regex = _re.compile('\W+')
 
     def _clean_type(self):
         return self._clean_type_regex.sub('_', self['type'])
 
+    def __str__(self):
+        method_name = '_str_{}'.format(self._clean_type())
+        method = getattr(self, method_name, None)
+        if not method:
+            return self['type']
+        details = method()
+        return '{}: {}'.format(self['type'], details)
+
+    def _str_public_key_packet(self):
+        return self._str_generic_key_packet()
+
+    def _str_public_subkey_packet(self):
+        return self._str_generic_key_packet()
+
+    def _str_secret_key_packet(self):
+        return self._str_generic_key_packet()
+
+    def _str_secret_subkey_packet(self):
+        return self._str_generic_key_packet()
+
+    def _str_generic_key_packet(self):
+        return self['fingerprint'][-8:].upper()
+
+    def _str_signature_packet(self):
+        lines = [self['signature-type']]
+        if self['unhashed-subpackets']:
+            lines.append('  unhashed subpackets:')
+            for subpacket in self['unhashed-subpackets']:
+                lines.append('    {}'.format(subpacket['type']))
+        return '\n'.join(lines)
+
+    def _str_user_id_packet(self):
+        return self['user']
+
     def from_bytes(self, data):
         offset = self._parse_header(data=data)
         packet = data[offset:offset + self['length']]
@@ -301,8 +408,111 @@ class PGPPacket (dict):
             raise NotImplementedError(
                 'algorithm-specific key fields for {}'.format(
                     self['public-key-algorithm']))
+        fingerprint = _hashlib.sha1()
+        fingerprint.update(b'\x99')
+        fingerprint.update(_struct.pack('>H', len(data)))
+        fingerprint.update(data)
+        self['fingerprint'] = fingerprint.hexdigest()
         return offset
 
+    def _parse_secret_key_packet(self, data):
+        self._parse_generic_secret_key_packet(data=data)
+
+    def _parse_secret_subkey_packet(self, data):
+        self._parse_generic_secret_key_packet(data=data)
+
+    def _parse_generic_secret_key_packet(self, data):
+        offset = self._parse_generic_public_key_packet(data=data)
+        string_to_key_usage = data[offset]
+        offset += 1
+        if string_to_key_usage in [255, 254]:
+            self['symmetric-encryption-algorithm'] = (
+                self._symmetric_key_algorithms[data[offset]])
+            offset += 1
+            offset += self._parse_string_to_key_specifier(data=data[offset:])
+        else:
+            self['symmetric-encryption-algorithm'] = (
+                self._symmetric_key_algorithms[string_to_key_usage])
+        if string_to_key_usage:
+            block_size_bits = self._cipher_block_size.get(
+                self['symmetric-encryption-algorithm'], None)
+            if block_size_bits % 8:
+                raise NotImplementedError(
+                    ('{}-bit block size for {} is not an integer number of bytes'
+                     ).format(
+                         block_size_bits, self['symmetric-encryption-algorithm']))
+            block_size = block_size_bits // 8
+            if not block_size:
+                raise NotImplementedError(
+                    'unknown block size for {}'.format(
+                        self['symmetric-encryption-algorithm']))
+            self['initial-vector'] = data[offset: offset + block_size]
+            offset += block_size
+        if string_to_key_usage in [0, 255]:
+            key_end = -2
+        else:
+            key_end = 0
+        self['secret-key'] = data[offset:key_end]
+        if key_end:
+            self['secret-key-checksum'] = data[key_end:]
+
+    def _parse_signature_subpackets(self, data):
+        offset = 0
+        while offset < len(data):
+            o, subpacket = self._parse_signature_subpacket(data=data[offset:])
+            offset += o
+            yield subpacket
+
+    def _parse_signature_subpacket(self, data):
+        subpacket = {}
+        first = data[0]
+        offset = 1
+        if first < 192:
+            length = first
+        elif first >= 192 and first < 255:
+            second = data[offset]
+            offset += 1
+            length = ((first - 192) << 8) + second + 192
+        else:
+            length = _struct.unpack(
+                '>I', data[offset: offset + 4])[0]
+            offset += 4
+        subpacket['type'] = self._signature_subpacket_types[data[offset]]
+        offset += 1
+        subpacket['data'] = data[offset: offset + length - 1]
+        offset += len(subpacket['data'])
+        return (offset, subpacket)
+
+    def _parse_signature_packet(self, data):
+        self['signature-version'] = data[0]
+        offset = 1
+        if self['signature-version'] != 4:
+            raise NotImplementedError(
+                'signature packet version {}'.format(
+                    self['signature-version']))
+        self['signature-type'] = self._signature_types[data[offset]]
+        offset += 1
+        self['public-key-algorithm'] = self._public_key_algorithms[
+            data[offset]]
+        offset += 1
+        self['hash-algorithm'] = self._hash_algorithms[data[offset]]
+        offset += 1
+        hashed_count = _struct.unpack('>H', data[offset: offset + 2])[0]
+        offset += 2
+        self['hashed-subpackets'] = data[offset: offset + hashed_count]
+        offset += hashed_count
+        unhashed_count = _struct.unpack('>H', data[offset: offset + 2])[0]
+        offset += 2
+        self['unhashed-subpackets'] = list(self._parse_signature_subpackets(
+            data=data[offset: offset + unhashed_count]))
+        offset += unhashed_count
+        self['signed-hash-word'] = data[offset: offset + 2]
+        offset += 2
+        self['signature'] = data[offset:]
+
+    def _parse_user_id_packet(self, data):
+        self['user'] = str(data, 'utf-8')
+
     def to_bytes(self):
         pass
 
@@ -315,6 +525,50 @@ def packets_from_bytes(data):
         yield packet
 
 
+class PGPKey (object):
+    def __init__(self, fingerprint):
+        self.fingerprint = fingerprint
+        self.public_packets = None
+        self.secret_packets = None
+
+    def __str__(self):
+        lines = ['key: {}'.format(self.fingerprint)]
+        if self.public_packets:
+            lines.append('  public:')
+            for packet in self.public_packets:
+                lines.extend(self._str_packet(packet=packet, prefix='    '))
+        if self.secret_packets:
+            lines.append('  secret:')
+            for packet in self.secret_packets:
+                lines.extend(self._str_packet(packet=packet, prefix='    '))
+        return '\n'.join(lines)
+
+    def _str_packet(self, packet, prefix):
+        lines = str(packet).split('\n')
+        return [prefix + line for line in lines]
+
+    def import_from_gpg(self):
+        key_export = _get_stdout(
+            ['gpg', '--export', self.fingerprint])
+        self.public_packets = list(
+            packets_from_bytes(data=key_export))
+        if self.public_packets[0]['type'] != 'public-key packet':
+            raise ValueError(
+                '{} does not start with a public-key packet'.format(
+                    self.fingerprint))
+        key_secret_export = _get_stdout(
+            ['gpg', '--export-secret-keys', self.fingerprint])
+        self.secret_packets = list(
+            packets_from_bytes(data=key_secret_export))
+
+    def export_to_gpg(self):
+        raise NotImplemetedError('export to gpg')
+
+    def import_from_key(self, key):
+        """Migrate the (sub)keys into this key"""
+        pass
+
+
 def migrate(old_key, new_key):
     """Add the old key and sub-keys to the new key
 
@@ -322,22 +576,14 @@ def migrate(old_key, new_key):
     signatures you'd made.  You will lose signature *on* your old key
     though, since sub-keys can't be signed (I don't think).
     """
-    old_key_export = _get_stdout(
-        ['gpg', '--export', old_key])
-    old_key_packets = list(
-        packets_from_bytes(data=old_key_export))
-    if old_key_packets[0]['type'] != 'public-key packet':
-        raise ValueError(
-            '{} does not start with a public-key packet'.format(
-                old_key))
-    old_key_secret_export = _get_stdout(
-        ['gpg', '--export-secret-keys', old_key])
-    old_key_secret_packets = list(
-        packets_from_bytes(data=old_key_secret_export))
-
-    import pprint
-    pprint.pprint(old_key_packets)
-    pprint.pprint(old_key_secret_packets)
+    old_key = PGPKey(fingerprint=old_key)
+    old_key.import_from_gpg()
+    new_key = PGPKey(fingerprint=new_key)
+    new_key.import_from_gpg()
+    new_key.import_from_key(key=old_key)
+
+    print(old_key)
+    print(new_key)
 
 
 if __name__ == '__main__':