├── README.md ├── chat.py ├── doubleTLS.py ├── flow.png └── test.py /README.md: -------------------------------------------------------------------------------- 1 | # P2P-TLS 2 | 3 | ### Would not recommend using in any production environmnent 4 | 5 | A learning experiment project in which I created a TLS connection between two hosts using self-signed certificates, and used keypair exchanges underneath that to facilitate additional password authentication. I can't really vouch for its security in practice, but it helped me learn how to code a bunch of cool things including self-signed certificate generation, multithreading, and keypair generation 6 | 7 | ## doubleTLS.py 8 | 9 | Contains an importable function which handles certificate creation, password authentication, and socket creation needed to facilitate a peer to peer encrypted connection. It accomplishes it by running a server side socket, and client side socket in parallel, so that both nodes of the connection are acting as a client/server simultaneously. The intention is that having both nodes in the system be a server would eliminate the possibility of abuse that comes with just 1 node being a server 10 | 11 | References used, and a description of the project can be found at the top of the script. Furthermore, a chart illustrating the process of what it's doing can be found in 'flow.png' 12 | 13 | This function requires that the 'cryptography' module be installed, running in python 3.6 or later. If any flaws are found in its design, feel free to let me know! I'd be more than happy to improve it 14 | 15 | ## test.py 16 | 17 | Contains the most basic implementation of doubleTLS.py. It describes the inputs and outputs of the function, and how to use the sockets/keys it returns to communicate between two hosts 18 | 19 | ## chat.py 20 | 21 | Contains a basic chatroom application built atop doubleTLS.py! Allowing a conversation to take place over the generated connection 22 | 23 | 24 | -------------------------------------------------------------------------------- /chat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.6 2 | import doubleTLS 3 | 4 | import tkinter as tk #Standard python GUI library 5 | import datetime #Datetime for chat output 6 | import textwrap #Neatly splitting up strings when displaying in chat window 7 | import re #User input validation 8 | import threading #To simultaneously send information with c['localS'], and recieve information with c['remoteS'] 9 | 10 | r_text = list() #used to share incoming messages between the chat listener thread, and the tkinter main loop for the chat (tkinter isn't very compatible with multithreading) 11 | BufferSize = 1024 12 | 13 | #################################################################################################################################### 14 | # Chat Functions 15 | #################################################################################################################################### 16 | 17 | #Accepts the remote socket, symmetric key, and chat prompt message, and opens a chat conversation 18 | def chat(sendSocket,recieveSocket,symmkeyLocal,symmkeyRemote): 19 | #Create window 20 | root = tk.Tk() 21 | root.resizable(False,False) 22 | 23 | def sendMessage(event): 24 | #Message from dialog box 25 | msg = textIn.get("1.0",tk.END).strip() 26 | 27 | #Send message to remote host 28 | sendSocket.send(symmkeyRemote.encrypt(bytes(msg,'utf-8'))) 29 | 30 | #Slice the message into 48 character lines, then append each to the console 31 | msg = textwrap.wrap(msg,48) 32 | console.config(state='normal') 33 | is1 = True 34 | for line in msg: 35 | if is1: 36 | console.insert('end',f"You {getNow()}" + line + '\n') 37 | else: 38 | console.insert('end',' '*14 + line + '\n') 39 | is1 = False 40 | console.config(state='disabled') 41 | console.see('end') #Scroll to bottom automatically 42 | 43 | #Erase the contents of the input box 44 | textIn.delete("1.0",tk.END) 45 | 46 | def getMessage(msg): 47 | #Slice the message into 48 character lines, then append each to the console 48 | msg = textwrap.wrap(msg,48) 49 | 50 | console.config(state='normal') 51 | is1 = True 52 | for line in msg: 53 | if is1: 54 | console.insert('end',f"Them {getNow()}" + line + '\n') 55 | else: 56 | console.insert('end',' '*14 + line + '\n') 57 | is1 = False 58 | console.config(state='disabled') 59 | console.see('end') 60 | 61 | def leave(): 62 | root.destroy() 63 | 64 | 65 | #Define its width and height, and position in the center of the screen 66 | if doubleTLS.win: 67 | w=499 68 | h=360 69 | else: 70 | w=505 71 | h=385 72 | 73 | root.geometry(f"{w}x{h}+{round((root.winfo_screenwidth()/2)-(w/2))}+{round((root.winfo_screenheight()/2)-(h/2))}") 74 | root.title("Crypto-Chat") 75 | 76 | #Window elements 77 | console = tk.Text(root,height=20,width=62,wrap=tk.WORD,yscrollcommand=True,background="#09295c",foreground="white",state='disabled') 78 | buttonFrame = tk.Frame(root) 79 | label = tk.Label(buttonFrame,text="Chat:") 80 | textIn = tk.Text(buttonFrame,height=1,width=40) 81 | sendBind = textIn.bind('',sendMessage) 82 | if doubleTLS.win: 83 | postBtn = tk.Button(buttonFrame,text="Send",width=10, command= lambda :sendMessage('')) 84 | exitBtn = tk.Button(buttonFrame,text="Exit",width=6,command=leave) 85 | else: 86 | postBtn = tk.Button(buttonFrame,text="Send",width=7, command= lambda :sendMessage('')) 87 | exitBtn = tk.Button(buttonFrame,text="Exit",width=3,command=leave) 88 | 89 | #Positioning window elements 90 | console.grid(row=0,column=0,sticky=tk.W) 91 | buttonFrame.grid(row=1,column=0,pady=3) 92 | label.grid(row=0,column=0,sticky=tk.W) 93 | textIn.grid(row=0,column=1) 94 | postBtn.grid(row=0,column=2) 95 | exitBtn.grid(row=0,column=3) 96 | 97 | #Start listener function for recieved messages 98 | listener = threading.Thread(target=chatlistener,args=(symmkeyLocal,recieveSocket,), daemon=True) 99 | listener.start() 100 | 101 | #Main loop 102 | rLen = len(r_text) 103 | while True: 104 | try: 105 | if rLen < len(r_text): 106 | rLen = len(r_text) 107 | getMessage(r_text[-1]) 108 | 109 | #Keep main window rolling 110 | root.update() 111 | 112 | #If listener dies, write disconnect message and disable send button. Also, exit the routine and go to default tkinter mainloop 113 | if not listener.is_alive(): 114 | postBtn.config(state='disabled') 115 | console.config(state='normal') 116 | console.insert('end','Chat partner has disconnected.') 117 | console.config(state='disabled') 118 | console.see('end') 119 | textIn.unbind(sendBind) 120 | break 121 | 122 | #If window is closed, quit without error 123 | except tk._tkinter.TclError: 124 | exit() 125 | 126 | root.mainloop() 127 | sendSocket.close() 128 | recieveSocket.close() 129 | 130 | 131 | #Accepts the remote socket object, and fernet symmetric key, to constantly listen for recieved messages 132 | def chatlistener(symmkeyLocal,recieveSocket): 133 | try: 134 | while True: 135 | #Recieve message from remote host 136 | message = recieveSocket.recv(BufferSize) 137 | message = symmkeyLocal.decrypt(message) 138 | message = message.decode('utf8') 139 | 140 | #Write message to console 141 | r_text.append(message) 142 | 143 | except ConnectionResetError: 144 | print("Chat partner has disconnected") 145 | except: 146 | pass 147 | 148 | #Gets the current hour/minute for showing as a timestamp in the console 149 | def getNow(): 150 | now = datetime.datetime.now() 151 | hm = '(' 152 | if now.hour < 10: 153 | hm += f'0{now.hour}:' 154 | else: 155 | hm += f'{now.hour}:' 156 | 157 | if now.minute < 10: 158 | hm += f'0{now.minute}): ' 159 | else: 160 | hm += f'{now.minute}): ' 161 | 162 | return hm 163 | 164 | def main(): 165 | params = { 166 | 'remoteaddress' :'10.0.0.13', #127.0.0.1 is used as an exit case in the script. So to connect to localhost, be sure to use your PC's LAN IP address 167 | 'port' : 5001, #Port for the script to listen/connect on 168 | 'hostpassword' : 'P@ssw0rd', #Password that someone connecting to your device will be required to enter when connecting 169 | 'remotepassword': 'P@ssw0rd', #Password to submit to the remote host to authenticate the connection 170 | 'keypassword' : 'G00dP@ssw0rd', #Password to unlock your certificate's private key (on first run, you'll be prompted for this when it's being created) 171 | 'timeout' : 5 #Connection timeout value as an integer value in seconds. (0 to listen forever) 172 | } 173 | 174 | s = doubleTLS.connect(params) 175 | if s: 176 | chat(s['localS'],s['remoteS'],s['localK'],s['remoteK']) 177 | 178 | main() 179 | -------------------------------------------------------------------------------- /doubleTLS.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.6 2 | 3 | ################################################################################################################################################################# 4 | #References 5 | # Generating self-signed certificate https://cryptography.io/en/latest/x509/tutorial/ 6 | # RSA Cryptography https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/ 7 | # TLS/SSL secured socket https://docs.python.org/3/library/ssl.html 8 | # Multithreading https://realpython.com/intro-to-python-threading/ 9 | # 10 | ################################################################################################################################################################# 11 | #Description 12 | # This function is intended to create a secure peer to peer connection between two remote hosts. It accomplishes this 13 | # by having both peers act as both a client and server at the same time. Each having their own self-signed certificate and password authentication 14 | # so that neither host can easily abuse the other 15 | # 16 | # It's worth noting that I'm not a cryptography, or data-in-transit security expert. I'm fairly certain that I've implemented 17 | # everything correctly, but be sure to scrutinize my code for security flaws before using this for any practical application. 18 | # I'm not liable if you get your secrets stolen. this project was done as a fun thing in response to the "Earn IT" act which 19 | # has noble goals on the surface, but aggressively targets privacy and end to end encryption in practice https://www.eff.org/document/earn-it-act 20 | # 21 | # The idea is that if I can spin this up in my free time over the span of a couple weeks, then what's to stop a team of 2-3 experts from building out 22 | # a peer to peer encrypted connection system in a weekend? It really makes the idea of trying to legislate encryption seem silly imo 23 | # 24 | # This has only really been tested in a LAN environment, and I'm not yet sure how it behaves over the internet 25 | # 26 | ################################################################################################################################################################# 27 | 28 | #Items necessary to perform operations with private/public keys 29 | from cryptography.hazmat.backends import default_backend 30 | from cryptography.hazmat.primitives import hashes 31 | from cryptography.hazmat.primitives.asymmetric import rsa 32 | from cryptography.hazmat.primitives.asymmetric import padding 33 | from cryptography.hazmat.primitives import serialization 34 | 35 | #Self-signed certificate creation 36 | from cryptography import x509 37 | from cryptography.x509.oid import NameOID 38 | 39 | #Symmetric key generation 40 | import cryptography.fernet 41 | from cryptography.fernet import Fernet 42 | 43 | #Python server/client module, as well as ssl module to wrap the socket in TLS 44 | import socket 45 | import ssl 46 | 47 | #Detecting whether the script is running in Windows or otherwise by importing the msvcrt module 48 | try: 49 | import msvcrt 50 | win=1 51 | except: 52 | win=0 53 | 54 | #Multithreading for simultaneously sending and recieving messages 55 | import threading 56 | 57 | #GUI inports 58 | import tkinter as tk #Standard python GUI library 59 | import re #For user input validations 60 | 61 | #For managing the self-signed cert, and incoming public certificates 62 | import os 63 | import sys 64 | 65 | #For creating a random alias for the certificate 66 | import random 67 | import datetime 68 | import getpass 69 | 70 | #Master function that encompasses the entire process 71 | def connect(args=dict()): 72 | 73 | #Globals used to recieve return values from different threads 74 | r_key = list() #used to store the remote public key recieved in passExchangeClient() 75 | c_socket = list() #used to pass the client socket object between the establishConnection function, and the main serverclient function 76 | p_auth = list() #used to confirm client password authentication 77 | 78 | #Hashing algorithm to use in key generation 79 | hashingAlgorithm = hashes.SHA512() 80 | passwd_hashingAlgorithm = hashes.SHA256() 81 | passwd_attempts = 4 82 | BufferSize = 1024 83 | 84 | 85 | #################################################################################################################################### 86 | # Key and Certificate generation and usage functions 87 | #################################################################################################################################### 88 | 89 | #Generate key pair 90 | def makeKey(): 91 | private_key = rsa.generate_private_key( 92 | public_exponent=65537, 93 | key_size=2048, 94 | backend=default_backend() 95 | ) 96 | public_key = private_key.public_key() 97 | return {'private':private_key,'public':public_key} 98 | 99 | 100 | #Encrypt a message with assymetric public key 101 | def encrypt(pub,message): 102 | encrypted = pub.encrypt( 103 | message, 104 | padding.OAEP( 105 | mgf=padding.MGF1(algorithm=hashingAlgorithm), 106 | algorithm=hashingAlgorithm, 107 | label=None 108 | ) 109 | ) 110 | return encrypted 111 | 112 | #Decrypt a message with assymetric private key 113 | def decrypt(priv,message): 114 | decrypted = priv.decrypt( 115 | message, 116 | padding.OAEP( 117 | mgf=padding.MGF1(algorithm=hashingAlgorithm), 118 | algorithm=hashingAlgorithm, 119 | label=None 120 | ) 121 | ) 122 | return decrypted 123 | 124 | #Encoding public key as bytestring 125 | def pubString(pub): 126 | pem = pub.public_bytes( 127 | encoding=serialization.Encoding.PEM, 128 | format=serialization.PublicFormat.SubjectPublicKeyInfo 129 | ) 130 | return pem 131 | #Unpacking a bytestring public key into a key object 132 | def readPub(pubString): 133 | public_key = serialization.load_pem_public_key( 134 | pubString, 135 | backend=default_backend() 136 | ) 137 | return public_key 138 | 139 | #Writing encrypted private key to file (for use with the certificate) 140 | def writeKey(priv,passwd): 141 | priv = priv.private_bytes( 142 | encoding=serialization.Encoding.PEM, 143 | format=serialization.PrivateFormat.TraditionalOpenSSL, 144 | encryption_algorithm=serialization.BestAvailableEncryption(passwd) 145 | ) 146 | with open('Identity/private_key.pem', 'wb') as f: 147 | f.write(priv) 148 | 149 | #Generating a self-signed certificate with an existing private key 150 | def makeCert(): 151 | if not os.path.isfile('Identity/certificate.pem'): 152 | #Generate private key 153 | key = makeKey()['private'] 154 | 155 | #Get password input from user for private key 156 | password = '' 157 | while True: 158 | if not args: 159 | passInp = passwordPrompt() 160 | else: 161 | passInp = passwordPromptNG() 162 | 163 | if passInp is None: 164 | exit() 165 | if not passInp == b'': 166 | password = passInp 167 | break 168 | 169 | #Generate ranom alias 170 | aliasID = f"{random.randint(1,10000000)}".zfill(8) 171 | alias = f"Anon{aliasID}" 172 | 173 | #The only data we're adding on are an organization name, and the computer's hostname 174 | subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, alias)]) 175 | 176 | cert = x509.CertificateBuilder().subject_name( 177 | subject 178 | ).issuer_name( 179 | issuer 180 | ).public_key( 181 | key.public_key() 182 | ).serial_number( 183 | x509.random_serial_number() 184 | ).not_valid_before( 185 | datetime.datetime.utcnow() 186 | ).not_valid_after( 187 | #Valid for one year 188 | datetime.datetime.utcnow() + datetime.timedelta(days=365) 189 | ).add_extension( 190 | x509.SubjectAlternativeName([x509.DNSName(u'localhost')]), 191 | critical=False, 192 | # Sign the certificate with the private key 193 | ).sign(key, hashes.SHA256(), default_backend()) 194 | 195 | #Write certificate to file 196 | with open('Identity/certificate.pem','wb') as fp: 197 | fp.write(cert.public_bytes(serialization.Encoding.PEM)) 198 | 199 | #Write private key to file 200 | writeKey(key,password) 201 | 202 | #################################################################################################################################### 203 | # Secure connection and Authentication functions 204 | #################################################################################################################################### 205 | 206 | #serverSocket: Used to listen for the initial inbound connection, and facilitates TLS over the entire transaction 207 | #remoteSocket: Facilitates the connection to the remote socket that's been connected to 208 | #c_socket[0] : Acts as a client socket to the remote server that's been connected to 209 | def clientServer(keypasswd,hostpassword,remoteaddress,remotepassword,Port,timeout): 210 | #Host socket object 211 | servercontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 212 | servercontext.load_cert_chain('Identity/certificate.pem', 'Identity/private_key.pem',password=keypasswd) 213 | 214 | serverSocketI = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 215 | serverSocket = servercontext.wrap_socket(serverSocketI, server_side=True) 216 | 217 | #Bind the server socket to localhost, and turn off timeout so it can listen forever 218 | try: 219 | serverSocket.bind(('0.0.0.0',Port)) 220 | except OSError: 221 | print("S: Likely port already in use (Linux has a brief timeout between runs)") 222 | return 223 | serverSocket.settimeout(None) 224 | 225 | #The socket accept operation has doesn't seem to have a means of exiting once it's started. So I worked around this by 226 | #having this function running in a separate thread, which makes a connection to localhost 227 | def exitCatchTM(Port): 228 | #Hold here until timeout criteria is reached, then close the connection 229 | x = datetime.datetime.utcnow().timestamp() 230 | while (not ((datetime.datetime.utcnow().timestamp() - x) > timeout)) or not timeout: 231 | pass 232 | 233 | if not c_socket: 234 | dummysocket = socket.create_connection(('127.0.0.1',Port)) 235 | clientContext = ssl.SSLContext(ssl.PROTOCOL_TLS) 236 | dummySocketS = clientContext.wrap_socket(dummysocket, server_hostname='127.0.0.1') 237 | 238 | def exitCatchKP(Port): 239 | input() 240 | if not c_socket: 241 | dummysocket = socket.create_connection(('127.0.0.1',Port)) 242 | clientContext = ssl.SSLContext(ssl.PROTOCOL_TLS) 243 | dummySocketS = clientContext.wrap_socket(dummysocket, server_hostname='127.0.0.1') 244 | 245 | #Activate client socket function, as well as a function that connects to localhost to fulfill the exit condition 246 | listener = threading.Thread(target=establishConnection,args=(remoteaddress,Port), daemon=True) 247 | listener.start() 248 | 249 | quitK = threading.Thread(target=exitCatchKP,args=(Port,), daemon=True) 250 | quitK.start() 251 | quitT = threading.Thread(target=exitCatchTM,args=(Port,), daemon=True) 252 | quitT.start() 253 | 254 | print(f"S: Listening on port {Port} (Press Enter to exit)...") 255 | 256 | #Start listening for connection 257 | serverSocket.listen(1) 258 | try: 259 | remoteSocket, address = serverSocket.accept() 260 | except ConnectionAbortedError: 261 | print("S: Connection Cancelled, or timed out") 262 | return 263 | 264 | #If the remote connection was localhost (operation cancelled), exit the script 265 | if address[0] == '127.0.0.1': 266 | remoteSocket.close() 267 | print("S: Connection Cancelled, or timed out") 268 | return 269 | if address[0] != remoteaddress: 270 | remoteSocket.close() 271 | print(f"S: Connection recieved from unexpected host ({address[0]})") 272 | return 273 | 274 | #Wait for the establish function to exit before continuing, then make sure it worked 275 | listener.join() 276 | 277 | #Exit if nothing was returned 278 | if not c_socket: 279 | return 280 | 281 | print(f"S: Established connection from {address[0]}") 282 | 283 | 284 | #Generate keypair for password exchange 285 | key = makeKey() 286 | print("S: Generated Keypair") 287 | 288 | #Hash the passwords before sending them over the wire 289 | h = hashes.Hash(passwd_hashingAlgorithm,backend=default_backend()) 290 | h.update(bytes(hostpassword,'utf8')) 291 | hostpassword = h.finalize() 292 | h = hashes.Hash(passwd_hashingAlgorithm,backend=default_backend()) 293 | h.update(bytes(remotepassword,'utf8')) 294 | remotepassword = h.finalize() 295 | 296 | #Activate password client function 297 | passwordClient = threading.Thread(target=passExchangeClient,args=(remotepassword,), daemon=True) 298 | passwordClient.start() 299 | 300 | #Send the other node the public key, then wait for password attempt 301 | remoteSocket.send(pubString(key['public'])) 302 | print(f"S: Sent public key to {address[0]}") 303 | 304 | #If the password matches, send a Granted message. Else send denied 305 | attempts = 0 306 | while attempts < passwd_attempts: 307 | 308 | passAttempt = remoteSocket.recv(BufferSize) 309 | #This block is in a try-except, in the event that the other person exits on password retry 310 | try: 311 | #If password match, send Granted response and break loop 312 | if decrypt(key['private'],passAttempt) == hostpassword: 313 | remoteSocket.send(bytes("Granted",'utf8')) 314 | print(f"S: Password match from {address[0]}") 315 | break 316 | else: 317 | remoteSocket.send(bytes("Denied",'utf8')) 318 | print(f"S: Password failed attempt from {address[0]}") 319 | attempts += 1 320 | except: 321 | print(f"S: {address[0]} Left during password authentication") 322 | return 323 | 324 | if attempts == passwd_attempts: 325 | print("S: Password attempts exceeded.") 326 | return 327 | 328 | #Wait for the client to process the response before continuing 329 | passwordClient.join() 330 | 331 | #Exit if client authentication failed 332 | if not p_auth: 333 | return 334 | 335 | #Symmetric Key Exchange 336 | #Generate and send it out via the remote public key 337 | symmkeyLocal = Fernet.generate_key() 338 | c_socket[0].send(encrypt(r_key[0],symmkeyLocal)) 339 | symmkeyLocal = Fernet(symmkeyLocal) 340 | print(f"C: Sent symmetric key to {address[0]}") 341 | 342 | #Recieve and decode it with local private key 343 | symmkeyRemote = remoteSocket.recv(BufferSize) 344 | symmkeyRemote = decrypt(key['private'],symmkeyRemote) 345 | symmkeyRemote = Fernet(symmkeyRemote) 346 | print(f"S: Recieved symmetric key from {address[0]}") 347 | 348 | #With all that information, return the active sockets and keys 349 | return { 350 | 'localS':remoteSocket, 351 | 'remoteS':c_socket[0], 352 | 'localK':symmkeyLocal, 353 | 'remoteK':symmkeyRemote 354 | } 355 | 356 | #Function that runs in a separate thread, and connects to a remote server while main simultaneously listens for its own remote connection 357 | def establishConnection(clientaddress,Port): 358 | #TLS Client context 359 | clientContext = ssl.SSLContext(ssl.PROTOCOL_TLS) 360 | #Attempt to connect to a remote address with a regular socket 361 | print(f"C: Attempting connection to {clientaddress} port {Port}...") 362 | 363 | #Ignore timeout errors and continually attempt to connect until it succeeds 364 | while True: 365 | try: 366 | clientSocketI = socket.create_connection((clientaddress, Port)) 367 | break 368 | except TimeoutError: 369 | pass 370 | except ConnectionAbortedError: 371 | print('C: A connection was established, but then refused by the host') 372 | except OSError: 373 | print('C: No route found found to host') 374 | 375 | #After connection, secure the socket 376 | clientSocket = clientContext.wrap_socket(clientSocketI, server_hostname=clientaddress) 377 | #Pass that socket up to the global scope pefore the therad ends, so that the main function can utilize it 378 | print(f"C: Connection established to {clientaddress}") 379 | 380 | #Get remote address and certificate to validate if a cert is good or not 381 | raddr = clientSocket.getpeername()[0] 382 | rcert = clientSocket.getpeercert(True) 383 | 384 | #Clean the remote address to use as a filename when storing remote public cert to disk 385 | raddr = re.sub(r'[^a-zA-Z0-9\.]','',raddr) 386 | #If there isn't currently a cert stored for the address, write to disk 387 | if not os.path.isfile(f'RemoteCerts/{raddr}'): 388 | fp = open(f'RemoteCerts/{raddr}','wb') 389 | fp.write(bytearray(b for b in rcert)) 390 | fp.close() 391 | print(f"C: {raddr} added to known hosts") 392 | 393 | #If it does exist, read its contents and compare it to the just retrieved one 394 | else: 395 | fp = open(f'RemoteCerts/{raddr}','rb') 396 | storedCert = b'' 397 | for c in fp: 398 | storedCert += c 399 | fp.close() 400 | 401 | if storedCert == rcert: 402 | print(f'C: {raddr} identity is the same') 403 | else: 404 | print(f'C: ALERT - {raddr} identity has changed\t <-------------------') 405 | 406 | #Send the socket up to the main thread, and disable timeout 407 | c_socket.append(clientSocket) 408 | c_socket[0].settimeout(None) 409 | 410 | return 411 | 412 | #Facilitates the client side of the password exchange 413 | def passExchangeClient(remotepassword): 414 | #Recieve public key, then send back an encrypted password attempt with it 415 | pubkey = c_socket[0].recv(BufferSize) 416 | pubkey = readPub(pubkey) 417 | print("C: Recieved public key from remote host") 418 | r_key.append(pubkey) 419 | 420 | attempts = 0 421 | while attempts < passwd_attempts: 422 | #Send password attempt 423 | c_socket[0].send(encrypt(pubkey,remotepassword)) 424 | print("C: Sent password attempt") 425 | 426 | #Then wait for a response and act on it 427 | response = c_socket[0].recv(BufferSize) 428 | if response == b'Granted': 429 | print("C: Password accepted by remote host") 430 | p_auth.append(1) 431 | break 432 | else: 433 | print("C: Password rejected by remote host") 434 | attempts += 1 435 | if attempts < passwd_attempts: 436 | #Provide a tkinter input if running gui, or exit the program on wrong attempt 437 | if not args: 438 | remotepassword = passwordWindow() 439 | if not remotepassword: 440 | c_socket[0].close() 441 | return 442 | else: 443 | c_socket[0].close() 444 | return 445 | if remotepassword == None: 446 | return 447 | 448 | #################################################################################################################################### 449 | # Input functions 450 | #################################################################################################################################### 451 | #Password retry prompt, no GUI edition (Unused because handling keyboard interrupt with multiple threads is terrible lol) 452 | def passwordWindowNG(): 453 | while True: 454 | print('-'*20) 455 | passwd = input("Please enter another password (blank to exit): ").strip() 456 | if len(passwd) > 0: 457 | h = hashes.Hash(passwd_hashingAlgorithm,backend=default_backend()) 458 | h.update(bytes(passwd,'utf8')) 459 | passwd = h.finalize() 460 | return passwd 461 | else: 462 | c_socket[0].close() 463 | exit() 464 | 465 | #Certificate password entry, no GUI edition 466 | def passwordPromptNG(): 467 | while True: 468 | print("A: Certificate has not yet been generated. Please enter a secure password to use as the unlock key:") 469 | while True: 470 | p1 = getpass.getpass("A: Password: ").strip() 471 | p2 = getpass.getpass("A: Confirm: ").strip() 472 | if (p1 == p2) and (len(p1) > 0): 473 | return bytes(p1,'utf8') 474 | else: 475 | print("A: Passwords don't match. Please try again") 476 | 477 | #Password prompt for handling incorrect password attempts as they come in 478 | def passwordPrompt(): 479 | root = tk.Tk() 480 | root.resizable(False,False) 481 | passwd = list() 482 | 483 | def confirmPassword(e=0): 484 | if p1.get().strip() == p2.get().strip(): 485 | passwd.append(bytes(p1.get().strip(),'utf8')) 486 | root.destroy() 487 | 488 | def exitOperation(): 489 | passwd.append(None) 490 | root.destroy() 491 | 492 | #Define window dimentions 493 | if win: 494 | w = 220 495 | h = 155 496 | else: 497 | w = 260 498 | h = 190 499 | root.geometry(f"{w}x{h}+{round((root.winfo_screenwidth()/2)-(w/2))}+{round((root.winfo_screenheight()/2)-(h/2))}") 500 | root.title('Cert Password') 501 | 502 | #Labels 503 | pL1 = tk.Label(root,text="Password:",width=11) 504 | pL2 = tk.Label(root,text="Confirm :",width=11) 505 | description = tk.Label(root,width=28,wraplength=180,text=f"Please enter a password to use for your certificate's private key (Used to keep people from stealing your self-signed cert)\n{'-'*20}") 506 | 507 | #Inputs 508 | p1 = tk.Entry(root,width=20,show='*') 509 | p2 = tk.Entry(root,width=20,show="*") 510 | 511 | #Button 512 | ok = tk.Button(root,text="OK",width=16,command=confirmPassword) 513 | cancel = tk.Button(root,text="Cancel",width=8,command=exitOperation) 514 | 515 | #Position of everything 516 | description.grid(row=0,column=0,columnspan=2) 517 | pL1.grid(row=1,column=0) 518 | pL2.grid(row=2,column=0) 519 | p1.grid(row=1,column=1) 520 | p2.grid(row=2,column=1) 521 | ok.grid(row=3,column=1) 522 | cancel.grid(row=3,column=0) 523 | 524 | #Binding for enter button 525 | root.bind('',confirmPassword) 526 | 527 | root.mainloop() 528 | 529 | #If no input, or cancelled, reutrn none 530 | if not passwd: 531 | return None 532 | elif passwd[0] is None: 533 | return None 534 | else: 535 | return passwd[0] 536 | 537 | def landingWindow(defaults=False): 538 | #Setting up the window 539 | startupFrame = tk.Tk() 540 | startupFrame.resizable(False,False) 541 | 542 | #Define window dimentions 543 | if win: 544 | w = 270 545 | h = 140 546 | else: 547 | w = 350 548 | h = 150 549 | startupFrame.geometry(f"{w}x{h}+{round((startupFrame.winfo_screenwidth()/2)-(w/2))}+{round((startupFrame.winfo_screenheight()/2)-(h/2))}") 550 | startupFrame.title('Connection startup') 551 | 552 | #Break function for the connect button 553 | returnVar = list() 554 | 555 | #This function is called by both the button, and a keyboard event: pressing return while on password (which sends a parameter by default) 556 | #So it has a dummy parameter with a default value to cover both situations. 557 | def connectHosts(e=0): 558 | #Get variables from the window 559 | server = serveraddr.get().strip() 560 | port = portinput.get().strip() 561 | lPassword = lPasswordField.get().strip() 562 | rPassword = rPasswordField.get().strip() 563 | kPassword = kPasswordField.get().strip() 564 | 565 | #Package them into a dictionary, and append them to the return list before closing the window 566 | returnVar.append({'remoteaddress':server,'Port':port,'hostpassword':lPassword,'remotepassword':rPassword,'keypasswd':kPassword}) 567 | startupFrame.destroy() 568 | 569 | def tab(event): 570 | event.widget.tk_focusNext().focus() 571 | return("break") 572 | 573 | 574 | #Input fields 575 | serveraddr = tk.Entry(startupFrame,width=27) 576 | portinput = tk.Entry(startupFrame,width=27) 577 | lPasswordField = tk.Entry(startupFrame,width=27,show='*') 578 | rPasswordField = tk.Entry(startupFrame,width=27,show='*') 579 | kPasswordField = tk.Entry(startupFrame,width=27,show='*') 580 | 581 | #Keep text from previous entry if provided 582 | if defaults: 583 | serveraddr.insert(tk.END,defaults['remoteaddress']) 584 | portinput.insert(tk.END,defaults['Port']) 585 | lPasswordField.insert(tk.END,defaults['hostpassword']) 586 | rPasswordField.insert(tk.END,defaults['remotepassword']) 587 | kPasswordField.insert(tk.END,defaults['keypasswd']) 588 | 589 | 590 | #Tab behavior 591 | serveraddr.bind('',tab) 592 | portinput.bind('',tab) 593 | lPasswordField.bind('',tab) 594 | rPasswordField.bind('',tab) 595 | kPasswordField.bind('',tab) 596 | 597 | #Enter behavior 598 | startupFrame.bind('',connectHosts) 599 | 600 | #Connect button 601 | connectBtn = tk.Button(startupFrame,text="Connect",width=20,command=connectHosts) 602 | 603 | #Labels 604 | serverLabel = tk.Label(startupFrame,text="Remote Address:") 605 | portLabel = tk.Label(startupFrame,text="Port: ") 606 | lPasswordLabel = tk.Label(startupFrame,text="Local Password:") 607 | rPasswordLabel = tk.Label(startupFrame,text="Remote Password:") 608 | kPasswordLabel = tk.Label(startupFrame,text="Key Password:") 609 | 610 | #Positions of everything 611 | serverLabel.grid(row=2,column=0) 612 | serveraddr.grid(row=2,column=1) 613 | portLabel.grid(row=3,column=0) 614 | portinput.grid(row=3,column=1) 615 | lPasswordLabel.grid(row=4,column=0) 616 | lPasswordField.grid(row=4,column=1) 617 | rPasswordLabel.grid(row=5,column=0) 618 | rPasswordField.grid(row=5,column=1) 619 | kPasswordLabel.grid(row=6,column=0) 620 | kPasswordField.grid(row=6,column=1) 621 | connectBtn.grid(row=7,column=0,columnspan=3) 622 | 623 | #Run the window 624 | startupFrame.mainloop() 625 | 626 | #If the OK button was used to exit, return the input 627 | if returnVar: 628 | return returnVar[0] 629 | else: 630 | return None 631 | 632 | #Window to get a new password from the user if a wrong one was input 633 | def passwordWindow(): 634 | #Window parameters 635 | root = tk.Tk() 636 | w = 370 637 | h = 80 638 | root.geometry(f"{w}x{h}+{round((root.winfo_screenwidth()/2)-(w/2))}+{round((root.winfo_screenheight()/2)-(h/2))}") 639 | root.title('Wrong password') 640 | 641 | #Return variable 642 | output = list() 643 | 644 | #Function to run on OK or enter press that exits the function if the password field is populated 645 | def passSubmit(e=0): 646 | if pField.get().strip(): 647 | output.append(pField.get().strip()) 648 | root.destroy() 649 | 650 | #Window objects and their positions 651 | pField = tk.Entry(root,show="*",width=30) 652 | description = tk.Label(root,text="Password to server was incorrect. Please enter another") 653 | okbutton = tk.Button(root,text="OK",width=15,command=passSubmit) 654 | description.grid(row=0,column=0) 655 | pField.grid(row=1,column=0) 656 | okbutton.grid(row=2,column=0) 657 | 658 | #Binding the return key to the passSubmit function, and then starting the window 659 | root.bind('',passSubmit) 660 | root.mainloop() 661 | 662 | #If the user exit the function early, return none. Else returned the hash representation of the password 663 | if not output: 664 | return None 665 | else: 666 | h = hashes.Hash(passwd_hashingAlgorithm,backend=default_backend()) 667 | h.update(bytes(output[0],'utf8')) 668 | output[0] = h.finalize() 669 | return output[0] 670 | 671 | #################################################################################################################################### 672 | # Main Loop 673 | #################################################################################################################################### 674 | def main(previousOpts=False): 675 | #Reset globals on re-call 676 | c_socket.clear() 677 | r_key.clear() 678 | p_auth.clear() 679 | 680 | #Create directories to house the host identity, and remote public certs 681 | if not os.path.isdir('Identity'): 682 | os.mkdir('Identity') 683 | if not os.path.isdir('RemoteCerts'): 684 | os.mkdir('RemoteCerts') 685 | 686 | #Generate self-signed certificate if it doesn't exist 687 | makeCert() 688 | 689 | #Run the prompt, and exit if they exited that window 690 | opts = landingWindow(previousOpts) 691 | if not opts: 692 | return 693 | 694 | #Loop to ensure good inputs 695 | inputCheck = True 696 | while inputCheck: 697 | #Exit if they quit the options window 698 | if not opts: 699 | return 700 | 701 | #Check that the port number is above 1000, and that all other inputs have something there before proceeding 702 | if not re.match(r'[0-9]?[1-9][0-9]{3}',opts['Port']): 703 | print("A: Port number must be an integer value >= 1000") 704 | opts = landingWindow(opts) 705 | elif opts['keypasswd'] == '': 706 | print("A: Please provide the password to your server key") 707 | opts = landingWindow(opts) 708 | elif opts['hostpassword'] == '': 709 | print("A: Please provide the password to set for your side of the connection") 710 | opts = landingWindow(opts) 711 | elif opts['remotepassword'] == '': 712 | print("A: Please provide a password to the other person's connection") 713 | opts = landingWindow(opts) 714 | elif opts['remoteaddress'] == '': 715 | print("A: Please provide the address of the machine to connect to") 716 | opts = landingWindow(opts) 717 | else: 718 | #Create a context that doesn't go anywhere, just for making sure the key password is correct before proceeding 719 | try: 720 | dummycontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 721 | dummycontext.load_cert_chain('Identity/certificate.pem', 'Identity/private_key.pem',password=opts['keypasswd']) 722 | inputCheck=False 723 | except: 724 | print("S: Server certificate password was incorrect.") 725 | opts = landingWindow(opts) 726 | 727 | opts['Port'] = int(opts['Port']) 728 | return clientServer(opts['keypasswd'],opts['hostpassword'],opts['remoteaddress'],opts['remotepassword'],opts['Port'],timeout=0) 729 | 730 | def mainNG(args): 731 | 732 | #Create directories to house the host identity, and remote public certs 733 | if not os.path.isdir('Identity'): 734 | os.mkdir('Identity') 735 | if not os.path.isdir('RemoteCerts'): 736 | os.mkdir('RemoteCerts') 737 | #Generate self-signed certificate if it doesn't exist 738 | makeCert() 739 | 740 | #Check that the port number is above 1000, and that all other inputs have something there before proceeding 741 | if not type(args['port']) == int: 742 | raise Exception("Port number must be an integer value >= 1000") 743 | elif args['port'] < 1000: 744 | raise Exception("Port number must be an integer value >= 1000") 745 | elif args['keypassword'] == '': 746 | raise Exception("No certificate key password provided") 747 | elif args['hostpassword'] == '': 748 | raise Exception("No server-side password value provided") 749 | elif args['remotepassword'] == '': 750 | raise Exception("No password attempt provided") 751 | elif args['remoteaddress'] == '': 752 | raise Exception("No remote address provided") 753 | elif not type(args['timeout']) == int: 754 | raise Exception("Timeout variable must be an integer value") 755 | elif args['timeout'] < 0: 756 | raise Exception("Timeout must be a positive value (0 for no timeout)") 757 | else: 758 | #Create a context that doesn't go anywhere, just for making sure the key password is correct before proceeding 759 | try: 760 | dummycontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 761 | dummycontext.load_cert_chain('Identity/certificate.pem', 'Identity/private_key.pem',password=args['keypassword']) 762 | except: 763 | raise Exception("Incorrect cerificate password provided") 764 | 765 | return clientServer(args['keypassword'],args['hostpassword'],args['remoteaddress'],args['remotepassword'],args['port'],args['timeout']) 766 | 767 | #Run the main function, and return the live sockets to whatever script called it 768 | if not args: 769 | return main() 770 | else: 771 | return mainNG(args) 772 | -------------------------------------------------------------------------------- /flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrispyth42/P2P-TLS/db92df1c6d7c954b1cb8881130f4d00de3a47264/flow.png -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import doubleTLS 2 | 3 | #Parameters for accessing it no gui (Throws error on incorrect input) 4 | params = { 5 | 'remoteaddress' :'10.0.0.13', #127.0.0.1 is used as an exit case in the script. So to connect to localhost, be sure to use your PC's LAN IP address 6 | 'port' : 5001, #Port for the script to listen/connect on 7 | 'hostpassword' : 'p@ssw0rd', #Password that someone connecting to your device will be required to enter when connecting 8 | 'remotepassword': 'p@ssw0rd', #Password to submit to the remote host to authenticate the connection 9 | 'keypassword' : 'G00dP@ssw0rd', #Password to unlock your certificate's private key (on first run, you'll be prompted for this when it's being created) 10 | 'timeout' : 0 #Connection timeout value as an integer value in seconds. (0 to listen forever) 11 | } 12 | 13 | c = doubleTLS.connect(params) 14 | #Create the connection using the arguments specified above. If successful, it returns: 15 | # c['localS'] : TLS wrapped socket connection where the local machine's certificate is being used 16 | # c['localK'] : Fernet symmetric key generated on the local machine 17 | # c['remoteS'] : TLS wrapped socket connection where the remote machine's certificate is being used 18 | # c['remoteK'] : Fernet symmetric key generated on the remote machine 19 | 20 | #Exit early if the connect function didn't return anything 21 | print('-'*40) 22 | if not c: 23 | print('Connection failed') 24 | exit() 25 | 26 | #Details of objects returned by the connect function 27 | print("Connection Success!") 28 | print(f'Running on port {params["port"]}\n') 29 | 30 | print(f'Serving on {c["localS"].getsockname()[0]} to {c["localS"].getpeername()[0]} with {c["localS"].version()}') 31 | print(f'\tServer symmetric key: {c["localK"]._encryption_key}') 32 | 33 | print(f'Connected to {c["remoteS"].getpeername()[0]} with {c["remoteS"].version()}') 34 | print(f'\tRemote symmetric key: {c["remoteK"]._encryption_key}\n') 35 | 36 | 37 | #Sending an encrypted message to the other host 38 | msgOUT = f"Hello from {c['localS'].getsockname()[0]}!" 39 | msgOUT = c['localK'].encrypt(bytes(msgOUT,'utf8')) #Encrypt it using the server side symmetric key 40 | c['localS'].send(msgOUT) #Send it out using the server socket 41 | 42 | #Recieve and decrypt a message from the other host 43 | msgIN = c['remoteS'].recv(1024) #Recieve the message coming in from the remote socket 44 | msgIN = c['remoteK'].decrypt(msgIN) #Decrypt it with the remote symmetric key before printing 45 | print(f"Remote server says:\n\t{msgIN}") 46 | 47 | --------------------------------------------------------------------------------