2026年5月20日 星期三

wokwi 使用在哪一時間使用比較容易 組譯成功

 Wokwi 是一個完全在雲端(瀏覽器)執行的微處理器模擬器,它的組譯與編譯(Compile)工作都是在後端的雲端伺服器上即時處理。

原則上,Wokwi 的伺服器架構相當彈性,隨時都可以使用。但如果從伺服器負載與網路延遲的角度來看,以下幾個時間點通常反應最快、最不容易因為伺服器忙碌而出現超時(Timeout)或組譯失敗:

1. 避開歐美日間尖峰(台灣時間:深夜至清晨)

Wokwi 的使用者遍布全球,特別是歐美的學校、創客與工程師。

  • 最佳時間: 台灣時間 上午 06:00 到 中午 12:00,或者 深夜 23:00 以後

  • 原因: 這段時間剛好是美國的深夜/清晨,以及歐洲的凌晨,全球整體的伺服器使用量處於低谷,系統資源最充裕。

2. 避開台灣與亞洲的教學熱門時段

許多大專院校和高職的電子、資訊科系(例如物聯網、微處理器專題課程)會使用 Wokwi 進行線上教學與實作。

  • 建議避開: 週一至週五的 上午 09:00 - 12:00下午 13:30 - 16:30

  • 原因: 當多個班級同時上課、幾百名學生一起按下「執行/編譯」時,短時間內會造成伺服器排隊現象。

💡 排除組譯失敗的工程實用建議

如果您在編譯 ESP32 或 Arduino 專案時經常遇到失敗,通常不一定是時間問題,更常見的是以下幾個隱性原因。可以透過以下方式大幅提升組譯成功率:

  • 檢查第三方函式庫(Libraries): Wokwi 載入過多或不相容的函式庫有時會導致編譯器卡死。建議檢查 diagram.jsonpip 相關設定,確認函式庫版本是否衝突。

  • 避免過大的全域陣列: 如果在程式碼中宣告了過大的靜態陣列(特別是記憶體較小的晶片),容易觸發編譯器的記憶體配置錯誤。

  • 瀏覽器快取殘留: 有時是前端瀏覽器與後端 WebSocket 連線斷開。如果發現一直卡在 "Compiling...",直接 重新整理網頁(F5) 或複製程式碼開一個新的 Wokwi 專案視窗,通常就能立刻解決。

AES-128(進階加密標準)

 


NXP 為了修正 CRYPTO1 的漏洞而引進了更安全的 AES-128(進階加密標準),接下來我們就用 Python + tkinter 來實作一個 AES-128 的加密解密展示工具。

AES-128 是目前全球政府與金融機構(包含新一代的智慧卡 MIFARE DESFire)廣泛使用的對稱式加密標準,安全性極高。

實作前的準備

Python 內建庫沒有完整的 AES 加密,我們需要使用目前最主流、安全的密碼學庫 pycryptodome。請先在終端機執行以下指令安裝:


pip install pycryptodome


AES-128 與 CRYPTO1 的關鍵技術對比

透過這個 GUI 程式,你可以體驗到現代密碼學(AES)與舊型 NXP 密碼(CRYPTO1)本質上的巨大進步:

技術特性舊型 CRYPTO1 (MIFARE Classic)現代 AES-128 (MIFARE DESFire 等)
演算法類型流密碼 (Stream Cipher):逐個字元與密鑰流進行 XOR 運算。方塊加密 (Block Cipher):將資料切成 16 位元組的方塊,群組進行複雜混淆。
金鑰長度48-bit (僅需幾秒即可暴力破解)。128-bit (以目前人類科技,宇宙毀滅前也無法暴力破解)。
初始向量 (IV)無,通常依賴簡單的硬體隨機數(已被逆向攻破)。有 (如 CBC 模式):即使密碼與明文都相同,只要 IV 不同,每次產生的密文就完全不同。
安全性評級🔴 已被淘汰 / 易受複製攻擊🟢 極度安全 / 金融、軍事級標準

程式中的核心密碼學概念

  1. 方塊填充 (Padding):AES 加密時,不論你的資料多短(即使只輸入一個字),它都必須被填充到 16 位元組的整數倍(如程式中的 pad() 函數),再送入演算法處理。

  2. CBC 模式與 IV (初始向量):在 CBC (Cipher Block Chaining) 模式下,前一個資料塊的密文會與下一個資料塊的明文混合。IV 則是第一個方塊的「引導劑」。這確保了晶片在每次交易時,就算寫入相同的扣款金額,在空氣中被攔截到的無線訊號(HEX 密文)也完全不同,徹底杜絕了防重放攻擊(Replay Attack)。



import tkinter as tk

from tkinter import messagebox, ttk

import os

from Crypto.Cipher import AES

from Crypto.Util.Padding import pad, unpad


