def write_rsa_pkcs1_private(opts: argparse.Namespace, rsa: RSAFactors) -> None: ''' Write the PKCS#1 private key. Citation: https://tls.mbed.org/kb/cryptography/asn1-key-structures-in-der-and-pem Citation: http://blog.oddbit.com/2011/05/08/converting-openssh-public-keys/ Citation: https://tools.ietf.org/html/rfc3447 Note that this wraps at 64 to match SSH. ''' pkcs1_seq = univ.Sequence() pkcs1_seq.setComponentByPosition(0, univ.Integer(rsa.m_version)) # Version (0-std, 1-multiprime) pkcs1_seq.setComponentByPosition(1, univ.Integer(rsa.m_modulus)) # modulus pkcs1_seq.setComponentByPosition(2, univ.Integer(rsa.m_public_exponent)) # publicExponent pkcs1_seq.setComponentByPosition(3, univ.Integer(rsa.m_private_exponent)) # privateExponent pkcs1_seq.setComponentByPosition(4, univ.Integer(rsa.m_prime1)) # prime1 pkcs1_seq.setComponentByPosition(5, univ.Integer(rsa.m_prime2)) # prime2 pkcs1_seq.setComponentByPosition(6, univ.Integer(rsa.m_exponent1)) # exponent1 pkcs1_seq.setComponentByPosition(7, univ.Integer(rsa.m_exponent2)) # exponent2 pkcs1_seq.setComponentByPosition(8, univ.Integer(rsa.m_crt_coefficient)) # coefficient data = base64.b64encode(der_encoder.encode(pkcs1_seq)) data_str = str(data, 'utf-8').rstrip() data_str = '\n'.join(textwrap.wrap(data_str, 64)) path = opts.out infov(opts, f'writing {path} - PCKS#1 private') with open(path, 'w') as ofp: ofp.write(f'''\ -----BEGIN RSA PRIVATE KEY----- {data_str} -----END RSA PRIVATE KEY----- ''')
def read_input(opts: argparse.Namespace) -> Tuple[bytes, int, int]: ''' Grab the bytes. ''' data = read_raw_input(opts) if b'-----BEGIN JOES RSA ENCRYPTED DATA-----' in data: # This is the PEM format, grab the binary data. first, *b64, last = str(data, 'utf-8').strip().split('\n') if first != '-----BEGIN JOES RSA ENCRYPTED DATA-----': err('invalid encrypted format: prefix not found') if last != '-----END JOES RSA ENCRYPTED DATA-----': err('invalid encrypted format: suffix not found') b64_str = ''.join([o.strip() for o in b64]) data = base64.b64decode(b64_str) # Check the binary fields. if data[:8] != b'joes-rsa': err(f'invalid encrypted format: expected prefix not found: {data[:8]}') version = struct.unpack('>H', data[8:10])[0] if version != 0: err(f'invalid encrypted format version: {version}, expected 0') padding = struct.unpack('>H', data[10:12])[0] infov(opts, f'version: {version}') infov(opts, f'size: {len(data[12:])}') return data[12:], len(data[12:]), padding
def read_raw_input(opts: argparse.Namespace) -> bytes: ''' Read the raw ciphertext data. It can be either binary or PEM. ''' if opts.input: with open(opts.input, 'rb') as ifp: return ifp.read() infov(opts, 'reading from stdin, type ^D on a new line to exit') return bytes(sys.stdin.read(), 'utf-8')
def main() -> None: ''' main ''' opts = getopts() if opts.seed: random.seed(opts.seed) modulus, pubexp = read_public_key_file(opts, opts.key) infovv(opts, f'modulus: 0x{modulus:x}') infovv(opts, f'pubexp : 0x{pubexp:x}') encrypt(opts, modulus, pubexp) infov(opts, 'done')
def main() -> None: ''' main ''' opts = getopts() if opts.seed: random.seed(opts.seed) rsa = read_pkcs1_prikey(opts) if opts.verbose > 1: infov(opts, 'RSA Parameters') dump_namedtuple(rsa) decrypt(opts, rsa) infov(opts, 'done')
def read_input(opts: argparse.Namespace) -> bytes: ''' Read the input data. There are two possible sources: 1. A file specified by the -i option. 2. stdin. In both cases, all data is read into memory which limits the file size to available memory. ''' if opts.input: with open(opts.input, 'rb') as ifp: return ifp.read() infov(opts, 'reading from stdin, type ^D on a new line to exit') return bytes(sys.stdin.read(), 'utf-8')
def main() -> None: ''' Main entry point. ''' opts = getopts() if opts.seed: random.seed(opts.seed) public_exponent = get_int_arg(opts.encrypt_exponent) if opts.primes: prime1 = get_int_arg(opts.primes[0]) prime2 = get_int_arg(opts.primes[1]) else: prime1 = generate_prime(opts) prime2 = generate_prime(opts) assert prime1 != prime2 rsa = RSAFactors(prime1, prime2, public_exponent) write_keys(opts, rsa) infov(opts, 'done')
def write_rsa_ssh_public(opts: argparse.Namespace, rsa: RSAFactors) -> None: ''' Write SSH format: https://tools.ietf.org/html/rfc4716. Citation: http://blog.oddbit.com/2011/05/08/converting-openssh-public-keys/ Citation: http://blog.thedigitalcatonline.com/blog/2018/04/25/rsa-keys/ The base64 fields are composed of 3 length/data pairs with all data in big endian format. 1. Prefix (the algorithm): length is 7 data is "ssh-rsa" 2. Public (encryption) key: length is the number of bytes in the public key data is the key 3. Modulus: length is the number of bytes + 1 (to guarantee unsigned) data is the modulus ''' # Prefix. pre_bytes = bytes('ssh-rsa', 'utf-8') pre_size = len(pre_bytes).to_bytes(4, byteorder='big') # Public key. pub_num_bytes = math.ceil(math.log(rsa.m_public_exponent, 256)) pub_bytes = rsa.m_public_exponent.to_bytes(pub_num_bytes, byteorder='big') pub_size = pub_num_bytes.to_bytes(4, byteorder='big') # Modulus. mod_num_bytes = 1 + math.ceil(math.log(rsa.m_modulus, 256)) mod_bytes = rsa.m_modulus.to_bytes(mod_num_bytes, byteorder='big') mod_size = mod_num_bytes.to_bytes(4, byteorder='big') # Byte data. byte_array = pre_size + pre_bytes + pub_size + pub_bytes + mod_size + mod_bytes data = base64.b64encode(byte_array) data_str = str(data, 'utf-8').rstrip() # Write to file. path = opts.out + '.pub' infov(opts, f'writing {path} - SSH public') with open(path, 'w') as ofp: ofp.write(f'''\ ssh-rsa {data_str} {getpass.getuser()}@{socket.gethostname()} ''')
def write_rsa_pkcs1_pem_public(opts: argparse.Namespace, rsa: RSAFactors) -> None: ''' Write PEM RSA public key: PKCS#1. Citation: http://blog.oddbit.com/2011/05/08/converting-openssh-public-keys/ Note that this wraps at 64 to match SSH. ''' pkcs1_seq = univ.Sequence() pkcs1_seq.setComponentByPosition(0, univ.Integer(rsa.m_modulus)) pkcs1_seq.setComponentByPosition(1, univ.Integer(rsa.m_public_exponent)) data = base64.b64encode(der_encoder.encode(pkcs1_seq)) data_str = str(data, 'utf-8').rstrip() data_str = '\n'.join(textwrap.wrap(data_str, 64)) path = opts.out + '.pub.pem' infov(opts, f'writing {path} - PCKS#1 public') with open(path, 'w') as ofp: ofp.write(f'''\ -----BEGIN RSA PUBLIC KEY----- {data_str} -----END RSA PUBLIC KEY----- ''')
def decrypt(opts: argparse.Namespace, rsa: namedtuple) -> None: ''' Decrypt the file data. ''' infov(opts, 'reading the input data') ciphertext, size, padding = read_input(opts) plaintext = bytes([]) bytes_per_block = 1 + (math.ceil(math.log(rsa.modulus, 2)) // 8) infov(opts, f'bytes/block: {bytes_per_block}') infov(opts, f'padding: {padding}') for i in range(0, size, bytes_per_block): end = i + bytes_per_block block = ciphertext[i:end] block = bytes([0]) + block # Convert the block to an integer for computation. block_int = int.from_bytes(block, 'big') # Decrypt. block_dec_int = int(pow(block_int, rsa.priexp, rsa.modulus)) # Added to the decrypted bytes array. block_bytes = block_dec_int.to_bytes(bytes_per_block, byteorder='big') plaintext += block_bytes[1:] # ignore the extra byte added by encrypt if padding: plaintext = plaintext[:-padding] if opts.output: with open(opts.output, 'wb') as ofp: ofp.write(plaintext) else: sys.stdout.write(str(plaintext, 'utf-8'))
def read_pkcs1_prikey(opts: argparse.Namespace) -> namedtuple: ''' Read the RSA private key file. ''' infov(opts, f'reading key file {opts.key}') with open(opts.key, 'r') as ifp: first, *b64, last = ifp.readlines() assert first.strip() == '-----BEGIN RSA PRIVATE KEY-----' assert last.strip() == '-----END RSA PRIVATE KEY-----' b64_str = ''.join([o.strip() for o in b64]) b64_bytes = base64.b64decode(b64_str) # This is decoded raw, with no structure, that is why # recursion is disabled. _, msg = der_decoder.decode(b64_bytes, asn1Spec=univ.Sequence(), recursiveFlag=False) version, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) modulus, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) pubexp, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) priexp, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) prime1, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) prime2, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) exponent1, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) exponent2, msg = der_decoder.decode(msg, asn1Spec=univ.Integer()) crt_coeff, _ = der_decoder.decode(msg, asn1Spec=univ.Integer()) rec = { 'version': version, 'modulus': int(modulus), 'pubexp': int(pubexp), 'priexp': int(priexp), 'prime1': int(prime1), 'prime2': int(prime2), 'exponent1': int(exponent1), 'exponent2': int(exponent2), 'crt_coeff': int(crt_coeff), } ntdef = namedtuple('_', sorted(rec.keys())) return ntdef(**rec)
def read_public_key_file(opts: argparse.Namespace, path: str) -> Tuple[int, int]: ''' Figure out which type of file this is and read it. ''' infov(opts, f'opening key file: {path}') with open(path, 'r') as ifp: data = ifp.read() if '-----BEGIN RSA PUBLIC KEY-----' in data: infov(opts, f'key type: PKCS#1 (RSA) PEM public key file') return read_pem_rsa_pubkey(path) if 'ssh-rsa' in data: infov(opts, f'key type: SSH RSA public key file') _, pub, mod = read_ssh_rsa_pubkey(path) return mod, pub err(f'unrecognized file format in {path}.') return -1, -1
def encrypt(opts: argparse.Namespace, modulus: int, pubexp: int) -> None: ''' Encrypt the input using RSA. ''' infov(opts, 'reading the input data') plaintext = read_input(opts) infov(opts, f'read {len(plaintext)} bytes') num_bits = int(math.ceil(math.log(modulus, 2))) bytes_per_block = num_bits // 8 # based on bits infov(opts, f'num_bits: {num_bits}') infov(opts, f'bytes/block: {bytes_per_block}') assert bytes_per_block < 0xffff # we only allocate 2 bytes for padding padding = 0 while len(plaintext) % bytes_per_block: padding += 1 plaintext += b'x' infov(opts, f'padding: {padding}') assert (len(plaintext) % bytes_per_block) == 0 ciphertext = bytes([]) for i in range(0, len(plaintext), bytes_per_block): end = i + bytes_per_block block = plaintext[i:end] # Convert the block to an integer for computation. # Arbitrarily chose big endian because consistency is needed and # 'big' is fewer letters than 'little'. Also because 'big' is # 'network order'. block_int = int.from_bytes(block, 'big') # Encrypt. # Use the fast modular exponentiation algorithm provided by # python. block_enc_int = int(pow(block_int, pubexp, modulus)) # Add to the encrypted bytes array. # The MSB is always zero. block_bytes = block_enc_int.to_bytes(bytes_per_block + 1, byteorder='big') ciphertext += block_bytes # Setup the prefix. version = 0 prefix = bytes('joes-rsa', 'utf-8') prefix += version.to_bytes(2, 'big') prefix += padding.to_bytes(2, 'big') ciphertext = prefix + ciphertext # At this point the data is encrypted. # If the user did not specify binary output, output in base64. if opts.binary: encb = ciphertext encs = str(ciphertext) mode = 'wb' else: b64 = base64.b64encode(ciphertext) data_str = str(b64, 'utf-8').rstrip() data_str = '\n'.join(textwrap.wrap(data_str, 64)) encs = f'''\ -----BEGIN JOES RSA ENCRYPTED DATA----- {data_str} -----END JOES RSA ENCRYPTED DATA----- ''' mode = 'w' # Write out the data. if opts.output: infov(opts, f'writing to {opts.output}') with open(opts.output, mode) as ofp: if opts.binary: ofp.write(encb) else: ofp.write(encs) else: infov(opts, 'writing to stdout') sys.stdout.write(encs)