#!/usr/bin/python3 """ * File : pure.py [function library for pcp2] * Version : 2.0 * Date : 7 Jan 2025 * License : BSD * * Copyright (c) 2003 - 2025 * Ralf Senderek, Ireland. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by Ralf Senderek. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * """ import sys, os Modulus = ModulusLength = Encryption = Decryption = 0 Securityhash = Hash = 0 HashModulus = HashModulusLength = Generator = 1 Block = 0 protected = "None" UserID = ENTROPY = PASS = "" Minimal_key_length = Minimal_hashkey_length = 1300 # bits # for encryption a text block of 1024 bits plus a 256 bit challenge # is encrypted in one go. ModulusMargin = 520 Minimal_signingkey_length = Minimal_hashkey_length + ModulusMargin # signature data consists of a 256 bit timestamp | a 256 bit Nonce | blinded hash(message) # and this data must be 8 bits smaller than the signing key's modulus pow256 = pow(2,256) pow1024 = pow(2,1024) pow1280 = pow(2,1280) # OS specific settings OS = "unix" EOL = "\n" Home = os.environ['HOME'] + "/.pcp/" ERR_USE = 1 ERR_PERM = 2 ERR_PASSWORD = 3 ERR_CORRUPT = 4 ERR_WRONGKEY = 5 ERR_KEY = 6 ASKPASS = "/usr/bin/systemd-ask-password" if not os.path.isfile(ASKPASS) : print ("Error: Please install " + ASKPASS + " to ensure safe password input") ASKPASS = "" #-----------------------------------------------------------# def sanitize(data): forbidden = "\"ยง&$%()[]{}=?*+~,<>|\\" good = "" for i in range(len(data)) : if (data[i] not in forbidden) and (ord(data[i]) < 128): good = good + data[i] return good #-----------------------------------------------------------# def unix (command) : if os.name == "posix" : Pipe = os.popen(sanitize(command), "r") Result = Pipe.read() Pipe.close() return Result #-----------------------------------------------------------# def print_banner(): print (EOL+"##### Pure Crypto Project Version 2.0 #####") print (" based on Modular Exponentiation") print (" and RSA alone"+EOL) #-----------------------------------------------------------# def Line (S) : # remove all end of line characters while S[-1] == 10 or S[-1] == 13 : S = S[:-1] return S #-----------------------------------------------------------# def countbits(Number): Bits = 0 while Number > 0 : Number = Number // 2 Bits = Bits + 1 return Bits #-----------------------------------------------------------# def toLong (S) : # strips everything not decimal from input bytes and converts to integer # strip from the begining while S[0] < 48 or S[0] > 57 : S = S[1:] # strip from the end while S[0] < 48 or S[0] > 57 : S = S[:-1] return int(S) #-----------------------------------------------------------# def toString (Bytes) : try: return str(Bytes.decode()) except: # Bytes do not represent a string return "" #-----------------------------------------------------------# def toBytes (Number) : return str(Number).encode() #-----------------------------------------------------------# def toHex (Number) : return f'{Number:#x}'[2:] #-----------------------------------------------------------# def BytesToLong (B): # returns integer < Modulus Number = 0 index = 0 while Number*256+255 < Modulus and index < len(B) : Number = Number * 256 + B[index] index = index + 1 return Number #-----------------------------------------------------------# def NumBytes (Number) : Num = 0 X = Number while X > 0 : X = X // 256 Num += 1 return Num #-----------------------------------------------------------# def LongListToBytes (LL): PlainText = b'' Length = 0 for X in LL: Length = int (X[1]) Number = int (X[0]) PlainText = PlainText + Number.to_bytes(Length, 'big') [0:] return PlainText # bytes #-----------------------------------------------------------# def LongListToString (LL): PlainText = "" X = 0 Remainder = 0 for ClearBlock in LL : X = ClearBlock PlainBlock = "" while X > 0 : Remainder = int (X % 256) X = X // 256 PlainBlock = chr(Remainder) + PlainBlock PlainText = PlainText + PlainBlock return PlainText # String #-----------------------------------------------------------# def ModExp (Base, Exp, Mod): Hash = 1 X = Exp Factor = Base while X > 0 : Remainder = X % 2 X = X // 2 if Remainder == 1: Hash = Hash * Factor % Mod Factor = Factor * Factor % Mod return Hash #-----------------------------------------------------------# # message is of type bytes # hash value is of type integer #-----------------------------------------------------------# def hash256(Message) : return SDLH256(Message) #-----------------------------------------------------------# def Long_hash256(Number) : return Long_SDLH256(Number) #-----------------------------------------------------------# def hash(Message) : return SDLH(Message) #-----------------------------------------------------------# def SDLH(Message) : # Message is of type bytes # returns integer # # Calculates hash(x) = Generator^x mod HashModulus # # Speeding up the computation by avoiding to convert the string into # a long integer # This is the code splitting each processed character into # two ModExp()-operations # Hash = 1 # for Character in Message: # (1) A = ModExp(Generator, ord(Character), HashModulus) # (2) B = ModExp(Hash, 256, HashModulus) # (3) Hash = (A * B) % HashModulus # return Hash # Implementation with precomputed table # precomputing table for step (1) table = [] table.append(1) index = 1 Number = 1 while index < 256 : Number = (Number * Generator) % HashModulus table.append(Number) index = index + 1 # process the message character by character Hash = 1 for Byte in Message: A = table[Byte] # step (1) B = Hash # step (2) index = 8 while index > 0 : B = (B * B) % HashModulus index = index - 1 Hash = (A * B) % HashModulus # step (3) return Hash #-----------------------------------------------------------# def SDLH256(Message) : # Message is of type bytes # returns integer (256 bits) Hash256 = 0 X = SDLH(Message) Size = pow256 while X > 0 : Section = X % Size X = X // Size Hash256 = Hash256 ^ Section return Hash256 #-----------------------------------------------------------# def Long_SDLH256(Number) : # Message is of type integer # returns integer # # Calculates hash(x) = Generator^Number mod HashModulus # and XORs all sections of 256 bit in the resulting hash value # Hash256 = 0 X = ModExp(Generator, Number, HashModulus) Size = pow256 while X > 0 : Section = X % Size X = X // Size Hash256 = Hash256 ^ Section return Hash256 #-----------------------------------------------------------# def error_ownkeys(): print() print("You need to generate your own signing key and encryption key.") print("Both keys must be stored in the directory", Home ,".") print("For more information see: https://senderek.ie/pcp2/quickstart .") #-----------------------------------------------------------# def read_cryptosystem(Keytype, verbose) : import os, sys global Modulus, Encryption, Decryption, HashModulus, Generator global ModulusLength, HashModulusLength, Block, UserID, protected, Securityhash if Keytype == "encryptionkey" : KeyfileName = "encryptionkey" else: KeyfileName = "signingkey" try: FILE = open(Home + KeyfileName, "rb") Content = FILE.readlines() FILE.close() try: Modulus = toLong(Content[0]) Encryption = toLong(Content[1]) Decryption = toLong(Content[2]) HashModulus = toLong(Content[3]) Generator = toLong(Content[4]) UserID = Line(Content[5]) if Content[6][:19] != b'Securityhash = None' : StoredSecHash = toLong(Content[6][15:]) protected = Line(Content[7])[13:] # check the integrity of the stored securityhash print_securityhash(False) if StoredSecHash != Securityhash : print (EOL + "Warning: The key material", end="") print (" is inconsistent with the stored Security-Hash.") print ("Your key has been tampered with !."+ EOL) sys.exit(ERR_CORRUPT) else: print ("The integrity for", KeyfileName ,"is checked successfully!") except: print (EOL + "The keyfile", KeyfileName ,"is corrupt !" + EOL) sys.exit(ERR_CORRUPT) except IOError : print ("Your keyfile", KeyfileName , "is unavailable") error_ownkeys() sys.exit(ERR_KEY) # calculate length of moduli in bits ModulusLength = countbits(Modulus) HashModulusLength = countbits(HashModulus) # calculate blocks Block = ModulusLength // 8 # paranoia setting Block < Modulus Block = Block - 1 # in Bytes if verbose : print("User ID : ", toString(UserID)) print("Modulus length : ", ModulusLength, " bits") print("Hashmodulus length : ", HashModulusLength, " bits") if ModulusLength < Minimal_key_length : print ("Warning: Your Key is too short! ( must be at least ", Minimal_key_length, " Bits )") sys.exit(ERR_WRONGKEY) if HashModulusLength < Minimal_hashkey_length : print ("Warning: Your Hashkey is too short! ( must be at least", Minimal_hashkey_length, " Bits )") sys.exit(ERR_WRONGKEY) if (ModulusLength - ModulusMargin < HashModulusLength) and (KeyfileName == "signingkey") : print ("Warning: The signing key\'s modulus (" ,str(ModulusLength), end=" ") print (") must be at least",ModulusMargin , "bits longer ", end="") print ("than the hash modulus (", str(HashModulusLength), ") !") # because the signature material consists of a hash plus additional # information like timestamp and a Nonce sys.exit(ERR_WRONGKEY) #-----------------------------------------------------------# def burn_privatekey(): global Hash, PASS, Decryption PASS = str(Modulus) Hash = Modulus Decryption = Modulus #-----------------------------------------------------------# def print_securityhash(Print): global Securityhash # uses the present cryptosystem # the securityhash does not include the Decryption part of any key Message = toBytes(Modulus) + toBytes(Encryption) + toBytes(HashModulus) + toBytes(Generator) + UserID TestHash = 0 TestHash = hash256(Message) if Print: print ("Securityhash: ") print (str(TestHash)) Securityhash = TestHash #-----------------------------------------------------------# def key_valid(K): # returns K if valid, else None. if len(K) >= 14 : TestMessage = K[1] + K[2] + K[3] + K[4] + K[5] + K[6] # modulus, encryption, HashModulus, Generator, UserID, Securityhash # nothing else is a valid public key body TestHash = EncryptedSignature = SignatureNumber = PlainHash = 0 TestHash = hash(TestMessage) ClearSignature = "" EncryptedSignature = toLong(K[12]) SignatureNumber = ModExp(EncryptedSignature, Encryption, Modulus) Info = SignatureNumber // pow(2,HashModulusLength) # recover the Nonce BlindNonce = int (Info % pow256) Nonce = BlindNonce ^ Securityhash BlindedHash = SignatureNumber % pow(2,HashModulusLength) PlainHash = BlindedHash ^ hash(toBytes(Nonce)) if PlainHash == TestHash : return K else: print("The signature on [",toString(Line(K[5])),"] is bad!") return None else : print ("Public key is not properly signed or is corrupted.") return None #-----------------------------------------------------------# def read_keylist(enconly): # True selects only encryption keys (enc) else only signing keys (verify) import glob KeyList = [] KeyHome = Home + "trusted-keys/" SignedPubKeys = Home + "trusted-keys/*.sig" try: KeyFiles = glob.glob(SignedPubKeys) except: print (EOL + "No trusted keys available" + EOL) sys.exit(ERR_USE) for File in KeyFiles : # read Key if not siginingkey's public part if not ("signingkey.sig" in File): try : FILE = open (File, "rb") KeyData = FILE.readlines() FILE.close() # a signed pubkey exceeds 10 lines if len(KeyData) >= 10: if enconly and not (b'signing' in KeyData[5]): KeyList.append(KeyData) if not enconly and (b'signing' in KeyData[5]): KeyList.append(KeyData) except IOError : print ("cannot open " + File) ### sys.exit(ERR_PERM) return KeyList #-----------------------------------------------------------# def print_pubkeylist(): KeyList = read_keylist(True) i = 0 for Key in KeyList : print (str(i) +": ", toString(Line(Key[5])[:75])) i = i + 1 #-----------------------------------------------------------# def select_cryptosystem(User, onlyEncryptionkeys): # User is of type str # True selects only encryption key (enc) else only signing keys (verify) global Modulus, ModulusLength, Encryption, UserID, HashModulus global HashModulusLength, Generator, Block import os, re, sys KeyList = read_keylist(onlyEncryptionkeys) UserID = b'' # UserID is of type bytes try: X = int(User) UserID = Line(KeyList[X][5]) # UserID is of type bytes except: pass if not UserID: UserID = User.encode() for PubKey in KeyList : if len(PubKey) >= 10 and (UserID in Line(PubKey[5])) : # checking validity with user's pubkey read_cryptosystem("signingkey", False) if key_valid(PubKey) : Modulus = toLong(PubKey[1]) Encryption = toLong(PubKey[2]) HashModulus = toLong(PubKey[3]) Generator = toLong(PubKey[4]) UserID = Line(PubKey[5]) # calculate length of moduli in bits ModulusLength = countbits(Modulus) HashModulusLength = countbits(HashModulus) # calculate blocks Block = ModulusLength // 8 # paranoia setting Block < Modulus Block = Block - 1 # in Bytes if ModulusLength < Minimal_key_length : print ("Warning: Key is too short! (", ModulusLength, " Bits)") sys.exit(ERR_USE) print() print ("Trusted public key found!") print("User ID : ", toString(UserID)) print("Modulus length : ", ModulusLength, " bits") print("Hashmodulus length : ", HashModulusLength, " bits") print_securityhash(True) # check the integrity of the stored securityhash try: StoredSecHash = toLong(PubKey[6]) if StoredSecHash != Securityhash : print (EOL + "Warning: The signed key material", end="") print (" is inconsistent with the signed Securityhash.") print (" DO NOT USE THIS KEY!"+ EOL) print (" Check with the recipient immediately."+ EOL) sys.exit(ERR_CORRUPT) else: print ("The key\'s integrity is checked successfully.") except: sys.exit(ERR_CORRUPT) return PubKey else: print ("Public key [" + Line(PubKey[5]).decode() + "] is NOT a TRUSTED KEY.\n") return None print("No signed public key found for ",toString(UserID)) return None #-----------------------------------------------------------# def getNonce(): # return a random Nonce (int) of length 256 bit import os # maybe there are better ways to get a nonce N = b'' if OS == "unix" : try : FILE = os.open("/dev/random", 0, 0) Bytes = os.read(FILE, 64) # 512 bit N = hash256(Bytes) # 256 bit except: print ("Error: Cannot read random data from /dev/random") sys.exit(ERR_PERM) else: pass # Wish I had /dev/random return 0 return N #-----------------------------------------------------------# def create_OAEP(Message): # returns integer with OAEP padded Nonce of 512 Bit Block = C1 = C2 = H = 0 Nonce2 = getNonce() H = Long_hash256(Nonce2) C1 = Message ^ H # XOR operation C2 = Nonce2 ^ Long_hash256(C1) Block = C1 * pow256 + C2 return Block #-----------------------------------------------------------# def extract_OAEP(Block): # returns 256 Bit Nonce from Long Message = Nonce2 = C1 = C2 = H = 0 C1 = Block // pow256 C2 = Block % pow256 H = Long_hash256(C1) Nonce2 = C2 ^ H H = Long_hash256(Nonce2) Message = C1 ^ H return Message #-----------------------------------------------------------# def encrypt (P, E, M): # P : bytes, E, M : int # returns List of pairs (Integer, NumBytes) Crypt = [] # The padding method currently implemented is hash-chain-padding # Block 1: A 256 bit nonce is transmitted in the first crypto-block with OAEP # # Block 2: PAD1 = nonce, PAD2 = hash(nonce), CHALLENGE = PAD1 XOR PAD2 # 1. Messageblock XOR PAD1 || CHALLENGE # CHALLENGE has length 256 bit # # Block i: Messageblock XOR (1024 bit extended PAD1) || PAD1 XOR hash(PAD1) # set PAD1 = hash(PAD1) and PAD2 = hash(PAD2) for Block i+1 Nonce = getNonce() PAD = 0 PAD1 = Nonce PAD2 = Long_hash256(PAD1) FirstBlock = create_OAEP(Nonce) CryptBlock = ModExp(FirstBlock, E, M) Crypt.append(CryptBlock) # split the plain text P into chunks of max 1024 bit integers # record the length of the integer in bytes as pairs (integer,length) End = False i = 0 j = 0 Number = 0 PlainList = [] maxchunk = 128 Chunk = b'' Chunk2 = b'' Errors = False while j < len(P): if i < maxchunk: Chunk = Chunk + P[j:j+1] i += 1 j += 1 else: Number = int.from_bytes(Chunk, "big") PlainList.append(( Number , len(Chunk) )) i = 0 Chunk = b'' if Chunk: Number = int.from_bytes(Chunk, "big") PlainList.append(( Number , len(Chunk) )) for PBlock in PlainList : CHALLENGE = PAD1 ^ PAD2 # padding of PlainBlock with PAD1 extended to length of PlainBlock # create 1024 bit LONGPAD LONGPAD = ((PAD1 * pow256 + PAD1) * pow256 + PAD1) * pow256 + PAD1 # Numbytes | blinded plainblock | CHALLENGE | # 1281 1280 256 0 PlainNumber = PBlock[0] ^ LONGPAD # XOR operation PlainLength = PBlock[1] PlainBlock = (PlainLength * pow1280) + (PlainNumber * pow256) + CHALLENGE # the length of this plaintext block is 5*256 bits and less than the minimal modulus length if PlainBlock >= Modulus : # this block cannot be encrypted print("Fatal: encryption error") sys.exit(ERR_CORRUPT) CryptBlock = ModExp(PlainBlock, E, M) Crypt.append(CryptBlock) PAD1 = PAD2 PAD2 = Long_hash256(PAD1) return Crypt # LongList #-----------------------------------------------------------# def decrypt (C, D, M): # C : LongList of integer , D, M : Long # returns String Clear = [] # LongList ClearBlock = 0 count = len(C) # Padding inserted here. FirstBlock = ModExp(C[0] , D, M) Nonce = extract_OAEP(FirstBlock) C = C[1:] PAD = 0 PAD1 = Nonce PAD2 = Long_hash256(PAD1) ERRORS = "" for CryptBlock in C : PAD = PAD1 ^ PAD2 ClearBlock = ModExp(CryptBlock, D, M) # extract Challenge and Message CHALLENGE = ClearBlock % pow256 PaddedMessage = (ClearBlock // pow256) % pow1024 Length = ClearBlock // pow1280 if PAD != CHALLENGE: ERRORS = "yes" # create 1024 bit LONGPAD and remove padding with PAD1 LONGPAD = ((PAD1 * pow256 + PAD1) * pow256 + PAD1) * pow256 + PAD1 Message = PaddedMessage ^ LONGPAD # XOR operation Clear.append( (Message, Length) ) PAD1 = PAD2 PAD2 = Long_hash256(PAD1) count = count - 1 try: ClearText = LongListToBytes(Clear) except: # do NOT give any hint that the decryption has failed ! pass if ERRORS : return b'The cryptogram is corrupt or you do not have the key to decrypt it.\nTo prevent oracle attacks nothing is written.\n' else: return ClearText #-----------------------------------------------------------# def unlock_privatekey(H): import os, sys, stat # H is 256 bits long # check the existence of the entropy file if not os.path.isfile(Home + "entropy") : print("Your entropy file does not exist!") sys.exit(ERR_PERM) # check the permissions on the entropy file perms = stat.filemode(os.stat(Home + "entropy").st_mode) if perms != "-r--------" : print ("Your entropy file does not have secure permissions -r--------") sys.exit(ERR_PERM) # check if the entropy file is signed correctly. if not os.path.isfile(Home + "entropy.sig"): print("Your entropy file is not signed!") else: pass # it would be too expensive to check whether the signature verifies # every time a protected private key is used try : File = open(Home + "entropy", "rb") Entropy = File.read() File.close() # check if entropyfile is long enough if len(Entropy) < 1099999 : print ("FATAL ERROR: There is not enough entropy",) print (" available!") print (len(Entropy)) sys.exit(ERR_PERM) except: print (EOL + "Your entropy file is unavailable !" + EOL) sys.exit(ERR_PERM) X = H IndexList = [] while X > 0 : Remainder = X % 1000000 X = X // 1000000 IndexList.append(Remainder) Block = Modulus // 8 # number of bytes needed to pad the decryption exponent OTP = Modulus # Initial value of PAD for index in IndexList : PadString = Entropy[int(index) : int(index) + int(Block+1)] NewPad = BytesToLong(PadString) print ("*", end="") OTP = OTP ^ NewPad print () return Decryption ^ OTP # otp-method #-----------------------------------------------------------# def load_privatekey(): import os, sys global Decryption # makes private key available in Decryption if protected == b'otp' : print (EOL + "Your private key is protected." + EOL) if ASKPASS : print("Please enter your passphrase to to unlock it: ") PASS = unix(ASKPASS) print () else: if pure.OS == "unix": os.system("stty -echo") print () PASS = input("Please enter your new passphrase to protect your private key : ") print () os.system("stty echo") else: print("Cannot unlock the RSA private key.") sys.exit(ERR_PASSWORD) Hash = hash256( Line(toBytes(PASS)) ) print (EOL + "Unlocking your private key.") Decryption = unlock_privatekey(Hash) # Burn sensitive information PASS = str(Modulus) Hash = Modulus else: print (EOL + "Your private key is not protected !") # check if private key works well with a challenge Number = getNonce() EncryptedNumber = 0 EncryptedNumber = ModExp(Number, Encryption, Modulus) Challenge = 0 Challenge = ModExp(EncryptedNumber, Decryption, Modulus) if Challenge == Number : print ("Your private key is good.") else : print ("FATAL : Your private key is not working correctly.") if protected == b'otp' : print (" Maybe you entered an incorrect passphrase?") else: print (" If you have a backup, use it.") sys.exit(ERR_CORRUPT) #-----------------------------------------------------------# # Copyright 2003 - 2025, Ralf Senderek, Ireland # #-----------------------------------------------------------#