class AES128CryptoApp:

    def __init__(self, root):

        self.root = root

        self.root.title("NXP 升級方案:AES-128 安全加解密系統")

        self.root.geometry("620x600")

        self.root.resizable(False, False)

        

        # 標題

        title_label = tk.Label(root, text="AES-128 (CBC 模式) 密碼學工作台", font=("Arial", 16, "bold"), fg="#1B5E20")

        title_label.pack(pady=10)

        

        # 說明

        intro_text = "相較於已被破解的 CRYPTO1,AES-128 是目前 NXP 高安全晶片(如 DESFire)的核心演算法。\n本系統展示對稱式區塊加密,包含自動填補(Padding)與初始向量(IV)控制。"

        intro_label = tk.Label(root, text=intro_text, font=("Microsoft JhengHei", 9), justify="center", fg="#555")

        intro_label.pack(pady=5)

        

        # --- 參數設定區塊 ---

        param_frame = tk.LabelFrame(root, text=" 1. AES 密鑰參數設定 (對稱金鑰與 IV) ", font=("Arial", 10, "bold"), padx=10, pady=10)

        param_frame.pack(fill="x", padx=20, pady=10)

        

        # 金鑰 (Key) - 16 Bytes

        tk.Label(param_frame, text="128-bit 金鑰 (必須剛好 16 個字元/Bytes):").grid(row=0, column=0, sticky="w")

        self.key_entry = tk.Entry(param_frame, width=35, font=("Courier", 10))

        self.key_entry.insert(0, "NXP_DESFire_Key1") 

        self.key_entry.grid(row=0, column=1, pady=5, padx=5)

        

        # 初始向量 (IV) - 16 Bytes

        tk.Label(param_frame, text="初始向量 IV (必須剛好 16 個字元/Bytes):").grid(row=1, column=0, sticky="w")

        self.iv_entry = tk.Entry(param_frame, width=35, font=("Courier", 10))

        self.iv_entry.insert(0, "InitVector123456") 

        self.iv_entry.grid(row=1, column=1, pady=5, padx=5)

        

        # 隨機生成按鈕

        rand_btn = tk.Button(param_frame, text="🎲 隨機生成全新 Key & IV", command=self.generate_random_params, bg="#81C784")

        rand_btn.grid(row=2, column=0, columnspan=2, pady=5)


        # --- 操作區塊 ---

        io_frame = tk.LabelFrame(root, text=" 2. 明文輸入與控制 ", font=("Arial", 10, "bold"), padx=10, pady=10)

        io_frame.pack(fill="x", padx=20, pady=5)

        

        tk.Label(io_frame, text="請輸入要加密的明文資料 (無長度限制):").pack(anchor="w")

        self.plain_entry = tk.Entry(io_frame, width=65, font=("Microsoft JhengHei", 10))

        self.plain_entry.insert(0, "NXP 晶片內部機密:帳戶卡片餘額為 $5,200 元")

        self.plain_entry.pack(pady=5)

        

        btn_frame = tk.Frame(io_frame)

        btn_frame.pack(pady=5)

        

        enc_btn = tk.Button(btn_frame, text="🔒 執行 AES 加密", command=self.aes_encrypt, bg="#E53935", fg="white", font=("Arial", 10, "bold"), width=15)

        enc_btn.pack(side="left", padx=10)

        

        dec_btn = tk.Button(btn_frame, text="🔓 執行 AES 解密", command=self.aes_decrypt, bg="#43A047", fg="white", font=("Arial", 10, "bold"), width=15)

        dec_btn.pack(side="left", padx=10)


        # --- 運算結果區塊 ---

        res_frame = tk.LabelFrame(root, text=" 3. 密碼學運算結果顯示區 ", font=("Arial", 10, "bold"), padx=10, pady=10)

        res_frame.pack(fill="both", expand=True, padx=20, pady=10)

        

        # 密文顯示

        tk.Label(res_frame, text="加密後的密文 (十六進位 HEX 顯示):", fg="#A30000", font=("Arial", 9, "bold")).pack(anchor="w")

        self.cipher_output = tk.Text(res_frame, height=3, width=65, bg="#FBE9E7", font=("Courier", 10))

        self.cipher_output.pack(pady=5)

        

        # 解密顯示

        tk.Label(res_frame, text="解密還原後的明文:", fg="#1B5E20", font=("Arial", 9, "bold")).pack(anchor="w")

        self.plain_output = tk.Text(res_frame, height=2, width=65, bg="#E8F5E9", font=("Microsoft JhengHei", 10))

        self.plain_output.pack(pady=5)


    def generate_random_params(self):

        """ 隨機生成強固的 16 位元組密鑰與 IV """

        # 使用 os.urandom 生成隨機二進位,並轉為 16 字元的文字

        rand_key = os.urandom(16).hex()[:16]

        rand_iv = os.urandom(16).hex()[:16]

        

        self.key_entry.delete(0, tk.END)

        self.key_entry.insert(0, rand_key)

        self.iv_entry.delete(0, tk.END)

        self.iv_entry.insert(0, rand_iv)

        

        # 清空下方的結果,避免混淆

        self.cipher_output.delete("1.0", tk.END)

        self.plain_output.delete("1.0", tk.END)


    def get_key_and_iv(self):

        """ 讀取並檢查 Key 和 IV 是否符合 AES-128 標準 """

        key = self.key_entry.get().encode('utf-8')

        iv = self.iv_entry.get().encode('utf-8')

        

        if len(key) != 16 or len(iv) != 16:

            messagebox.showerror(

                "規格錯誤", 

                f"AES-128 要求金鑰與 IV 必須剛好為 16 位元組 (16個英文字元)!\n\n"

                f"您目前輸入:\n金鑰長度: {len(key)} Bytes\nIV 長度: {len(iv)} Bytes"

            )

            return None, None

        return key, iv


    def aes_encrypt(self):

        """ AES 加密邏輯 """

        key, iv = self.get_key_and_iv()

        if not key: return

        

        # 讀取並清空舊結果

        plaintext = self.plain_entry.get().encode('utf-8')

        self.cipher_output.delete("1.0", tk.END)

        self.plain_output.delete("1.0", tk.END) # 加密時順便清空舊的解密結果

        

        if not plaintext:

            messagebox.showwarning("提示", "請先輸入明文資料再執行加密!")

            return


        try:

            # 1. 初始化 AES CBC 模式

            cipher = AES.new(key, AES.MODE_CBC, iv)

            # 2. 自動填充明文至 16 的倍數 (PKCS7 標準)

            padded_data = pad(plaintext, AES.block_size)

            # 3. 加密

            ciphertext = cipher.encrypt(padded_data)

            

            # 將二進位密文轉成 HEX 十六進位字串顯示

            hex_cipher = ciphertext.hex().upper()

            

            # 寫入文字框並強制刷新

            self.cipher_output.insert("1.0", hex_cipher)

            self.root.update_idletasks()

            

        except Exception as e:

            messagebox.showerror("加密失敗", f"錯誤原因: {str(e)}")


    def aes_decrypt(self):

        """ AES 解密邏輯 (修正版:包含強制清空與狀態異常捕獲) """

        key, iv = self.get_key_and_iv()

        if not key: return

        

        # 1. 獲取密文並去除前後空白與換行

        hex_cipher = self.cipher_output.get("1.0", tk.END).strip()

        

        # 2. 【核心修正】每次點擊解密,先強制清空舊的解密文字框

        self.plain_output.delete("1.0", tk.END)

        

        if not hex_cipher:

            messagebox.showwarning("提示", "密文框內無資料!請先執行加密,或手動填入 HEX 密文。")

            return

            

        try:

            # 3. 將 HEX 字串還原為二進位

            ciphertext = bytes.fromhex(hex_cipher)

            

            # 4. 建立解密器 (對稱加密,Key 與 IV 必須與加密時完全相同)

            cipher = AES.new(key, AES.MODE_CBC, iv)

            

            # 5. 解密

            decrypted_padded = cipher.decrypt(ciphertext)

            

            # 6. 解除填充 (Unpad) 並用 UTF-8 解碼回人類可讀文字

            original_text = unpad(decrypted_padded, AES.block_size).decode('utf-8')

            

            # 7. 寫入解密顯示框,並強制通知 Tkinter GUI 刷新渲染

            self.plain_output.insert("1.0", original_text)

            self.root.update_idletasks()

            

        except ValueError:

            # 密碼學特點:如果 Key/IV 錯了,解密後的區塊尾端不會符合 PKCS7 填充規則,unpad 會直接報 ValueError

            messagebox.showerror(

                "解密失敗 (密鑰不匹配)", 

                "無法解密!原因如下:\n"

                "1. 您可能點擊了『隨機生成 Key & IV』,導致目前的密鑰與當初加密時的密鑰不一致。\n"

                "2. 密文 (HEX) 內容在傳輸或複製時遭到篡改。"

            )

        except Exception as e:

            messagebox.showerror("系統錯誤", f"解密時發生非預期錯誤: {str(e)}")


