├── __init__.py ├── crypto ├── __init__.py ├── salsa20.py ├── public_key.py ├── private_key.py ├── keygen.py ├── pkcs.py ├── protocol.py └── qcmdpc.py ├── attacks ├── __init__.py └── distinguisher.py ├── operations ├── __init__.py ├── conversion.py ├── randomgen.py ├── arithmetic.py └── keyio.py ├── test.py ├── pqp └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crypto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /attacks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /operations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /attacks/distinguisher.py: -------------------------------------------------------------------------------- 1 | class Distinguisher: 2 | 3 | def __init__(self, block_weight, block_error): 4 | self.parity = (block_error + block_error) % 2 5 | 6 | def distinguish(self, c): 7 | if sum(list(c)) % 2 == self.parity: 8 | return True 9 | else: 10 | return False -------------------------------------------------------------------------------- /crypto/salsa20.py: -------------------------------------------------------------------------------- 1 | def rot(int32, steps): 2 | return ((int32 << steps) | (int32 >> (np.uint32(32) - steps))) 3 | 4 | def q_round(a, b, c, d): 5 | global x 6 | x[a] = x[a] + x[b]; x[d] = rot(x[d] ^ x[a],16); 7 | x[c] = x[c] + x[d]; x[b] = rot(x[b] ^ x[c],12); 8 | x[a] = x[a] + x[b]; x[d] = rot(x[d] ^ x[a], 8); 9 | x[c] = x[c] + x[d]; x[b] = rot(x[b] ^ x[c], 7); -------------------------------------------------------------------------------- /crypto/public_key.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | class PublicKey: 20 | 21 | def __init__(self): 22 | self.block_length = 4801 # this is insecure, but OK for tests 23 | self.block_weight = 45 24 | self.block_error = 42 25 | 26 | self.G = [] 27 | 28 | -------------------------------------------------------------------------------- /crypto/private_key.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | class PrivateKey: 20 | 21 | def __init__(self): 22 | self.block_length = 4801 # this is insecure, but OK for tests 23 | self.block_weight = 45 24 | self.block_error = 42 25 | 26 | self.H_0 = [] 27 | self.H_1 = [] 28 | self.H_1inv = [] -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from crypto.protocol import * 4 | 5 | message = b'this is a really secret message that is padded with some random.' 6 | 7 | # create a my_protocol wrapper object 8 | my_protocol = Protocol() 9 | 10 | # generate keypair 11 | my_protocol.generate_keypair() 12 | 13 | # encrypt and compute ciphertext / simulate sender 14 | ciphertext = my_protocol.encrypt_message(message, my_protocol.pub_key) 15 | 16 | # output the ciphertext 17 | print ciphertext 18 | 19 | # decrypt ciphertext / simulate receiver 20 | message, verified = my_protocol.decrypt_message(ciphertext) 21 | 22 | if verified: 23 | print 'Message: ', message 24 | else: 25 | print 'Something has been tampered with!' 26 | 27 | ############################################################ 28 | 29 | #from attacks.distinguisher import * 30 | 31 | # Distinguisher susceptibility 32 | # https://grocid.net/2015/01/28/attack-on-prime-length-qc-mdpc/ 33 | 34 | #distinguisher = Distinguisher(priv_key.block_error, priv_key.block_weight) 35 | #if distinguisher.distinguish(c_0) and distinguisher.distinguish(c_1): 36 | # print 'Both blocks distinguished' 37 | -------------------------------------------------------------------------------- /operations/conversion.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import numpy as np 20 | 21 | from binascii import hexlify 22 | from hashlib import sha512 23 | 24 | def to_bin(vec, length): 25 | num = int(''.join([str(x) for x in list(vec)]), 2) 26 | return ('%%0%dx' % (length << 1) % num).decode('hex')[-length:] # libnum 27 | 28 | def from_bin(binary): 29 | return np.array([int(x) for x in bin(int(hexlify(binary), 16))[2:]]) 30 | 31 | # just some packing operation 32 | def pack(vec): 33 | return sha512(''.join([str(x) for x in list(vec)])).digest() 34 | 35 | def to_int(vec): 36 | s = ''.join(str(x) for x in vec[::-1]) 37 | return int(s, 2) 38 | 39 | def from_int(num): 40 | return np.array([int(x) for x in bin(num)[2:]][::-1]) -------------------------------------------------------------------------------- /operations/randomgen.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import numpy as np 20 | 21 | from random import SystemRandom 22 | 23 | class RandomGenerator: 24 | 25 | def __init__(self): 26 | self.gen = SystemRandom() 27 | 28 | def get_random_vector(self, length): 29 | random_vector = np.array([self.gen.randrange(2) for i in range(length)]) 30 | return random_vector 31 | 32 | def get_random_weight_vector(self, length, weight): 33 | random_indices = set([self.gen.randrange(length) for i in range(weight)]) 34 | 35 | while len(random_indices) < weight: 36 | random_indices.update([self.gen.randrange(length)]) 37 | 38 | random_vector = np.zeros(length, dtype='int') 39 | random_vector[list(random_indices)] = 1 40 | 41 | return random_vector 42 | 43 | def flip_coin(self): 44 | return self.gen.randrange(2) 45 | -------------------------------------------------------------------------------- /crypto/keygen.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import numpy as np 20 | from base64 import b64encode, decodestring 21 | 22 | from operations.arithmetic import * 23 | from crypto.private_key import * 24 | from crypto.public_key import * 25 | from operations.randomgen import * 26 | 27 | class Keygen: 28 | 29 | def __init__(self): 30 | self.block_length = 4801 31 | self.block_weight = 45 32 | self.block_error = 42 33 | 34 | self.rate = [1,2] 35 | self.randgen = RandomGenerator() 36 | 37 | def generate(self): 38 | # create keypair 39 | priv_key = PrivateKey() 40 | pub_key = PublicKey() 41 | 42 | # set private-key parameters 43 | priv_key.H_0 = self.randgen.get_random_weight_vector(self.block_length, self.block_weight) 44 | priv_key.H_1 = self.randgen.get_random_weight_vector(self.block_length, self.block_weight) 45 | priv_key.H_1inv = exp_poly(priv_key.H_1, 2**1200 - 2) 46 | 47 | priv_key.block_length = self.block_length 48 | priv_key.block_weight = self.block_weight 49 | priv_key.block_error = self.block_error 50 | 51 | # set public-key parameters 52 | pub_key.G = mul_poly(priv_key.H_0, priv_key.H_1inv) 53 | 54 | pub_key.block_length = self.block_length 55 | pub_key.block_weight = self.block_weight 56 | pub_key.block_error = self.block_error 57 | 58 | return priv_key, pub_key 59 | -------------------------------------------------------------------------------- /crypto/pkcs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | 2011 February 5 - borrowed from sqlite.org 4 | 5 | The author disclaims copyright to this source code. In place of 6 | a legal notice, here is a blessing: 7 | 8 | May you do good and not evil. 9 | May you find forgiveness for yourself and forgive others. 10 | May you share freely, never taking more than you give. 11 | 12 | 13 | https://github.com/janglin/crypto-pkcs7-example 14 | ''' 15 | 16 | import binascii 17 | import StringIO 18 | 19 | class PKCS7Encoder(object): 20 | ''' 21 | RFC 2315: PKCS#7 page 21 22 | Some content-encryption algorithms assume the 23 | input length is a multiple of k octets, where k > 1, and 24 | let the application define a method for handling inputs 25 | whose lengths are not a multiple of k octets. For such 26 | algorithms, the method shall be to pad the input at the 27 | trailing end with k - (l mod k) octets all having value k - 28 | (l mod k), where l is the length of the input. In other 29 | words, the input is padded at the trailing end with one of 30 | the following strings: 31 | 01 -- if l mod k = k-1 32 | 02 02 -- if l mod k = k-2 33 | . 34 | . 35 | . 36 | k k ... k k -- if l mod k = 0 37 | The padding can be removed unambiguously since all input is 38 | padded and no padding string is a suffix of another. This 39 | padding method is well-defined if and only if k < 256; 40 | methods for larger k are an open issue for further study. 41 | ''' 42 | def __init__(self, k=16): 43 | self.k = k 44 | 45 | ## @param text The padded text for which the padding is to be removed. 46 | # @exception ValueError Raised when the input padding is missing or corrupt. 47 | def decode(self, text): 48 | ''' 49 | Remove the PKCS#7 padding from a text string 50 | ''' 51 | nl = len(text) 52 | val = int(binascii.hexlify(text[-1]), 16) 53 | if val > self.k: 54 | raise ValueError('Input is not padded or padding is corrupt') 55 | 56 | l = nl - val 57 | return text[:l] 58 | 59 | ## @param text The text to encode. 60 | def encode(self, text): 61 | ''' 62 | Pad an input string according to PKCS#7 63 | ''' 64 | l = len(text) 65 | output = StringIO.StringIO() 66 | val = self.k - (l % self.k) 67 | for _ in xrange(val): 68 | output.write('%02x' % val) 69 | return text + binascii.unhexlify(output.getvalue()) -------------------------------------------------------------------------------- /operations/arithmetic.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import numpy as np 20 | import pyfftw 21 | 22 | def div_poly(x, y): 23 | D = np.fft.rfft(np.array(list(y) + [0] * (len(x) - len(y)))) 24 | conjugate = np.conj(D) 25 | result = np.fft.irfft(np.fft.rfft(x) * conjugate / (D * conjugate)) 26 | return np.array([int(np.round(x) % 2) for x in result.real]) 27 | 28 | def shift_poly(x, n): 29 | return np.hstack((np.zeros(n, dtype=np.int), x[:-n])) 30 | 31 | def fftw_(x): 32 | a = pyfftw.empty_aligned(len(x), dtype='complex128') 33 | b = pyfftw.empty_aligned(len(x), dtype='complex128') 34 | fft_object = pyfftw.FFTW(a, b) 35 | X = fft_object(x) 36 | return X 37 | 38 | def ifftw_(x): 39 | a = pyfftw.empty_aligned(len(x), dtype='complex128') 40 | b = pyfftw.empty_aligned(len(x), dtype='complex128') 41 | fft_object = pyfftw.FFTW(a, b, direction='FFTW_BACKWARD') 42 | X = fft_object(x) 43 | return X 44 | 45 | def mul_poly(x, y): 46 | X = fftw_(x) 47 | Y = fftw_(y) 48 | return np.round(ifftw_(X * Y).real).astype('int') % 2 49 | 50 | def to_sparse_represenation(x): 51 | return x.nonzero()[0] 52 | 53 | def sparse_factor_mul(x, y): 54 | result = np.zeros(mod, dtype=np.int) 55 | for index in y: 56 | result += np.roll(x, index) 57 | return result 58 | 59 | def square_sparse_poly(x, times=1): 60 | indices = x.nonzero()[0] 61 | mod = len(x) 62 | indices *= pow(2, times, mod) 63 | result = np.zeros(mod, dtype=np.int) 64 | for index in indices: result[index % mod] ^= 1 65 | return result 66 | 67 | def exp_poly(x, n): 68 | y = np.zeros(len(x), dtype=np.int) 69 | y[0] = 1 70 | 71 | a = pyfftw.empty_aligned(len(x), dtype='complex128') 72 | b = pyfftw.empty_aligned(len(x), dtype='complex128') 73 | fft_object = pyfftw.FFTW(a, b) 74 | fft_object_inv = pyfftw.FFTW(a, b, direction='FFTW_BACKWARD') 75 | 76 | while n > 1: 77 | if n % 2 == 0: 78 | x = square_sparse_poly(x) 79 | n = n / 2 80 | else: 81 | # precision does not allow us to stay in FFT domain 82 | # hence, interchanging ifft(fft). 83 | X = np.copy(fft_object(x)) 84 | Y = np.copy(fft_object(y)) 85 | y = np.round(fft_object_inv(X * Y).real).astype('int') % 2 86 | x = square_sparse_poly(x) 87 | n = (n - 1) / 2 88 | return np.array([int(np.round(x) % 2) for x in mul_poly(x, y)]) 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /pqp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | This file is part of libPQP 5 | Copyright (C) 2016 Carl Londahl 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ''' 20 | 21 | import numpy as np 22 | 23 | from crypto.protocol import * 24 | import argparse 25 | 26 | class CLITool: 27 | 28 | def __init__(self): 29 | self.protocol = Protocol() 30 | 31 | def load_pubkey(self, filename): 32 | f = open(filename, 'r') 33 | self.protocol.set_public_key(f.read()) 34 | 35 | def load_privkey(self, filename): 36 | f = open(filename, 'r') 37 | self.protocol.set_private_key(f.read()) 38 | 39 | def encrypt(self, filename, write=False): 40 | print 'Encrypting:', filename 41 | f = open(filename, 'r') 42 | ciphertext = self.protocol.encrypt_message(f.read(), self.protocol.pub_key) 43 | if not write: 44 | print ciphertext 45 | else: 46 | return ciphertext 47 | 48 | def decrypt(self, filename, write=False): 49 | print 'Decrypting:', filename 50 | f = open(filename, 'r') 51 | message, verified = self.protocol.decrypt_message(f.read()) 52 | if verified: print 'MAC verified.' 53 | else: print 'Invalid MAC!' 54 | if not write: 55 | print message 56 | else: 57 | return message 58 | 59 | def generate_keypair(self, priv_filename, pub_filename): 60 | print 'Generating keypair...' 61 | self.protocol.generate_keypair() 62 | 63 | f = open(priv_filename, 'w') 64 | f.write(self.protocol.get_private_key()) 65 | f = open(pub_filename, 'w') 66 | f.write(self.protocol.get_public_key()) 67 | 68 | print 'Wrote private key {priv_filename} and public key {pub_filename}.'\ 69 | .format(priv_filename=priv_filename, pub_filename=pub_filename) 70 | 71 | cli = CLITool() 72 | 73 | parser = argparse.ArgumentParser() 74 | parser.add_argument('--pubkey', nargs=1, help='load public key') 75 | parser.add_argument('--privkey', nargs=1, help='load private key') 76 | parser.add_argument('--encrypt', nargs=1, help='encrypt message', metavar=('MESSAGE')) 77 | parser.add_argument('--decrypt', nargs=1, help='decrypt message', metavar=('CIPHERTEXT')) 78 | parser.add_argument('--output', nargs=1, help='output to file', metavar=('OUTFILE')) 79 | parser.add_argument('--gen', nargs=2, help='generate keypair', metavar=('PRIVKEY', 'PUBKEY')) 80 | 81 | args = vars(parser.parse_args()) 82 | 83 | if args['decrypt'] != None and args['privkey'] != None: 84 | cli.load_privkey(args['privkey'][0]) 85 | if args['output'] == None: 86 | cli.decrypt(args['decrypt'][0]) 87 | else: 88 | f = open(args['output'][0], 'w') 89 | f.write(cli.decrypt(args['decrypt'][0], write=True)) 90 | 91 | elif args['encrypt'] != None and args['pubkey'] != None: 92 | cli.load_pubkey(args['pubkey'][0]) 93 | if args['output'] == None: 94 | cli.encrypt(args['encrypt'][0]) 95 | else: 96 | f = open(args['output'][0], 'w') 97 | f.write(cli.encrypt(args['encrypt'][0], write=True)) 98 | 99 | elif args['gen']: 100 | priv_filename, pub_filename = args['gen'] 101 | cli.generate_keypair(priv_filename, pub_filename) 102 | 103 | else: parser.print_help() -------------------------------------------------------------------------------- /operations/keyio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | from pyasn1.codec.der import encoder, decoder 20 | 21 | import pyasn1.type.univ 22 | import pyasn1.type.namedtype as namedtype 23 | import base64 24 | 25 | from crypto.keygen import * 26 | from crypto.private_key import * 27 | from crypto.public_key import * 28 | 29 | class ASN1PublicKey(pyasn1.type.univ.Sequence): 30 | componentType = namedtype.NamedTypes( 31 | namedtype.NamedType('G', pyasn1.type.univ.BitString()) 32 | ) 33 | 34 | class ASN1PrivateKey(pyasn1.type.univ.Sequence): 35 | componentType = namedtype.NamedTypes( 36 | namedtype.NamedType('H0', pyasn1.type.univ.BitString()), 37 | namedtype.NamedType('H1', pyasn1.type.univ.BitString()), 38 | namedtype.NamedType('H1inv', pyasn1.type.univ.BitString()), 39 | ) 40 | 41 | class ASN1Ciphertext(pyasn1.type.univ.Sequence): 42 | componentType = namedtype.NamedTypes( 43 | namedtype.NamedType('C0', pyasn1.type.univ.BitString()), 44 | namedtype.NamedType('C1', pyasn1.type.univ.BitString()), 45 | namedtype.NamedType('Sym', pyasn1.type.univ.OctetString()), 46 | ) 47 | 48 | class IO: 49 | 50 | def to_bitstring(self, vec): 51 | return pyasn1.type.univ.BitString('\'' + ''.join([str(x) for x in vec]) + '\'B') 52 | 53 | def extract_der_priv_key(self, seq): 54 | seq = seq.replace('-----BEGIN PQP PRIVATE KEY-----\n', '') 55 | seq = seq.replace('-----END PQP PRIVATE KEY-----\n', '') 56 | der = decoder.decode(base64.decodestring(seq), asn1Spec=ASN1PrivateKey())[0] 57 | priv_key = PrivateKey() 58 | priv_key.H_0 = np.array(list(der['H0'])) 59 | priv_key.H_1 = np.array(list(der['H1'])) 60 | priv_key.H_1inv = np.array(list(der['H1inv'])) 61 | priv_key.block_length = len(priv_key.H_0) 62 | return priv_key 63 | 64 | def get_der_priv_key(self, priv_key): 65 | template = '-----BEGIN PQP PRIVATE KEY-----\n{}-----END PQP PRIVATE KEY-----\n' 66 | 67 | der = ASN1PrivateKey() 68 | der['H0'] = self.to_bitstring(priv_key.H_0) 69 | der['H1'] = self.to_bitstring(priv_key.H_1) 70 | der['H1inv'] = self.to_bitstring(priv_key.H_1inv) 71 | 72 | data = base64.encodestring(encoder.encode(der)) 73 | return template.format(data) 74 | 75 | def extract_der_pub_key(self, seq): 76 | seq = seq.replace('-----BEGIN PQP PUBLIC KEY-----', '') 77 | seq = seq.replace('-----END PQP PUBLIC KEY-----', '') 78 | seq = seq.strip('\n') 79 | der = decoder.decode(base64.decodestring(seq), asn1Spec=ASN1PublicKey())[0] 80 | pub_key = PublicKey() 81 | pub_key.G = np.array(list(der['G'])) 82 | pub_key.block_length = len(pub_key.G) 83 | return pub_key 84 | 85 | def get_der_pub_key(self, pub_key): 86 | template = '-----BEGIN PQP PUBLIC KEY-----\n{}-----END PQP PUBLIC KEY-----\n' 87 | der = ASN1PublicKey() 88 | der['G'] = self.to_bitstring(pub_key.G) 89 | data = base64.encodestring(encoder.encode(der)) 90 | return template.format(data) 91 | 92 | def get_der_ciphertext(self, c_0, c_1, symmetric_stream): 93 | template = '-----BEGIN PQP MESSAGE-----\n{}-----END PQP MESSAGE-----\n' 94 | der = ASN1Ciphertext() 95 | der['C0'] = self.to_bitstring(c_0) 96 | der['C1'] = self.to_bitstring(c_1) 97 | der['Sym'] = symmetric_stream 98 | data = base64.encodestring(encoder.encode(der)) 99 | return template.format(data) 100 | 101 | def extract_der_ciphertext(self, seq): 102 | seq = seq.replace('-----BEGIN PQP MESSAGE-----', '') 103 | seq = seq.replace('-----END PQP MESSAGE-----', '') 104 | seq = seq.strip('\n') 105 | der = decoder.decode(base64.decodestring(seq), asn1Spec=ASN1Ciphertext())[0] 106 | c_0 = np.array(list(der['C0'])) 107 | c_1 = np.array(list(der['C1'])) 108 | symmetric_stream = der['Sym'].asOctets() 109 | return c_0, c_1, symmetric_stream 110 | 111 | 112 | 113 | #bask = IO() 114 | #encoded = bask.get_der_ciphertext([1],[0], '\x00') 115 | #print bask.extract_der_ciphertext(encoded) 116 | 117 | 118 | -------------------------------------------------------------------------------- /crypto/protocol.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import numpy as np 20 | 21 | from Crypto.Cipher import AES 22 | from hashlib import sha512, sha256 23 | 24 | from operations.arithmetic import * 25 | from operations.conversion import * 26 | from operations.keyio import * 27 | 28 | from crypto.private_key import * 29 | from crypto.public_key import * 30 | from crypto.qcmdpc import * 31 | from crypto.keygen import * 32 | from crypto.pkcs import * 33 | 34 | class Protocol: 35 | 36 | def __init__(self): 37 | # instantiate primitives 38 | self.asymmetric_cipher = McEliece() 39 | self.randgen = RandomGenerator() 40 | self.io = IO() 41 | self.padding = PKCS7Encoder() 42 | 43 | # just some random salts 44 | self.saltA = b'this is just a salt' 45 | self.saltB = b'this is another a salt' 46 | self.ivSalt = b'third salt' 47 | 48 | def generate_keypair(self): 49 | # instantiate keygenerator and set keypair 50 | keygen = Keygen() 51 | self.priv_key, self.pub_key = keygen.generate() 52 | self.asymmetric_cipher.set_private_key(self.priv_key) 53 | 54 | def set_private_key(self, key): 55 | key = self.io.extract_der_priv_key(key) 56 | self.priv_key = key 57 | self.asymmetric_cipher.set_private_key(key) 58 | 59 | def set_public_key(self, key): 60 | key = self.io.extract_der_pub_key(key) 61 | self.pub_key = key 62 | 63 | def get_private_key(self): 64 | return self.io.get_der_priv_key(self.priv_key) 65 | 66 | def get_public_key(self): 67 | return self.io.get_der_pub_key(self.pub_key) 68 | 69 | def generate_mac(self, message, token, key): 70 | return sha256(message + str(token) + key).digest() 71 | 72 | def symmetric_cipher_enc(self, message, mac, key, iv): 73 | symmetric_cipher = AES.new(key, AES.MODE_CBC, iv) 74 | padded = self.padding.encode(message + mac) 75 | return symmetric_cipher.encrypt(padded) 76 | 77 | def symmetric_cipher_dec(self, ciphertext, key, iv): 78 | symmetric_cipher = AES.new(key, AES.MODE_CBC, iv) 79 | decrypted_padded = symmetric_cipher.decrypt(ciphertext) 80 | decrypted = self.padding.decode(decrypted_padded) 81 | mac = decrypted[-32:] 82 | message = decrypted[:-32] 83 | return message, mac 84 | 85 | def encrypt_message(self, message, recv_pub_key): 86 | # generate random data 87 | randomized = self.randgen.get_random_vector(self.pub_key.block_length) 88 | token = pack(randomized) 89 | 90 | # derive keys 91 | keyA = sha256(str(token) + self.saltA).digest() # just some conversion 92 | keyB = sha256(str(token) + self.saltB).digest() 93 | 94 | # derive iv 95 | iv = sha512(str(token) + self.ivSalt).digest()[0:16] 96 | 97 | # generate mac 98 | mac = self.generate_mac(message, token, keyB) 99 | 100 | c_0, c_1 = self.asymmetric_cipher.encrypt(recv_pub_key, randomized) 101 | 102 | # generate ciphertext 103 | return self.io.get_der_ciphertext(c_0, c_1, \ 104 | self.symmetric_cipher_enc(message, mac, keyA, iv)) 105 | 106 | def decrypt_message(self, ciphertext): 107 | # extract ciphertext data from DER 108 | rc_0, rc_1, symmetric_stream = self.io.extract_der_ciphertext(ciphertext) 109 | 110 | # decrypt necessary data 111 | decrypted_token = pack(self.asymmetric_cipher.decrypt(rc_0, rc_1)) 112 | 113 | # derive keys from data 114 | decrypted_keyA = sha256(str(decrypted_token) + self.saltA).digest() # just some conversion 115 | decrypted_keyB = sha256(str(decrypted_token) + self.saltB).digest() 116 | 117 | # derive iv 118 | decrypted_iv = sha512(str(decrypted_token) + self.ivSalt).digest()[0:16] 119 | 120 | # decrypt ciphertext and derive mac 121 | decrypted_message, decrypted_mac = self.symmetric_cipher_dec(symmetric_stream, \ 122 | decrypted_keyA, decrypted_iv) 123 | 124 | receiver_mac = self.generate_mac(decrypted_message, decrypted_token, decrypted_keyB) 125 | 126 | return decrypted_message, receiver_mac == decrypted_mac 127 | 128 | -------------------------------------------------------------------------------- /crypto/qcmdpc.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of libPQP 3 | Copyright (C) 2016 Carl Londahl 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | from operations.arithmetic import * 20 | from operations.randomgen import * 21 | 22 | from copy import copy 23 | 24 | class McEliece: 25 | 26 | def __init__(self): 27 | self.randgen = RandomGenerator() 28 | 29 | def set_private_key(self, priv_key): 30 | self.H_0 = priv_key.H_0 31 | self.H_1 = priv_key.H_1 32 | 33 | self.G = mul_poly(priv_key.H_0, priv_key.H_1inv) # compute public key 34 | 35 | self.block_length = priv_key.block_length 36 | self.block_error = priv_key.block_error 37 | self.block_weight = priv_key.block_weight 38 | 39 | def get_public_key(self): 40 | pub_key = PublicKey() 41 | pub_key.set_params(self.G, self.block_error) 42 | return pub_key 43 | 44 | def encrypt(self, pub_key, m): 45 | # non-constant weight to achieve cipertext indistinguishability 46 | v = (mul_poly(pub_key.G, m) + self.randgen.get_random_weight_vector( \ 47 | pub_key.block_length, pub_key.block_error + self.randgen.flip_coin())) % 2 48 | u = (m + self.randgen.get_random_weight_vector(pub_key.block_length, \ 49 | pub_key.block_error + self.randgen.flip_coin())) % 2 50 | return u, v 51 | 52 | def syndrome(self, c_0, c_1): 53 | return (mul_poly(self.H_0, c_0) + mul_poly(self.H_1, c_1)) % 2 54 | 55 | def decrypt(self, c_0, c_1): 56 | synd = self.syndrome(c_0, c_1) 57 | 58 | # compute correlations with syndrome 59 | H0_ind = np.nonzero(self.H_0)[0] 60 | H1_ind = np.nonzero(self.H_1)[0] 61 | 62 | unsat_H0 = np.zeros(self.block_length) 63 | for i in H0_ind: 64 | for j in range(len(synd)): 65 | if synd[j]: unsat_H0[(j-i) % self.block_length] += 1 66 | 67 | unsat_H1 = np.zeros(self.block_length) 68 | for i in H1_ind: 69 | for j in range(len(synd)): 70 | if synd[j]: unsat_H1[(j-i) % self.block_length] += 1 71 | 72 | rounds = 10 73 | delta = 5 74 | threshold = 100 75 | r = 0 76 | 77 | while True: 78 | max_unsat = max(unsat_H0.max(), unsat_H1.max()) 79 | 80 | # if so, we are done decoding 81 | if max_unsat == 0: 82 | break 83 | 84 | # we have reach the upper bound on rounds 85 | if r >= rounds: 86 | raise ValueError('Decryption error') 87 | break 88 | r += 1 89 | 90 | # update threshold 91 | if max_unsat > delta: threshold = max_unsat - delta 92 | 93 | round_unsat_H0 = copy(unsat_H0) 94 | round_unsat_H1 = copy(unsat_H1) 95 | 96 | # first block sweep 97 | for i in range(self.block_length): 98 | if round_unsat_H0[i] <= threshold: continue 99 | 100 | for j in H0_ind: 101 | increase = (synd[(i+j) % self.block_length] == 0) 102 | for k in H0_ind: 103 | m = (i+j-k) % self.block_length 104 | if increase: 105 | unsat_H0[m] +=1 106 | else: 107 | unsat_H0[m] -=1 108 | 109 | for k in H1_ind: 110 | m = (i+j-k) % self.block_length 111 | if increase: 112 | unsat_H1[m] +=1 113 | else: 114 | unsat_H1[m] -=1 115 | 116 | synd[(i+j) % self.block_length] ^= 1 117 | 118 | c_0[i] ^= 1 119 | 120 | # second block sweep 121 | for i in range(self.block_length): 122 | if round_unsat_H1[i] <= threshold: continue 123 | 124 | for j in H1_ind: 125 | increase = (synd[(i+j) % self.block_length] == 0) 126 | 127 | for k in H0_ind: 128 | m = (i+j-k) % self.block_length 129 | if increase: 130 | unsat_H0[m] +=1 131 | else: 132 | unsat_H0[m] -=1 133 | 134 | for k in H1_ind: 135 | m = (i+j-k) % self.block_length 136 | if increase: 137 | unsat_H1[m] +=1 138 | else: 139 | unsat_H1[m] -=1 140 | 141 | synd[(i+j) % self.block_length] ^= 1 142 | 143 | c_1[i] ^= 1 144 | return c_0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libPQP - a Python post-quantum library 2 | 3 | *Update 8 September 2016*: Current version of libPQP is deprecated due to a [newly published attack](http://eprint.iacr.org/2016/858.pdf). Possible mitigations: 4 | * Forward-secrecy method where the secret key and corresponding public key gets updated over short intervals. 5 | * Automatically reject any message which is decoded with higher error rate than some constant t, where t implies a negligable decoding error. 6 | 7 | This is a simplistic prototype of a post-quantum cryptography library in Python. PQP stands for Post-Quantum PGP. The library is not production ready and should not be used in a real-life context, but works fine for testing purposes. The plan is, once the code has been audited, to translate it to Javascript and create a webapp. 8 | 9 | In this prototype, the focus has mainly been on making the QC-MDPC part efficient and not the actual protocol. Hence, you may find vulnerabilities in the current implementation of the protocol. Also, the primitives used in the code are not the ones mentioned below. This prototype uses: 10 | 11 | * AES-256(m, k, iv) as symmetric cipher, 12 | * SHA-256(token + salt) as PBKDF2, 13 | * A truncated SHA-512(token + salt) for iv. 14 | 15 | The final product will use Salsa-20 as symmetric-cipher primitive and Poly1305 for authentication purposes. Moreover, PBKDF2 or similar will be used for symmetric-key generation. 16 | 17 | Speed-ups in the decoding use the fast fourier transform (FFT) to achieve O(n log n) complexity in modular polynomial multiplications, instead of O(n²). Because the FFT implementation in Numpy is restricted to certain lengths (multiples of powers of 2), we use [pyfftw](https://pypi.python.org/pypi/pyFFTW) which is a wrapper for [FFTW3](https://github.com/FFTW/fftw3). FFTW3 implements Winograd's FFT algoritm and supports prime-length blocks. See below for known vulnerabilities. 18 | 19 | Below are given the proposed parameters for rate R = 1/2. 20 | 21 | | Public-key size | Private-key size | Rate | Error weight | Bit security | 22 | | ---------------:|-----------------:| --------------:|--------------:|-------------:| 23 | | 4801 | 9602 | 1/2 | 84 | 80 | 24 | | 9857 | 19714 | 1/2 | 134 | 128 | 25 | | 32771 | 65542 | 1/2 | 264 | 256 | 26 | 27 | Since the encrypted token is a codeword of length 9602 (for 80-bit security), we add approximately 1200 bytes of data to the ciphertext. Apart from this, a 32-byte MAC is included. This inflates a (padded) message of size M to size 1232 + M. For higher security levels, the inflation will be larger — but still constant. In the DER format, the inflation is about 35 %. 28 | 29 | # What is post-quantum cryptography? 30 | 31 | Today, most security assertions depend on primitives based on number theory. In principle, all of these primitives stand and fall on the problem of factoring integers. In the 1980's, a theoretical model of a computer that exploits certain quantum mechanical effects to achieve better complexity in certain classes of problems (BQP) was proposed. It so happens that the problem of factoring integers is contained in this class. Such a computer, called a quantum computer, can factor any integer N in time polynomial to the number of bits in N. This poses an actual problem for the security industry because RSA, ECC and DH, to name a few, can be broken efficiently. In turn, such a break causes the whole security architecture, upon which the secure internet is built, to collapse. Even symmetric primitives, such as AES, are subject to quantum attacks. However, the impact is much less severe; the speed-up in attacks is only a square-root factor. 32 | 33 | To remedy the problem of quantum attacks, post-quantum cryptography was proposed. There has been many candidates, often based on so-called NP-complete problems. One such candidate is McEliece public-key cryptosystem, which is based on a hard problem called random linear decoding. Most of the linear-decoding problems are hard, but we should point out that some linear codes with special properties are very easy to decode. McEliece PKC extends this idea by defining such an easily decodable linear code, which becomes the private key. Then, a scrambled (random-looking) version of the linear code is used as public key. This faces the holder of the private key with an easy problem, but the attacker faces a hard problem. 34 | 35 | ``` 36 | 1. Generate a linear code with generator matrix G (usually a Goppa code). 37 | 2. Compute G' = S × G × P, where S is an invertible matrix and P a permutation matrix. 38 | 3. Return keypair G, G' 39 | ``` 40 | 41 | Suppose Bob wants to send Alice a message m. To encrypt, he does the following: 42 | 43 | ``` 44 | 1. Retrieves Alice's public key G'. 45 | 2. Compute ciphertext c = m × G' + e and send it to Alice 46 | ``` 47 | 48 | Note that these operations require basically no work at all, so encryption is very fast. Now Alice receives the ciphertext c. 49 | 50 | ``` 51 | 1. Alice obtains c. Knowing S, G and P, she computes u = c × inv(P) = m × (S × G) = (m × S) × G + e × inv(P). 52 | 2. Using the generator matrix G (which defined an efficiently decodable code), she can decode m × S. 53 | 3. Having c' = m × S, the message is formed by removing the S, i.e., c' × inv(S) = m × S × inv(S) = m. 54 | ``` 55 | 56 | The QC-MDPC McEliece is a bit different, but the principle is the same. Instead of using matrices, it operates on polynomials (or, equivalently, circular matrices). Here, the private key consists of two sparse polynomials H₀ and H₁, which can be used to perform efficient decoding. H₀ and H₁ form the private key. The public key is H₀ × inv(H₁), which is not sparse and (presumably) cannot be used for efficient decoding. Encryption and decryption is done is similar ways to the above. 57 | 58 | 59 | # High-level description 60 | 61 | ###The sender side 62 | 63 | In this section, we will briefly describe the protocol. Much like a Fujisaki-Okamoto transform, it contains both an asymmetric part and a symmetric one. Consider the following scenario. Assume that Bob wants to send Alice a message. Denote Alice's keypair (pubkey, privkey). Bob takes the following steps: 64 | 65 | ``` 66 | 1. Bob picks a random token T. 67 | 2. He then uses Alice's public key denoted pubkey and encrypts the token T using QC-MDPC McEliece. 68 | 3. The token T is used to generate the symmetric key k₁ and the MAC key k₂ (PBKDF2). 69 | 4. The error vector used in the second step is concatenated with the message and a MAC is generated using k₂. 70 | 4. The message and the MAC are then encrypted with the symmetric key k₁. 71 | 5. The ciphertext is the concatenation of the encrypted token and encrypted message + MAC. 72 | ``` 73 | 74 | The ciphertext can now be distributed to Alice, using arbitrary means of communication. Below is a graphical interpretation of the above steps. 75 | 76 | 77 | ![protocol sender](https://raw.githubusercontent.com/grocid/encrypt.life-python/master/sender.png) 78 | 79 | ###The receiver end 80 | 81 | Now Alice wants to decrypt the message sent by Bob. She performs the following steps in order to do so: 82 | 83 | ``` 84 | 1. Alice decrypts the encrypted token T using her private key privkey. In the decryption, the error vector is determined. 85 | 2. Using the decrypted token T, she derives the same symmetric key k₁ as Bob and decrypts the message. 86 | 3. The message and the MAC are extracted. 87 | 4. The MAC of the message and error vector is verified using the key k₂ derived from the token. 88 | 5. If the verification returns True, Alice accepts the message. Otherwise she rejects it. 89 | ``` 90 | 91 | This completes the outline of the protocol. Below is a graphical interpretation of the above steps. 92 | 93 | ![protocol receiver](https://raw.githubusercontent.com/grocid/encrypt.life-python/master/receiver.png) 94 | 95 | ##Key format 96 | 97 | The keys are encoded in Base64 and ASN.1, just like the normal ascii-armored keys used in public-key cryptography. The public-key structures contains only the generator polynomial G. 98 | 99 | ``` 100 | class ASN1PublicKey(pyasn1.type.univ.Sequence): 101 | componentType = namedtype.NamedTypes( 102 | namedtype.NamedType('G', pyasn1.type.univ.BitString()) 103 | ) 104 | ``` 105 | 106 | A typical (encoded) public key has the following appearance: 107 | ``` 108 | -----BEGIN PQP PUBLIC KEY----- 109 | MIICXgOCAloHMFAZsKjyqD4MKBPFkLCdUBAqC6rY4jdlOk/RQc4MiGfkUSw6hBh41eFa1XogH9MN 110 | b49TPLQRZYBXygp7eGP1oUM4PqvvdwwOlUoTPNdWYeAiqEpOe4nP7sq+fir54nl84I/ArMdCsUyo 111 | hgYtCVumC0XkYMlWW8tsW+RU94gQoQd7pLvgs98ah0gTNARRjW8/yALfGrB1pV7ZuLUScfNIFq8q 112 | ZIZE8P80VK6kS0tcy+k4h9vHeB6QGPlPGB/jQ7mz8jDzw7v3m5QfnS1nlHvBSYWU/hADIIy13uh2 113 | mEJmtIzDbWOZ7v4OJtDXrKgK9iMJ4OjCtntbmdAMSNwdMhNp2mH2O5a7b0MoELVILx6CTpjB64D2 114 | toFwXwl867QEgBCk4imZHZxMgnLw9TxnM8g+5gQzfC5BCI6afaGS9lZXwbM+ssZN2DbRZIeVS7rI 115 | 12nul2rqquMC0buMM0Yt6ebD3bMRTeuUY3KLmEkUVjn3fTg7YUm82UuyGmG3cKqB0AZb24yswXl7 116 | z3bOrMTMrggQXw0KPR3AoPh49+PG9pt9ySRp/9KZ8k+apXBtvxCOIx3J+6WYeB9zGTnu841jl+WD 117 | LTBW5ePqglxGjown9lZlmw2Rgpsl7o6wSG5lb/d+B6hTw8w6KIowsQywEyDuF45B7U6W5EE8kgS1 118 | 9S01PKqkpYd9YV7oJzpYLFuS6dDW71WavV9DdW29uNOn5OlBAk79SbAjsliSojSVv5ZrYMPp16Sp 119 | 0YOkRoH/WeLL+xBwJtqMNRii34nH/B45ibEibCbFpO4rEDUs01aYAA== 120 | -----END PQP PUBLIC KEY----- 121 | ``` 122 | 123 | The private key contains the two secret-key polynomials H₀ and H₁. Because inversion takes quite a while to perform, the inverse of H₁ is also contained in the private-key structure. This is not necessary, since inv(H₁) is only used when deriving the public key. 124 | 125 | 126 | ``` 127 | class ASN1PrivateKey(pyasn1.type.univ.Sequence): 128 | componentType = namedtype.NamedTypes( 129 | namedtype.NamedType('H0', pyasn1.type.univ.BitString()), 130 | namedtype.NamedType('H1', pyasn1.type.univ.BitString()), 131 | namedtype.NamedType('H1inv', pyasn1.type.univ.BitString()), 132 | ) 133 | ``` 134 | 135 | Below is a typical private key given. As we can see, the elements H₀ and H₁ are very sparse. These polynomials could be encoded more efficiently, but we don't care too much about private-key size and is therefore left as is. 136 | 137 | ``` 138 | -----BEGIN PQP PRIVATE KEY----- 139 | MIIHGgOCAloHAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAEAACAAA 140 | AAAAAAAAAAAAAAQAAQAAAAAQAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAA 141 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 142 | AAAAAAAAAAEAAAABAAAAAAAAAAAAAAEAAAAAAAABAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAA 143 | AAAAAAAAAAAAAAAAAAEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAI 144 | AAAAAAAAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAA 145 | AAAAAAAAAAAAAAAAAAAAAAABAAAAAAgAAQAAAABAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA 146 | AAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAIACAAAAAAAAQAAAAAAAAAAAAAEAAAAAAAAACBAAAAA 147 | AAAIACAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAA 148 | AAAAAAAACAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 149 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAOCAloHAAAAAAAAAAAABAAA 150 | AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA 151 | AAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAABAAAAAAAAAAAAAAAAAAAAAAAAAA 152 | AAAAIAAABAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAABAAAAA 153 | AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAQAAQgAAAAAAAAAAAA 154 | AAgAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAgAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA 155 | AAAAAAIAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAEAAAAAAAAAIAAAAAAAAAAA 156 | AAAAAAAAAAAAAABAAAAAAAAAAAAAIAAAAABAAAAEAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 157 | AAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 158 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAEAAAAAAAIAAAAAAAEIAAEAAAAAAAAAAAAAAAA 159 | AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAABAAAABIAAAAAAAAAAQAAAAABAAAAAAAAAAAAAAAAQAAAA 160 | AAAAAAAAAAAAAAAAAAAAAAAAAAOCAloHPmFZHOhZjZWveH7OKtPghL5WWVE2/JctV4GYBnliCjZ3 161 | qyR/x741dDTCa8O1vAq0HaezSJL0H4rehid7KaLkQ4OTwwbwk22nLuQIi6ShXYCL1Tlbd4POVxGa 162 | RSQn/zQjIGGZ009mNraWv+MyyNDE5WMfl0VqcjYhYacAgf7NXc3tUHKXCYHxzf9P3IbQvwFUOTUC 163 | A2x4kI0yGaBs2Y/wtJkSkvsayxlSqNIu8Ob5I3aFqndhVLvlKgsG00iLzLLFRqSYTCeYnM1pV9Mv 164 | ZgRM5Esdv/O9XNcdKMTGRcV357jqdN+MvJhxitmyvIUpez9kh6+yt+PRtXUzWyVs2x6mf/OScL5f 165 | HznX3uePZl02G4ug/ro3eh/T8wjC9j6CtX3WgnqYKo2n68+fvt6EaEL7lZoMnVgTHHwx1HuQl0pl 166 | OEj2WnwHcAuYleOrNtKMtDpji0e90cXGURujMymS58ZNCyHdX7qCm7MlfmS0l6YZ/my8vRGxPG83 167 | qksVuNMvB9dKEzpTzWLIlygMDvjzn9GN/071iRp9lNaPliZD4x8Dt6d4QS3pOUCw/oPWNUCHJUOW 168 | dBUO5ziqZ+4Xt68Gce4FB7/jb2ejsAPstzzrpLKvvpmo4FL+ibx17TnbEhyNlJ+N94CQ+TSELzqi 169 | QHzg3PtOxZOPxbT5RQSkSQjVIaUH6/k2TC20iorr6gsH8Oogz24to+E41aZT0NBzxrZvuI/yuB0N 170 | XAT/wwcIth3UVQoT5Y0lnXVUKnBF89PHUayLawWgxPiDx5EHhjsWqyB/G/VB2ZhFx6qnfE+qAA== 171 | -----END PQP PRIVATE KEY----- 172 | ``` 173 | 174 | The messages are structured in a similar way. The encrypted token (C₀, C₁) is stored in two separate Bitstrings and the symmetric ciphertext are stored as an OctetString. 175 | 176 | ``` 177 | class ASN1Ciphertext(pyasn1.type.univ.Sequence): 178 | componentType = namedtype.NamedTypes( 179 | namedtype.NamedType('C0', pyasn1.type.univ.BitString()), 180 | namedtype.NamedType('C1', pyasn1.type.univ.BitString()), 181 | namedtype.NamedType('Sym', pyasn1.type.univ.OctetString()), 182 | ) 183 | ``` 184 | We encrypted the message 185 | ``` 186 | this is a really secret message that is padded with some random. 187 | ``` 188 | and put it into the ASN.1 format. Below is the ciphertext. 189 | 190 | ``` 191 | -----BEGIN PQP MESSAGE----- 192 | MIIFHgOCAloHOjScypeSXUoEDjfC+E3GGYbQ7t2QQ8Z7LtfPG0SOjySJWuWszQ2F593vDr41hQFV 193 | dpdgSElPrWMiAxhqZ11fj3DVXCTZtu5DwXBijtVKg55xs/fu9rh6RvywIKdRLTpxJfVrN1oGA7rs 194 | oaPvToZPOziXdymst4Z3dDc5vcyPORQXZz3KmhLKowFtrHpPx7HMVdi2iXy2ohKGBafdtpZsOHek 195 | K8JgtLQhlC/cQbXkd5uWniWn/qSei6zKVpOCO7PvqLadJCJI2rufue2Lie+AmQGVmn4DK8X5c+I8 196 | 4BEVTY6PtZ819vyQmOARO0qSNr/6qiV0u1X7VFLGw+tvYjrDAvmiWTujQ7uY9Qs2ef0idk+4GmnK 197 | JqImt+ExnKedH8O5NWxZewHnFoU23gL3Qz1eODnHYVOQxfvUtQnuZMRuHgbrc+av22pi0C+aj0qR 198 | JW4vcqNEazeD6usot28Uo7tpnqs1kAssHqGAQ0rAtDkdogpq5ntQidb4yheaj1orgcT7/VyugKbH 199 | Di0NeyoG/wo9vi5aCDlw0e0KPHKat0wvR21FYSCqtd9Gmc6/McDGYTwaJLZ4NK53ETnaJh46X9jd 200 | OZTL9eSOKrbElZjlIMWAVXZgU465lGoENZVDNCV9AgzMsku0o/VV2Djh8Hw/Ggx0sAlpbjc+AuD4 201 | xEa3gETXKdmsq/PjATe9c9+5I0oO17rIjwlc+Wc5w4NGJuBrm0MNtkzEzVyteXsn00njiNdrxRks 202 | gaUcpkwleLawIZh7gs1X3SRI7+VWvBy/8t/n9LxERC6/rkyJbC5kAAOCAloHHtIcwG1S+00bmFb8 203 | C5uYTW47XcP/eP+U9tjVswkgj/wu/RjmqGuGISw8ys2vmi79429gvXQRwLiH3NaPP8WpF2vo52H0 204 | gVVLYqmWtMG5gLKwxEFr22OGe/nNnj49Rk57nhpp+i2EUFmWbigrAcClcVxeB5BYlxlM08VeTJeU 205 | OeRlRJaaBFk2GRETnAPST3RsyLwz9dW0njZkeQGPG+rIXp5J/exUsn+WSq/omYivTujCHACZ1Fxt 206 | Bx0fD1PXxGVhGrXodENAn1R0AeG/E3g/M++vfTaG2nba+kkO/2lrMKfWTOp/cvpyzHk9gNDc5EXM 207 | A1MqUI7z9/defLHVIwo6u5xHjAx3c+qZ2YwEJmaar7c70NUfjvOPhGQsH9Pcs18BVv7WyswDT8/C 208 | wtKCv9PY5BVUBvp2wKCfmhuPdNYxzA3F/zPL+ryFUjMrdBnWmYaI1hQXdeUY0DzM6mWPEfLgWnIG 209 | PKX6CmsbJNQs3+GC31ps7GkCZkYdBoSlBf9faFnLCzsx2AMKOx32ZKVyU4v0WzvRNF1VQyq6NQyh 210 | Ap7phcIvq9DkBSdWe2pEpbctYbUqVLeZ5tpz0zCFWrdDHvvDSg4OA3J3QEvSA4V1fCzQY6QG9EGC 211 | 8mlXorT+aKlvJz6gWOZcurwrM307cYOx9yC0QACQPItWXXt8E6fpXjxgJs4mMwnWGoW7H51xziRH 212 | m+VIP0qdHCvEzI2180qE0oEOfIh8MbLFWktovkjy94US6AGTxZ/GP2o8ALKvRfvB603X5m8PjBlj 213 | JGPyik2nqjqIJXK95F9omr4FgARgSUda/5bzJK/tJUuixFOWWaimI+WtgJDsIV8velkaaVvxL8xz 214 | K7PlOCNoMaZYC+z/GkenwwvNr0f+uGiPYShao0/Ie7NhC2C2tj61OENmc+OJ1zy1qLE1ApJOlVL3 215 | KHde 216 | -----END PQP MESSAGE----- 217 | ``` 218 | 219 | #Performance 220 | Below is a graph of timings of my computer (Macbook Pro 15" Retina 2 GHz Intel Core i7) running 1000 decryptions of a small plaintext (thus, isolating the asymmetric timing) using the same private key (σ = 0.0386, μ = 0.493): 221 | 222 | ![timings](https://raw.githubusercontent.com/grocid/encrypt.life-python/master/timings.png) 223 | 224 | Here is the output of encrypting and decrypting a 190 mb video file: 225 | 226 | ``` 227 | $ time ./pqp --encrypt libpqp.mov --pubkey pub --output libpqp.enc 228 | Encrypting: libpqp.mov 229 | 230 | real 0m11.331s 231 | user 0m9.293s 232 | sys 0m1.715s 233 | 234 | $ time ./pqp --decrypt libpqp.enc --privkey priv --output libpqp2.mov 235 | Decrypting: libpqp.enc 236 | MAC verified. 237 | 238 | real 0m11.422s 239 | user 0m8.633s 240 | sys 0m2.446s 241 | 242 | $ ls -la libpqp* 243 | -rw-r--r-- 1 carl staff 258900672 17 Jul 00:57 libpqp.enc 244 | -rw-r--r--@ 1 carl staff 191652442 16 Jul 23:22 libpqp.mov 245 | -rw-r--r--@ 1 carl staff 191652442 17 Jul 00:58 libpqp2.mov 246 | 247 | $ md5 libpqp2.mov 248 | MD5 (libpqp2.mov) = 88ebe9d8aa74ebba7c5de6faa048af46 249 | 250 | $ md5 libpqp.mov 251 | MD5 (libpqp.mov) = 88ebe9d8aa74ebba7c5de6faa048af46 252 | ``` 253 | 254 | #Command-line tool 255 | The library can be invoked by a supplied CLI tool (the pqp file). Padding is not included, so some kind of PKCS padding will be added. 256 | 257 | To generate a keypair, run the following command: 258 | ``` 259 | pqp --gen [private-key file] [public-key file] 260 | ``` 261 | This creates two files in the same folder containing the two keys. 262 | 263 | To encrypt a file, call pqp as follows: 264 | ``` 265 | pqp --encrypt [plaintext file] --pubkey [public-key file] 266 | ``` 267 | 268 | This writes an ASN.1 encoded ciphertext to stdout. Appending --output writes to file: 269 | ``` 270 | pqp --encrypt [plaintext file] --pubkey [public-key file] --outout [ciphertext file] 271 | ``` 272 | 273 | To decrypt, invoke the following command. To write to file, use the above methodology. 274 | ``` 275 | pqp --decrypt [ciphertext file] --privkey [private-key file] 276 | ``` 277 | 278 | #Installation 279 | 280 | Below are the required steps to make libPQP run on Linux (tested on Ubuntu 16.04 Xenial): 281 | 282 | ``` 283 | git clone https://github.com/grocid/libPQP.git 284 | sudo pip install numpy 285 | sudo pip install pycrypto 286 | sudo apt-get install libfftw3-dev 287 | sudo pip install pyFFTW 288 | sudo pip install pyasn1 289 | ``` 290 | 291 | #Possible vulnerabilities 292 | 293 | ##Decryption oracle 294 | The protocol can be designed using normal McEliece or Niederreiter. In case of McEliece, the error vector should be part of the authentication (for instance, generate MAC using a concatenation of message and error vector). Such a measure will mitigate the usual decryption oracle attack, described below. 295 | 296 | ``` 297 | 1. Intercept an encrypted message. 298 | 2. Pick a random bit of the ciphertext. 299 | 3. Flip it. If decryption fails, this was not an error position. 300 | 4. Repeat until all error positions have been unraveled. 301 | ``` 302 | 303 | Obviously, there is an implicit assumption that the receiver will either reject any error larger than T or the decoder will fail (which rarely is the case). 304 | 305 | If the protocol instead is designed using the Niederreiter model, the error vector will be/encode the token. In this case, there is no need to authenticate the error vector. Since any flipped bit in the ciphertext will cause the receiver to decode a different token, it will break the decryption oracle. 306 | 307 | ##Timing attacks 308 | 309 | This is a slight variation of the above. Instead of observing decryption errors, we measure the timing. There has been some effort in making decoding run in constant time. See [this paper](http://www.win.tue.nl/~tchou/papers/qcbits.pdf). 310 | 311 | The decoding we use is probabilistic and susceptible to timing attacks. Below is an image with the timing attack implemented running 15 decryptions for each bit flipped. 312 | 313 | ![timings](https://github.com/grocid/encrypt.life-python/blob/master/timingattack.png) 314 | 315 | However, in the PGP-like setting we do not worry too much about this. 316 | 317 | ##Distinguishing attacks 318 | 319 | The simplest imaginable distinguisher will detect a constant-error encryption with probability 1. 320 | 321 | ``` 322 | 1. Pick a ciphertext block with block weight l and error weight w. 323 | 2. Sum all symbols mod 2 and check if it equals (l + w) mod 2. 324 | ``` 325 | 326 | The theory is described in more detail [here](https://grocid.net/2015/01/28/attack-on-prime-length-qc-mdpc/). There is an easy counter-measure; we can thwart this attack completely by picking the error weight odd with probability 1/2: 327 | 328 | ``` 329 | 1. Flip a balanced coin. 330 | 2. If the coin shows tails, pick a position at random and flip it. 331 | ``` 332 | 333 | This attack is contained in [distinguisher.py](https://github.com/grocid/encrypt.life-python/blob/master/distinguisher.py). 334 | 335 | ##Squaring/subcode attacks 336 | 337 | Squaring attacks exploit that (the now deprecated) p = 4800 = 2⁶ × 75. By squaring the polynomial, the vector space decreases in size by a factor 2 (which can be done six times). It may also lead to collisions in the error vector, causing a decrease in error weight. This allows an attacker to go quite far below 80-bit security. See [this paper](http://link.springer.com/article/10.1007/s10623-015-0099-x). 338 | 339 | This attack can be mitigated by picking a prime block length p. In the example above, p = 4801. 340 | 341 | #Academic papers 342 | [MDPC-McEliece: New McEliece Variants from Moderate Density Parity-Check Codes](https://eprint.iacr.org/2012/409.pdf) 343 | 344 | [Lightweight Code-based Cryptography: QC-MDPC McEliece Encryption on Reconfigurable Devices](https://www.date-conference.com/files/proceedings/2014/pdffiles/03.3_1.pdf) 345 | 346 | [Squaring attacks on McEliece public-key cryptosystems using quasi-cyclic codes of even dimension](http://link.springer.com/article/10.1007/s10623-015-0099-x) 347 | 348 | #Acknowledgements 349 | Miroslav Kratochvil (creator of [codecrypt](https://github.com/exaexa/codecrypt)) for pointing out a weakness in the protocol. --------------------------------------------------------------------------------