2026年1月16日 星期五

RFID 門禁與支付系統整合模擬器

 

RFID 門禁與支付系統整合模擬器

這是一個非常完整的系統整合。在現實世界的 RFID 應用(如門禁系統或小額支付)中,Reader 只是一個「傳聲筒」,真正的決策(如扣款、權限比對)是在遠端的後端資料庫 (Backend Database) 完成的。

這個模擬器將整合:

  1. Nonce 挑戰應答:確保通訊安全。

  2. 後端資料庫:模擬儲存標籤的 UID、持有人姓名及餘額。

  3. 交易邏輯:如果驗證成功且餘額充足,則進行扣款。



RFID 門禁與支付系統整合模擬器




系統運作流程解析:

  1. 多層防護機制

    • 物理層:模擬標籤靠近 Reader。

    • 通訊層:使用 Nonce 確保每次傳輸的密文都不同,防止駭客監聽。

    • 應用層:後端資料庫檢查 UID 是否合法,以及是否有足夠的餘額完成動作。

  2. 後端連動 (Backend Binding)

    • 當您點擊不同人的按鈕時,系統會自動查找該標籤對應的密鑰進行「虛擬解密」。

    • 如果張小明的錢被扣光了,系統會自動從「交易成功」轉為「餘額不足」。

  3. 實務觀察點

    • 安全性:嘗試修改程式碼中的 tag_key,模擬駭客使用錯誤密鑰,您會看到系統立即噴出「驗證失敗」。

    • 資料完整性:所有變動(餘額扣除)都會即時反映在最上方的資料庫表格中。

這已經是一個具備 安全傳輸、身份認證、資料庫互動 的完整 RFID 應用雛形。


import tkinter as tk

from tkinter import ttk, messagebox

import random

import time

from threading import Thread


# --- 安全加密核心 (Nonce-based Stream Cipher) ---

class SecureCipher:

    @staticmethod

    def process(hex_data, hex_key, hex_nonce):

        try:

            val_data = int(hex_data.replace(" ", ""), 16)

            val_nonce = int(hex_nonce.replace(" ", ""), 16)

            # 結合 Nonce 進行混淆

            mixed_hex = hex(val_data ^ val_nonce)[2:].upper()

            

            # 使用金鑰生成串流 (LSFR 簡化版)

            key_bin = bin(int(hex_key, 16))[2:].zfill(len(hex_key)*4)

            plain_bin = bin(int(mixed_hex, 16))[2:].zfill(len(mixed_hex)*4)

            state = [int(b) for b in key_bin[:8]]

            keystream = []

            for _ in range(len(plain_bin)):

                out = state[-1]

                feedback = state[0] ^ state[2]

                state = [feedback] + state[:-1]

                keystream.append(out)

            

            res_bits = [int(plain_bin[i]) ^ keystream[i] for i in range(len(plain_bin))]

            hex_out = hex(int("".join(map(str, res_bits)), 2))[2:].upper().zfill(len(mixed_hex))

            return " ".join(hex_out[i:i+2] for i in range(0, len(hex_out), 2))

        except: return "ERR"


# --- 後端資料庫 ---

class BackendDatabase:

    def __init__(self):

        # 模擬儲存:使用者金鑰與餘額

        self.records = {

            "53 71 37 A4 F6": {"name": "張小明", "balance": 1200, "key": "AC55"},

            "12 AB 34 CD EF": {"name": "李華", "balance": 80, "key": "B88B"}

        }

        # 模擬商品標籤 (無金鑰,僅供識別價格)

        self.products = {

            "AA BB CC 01": {"item": "蘋果", "price": 30},

            "AA BB CC 02": {"item": "牛奶", "price": 85},

            "AA BB CC 03": {"item": "麵包", "price": 45}

        }


# --- 主程式介面 ---