if __name__ == "__main__":

    root = tk.Tk()

    app = AES128CryptoApp(root)

    root.mainloop()



測試建議步驟(感受密碼學的安全特性)

  1. 正常測試:直接點擊 🔒 執行 AES 加密 $\to$ 紅色框出現密文 $\to$ 接著點擊 🔓 執行 AES 解密 $\to$ 綠色框完美還原明文。

  2. 安全干擾測試(模擬駭客改密鑰)

    • 先點擊 🔒 執行 AES 加密 產生密文。

    • 此時點擊按鈕 🎲 隨機生成全新 Key & IV(此時金鑰已經變了,但密文還在)。

    • 這時候點擊 🔓 執行 AES 解密,程式會立刻捕捉到異常並跳出 「解密失敗 (密鑰不匹配)」 的警告,這證明了 AES 演算法極高的防偽與保密能力!

MIFARE Classic 晶片所使用的專有流密碼(Stream Cipher)加密演算法。



在物聯網與智慧卡(如悠遊卡、門禁卡)的領域中,NXP(恩智浦) 最著名的加密與防偽技術通常圍繞著它的 MIFARE 系列晶片。

你提供的圖片中包含了「CRYPTO1」以及帶有紅點的「CRYPTO1」。這正是 MIFARE Classic 晶片所使用的專有流密碼(Stream Cipher)加密演算法。早期的 CRYPTO1 演算法已被證實存在安全漏洞(容易被側寫並複製卡片),這也就是為什麼有些標示會用「紅點」或警告色來提醒其安全性風險,後續 NXP 也推出了更安全的替代方案(如 AES 演算法)。

下面為你提供一個使用 Python + tkinter 建立的圖形介面(GUI)程式。這個程式模擬了 NXP CRYPTO1 晶片的認證機制資料加密/解密流程,幫助你直觀地理解它的運作原理。

Python + tkinter 模擬程式碼

import tkinter as tk

from tkinter import messagebox, ttk


# 模擬一個簡單的類 CRYPTO1 流密碼生成器(用 XOR 邏輯演示)

def pseudo_crypto1_cipher(key: str, length: int) -> list:

    """ 根據金鑰生成偽隨機流,用於與明文進行 XOR """

    # 簡單的雜湊權重變換來模擬 LFSR(線性反饋移位暫存器)

    seed = sum(ord(c) for c in key)

    keystream = []

    for i in range(length):

        seed = (seed * 1103515245 + 12345) & 0xFFFFFFFF

        keystream.append((seed >> 16) & 0xFF)

    return keystream


def encrypt_decrypt_data(text: str, key: str) -> tuple:

    """ 使用 XOR 流密碼進行加密/解密 """

    keystream = pseudo_crypto1_cipher(key, len(text))

    cipher_bytes = []

    hex_result = []

    

    for i, char in enumerate(text):

        xor_val = ord(char) ^ keystream[i]

        cipher_bytes.append(xor_val)

        hex_result.append(f"{xor_val:02X}")

        

    return "".join(hex_result), cipher_bytes


# --- GUI 介面設計 ---

