import struct import time import sys import base64 import math openpgp_publickey_format = ">BIB" mpi_format = ">H" packet_length_format = ">I" crc_format = ">I" # determine the index of the highest bit set to 1 def count_bits(B): R = 0 i = 0 while B > 0: i += 1 if B & 0x1: R = i B >>= 1 return R # convert a number to an array of bytes # the bytes in the array are stored in big-endian order. # the most significant byte is stored as the first item # in the array def number_to_bytes(B): R = [] bits = 0 while B > 0xff: bits += 8 R.append(B & 0xff) B >>= 8 R.append(B) bits += count_bits(B) return bits, ''.join(map(chr, reversed(R))) # An MPI is a byte array that starts with a two byte # length header. The length is given in bits def number_to_mpi(B): C, A = number_to_bytes(B) return struct.pack(mpi_format, C) + A # A PGP public key header consists of a byte "4", # an integer (4 byte) timestamp and a byte "1" (RSA) def public_key_header(T): return struct.pack(openpgp_publickey_format, 4, T, 1) # A public key packet is the public key header # plus 2 MPI numbers, the RSA modulus N and # the RSA exponent e. def public_key_packet(t, n, e): return ''.join((public_key_header(t), number_to_mpi(n), number_to_mpi(e),)) # A comment or userid packet is a string encoded as utf-8 def userid_packet(s): return s.encode('utf8') # The PGP format is a stream of "packets". # Each packet has a header. This header consists of a tag # and a length field. The tag has flags to determine if it is a # "new" or "old" packet. The only supported encoding is "new". def encode_packet(packet_bytes, tag = 6): # 0x80, 8th bit always set, 7th bit set --> new packet h = 0x80 | 0x40 # 0-5 bits -> the tag h |= tag header = chr(h) # dude, this is totally how you may save 2 or 3 bytes with minimal complexity l = len(packet_bytes) if l < 192: header += chr(l) elif l < 8384: l -= 192 o1 = l >> 0xff o2 = l & 0xff header += chr(o1 + 192) + chr(o2) else: header += chr(0xff) + struct.pack(packet_length_format, l) return header + packet_bytes # When you encode binary data as an ascii text with base64 # this data becomes fragile. So a CRC code is needed to # fix this. def crc24(s): R = 0xB704CE for char in s: B = ord(char) R ^= B << 16 for i in range(8): R <<= 1; if R & 0x1000000: R ^= 0x1864CFB return R & 0xFFFFFF # Create a public key for consumption by Phuctor. # The public key needs to contain 2 packets # one for the key data (n, e) # one for the comment # It must be in the armor / ascii format. def enarmored_public_key(n, e, comment, t): R = [] # the header R.append("-----BEGIN PGP PUBLIC KEY BLOCK-----") R.append("") # the packets in bytes A = encode_packet(public_key_packet(t, n, e), 6) A += encode_packet(userid_packet(comment), 13) # the packets in base64 encoding with line length max 76 s=base64.b64encode(A) i = 0 while i < len(s): R.append(s[i:i+76]) i += 76 # the CRC R.append("="+base64.b64encode(struct.pack(crc_format, crc24(A))[1:])) # the footer R.append("") R.append("-----END PGP PUBLIC KEY BLOCK-----") return '\n'.join(R) # read a file with comma separated lines # each line is in the TMSR format: e,n,comment if __name__ == "__main__": ser = 1 for x in sys.stdin: x = x.strip() # ignore empty lines if len(x) == 0 or x.startswith('#'): continue # the comment may contain comma's so split on the first 2 e,n,comment = x.split(',', 2) t0 = int(time.time()) with open("{0}.txt".format(ser), "wb") as stream: stream.write(enarmored_public_key(int(n), int(e), comment, t0)) ser += 1