1231. 2025. 2. 26. 09:47
import tkinter as tk
from tkinter import ttk, scrolledtext, simpledialog
import requests
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
import base64
import threading
import time
import random
import string
import hashlib

class ConsoleStyle:
    BG_COLOR = 'black'
    FG_COLOR = '#00ff00'  # 연한 녹색 (터미널 스타일)
    FONT = ('Consolas', 10)  # 고정폭 폰트
    CURSOR_COLOR = '#00ff00'
    SELECT_BG = '#003300'  # 선택 영역 배경색
    SELECT_FG = '#00ff00'  # 선택 영역 텍스트색

class ChatClientGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Secure Terminal Chat")
        self.setup_gui()
        self.setup_client()
        self.apply_console_style()
        self.setup_shortcuts()  # 단축키 설정 추가
        
        # 암호화 관련 변수 초기화
        self.secret_key = None
        self.iv = None

    def setup_shortcuts(self):
        # Ctrl+O 단축키 바인딩
        self.root.bind('<Control-o>', lambda e: self.clear_chat())

    def clear_chat(self):
        # 채팅 영역의 모든 내용을 삭제
        self.chat_area.delete(1.0, tk.END)
        self.show_system_message("대화 내용이 삭제되었습니다.")

    def apply_console_style(self):
        # 윈도우 스타일
        self.root.configure(bg=ConsoleStyle.BG_COLOR)
        
        # 스타일 설정
        style = ttk.Style()
        style.configure('Console.TFrame', background=ConsoleStyle.BG_COLOR)
        style.configure('Console.TLabel', 
                       background=ConsoleStyle.BG_COLOR,
                       foreground=ConsoleStyle.FG_COLOR,
                       font=ConsoleStyle.FONT)
        style.configure('Console.TButton',
                       background=ConsoleStyle.BG_COLOR,
                       foreground=ConsoleStyle.FG_COLOR,
                       font=ConsoleStyle.FONT)
        style.configure('Console.TEntry',
                       background=ConsoleStyle.BG_COLOR,
                       foreground=ConsoleStyle.FG_COLOR,
                       font=ConsoleStyle.FONT,
                       fieldbackground=ConsoleStyle.BG_COLOR)

        # 채팅 영역 스타일
        self.chat_area.configure(
            bg=ConsoleStyle.BG_COLOR,
            fg=ConsoleStyle.FG_COLOR,
            font=ConsoleStyle.FONT,
            insertbackground=ConsoleStyle.CURSOR_COLOR,
            selectbackground=ConsoleStyle.SELECT_BG,
            selectforeground=ConsoleStyle.SELECT_FG
        )

        # 입력 영역 스타일
        self.message_entry.configure(
            style='Console.TEntry',
            font=ConsoleStyle.FONT
        )

    def setup_client(self):
        self.client_id = self.generate_client_id()
        self.running = True
        self.server_url = None
        
        # 서버 연결 프레임
        self.connect_frame = ttk.Frame(self.root, padding="10", style='Console.TFrame')
        self.connect_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        ttk.Label(self.connect_frame, text="서버 IP:", style='Console.TLabel').grid(row=0, column=0, padx=5)
        self.server_ip = ttk.Entry(self.connect_frame, width=20, style='Console.TEntry')
        self.server_ip.insert(0, "localhost")
        self.server_ip.grid(row=0, column=1, padx=5)
        
        # PIN 입력 추가
        ttk.Label(self.connect_frame, text="PIN (4자리):", style='Console.TLabel').grid(row=1, column=0, padx=5, pady=5)
        self.pin_entry = ttk.Entry(self.connect_frame, width=10, style='Console.TEntry', show="*")
        self.pin_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
        
        self.connect_btn = ttk.Button(self.connect_frame, text="연결", 
                                    command=self.connect_to_server, 
                                    style='Console.TButton')
        self.connect_btn.grid(row=1, column=2, padx=5)

        # 메시지 영역은 처음에는 숨김
        self.message_frame.grid_remove()

    def setup_gui(self):
        # 메시지 프레임
        self.message_frame = ttk.Frame(self.root, padding="10", style='Console.TFrame')
        self.message_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 채팅 영역
        self.chat_area = scrolledtext.ScrolledText(
            self.message_frame, 
            wrap=tk.WORD, 
            width=80,  # 더 넓게
            height=24  # 더 높게
        )
        self.chat_area.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 메시지 입력
        self.message_entry = ttk.Entry(
            self.message_frame,
            style='Console.TEntry',
            width=70  # 더 넓게
        )
        self.message_entry.grid(row=1, column=0, sticky=(tk.W, tk.E), padx=5, pady=5)
        self.message_entry.bind('<Return>', lambda e: self.send_message())
        
        # 전송 버튼
        self.send_button = ttk.Button(
            self.message_frame, 
            text="전송", 
            command=self.send_message,
            style='Console.TButton'
        )
        self.send_button.grid(row=1, column=1, sticky=(tk.E), pady=5)
        
        # Grid 설정
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(1, weight=1)
        self.message_frame.columnconfigure(0, weight=1)

    def generate_client_id(self):
        return ''.join(random.choices(string.ascii_letters + string.digits, k=8))
    
    def generate_key_from_pin(self, pin):
        """PIN을 32바이트 키와 16바이트 IV로 변환"""
        # PIN을 바이트로 변환
        pin_bytes = pin.encode('utf-8')
        
        # SHA-256으로 해시하여 32바이트 키 생성
        key_hash = hashlib.sha256(pin_bytes).digest()
        
        # MD5로 해시하여 16바이트 IV 생성
        iv_hash = hashlib.md5(pin_bytes).digest()
        
        return key_hash, iv_hash

    def connect_to_server(self):
        # PIN 입력 확인
        pin = self.pin_entry.get().strip()
        if not pin or len(pin) != 4 or not pin.isdigit():
            self.show_system_message("유효한 4자리 PIN을 입력해주세요.")
            return
            
        # PIN에서 암호화 키와 IV 생성
        self.secret_key, self.iv = self.generate_key_from_pin(pin)
        
        server_ip = self.server_ip.get()
        self.server_url = f'http://{server_ip}:5000'
        
        try:
            response = requests.post(f"{self.server_url}/register", 
                                   json={"client_id": self.client_id})
            if response.status_code == 200:
                self.show_system_message(f"서버에 연결되었습니다. (PIN: {pin})")
                self.connect_frame.grid_remove()
                self.message_frame.grid()
                self.receive_thread = threading.Thread(target=self.receive_messages)
                self.receive_thread.daemon = True
                self.receive_thread.start()
            else:
                self.show_system_message("서버 연결 실패")
        except Exception as e:
            self.show_system_message(f"연결 오류: {str(e)}")

    def encrypt_message(self, message):
        if not self.secret_key or not self.iv:
            self.show_system_message("암호화 키가 설정되지 않았습니다.")
            return None
            
        try:
            message_bytes = message.encode('utf-8')
            padded_message = pad(message_bytes, AES.block_size)
            cipher = AES.new(self.secret_key, AES.MODE_CBC, IV=self.iv)
            encrypted_message = cipher.encrypt(padded_message)
            return base64.b64encode(encrypted_message).decode('utf-8')
        except Exception as e:
            self.show_system_message(f"암호화 오류: {str(e)}")
            return None

    def decrypt_message(self, encrypted_message):
        if not self.secret_key or not self.iv:
            self.show_system_message("복호화 키가 설정되지 않았습니다.")
            return None
            
        try:
            encrypted_bytes = base64.b64decode(encrypted_message)
            cipher = AES.new(self.secret_key, AES.MODE_CBC, IV=self.iv)
            decrypted_padded = cipher.decrypt(encrypted_bytes)
            return unpad(decrypted_padded, AES.block_size).decode('utf-8')
        except Exception as e:
            # 복호화 실패 시 상세 오류는 콘솔에만 출력
            print(f"복호화 오류: {str(e)}")
            return "[암호화된 메시지 - PIN이 일치하지 않습니다]"

    def send_message(self):
        if not self.server_url:
            return
            
        message = self.message_entry.get().strip()
        if message:
            encrypted_message = self.encrypt_message(message)
            if encrypted_message:
                try:
                    response = requests.post(
                        f"{self.server_url}/send",
                        json={
                            "sender_id": self.client_id,
                            "message": encrypted_message
                        }
                    )
                    if response.status_code == 200:
                        self.message_entry.delete(0, tk.END)
                        self.show_message("me", message)
                except Exception as e:
                    self.show_system_message(f"전송 오류: {str(e)}")

    def receive_messages(self):
        while self.running:
            if not self.server_url:
                time.sleep(1)
                continue
                
            try:
                response = requests.post(
                    f"{self.server_url}/receive",
                    json={"client_id": self.client_id}
                )
                if response.status_code == 200:
                    messages = response.json()['messages']
                    for encrypted_message in messages:
                        try:
                            decrypted_message = self.decrypt_message(encrypted_message)
                            if decrypted_message:
                                self.root.after(0, self.show_message, "other", decrypted_message)
                        except Exception as e:
                            print(f"메시지 처리 오류: {str(e)}")
            except Exception as e:
                print(f"수신 오류: {str(e)}")
            time.sleep(1)

    def show_message(self, sender, message):
        timestamp = time.strftime("%H:%M:%S")
        if sender == "me":
            formatted_message = f"[{timestamp}] >>> {message}\n"
        else:
            # 상대방 메시지는 ID 없이 표시
            formatted_message = f"[{timestamp}] <<< {message}\n"
        self.chat_area.insert(tk.END, formatted_message)
        self.chat_area.see(tk.END)

    def show_system_message(self, message):
        timestamp = time.strftime("%H:%M:%S")
        formatted_message = f"[{timestamp}] === {message} ===\n"
        self.chat_area.insert(tk.END, formatted_message)
        self.chat_area.see(tk.END)

    def on_closing(self):
        self.running = False
        if self.server_url:
            try:
                requests.post(f"{self.server_url}/unregister", 
                            json={"client_id": self.client_id})
            except:
                pass
        self.root.destroy()

def main():
    root = tk.Tk()
    root.configure(bg='black')
    # 윈도우 크기 설정
    root.geometry("800x600")  # 더 큰 창 크기
    app = ChatClientGUI(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

if __name__ == '__main__':
    main()