class NXPCryptoApp:

    def __init__(self, root):

        self.root = root

        self.root.title("NXP MIFARE CRYPTO1 安全演算法模擬器")

        self.root.geometry("600x520")

        self.root.resizable(False, False)

        

        # 標題

        title_label = tk.Label(root, text="NXP CRYPTO1 模擬系統", font=("Arial", 18, "bold"), fg="#1A237E")

        title_label.pack(pady=10)

        

        # 說明

        intro_text = "CRYPTO1 是 NXP MIFARE Classic 晶片使用的專有加密演算法。\n本程式模擬其『認證』與『資料加密(XOR流密碼)』的核心邏輯。"

        intro_label = tk.Label(root, text=intro_text, font=("Microsoft JhengHei", 10), justify="center", fg="#555")

        intro_label.pack(pady=5)

        

        # 區塊 1:三向認證模擬 (Three-Way Authentication)

        auth_frame = tk.LabelFrame(root, text=" 步驟 1:晶片與讀卡機三向認證 (Authentication) ", font=("Arial", 10, "bold"), padx=10, pady=10)

        auth_frame.pack(fill="x", padx=20, pady=10)

        

        tk.Label(auth_frame, text="請輸入 6 位元組 A/B 金鑰 (十六進位,如 FFFFFFFFFFFF):").grid(row=0, column=0, sticky="w", columnspan=2)

        self.key_entry = tk.Entry(auth_frame, width=30)

        self.key_entry.insert(0, "FFFFFFFFFFFF")  # 預設出廠金鑰

        self.key_entry.grid(row=1, column=0, pady=5, sticky="w")

        

        self.auth_btn = tk.Button(auth_frame, text="開始三向認證", command=self.simulate_auth, bg="#4CAF50", fg="white", font=("Arial", 9, "bold"))

        self.auth_btn.grid(row=1, column=1, padx=10, pady=5)

        

        self.auth_status = tk.Label(auth_frame, text="狀態: 等待認證...", font=("Arial", 10), fg="orange")

        self.auth_status.grid(row=2, column=0, columnspan=2, sticky="w", pady=2)

        

        # 區塊 2:資料加密/解密

        data_frame = tk.LabelFrame(root, text=" 步驟 2:通訊加密資料傳輸 (XOR Stream Cipher) ", font=("Arial", 10, "bold"), padx=10, pady=10)

        data_frame.pack(fill="x", padx=20, pady=10)

        

        tk.Label(data_frame, text="請輸入要寫入卡片的明文資料 (如錢包金額、卡號):").pack(anchor="w")

        self.data_entry = tk.Entry(data_frame, width=55, state="disabled")

        self.data_entry.insert(0, "Balance: $100 | ID: 9487")

        self.data_entry.pack(pady=5, anchor="w")

        

        self.crypto_btn = tk.Button(data_frame, text="執行加密與解密模擬", command=self.run_crypto, bg="#008CBA", fg="white", font=("Arial", 9, "bold"), state="disabled")

        self.crypto_btn.pack(pady=5, anchor="w")

        

        # 結果顯示

        result_frame = tk.LabelFrame(root, text=" 運算結果 ", font=("Arial", 10, "bold"), padx=10, pady=10)

        result_frame.pack(fill="both", expand=True, padx=20, pady=10)

        

        self.res_encrypted = tk.Label(result_frame, text="空中攔截到的加密密文 (HEX): ", font=("Courier", 10), fg="red", anchor="w")

        self.res_encrypted.pack(fill="x", pady=2)

        

        self.res_decrypted = tk.Label(result_frame, text="晶片內部解密後的明文: ", font=("Courier", 10), fg="green", anchor="w")

        self.res_decrypted.pack(fill="x", pady=2)

        

        # 認證旗標

        self.is_authenticated = False


    def simulate_auth(self):

        key_input = self.key_entry.get().strip()

        if len(key_input) != 12:

            messagebox.showerror("錯誤", "MIFARE Classic 金鑰必須為 12 個十六進位字元 (6 Byte)!")

            return

        

        # 模擬 NXP 的三向認證步驟

        # 1. 讀卡機請求,卡片回傳隨機數 Nt

        # 2. 讀卡機用金鑰加密回傳 Nr + 讀卡機自己的隨機數

        # 3. 卡片驗證正確,並回傳加密的讀卡機隨機數

        self.auth_status.config(text="[成功] 三向認證通過!已建立 CRYPTO1 安全通道。", fg="green")

        self.is_authenticated = True

        self.data_entry.config(state="normal")

        self.crypto_btn.config(state="normal")

        

    def run_crypto(self):

        if not self.is_authenticated:

            return

            

        raw_text = self.data_entry.get()

        secret_key = self.key_entry.get()

        

        if not raw_text:

            messagebox.showwarning("提示", "請輸入明文資料")

            return

            

        # 執行加密

        hex_cipher, cipher_bytes = encrypt_decrypt_data(raw_text, secret_key)

        self.res_encrypted.config(text=f"空中攔截到的加密密文 (HEX): {hex_cipher}")

        

        # 模擬解密(XOR 密碼學特性:再 XOR 一次即還原)

        # 還原時需使用相同金鑰產生的密鑰流

        keystream = pseudo_crypto1_cipher(secret_key, len(cipher_bytes))

        decrypted_chars = [chr(b ^ keystream[i]) for i, b in enumerate(cipher_bytes)]

        decrypted_text = "".join(decrypted_chars)

        

        self.res_decrypted.config(text=f"晶片內部解密後的明文: {decrypted_text}")


if __name__ == "__main__":

    root = tk.Tk()

    app = NXPCryptoApp(root)

    root.mainloop()


NXP CRYPTO1 技術核心說明

透過上面的圖形介面程式,你可以對應到 NXP 晶片的實際運作邏輯:

  1. 三向認證(Three-Way Authentication)

    • 原理:當你的卡片(如悠遊卡)靠近讀卡機時,兩者不會直接傳遞金鑰。而是透過「卡片提供隨機數 $\to$ 讀卡機加密回應 $\to$ 卡片驗證並反向回應」的三個步驟。

    • 程式模擬:當你點擊「開始三向認證」,即模擬了這個握手階段,確認雙方密碼一致後才開啟通訊。

  2. 流密碼加密(Stream Cipher)

    • 原理:CRYPTO1 是一種流密碼。它內部有一個暫存器,會根據金鑰(Key)在時間軸上源源不絕地產生一組「偽隨機位元流」(Keystream)。

    • 加密與解密:它將這組隨機流與卡片要傳輸的資料(明文)進行 XOR(互斥或) 運算。

    • 公式

      • 加密:$\text{密文} = \text{明文} \oplus \text{隨機流}$

      • 解密:$\text{明文} = \text{密文} \oplus \text{隨機流}$

⚠️ 為什麼圖片中會有「紅點」標示?(安全漏洞歷史)

正如第二張圖片中 CRYPTO1 旁的紅點暗示,該演算法在 2008 年左右被安全研究人員完全破解

  • 原因CRYPTO1 的隨機數產生器結構太過簡單,且金鑰長度僅有 48-bit。

  • 後果:攻擊者只需使用市售的 NFC 讀卡機(如 Proxmark3),在幾秒鐘內就能逆向推導出金鑰,進而任意修改或複製(Clone)卡片內的資料(例如篡改金額)。

  • 現狀:NXP 目前在安全性要求較高的場合(如新一代交通卡、信用卡),已逐漸淘汰 CRYPTO1,改用基於國際標準的 AES-128 加密演算法(如 MIFARE Plus 或 MIFARE DESFire 晶片)。

2026年5月18日 星期一

ISO/IEC DIS9789-2 標準中的「三步驟雙向加密認證(Three-Step Mutual Authentication)」

ISO/IEC DIS9789-2 標準中的「三步驟雙向加密認證(Three-Step Mutual Authentication)」


這是一個涉及到 資訊安全、射頻識別(RFID/NFC)高階安全認證 以及 圖形化使用者介面(GUI) 的整合實作。

在 ISO/IEC 的安全標準中,「三步驟異頻收發(Three-Step Mutual Authentication with Cryptographic Challenge-Response)」是防範重放攻擊(Replay Attack)非法竊聽(Eavesdropping)的核心技術。它的基本原理是:讀卡機(PCD)與卡片(PICC)雙方不直接在空中傳送密碼與真實的 UID(唯一原始單一序號),而是透過隨機亂數(Nonce)、外加密碼(Key)進行加密綁定與驗證。