class RFIDIntegratedSystem:

    def __init__(self, root):

        self.root = root

        self.root.title("RFID 智慧結帳與安全支付系統")

        self.db = BackendDatabase()

        self.cart = [] # 購物籃

        self.is_processing = False

        self.setup_ui()


    def setup_ui(self):

        # 1. 資料庫與帳戶資訊

        db_frame = tk.LabelFrame(self.root, text="後端帳戶資料庫", padx=10, pady=5)

        db_frame.pack(fill="x", padx=10, pady=5)

        self.db_tree = ttk.Treeview(db_frame, columns=("UID", "Name", "Balance"), show="headings", height=2)

        for col in ("UID", "Name", "Balance"): self.db_tree.heading(col, text=col)

        self.db_tree.pack(fill="x")

        self.refresh_db_view()


        # 2. Reader 顯示幕

        display_frame = tk.Frame(self.root, bg="#2c3e50", pady=15)

        display_frame.pack(fill="x", padx=10)

        self.lbl_status = tk.Label(display_frame, text="系統待機中", font=("Arial", 16, "bold"), fg="#ecf0f1", bg="#2c3e50")

        self.lbl_status.pack()

        self.lbl_price = tk.Label(display_frame, text="總計: $0", font=("Arial", 12), fg="#f1c40f", bg="#2c3e50")

        self.lbl_price.pack()


        # 3. 標籤模擬按鈕

        action_frame = tk.Frame(self.root, pady=10)

        action_frame.pack()

        

        # 商品區

        tk.Label(action_frame, text="商品感應:").grid(row=0, column=0, padx=5)

        for i, (uid, info) in enumerate(self.db.products.items()):

            btn = tk.Button(action_frame, text=f"{info['item']} (${info['price']})", 

                            command=lambda u=uid: self.add_to_cart(u))

            btn.grid(row=0, column=i+1, padx=2)


        # 支付區

        tk.Label(action_frame, text="支付感應:").grid(row=1, column=0, padx=5, pady=10)

        for i, (uid, info) in enumerate(self.db.records.items()):

            btn = tk.Button(action_frame, text=f"感應 {info['name']} 的卡", 

                            command=lambda u=uid: self.start_checkout(u), bg="#d4efdf")

            btn.grid(row=1, column=i+1, padx=2)


        # 4. 交易日誌

        log_frame = tk.LabelFrame(self.root, text="交易日誌與傳輸監控")

        log_frame.pack(fill="both", padx=10, pady=5, expand=True)

        self.log = tk.Text(log_frame, height=10, bg="#fdfefe", font=("Consolas", 9))

        self.log.pack(fill="both", padx=5, pady=5)


    def refresh_db_view(self):

        # 修正:指向 self.db.records.items()

        for i in self.db_tree.get_children(): self.db_tree.delete(i)

        for uid, info in self.db.records.items():

            self.db_tree.insert("", tk.END, values=(uid, info["name"], f"${info['balance']}"))


    def add_to_cart(self, product_uid):

        if self.is_processing: return

        product = self.db.products[product_uid]

        self.cart.append(product)

        total = sum(item['price'] for item in self.cart)

        self.lbl_price.config(text=f"總計: ${total} (已感應 {len(self.cart)} 件商品)")

        self.write_log(f"[感應] 商品: {product['item']} - 價格: ${product['price']}")


    def start_checkout(self, user_uid):

        if not self.cart:

            messagebox.showwarning("提示", "購物籃是空的!")

            return

        if self.is_processing: return

        Thread(target=self.checkout_logic, args=(user_uid,), daemon=True).start()


    def checkout_logic(self, user_uid):

        self.is_processing = True

        total_amount = sum(item['price'] for item in self.cart)

        

        self.write_log(f"\n--- 開始安全支付程序 (總金額: ${total_amount}) ---")

        

        # 1. Reader 發送 Nonce 挑戰

        nonce = hex(random.getrandbits(16))[2:].upper().zfill(4)

        self.root.after(0, lambda: self.lbl_status.config(text=f"安全挑戰 Nonce: {nonce}", fg="#f1c40f"))

        time.sleep(1)


        # 2. 標籤加密應答 (模擬卡片內部的加密)

        user_key = self.db.records[user_uid]["key"]

        encrypted_resp = SecureCipher.process(user_uid, user_key, nonce)

        self.write_log(f"[傳輸] 密文應答: {encrypted_resp}")

        time.sleep(1)


        # 3. 後端驗證與扣款

        # 模擬後端解密

        decrypted_uid = SecureCipher.process(encrypted_resp, user_key, nonce)

        

        if decrypted_uid.replace(" ","") == user_uid.replace(" ",""):

            # 驗證通過,檢查餘額

            user = self.db.records[user_uid]

            if user["balance"] >= total_amount:

                user["balance"] -= total_amount

                self.root.after(0, lambda: self.lbl_status.config(text="支付成功!請取走商品", fg="#2ecc71"))

                self.write_log(f"[資料庫] 扣款成功!{user['name']} 剩餘餘額: ${user['balance']}")

                self.cart = [] # 清空購物籃

                self.root.after(0, lambda: self.lbl_price.config(text="總計: $0"))

            else:

                self.root.after(0, lambda: self.lbl_status.config(text="餘額不足,支付失敗", fg="#e67e22"))

                self.write_log(f"[拒絕] {user['name']} 餘額不足 (${user['balance']})")

        else:

            self.root.after(0, lambda: self.lbl_status.config(text="安全性錯誤: 驗證失敗", fg="#e74c3c"))

            self.write_log("[警告] 偵測到非法卡片或重播攻擊!")


        self.root.after(0, self.refresh_db_view)

        time.sleep(3)

        self.root.after(0, lambda: self.lbl_status.config(text="系統待機中", fg="#ecf0f1"))

        self.is_processing = False


    def write_log(self, msg):

        self.root.after(0, lambda: self.log.insert(tk.END, msg + "\n"))

        self.root.after(0, lambda: self.log.see(tk.END))


if __name__ == "__main__":

    root = tk.Tk()

    app = RFIDIntegratedSystem(root)

    root.mainloop()


沒有留言:

張貼留言

ASK 調變模擬器程式碼

ASK 調變模擬器程式碼 為什麼第二張圖要「放大 (Zoom)」? 1 kHz  的週期是 1000 us 。 如果不放大,第二張圖會因為波形太密而看起來像一塊實心的紅磚。 import tkinter as tk from tkinter import ttk import n...