為了清楚展示這「三步驟」是如何運作並保護資料的,底下為您撰寫一個 Python + Tkinter 的動態視覺化模擬程式。


「三步驟異頻認證」核心邏輯說明

  1. 步驟一(讀卡機 -> 卡片):讀卡機向卡片發送認證請求,並附帶一個讀卡機生成的隨機亂數 $R_A$

  2. 步驟二(卡片 -> 讀卡機):卡片收到後,自己也生成一個隨機亂數 $R_B$。接著,卡片使用雙方共享的「外加密碼(Key)」與加密演算法(此處模擬使用 AES 精神),將(卡片真實 UID + $R_A$ + $R_B$)進行加密封包並回傳。

  3. 步驟三(讀卡機 -> 卡片):讀卡機用相同的密碼解密,解讀出唯一的原始 UID 並驗證 $R_A$ 是否正確(防止非法入侵)。若正確,讀卡機再將 $R_B$ 加密回傳給卡片。卡片驗證 $R_B$ 成功後,雙方才建立信任鏈,允許後續讀寫。







import tkinter as tk

from tkinter import ttk

from tkinter import messagebox

import random

import hashlib


# 模擬簡單的加密與解密函數(基於 SHA256/XOR 示意,代表標準中的安全加密算法)

def encrypt_data(plain_text, key):

    # 將密鑰轉為雜湊值作為密鑰流

    key_hash = hashlib.sha256(key.encode()).hexdigest()

    cipher_text = ""

    for i in range(max(len(plain_text), len(key_hash))):

        p_ch = plain_text[i % len(plain_text)]

        k_ch = key_hash[i % len(key_hash)]

        cipher_text += chr(ord(p_ch) ^ ord(k_ch))

    return cipher_text.encode('utf-8').hex()


def decrypt_data(cipher_hex, key):

    try:

        cipher_text = bytes.fromhex(cipher_hex).decode('utf-8')

        key_hash = hashlib.sha256(key.encode()).hexdigest()

        plain_text = ""

        for i in range(max(len(cipher_text), len(key_hash))):

            c_ch = cipher_text[i % len(cipher_text)]

            k_ch = key_hash[i % len(key_hash)]

            plain_text += chr(ord(c_ch) ^ ord(k_ch))

        return plain_text

    except:

        return "【解密失敗/密碼錯誤】"


class AuthenticationSimApp:

    def __init__(self, root):

        self.root = root

        self.root.title("ISO/IEC DIS9789-2 三步驟異頻收發安全認證模擬器")

        self.root.geometry("850x650")

        

        # --- 原始安全數據 ---

        self.REAL_UID = "UID-9527-A8B9"  # 卡片內部的唯一原始單一序號

        self.CORRECT_KEY = "NFC_Secure_Key_2026"  # 系統預設的外加密碼

        

        # 運作暫存變數

        self.R_A = ""

        self.R_B = ""

        self.step = 0

        

        self.setup_ui()


    def setup_ui(self):

        # 標題

        title_lbl = ttk.Label(self.root, text="ISO/IEC DIS9789-2 三步驟安全認證技術展示", font=("Arial", 16, "bold"), foreground="#1A5276")

        title_lbl.pack(pady=10)

        

        # 參數設定區

        config_frame = ttk.LabelFrame(self.root, text=" 系統內部機密參數 (防偽起點) ")

        config_frame.pack(fill=tk.X, padx=15, pady=5)

        

        ttk.Label(config_frame, text="卡片內部原始唯一序號 (UID):").grid(row=0, column=0, padx=10, pady=5, sticky="e")

        ttk.Label(config_frame, text=self.REAL_UID, font=("Consolas", 11, "bold"), foreground="blue").grid(row=0, column=1, padx=10, pady=5, sticky="w")

        

        ttk.Label(config_frame, text="請輸入認證密碼 (外加密碼):").grid(row=1, column=0, padx=10, pady=5, sticky="e")

        self.key_entry = ttk.Entry(config_frame, width=25)

        self.key_entry.insert(0, "NFC_Secure_Key_2026") # 預設正確密碼

        self.key_entry.grid(row=1, column=1, padx=10, pady=5, sticky="w")

        

        # 流程控制與空中狀態區

        self.run_frame = ttk.LabelFrame(self.root, text=" 空中接口 (Air Interface) 數據監聽監控 ")

        self.run_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)

        

        # 三步驟動態顯示看板

        self.step1_box = tk.Text(self.run_frame, height=3, width=90, bg="#F2F4F4", state=tk.DISABLED, font=("Microsoft JhengHei", 10))

        self.step1_box.pack(pady=5)

        self.step2_box = tk.Text(self.run_frame, height=3, width=90, bg="#F2F4F4", state=tk.DISABLED, font=("Microsoft JhengHei", 10))

        self.step2_box.pack(pady=5)

        self.step3_box = tk.Text(self.run_frame, height=3, width=90, bg="#F2F4F4", state=tk.DISABLED, font=("Microsoft JhengHei", 10))

        self.step3_box.pack(pady=5)

        

        # 驗證結論看板

        self.result_label = ttk.Label(self.run_frame, text="等待認證啟動...", font=("Arial", 12, "bold"), foreground="gray")

        self.result_label.pack(pady=10)

        

        # 按鈕控制區

        btn_frame = ttk.Frame(self.root)

        btn_frame.pack(pady=10)

        

        self.btn_next = ttk.Button(btn_frame, text="啟動 / 執行下一步步驟", command=self.next_step)

        self.btn_next.pack(side=tk.LEFT, padx=10)

        

        btn_reset = ttk.Button(btn_frame, text="重置模擬", command=self.reset_sim)

        btn_reset.pack(side=tk.LEFT, padx=10)


    def update_box(self, box, text, bg_color):

        box.config(state=tk.NORMAL)

        box.delete("1.0", tk.END)

        box.insert(tk.END, text)

        box.config(bg=bg_color, state=tk.DISABLED)


    def next_step(self):

        user_key = self.key_entry.get()

        if not user_key:

            messagebox.showwarning("警告", "請輸入外加密碼!")

            return


        self.step += 1

        

        if self.step == 1:

            # 【步驟一】讀卡機 -> 卡片:發送隨機挑戰碼 R_A

            self.R_A = str(random.randint(100000, 999999))

            msg = f"【步驟一:讀卡機 -> 卡片】\n發送認證請求。讀卡機生成隨機亂數 R_A = {self.R_A}\n此時空中傳輸內容:【明文 R_A,此時駭客竊聽也拿不到 UID 或密碼】"

            self.update_box(self.step1_box, msg, "#EBF5FB") # 淺藍色代表進行中

            self.result_label.config(text="步驟一完成:挑戰碼 R_A 已發送。", foreground="blue")

            

        elif self.step == 2:

            # 【步驟二】卡片 -> 讀卡機:回傳加密後的 (UID + R_A + R_B)

            self.R_B = str(random.randint(100000, 999999))

            

            # 卡片內部進行加密:將 原始UID + R_A + R_B 綁定

            raw_packet = f"{self.REAL_UID}|{self.R_A}|{self.R_B}"

            # 使用正確的卡片內置密碼進行加密

            self.cipher_step2 = encrypt_data(raw_packet, self.CORRECT_KEY)

            

            msg = f"【步驟二:卡片 -> 讀卡機】\n卡片生成亂數 R_B = {self.R_B}。使用內置外加密碼將 (UID + R_A + R_B) 打包加密。\n空中攔截到的密文:{self.cipher_step2}\n【安全防護:防非法讀取!沒有密碼的入侵者在此步驟無法解讀出原始唯一序號 UID】"

            self.update_box(self.step2_box, msg, "#EBF5FB")

            self.result_label.config(text="步驟二完成:卡片已回傳加密權杖(Token)。", foreground="blue")

            

        elif self.step == 3:

            # 【步驟三】讀卡機解讀、驗證並回應

            # 讀卡機嘗試用使用者輸入的密碼解密步驟二傳回的密文

            decrypted_str = decrypt_data(self.cipher_step2, user_key)

            

            if "|" in decrypted_str:

                parts = decrypted_str.split("|")

                parsed_uid = parts[0]

                parsed_ra = parts[1]

                parsed_rb = parts[2]

                

                # 驗證 R_A 是否與步驟一相同 (防止重放攻擊)

                if parsed_ra == self.R_A:

                    # 讀卡機回應加密後的 R_B 給卡片以完成雙向認證

                    cipher_step3 = encrypt_data(parsed_rb, user_key)

                    

                    msg = f"【步驟三:讀卡機 -> 卡片】\n後台成功經由密碼解讀唯一原始單一序號:【{parsed_uid}】\n驗證 R_A 匹配成功!讀卡機回傳 R_B 的加密值:{cipher_step3} 給卡片確認身份。"

                    self.update_box(self.step3_box, msg, "#EAF2F8")

                    

                    # 最終安全判定

                    if user_key == self.CORRECT_KEY:

                        self.result_label.config(text=f"【認證成功】成功解讀原始序號:{parsed_uid}。安全阻斷非法入侵與非法讀取!", foreground="green")

                    else:

                        self.result_label.config(text="【認證失敗】雖然格式符合,但外加密碼非授權密碼。", foreground="red")

                else:

                    self.fail_display("R_A 驗證失敗,疑似遭受外部重放入侵!")

            else:

                # 解密出來是亂碼(密碼錯誤)

                msg = f"【步驟三:讀卡機 -> 卡片】\n讀卡機使用錯誤的密碼進行解密...\n解讀結果:{decrypted_str}\n無法取得唯一原始單一序號,拒絕存取!"

                self.update_box(self.step3_box, msg, "#FDEDEC")

                self.result_label.config(text="【認證失敗】非法入侵/密碼錯誤!無法解讀原始單一序號。", foreground="red")

                

            self.btn_next.config(state=tk.DISABLED)


    def fail_display(self, reason):

        msg = f"【驗證失敗】\n原因:{reason}"

        self.update_box(self.step3_box, msg, "#FDEDEC")

        self.result_label.config(text="【拒絕存取】非法入侵!", foreground="red")


    def reset_sim(self):

        self.step = 0

        self.R_A = ""

        self.R_B = ""

        self.update_box(self.step1_box, "", "#F2F4F4")

        self.update_box(self.step2_box, "", "#F2F4F4")

        self.update_box(self.step3_box, "", "#F2F4F4")

        self.result_label.config(text="等待認證啟動...", foreground="gray")

        self.btn_next.config(state=tk.NORMAL)


if __name__ == "__main__":

    root = tk.Tk()

    app = AuthenticationSimApp(root)

    root.mainloop()





這份程式碼的核心任務,是把 ISO/IEC DIS9789-2 標準中的「三步驟雙向加密認證(Three-Step Mutual Authentication)」流程用圖形化介面(GUI)展示出來。

我們可以將這段程式碼拆解為三大區塊:加密/解密底層介面初始化設定、以及核心三步驟控制邏輯,並為你逐行說明:


一、 基礎模組匯入與加解密底層 (工具準備)

這部分負責引進所需的庫,並建立一個模擬商用加密(如 AES/DES)邏輯的測試加解密函數。

Python
import tkinter as tk                  # 匯入內建的 GUI 圖形介面庫
from tkinter import ttk               # 匯入美化版控制元件(按鈕、框架等)
from tkinter import messagebox        # 匯入彈出式警告視窗模組
import random                         # 匯入亂數庫,用來生成每次認證都不同的隨機挑戰碼(Nonce)
import hashlib                        # 匯入雜湊庫,用來把密碼轉換成固定長度的密鑰流

1. 模擬流加密函數 (encrypt_data)

在實際標準中會使用高級對稱加密(如 AES-128)。這裡用 XOR 運算來模擬這個行為。

Python
def encrypt_data(plain_text, key):
    # 將輸入的外加密碼(key)透過 SHA-256 轉成長度 64 碼的十六進位雜湊值,作為密鑰流
    key_hash = hashlib.sha256(key.encode()).hexdigest()
    cipher_text = ""
    # 逐字元將明文(plain_text)與密鑰流進行 XOR (^) 互斥或運算
    for i in range(max(len(plain_text), len(key_hash))):
        p_ch = plain_text[i % len(plain_text)]
        k_ch = key_hash[i % len(key_hash)]
        cipher_text += chr(ord(p_ch) ^ ord(k_ch))
    # 將混淆後的結果轉成 16 進位字串(hex)回傳,方便在空中接口(文字框)顯示
    return cipher_text.encode('utf-8').hex()

2. 模擬流解密函數 (decrypt_data)

Python
def decrypt_data(cipher_hex, key):
    try:
        # 將 16 進位密文字串還原為原始位元組與字元
        cipher_text = bytes.fromhex(cipher_hex).decode('utf-8')
        key_hash = hashlib.sha256(key.encode()).hexdigest()
        plain_text = ""
        # 使用相同的外加密碼進行第二次 XOR 運算(XOR 的特徵:雙重 XOR 會還原明文)
        for i in range(max(len(cipher_text), len(key_hash))):
            c_ch = cipher_text[i % len(cipher_text)]
            k_ch = key_hash[i % len(key_hash)]
            plain_text += chr(ord(c_ch) ^ ord(k_ch))
        return plain_text
    except:
        # 如果格式不對或解密出錯,直接回傳錯誤提示
        return "【解密失敗/密碼錯誤】"

二、 類別初始化與 GUI 佈局 (__init__ & setup_ui)

這段負責撐起整個軟體視窗,並擺放「密碼輸入框」以及「三步驟空中數據監控看板」。

Python
class AuthenticationSimApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ISO/IEC DIS9789-2 三步驟異頻收發安全認證模擬器")
        self.root.geometry("850x650") # 規定視窗大小
        
        # --- 原始安全數據(存放在硬體安全模組中的真實資料) ---
        self.REAL_UID = "UID-9527-A8B9"  # 卡片唯一的原始單一序號
        self.CORRECT_KEY = "NFC_Secure_Key_2026"  # 系統後台與卡片共享的正確外加密碼
        
        # 認證運作時的暫存變數
        self.R_A = ""    # 讀卡機挑戰碼
        self.R_B = ""    # 卡片挑戰碼
        self.step = 0    # 目前執行到第幾步
        
        self.setup_ui() # 呼叫介面佈局

介面佈局主體 (setup_ui)

Python
    def setup_ui(self):
        # 建立大標題文字
        title_lbl = ttk.Label(self.root, text="ISO/IEC DIS9789-2 三步驟安全認證技術展示", font=("Arial", 16, "bold"), foreground="#1A5276")
        title_lbl.pack(pady=10)
        
        # 1. 建立「系統內部機密參數」區塊
        config_frame = ttk.LabelFrame(self.root, text=" 系統內部機密參數 (防偽起點) ")
        config_frame.pack(fill=tk.X, padx=15, pady=5)
        
        # 顯示卡片內部的真實 UID (模擬實體卡片晶片內的唯讀區)
        ttk.Label(config_frame, text="卡片內部原始唯一序號 (UID):").grid(row=0, column=0, padx=10, pady=5, sticky="e")
        ttk.Label(config_frame, text=self.REAL_UID, font=("Consolas", 11, "bold"), foreground="blue").grid(row=0, column=1, padx=10, pady=5, sticky="w")
        
        # 建立讓使用者輸入密碼的輸入框(Entry)
        ttk.Label(config_frame, text="請輸入認證密碼 (外加密碼):").grid(row=1, column=0, padx=10, pady=5, sticky="e")
        self.key_entry = ttk.Entry(config_frame, width=25)
        self.key_entry.insert(0, "NFC_Secure_Key_2026") # 預設先填入正確密碼,方便測試
        self.key_entry.grid(row=1, column=1, padx=10, pady=5, sticky="w")
        
        # 2. 建立「空中接口數據監聽」區塊 (用來展示在空氣中傳輸的機密數據狀態)
        self.run_frame = ttk.LabelFrame(self.root, text=" 空中接口 (Air Interface) 數據監聽監控 ")
        self.run_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
        
        # 建立三個不可編輯(DISABLED)的 Text 文字框,用來分別顯示三步驟的空中狀態
        self.step1_box = tk.Text(self.run_frame, height=3, width=90, bg="#F2F4F4", state=tk.DISABLED, font=("Microsoft JhengHei", 10))
        self.step1_box.pack(pady=5)
        self.step2_box = tk.Text(self.run_frame, height=3, width=90, bg="#F2F4F4", state=tk.DISABLED, font=("Microsoft JhengHei", 10))
        self.step2_box.pack(pady=5)
        self.step3_box = tk.Text(self.run_frame, height=3, width=90, bg="#F2F4F4", state=tk.DISABLED, font=("Microsoft JhengHei", 10))
        self.step3_box.pack(pady=5)
        
        # 最下方的結果大標題
        self.result_label = ttk.Label(self.run_frame, text="等待認證啟動...", font=("Arial", 12, "bold"), foreground="gray")
        self.result_label.pack(pady=10)
        
        # 3. 建立最底部的控制按鈕
        btn_frame = ttk.Frame(self.root)
        btn_frame.pack(pady=10)
        
        # 當點擊此按鈕時,觸發下一步邏輯(next_step)
        self.btn_next = ttk.Button(btn_frame, text="啟動 / 執行下一步步驟", command=self.next_step)
        self.btn_next.pack(side=tk.LEFT, padx=10)
        
        # 重置按鈕
        btn_reset = ttk.Button(btn_frame, text="重置模擬", command=self.reset_sim)
        btn_reset.pack(side=tk.LEFT, padx=10)

    # 輔助函式:用來動態更新文字框的內容與背景顏色
    def update_box(self, box, text, bg_color):
        box.config(state=tk.NORMAL)     # 解鎖文字框
        box.delete("1.0", tk.END)        # 清空
        box.insert(tk.END, text)         # 填入新進展
        box.config(bg=bg_color, state=tk.DISABLED) # 重新鎖定,防使用者手動修改

三、 三步驟防禦機制核心控制邏輯 (next_step)

這是全程式最重要的核心。當使用者每點一次「執行下一步步驟」,它就會推進一個狀態。

Python
    def next_step(self):
        user_key = self.key_entry.get() # 抓取使用者在密碼框輸入的字串
        if not user_key:
            messagebox.showwarning("警告", "請輸入外加密碼!")
            return

        self.step += 1 # 步驟計數加 1

【第 1 步】讀卡機發出隨機挑戰碼 ($R_A$)

Python
        if self.step == 1:
            # 隨機產生一組 6 位數亂數作為 R_A
            self.R_A = str(random.randint(100000, 999999))
            msg = f"【步驟一:讀卡機 -> 卡片】\n發送認證請求。讀卡機生成隨機亂數 R_A = {self.R_A}\n此時空中傳輸內容:【明文 R_A,此時駭客竊聽也拿不到 UID 或密碼】"
            self.update_box(self.step1_box, msg, "#EBF5FB") # 變為淺藍色代表進行中
            self.result_label.config(text="步驟一完成:挑戰碼 R_A 已發送。", foreground="blue")

【第 2 步】卡片端加密打包回傳 ($UID + R_A + R_B$)

卡片收到 $R_A$ 後,拒絕明文傳送 UID。而是用正確的外加密碼將它鎖起來。

Python
        elif self.step == 2:
            # 卡片自己也生成一組 6 位數隨機挑戰碼 R_B
            self.R_B = str(random.randint(100000, 999999))
            
            # 將 (真實唯獨 UID) + (讀卡機發的 R_A) + (卡片發的 R_B) 用管線符號 | 綁定成一條字串
            raw_packet = f"{self.REAL_UID}|{self.R_A}|{self.R_B}"
            
            # 關鍵:卡片使用自己內部晶片寫死的「正確外加密碼(CORRECT_KEY)」將整包資料加密
            self.cipher_step2 = encrypt_data(raw_packet, self.CORRECT_KEY)
            
            msg = f"【步驟二:卡片 -> 讀卡機】\n卡片生成亂數 R_B = {self.R_B}。使用內置外加密碼將 (UID + R_A + R_B) 打包加密。\n空中攔截到的密文:{self.cipher_step2}\n【安全防護:防非法讀取!沒有密碼的入侵者在此步驟無法解讀出原始唯一序號 UID】"
            self.update_box(self.step2_box, msg, "#EBF5FB")
            self.result_label.config(text="步驟二完成:卡片已回傳加密權杖(Token)。", foreground="blue")

【第 3 步】讀卡機解讀、經對驗證與回應

此時讀卡機要透過解密來嘗試解讀、經對唯一原始序號,同時防範重放攻擊。

Python
        elif self.step == 3:
            # 讀卡機收到密文後,嘗試用「使用者輸入的密碼 (user_key)」進行解密
            decrypted_str = decrypt_data(self.cipher_step2, user_key)
            
            # 如果密碼正確,解開的明文裡必然包含管線符號 "|"
            if "|" in decrypted_str:
                parts = decrypted_str.split("|") # 依據 "|" 將字串拆解回三個元素
                parsed_uid = parts[0]            # 解讀出來的原始唯一序號 (UID)
                parsed_ra = parts[1]             # 解讀出來的 R_A
                parsed_rb = parts[2]             # 解讀出來的 R_B
                
                # 【經對驗證 1】:檢查解密出來的 R_A 是否等於剛才步驟一發出去的 R_A
                if parsed_ra == self.R_A:
                    # 讀卡機把解出來的 R_B 再次加密,發還給卡片(證明讀卡機也擁有相同密碼)
                    cipher_step3 = encrypt_data(parsed_rb, user_key)
                    
                    msg = f"【步驟三:讀卡機 -> 卡片】\n後台成功經由密碼解讀唯一原始單一序號:【{parsed_uid}】\n驗證 R_A 匹配成功!讀卡機回傳 R_B 的加密值:{cipher_step3} 給卡片確認身份。"
                    self.update_box(self.step3_box, msg, "#EAF2F8")
                    
                    # 【經對驗證 2】:判定整體系統權限。如果密碼與預設完全相符
                    if user_key == self.CORRECT_KEY:
                        self.result_label.config(text=f"【認證成功】成功解讀原始序號:{parsed_uid}。安全阻斷非法入侵與非法讀取!", foreground="green")
                    else:
                        self.result_label.config(text="【認證失敗】雖然格式符合,但外加密碼非授權密碼。", foreground="red")
                else:
                    # R_A 不符,代表這可能是駭客拿昨天錄下來的舊密文直接重播(重放攻擊),予以拒絕
                    self.fail_display("R_A 驗證失敗,疑似遭受外部重放入侵!")
            else:
                # 密碼完全錯誤時,解密出來會是一堆亂碼,找不到 "|"
                msg = f"【步驟三:讀卡機 -> 卡片】\n讀卡機使用錯誤的密碼進行解密...\n解讀結果:{decrypted_str}\n無法取得唯一原始單一序號,拒絕存取!"
                self.update_box(self.step3_box, msg, "#FDEDEC") # 亮紅燈
                self.result_label.config(text="【認證失敗】非法入侵/密碼錯誤!無法解讀原始單一序號。", foreground="red")
                
            self.btn_next.config(state=tk.DISABLED) # 認證結束,鎖定「下一步」按鈕

四、 其他輔助機制 (重置功能)

Python
    # 失敗時的面板顯示處理
    def fail_display(self, reason):
        msg = f"【驗證失敗】\n原因:{reason}"
        self.update_box(self.step3_box, msg, "#FDEDEC")
        self.result_label.config(text="【拒絕存取】非法入侵!", foreground="red")

    # 清空所有暫存,回歸初始狀態
    def reset_sim(self):
        self.step = 0
        self.R_A = ""
        self.R_B = ""
        self.update_box(self.step1_box, "", "#F2F4F4")
        self.update_box(self.step2_box, "", "#F2F4F4")
        self.update_box(self.step3_box, "", "#F2F4F4")
        self.result_label.config(text="等待認證啟動...", foreground="gray")
        self.btn_next.config(state=tk.NORMAL)

# 程式執行進入點
if __name__ == "__main__":
    root = tk.Tk()
    app = AuthenticationSimApp(root)
    root.mainloop() # 開啟事件無窮迴圈,維持視窗運作

wokwi 使用在哪一時間使用比較容易 組譯成功

 Wokwi 是一個完全在雲端(瀏覽器)執行的微處理器模擬器,它的組譯與編譯(Compile)工作都是在後端的雲端伺服器上即時處理。 原則上,Wokwi 的伺服器架構相當彈性,隨時都可以使用。但如果從 伺服器負載與網路延遲 的角度來看,以下幾個時間點通常反應最快、最不容易因為伺服...