在現代軟體開發中,安全性已經不再是事後才考慮的議題,而是必須從設計階段就納入考量的核心需求。Python 作為廣泛使用的程式語言,提供了豐富的函式庫與工具來協助開發者實現各種安全機制。然而單純知道這些工具的存在並不夠,更重要的是理解何時該使用哪種工具,以及如何正確地使用它們來建構真正安全的應用程式。

密碼安全是資訊安全的第一道防線,不當的密碼處理方式可能導致整個系統的淪陷。Python 的 secrets 模組提供了密碼學安全的隨機數生成器,遠比一般的 random 模組更適合用於安全相關的場景。配合 hashlib 模組的雜湊演算法,開發者可以建立強健的密碼驗證機制,確保使用者的認證資訊即使在資料庫洩露的情況下也不會直接曝露。

除了運行時的資料保護,程式碼本身的安全性也越來越受到重視。商業軟體需要保護智慧財產權,防止逆向工程與未授權的修改。Python 作為直譯式語言,原始碼相對容易被檢視,這時程式碼混淆技術就變得重要。透過 pyarmor 等工具,可以大幅提高攻擊者分析程式碼的難度。

本文將從實務角度出發,深入探討 Python 在密碼安全、程式碼防護與網路掃描等資訊安全領域的應用。透過完整的程式碼範例與詳細的解說,協助讀者掌握這些關鍵技術的核心概念與實作方法。

使用 secrets 模組生成安全密碼

Python 標準函式庫提供了兩個可以生成隨機數的模組:random 與 secrets。兩者最關鍵的差異在於安全性,random 模組使用的是 Mersenne Twister 演算法,這是一個偽隨機數生成器,雖然在統計上表現良好,但並不適合安全相關的應用。攻擊者如果知道部分輸出值,可能推測出演算法的內部狀態,進而預測後續的輸出。

secrets 模組則使用作業系統提供的密碼學安全隨機數生成器,在 Linux 系統上是 /dev/urandom,在 Windows 上是 CryptGenRandom。這些隨機數來源經過精心設計,確保即使攻擊者觀察到大量輸出值,也無法預測下一個輸出。因此在生成密碼、安全令牌或任何需要不可預測性的場景中,都應該使用 secrets 模組而非 random 模組。

"""
安全密碼生成工具

這個模組展示如何使用 secrets 模組生成符合安全要求的密碼
包含基本密碼生成與符合複雜度要求的強密碼生成

@author 玄貓(BlackCat)
"""

import secrets
import string

def generate_basic_password(length=16):
    """
    生成基本的安全密碼
    
    使用 secrets 模組從字母、數字與標點符號中隨機選取字元
    這種方式生成的密碼具有密碼學安全性,但不保證包含所有字元類型
    
    參數:
        length: 密碼長度,預設為 16 個字元
        
    回傳:
        生成的密碼字串
    """
    # 定義密碼可使用的字元集合
    # string.ascii_letters 包含所有大小寫英文字母 (a-z, A-Z)
    # string.digits 包含所有數字 (0-9)
    # string.punctuation 包含常見的標點符號
    characters = string.ascii_letters + string.digits + string.punctuation
    
    # 使用串列生成式配合 secrets.choice 隨機選取字元
    # secrets.choice 會從給定的序列中密碼學安全地隨機選取一個元素
    # join 方法將所有字元連接成一個字串
    password = ''.join(secrets.choice(characters) for _ in range(length))
    
    return password

def generate_strong_password(length=16):
    """
    生成符合複雜度要求的強密碼
    
    確保密碼至少包含一個小寫字母、一個大寫字母、一個數字與一個特殊字元
    這符合大多數系統的密碼複雜度要求
    
    參數:
        length: 密碼長度,預設為 16 個字元,最少需要 4 個字元
        
    回傳:
        符合複雜度要求的強密碼字串
    """
    # 確保密碼長度足夠容納所有必要的字元類型
    if length < 4:
        raise ValueError('密碼長度必須至少為 4 個字元')
    
    # 初始化密碼字串
    password = []
    
    # 確保包含至少一個小寫字母
    password.append(secrets.choice(string.ascii_lowercase))
    
    # 確保包含至少一個大寫字母
    password.append(secrets.choice(string.ascii_uppercase))
    
    # 確保包含至少一個數字
    password.append(secrets.choice(string.digits))
    
    # 確保包含至少一個特殊字元
    password.append(secrets.choice(string.punctuation))
    
    # 計算剩餘需要填充的長度
    remaining_length = length - 4
    
    # 定義完整的字元集合
    all_characters = string.ascii_letters + string.digits + string.punctuation
    
    # 隨機填充剩餘的字元
    for _ in range(remaining_length):
        password.append(secrets.choice(all_characters))
    
    # 打亂密碼字元的順序
    # 避免密碼總是以「小寫-大寫-數字-符號」的固定模式開始
    # secrets.SystemRandom 提供密碼學安全的隨機排列
    secrets.SystemRandom().shuffle(password)
    
    # 將字元串列轉換為字串
    return ''.join(password)

def generate_url_safe_token(length=32):
    """
    生成 URL 安全的隨機令牌
    
    適用於密碼重設連結、API 金鑰等需要在 URL 中傳遞的場景
    生成的字串只包含 URL 安全字元(A-Z, a-z, 0-9, -, _)
    
    參數:
        length: 令牌的位元組長度,實際字元數會更長
        
    回傳:
        Base64 編碼的 URL 安全字串
    """
    # token_urlsafe 生成 URL 安全的隨機字串
    # 參數指定隨機位元組的數量
    # 實際回傳的字串長度會因為 Base64 編碼而更長
    return secrets.token_urlsafe(length)

# 使用範例
if __name__ == '__main__':
    # 生成基本密碼
    basic_pwd = generate_basic_password()
    print(f'基本密碼: {basic_pwd}')
    
    # 生成強密碼
    strong_pwd = generate_strong_password()
    print(f'強密碼: {strong_pwd}')
    
    # 生成 URL 安全令牌
    url_token = generate_url_safe_token()
    print(f'URL 令牌: {url_token}')

密碼生成只是第一步,更重要的是如何安全地儲存這些密碼。絕對不應該以明文形式儲存密碼,即使是加密儲存也存在金鑰管理的風險。業界標準做法是使用單向雜湊函數,配合鹽值來保護密碼。

使用 hashlib 實現密碼雜湊與驗證

雜湊函數是一種單向的數學函數,它可以將任意長度的輸入轉換為固定長度的輸出。單向的特性意味著無法從雜湊值反推原始輸入,這使得雜湊函數非常適合用於密碼儲存。當使用者註冊時,系統將密碼雜湊後儲存;當使用者登入時,系統將輸入的密碼雜湊後與儲存的雜湊值比對。這樣即使資料庫被竊取,攻擊者也無法直接取得使用者的原始密碼。

然而單純的雜湊仍然存在弱點。由於相同的輸入總是產生相同的雜湊值,攻擊者可以使用彩虹表攻擊,預先計算常見密碼的雜湊值來快速破解。解決方案是在雜湊前加入隨機的鹽值,每個使用者使用不同的鹽值,這樣即使兩個使用者使用相同的密碼,儲存的雜湊值也會不同。

"""
密碼雜湊與驗證工具

展示如何使用 hashlib 與鹽值來安全地儲存與驗證密碼
包含檔案完整性驗證的應用

@author 玄貓(BlackCat)
"""

import hashlib
import uuid
import os

def hash_password_with_salt(password):
    """
    使用鹽值雜湊密碼
    
    生成隨機鹽值並與密碼一起進行雜湊
    鹽值會附加在雜湊結果後面,方便後續驗證
    
    參數:
        password: 要雜湊的密碼字串
        
    回傳:
        格式為 「雜湊值:鹽值」的字串
    """
    # 使用 UUID4 生成隨機的鹽值
    # hex 方法將 UUID 轉換為十六進位字串
    # 每個使用者都有不同的鹽值,防止彩虹表攻擊
    salt = uuid.uuid4().hex
    
    # 將鹽值與密碼組合後進行雜湊
    # encode() 將字串轉換為位元組序列
    # hashlib.sha256() 建立 SHA-256 雜湊物件
    # hexdigest() 以十六進位字串回傳雜湊值
    password_hash = hashlib.sha256(
        (salt + password).encode('utf-8')
    ).hexdigest()
    
    # 將雜湊值與鹽值組合儲存
    # 使用冒號分隔,方便後續解析
    return f'{password_hash}:{salt}'

def verify_password(stored_hash, input_password):
    """
    驗證密碼是否正確
    
    將輸入的密碼使用相同的鹽值雜湊後,與儲存的雜湊值比對
    
    參數:
        stored_hash: 儲存的雜湊值字串,格式為「雜湊值:鹽值」
        input_password: 使用者輸入的密碼
        
    回傳:
        密碼正確回傳 True,否則回傳 False
    """
    # 分離雜湊值與鹽值
    # split(':') 以冒號為分隔符號切割字串
    stored_password_hash, salt = stored_hash.split(':')
    
    # 使用相同的鹽值雜湊輸入的密碼
    input_password_hash = hashlib.sha256(
        (salt + input_password).encode('utf-8')
    ).hexdigest()
    
    # 比對兩個雜湊值是否相同
    # 使用 == 運算子比對字串
    return stored_password_hash == input_password_hash

def calculate_file_hash(filepath, algorithm='sha256'):
    """
    計算檔案的雜湊值
    
    用於驗證檔案完整性,檢測檔案是否被篡改
    支援多種雜湊演算法
    
    參數:
        filepath: 檔案路徑
        algorithm: 雜湊演算法名稱,預設為 sha256
        
    回傳:
        檔案的雜湊值,若檔案不存在則回傳 None
    """
    # 檢查檔案是否存在
    if not os.path.exists(filepath):
        print(f'錯誤:檔案 {filepath} 不存在')
        return None
    
    try:
        # 建立指定演算法的雜湊物件
        # hashlib.new() 可以動態建立不同演算法的雜湊物件
        hasher = hashlib.new(algorithm)
        
        # 以二進位模式開啟檔案
        # 'rb' 模式表示讀取二進位資料
        with open(filepath, 'rb') as file:
            # 分塊讀取檔案內容
            # 這樣可以處理大型檔案而不會耗盡記憶體
            # 每次讀取 65536 位元組 (64 KB)
            while True:
                chunk = file.read(65536)
                
                # 如果讀取到檔案結尾,chunk 會是空的
                if not chunk:
                    break
                
                # 將讀取的資料塊更新到雜湊物件
                hasher.update(chunk)
        
        # 回傳最終的雜湊值
        return hasher.hexdigest()
        
    except Exception as error:
        print(f'計算檔案雜湊值時發生錯誤: {error}')
        return None

# 使用範例
if __name__ == '__main__':
    # 密碼雜湊範例
    password = 'MySecureP@ssw0rd'
    
    # 註冊時:雜湊密碼並儲存
    hashed = hash_password_with_salt(password)
    print(f'儲存的雜湊值: {hashed}\n')
    
    # 登入時:驗證密碼
    test_password = input('請輸入密碼進行驗證: ')
    if verify_password(hashed, test_password):
        print('密碼正確!')
    else:
        print('密碼錯誤!')
    
    # 檔案完整性驗證範例
    print('\n檔案完整性驗證:')
    test_file = __file__  # 使用當前腳本檔案作為測試
    file_hash = calculate_file_hash(test_file)
    if file_hash:
        print(f'{test_file} 的 SHA-256 雜湊值:')
        print(file_hash)
@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100

|使用者註冊|
start

:輸入密碼;

|系統處理|
:生成隨機鹽值\n(UUID);

:組合密碼與鹽值;

:執行 SHA-256 雜湊;

:儲存「雜湊值:鹽值」;

stop

|使用者登入|
start

:輸入密碼;

|系統處理|
:從資料庫讀取\n儲存的雜湊值;

:分離雜湊值與鹽值;

:使用相同鹽值\n雜湊輸入密碼;

if (雜湊值比對?) then (相符)
  :登入成功;
else (不符)
  :登入失敗;
endif

stop

@enduml

使用 pyarmor 進行程式碼混淆

Python 作為直譯式語言,原始碼會以 .py 檔案形式發布,即使編譯成 .pyc 位元碼檔案,也可以相對容易地被反編譯回原始碼。對於商業軟體或包含敏感邏輯的應用程式,這可能造成智慧財產權的風險。程式碼混淆是一種技術手段,透過重新命名變數與函數、插入無用程式碼、改變控制流程等方式,使程式碼難以閱讀與理解,提高逆向工程的難度。

pyarmor 是專為 Python 設計的程式碼混淆工具,它不僅混淆程式碼,還會將 Python 腳本轉換為位元碼並加密,執行時才解密並載入到記憶體中。這種方式提供了比單純混淆更強的保護,攻擊者即使取得 .pyc 檔案也無法直接反編譯。

# 安裝 pyarmor
# 使用 pip 套件管理工具從 PyPI 安裝
pip install pyarmor

# 建立測試檔案
cat > test_program.py << 'EOF'
"""
測試程式

這是一個簡單的 Python 程式,用於展示 pyarmor 的混淆效果
"""

def calculate_price(base_price, tax_rate, discount):
    """
    計算商品最終價格
    
    參數:
        base_price: 基礎價格
        tax_rate: 稅率(例如 0.05 表示 5%)
        discount: 折扣(例如 0.1 表示 9 折)
    
    回傳:
        最終價格
    """
    # 計算稅額
    tax_amount = base_price * tax_rate
    
    # 計算折扣後價格
    discounted_price = base_price * (1 - discount)
    
    # 計算最終價格(折扣後加稅)
    final_price = discounted_price + tax_amount
    
    return final_price

def main():
    """
    主程式
    """
    # 設定商品資訊
    product_name = '測試商品'
    base_price = 1000
    tax_rate = 0.05
    discount = 0.1
    
    # 計算價格
    final_price = calculate_price(base_price, tax_rate, discount)
    
    # 顯示結果
    print(f'商品名稱: {product_name}')
    print(f'原價: ${base_price}')
    print(f'稅率: {tax_rate * 100}%')
    print(f'折扣: {discount * 100}%')
    print(f'最終價格: ${final_price:.2f}')

if __name__ == '__main__':
    main()
EOF

# 使用 pyarmor 混淆程式碼
# obfuscate 命令會混淆指定的 Python 檔案
# 混淆後的檔案會存放在 dist 目錄中
pyarmor obfuscate test_program.py

# 檢視混淆後的程式碼
# 會看到程式碼被轉換為難以閱讀的形式
cat dist/test_program.py

# 執行混淆後的程式
# 功能完全正常,但原始碼已被保護
python dist/test_program.py

程式碼混淆雖然提高了逆向工程的難度,但並非絕對安全的保護措施。決心足夠的攻擊者仍然可能透過動態分析、記憶體傾印等方式破解混淆。因此混淆應該作為深度防禦策略的一環,配合其他安全措施如授權驗證、防篡改檢測等,才能提供較完整的保護。

使用 scapy 進行網路掃描

網路安全不僅關乎應用程式層的防護,也包含網路層的監控與掃描。scapy 是一個強大的 Python 函式庫,允許開發者以程式化的方式建構、發送與接收網路封包。這使得它非常適合用於網路探索、安全稽核與滲透測試等場景。

透過 scapy 可以實現各種網路掃描技術,從簡單的 ARP 掃描到複雜的 TCP SYN 掃描。ARP 掃描是一種輕量級的掃描方式,透過發送 ARP 請求來發現區域網路上的活動主機。相較於 ICMP ping 掃描,ARP 掃描更難被防火牆阻擋,因為 ARP 是網路運作的基礎協定。

"""
網路掃描工具

使用 scapy 實現區域網路主機發現
展示如何建構與發送自訂的網路封包

@author 玄貓(BlackCat)
"""

from scapy.all import ARP, Ether, srp
import ipaddress

def scan_network(ip_range):
    """
    掃描指定 IP 範圍內的活動主機
    
    使用 ARP 協定來發現區域網路上的裝置
    ARP 掃描的優勢是難被防火牆阻擋,因為 ARP 是二層協定
    
    參數:
        ip_range: IP 範圍,支援 CIDR 表示法(例如 192.168.1.0/24)
        
    回傳:
        包含 IP 位址與 MAC 位址的字典串列
    """
    try:
        # 驗證 IP 範圍格式
        # ipaddress 模組可以解析與驗證 IP 位址與網路
        ipaddress.ip_network(ip_range)
        
    except ValueError as error:
        print(f'錯誤:無效的 IP 範圍格式 - {error}')
        return []
    
    # 建立 ARP 請求封包
    # pdst 參數指定目標 IP 範圍
    # ARP 請求會詢問「誰擁有這個 IP 位址?」
    arp_request = ARP(pdst=ip_range)
    
    # 建立以太網路廣播封包
    # dst 設為 ff:ff:ff:ff:ff:ff 表示廣播到區域網路的所有裝置
    broadcast = Ether(dst='ff:ff:ff:ff:ff:ff')
    
    # 組合以太網路廣播與 ARP 請求
    # / 運算子用於堆疊網路層
    # 這會建立一個完整的二層網路封包
    packet = broadcast / arp_request
    
    # 發送封包並接收回應
    # srp 函數用於發送和接收二層封包
    # timeout 參數設定等待回應的時間(秒)
    # verbose=0 抑制詳細輸出
    # 回傳值是兩個串列的元組:已回應的封包和未回應的封包
    answered, unanswered = srp(packet, timeout=3, verbose=0)
    
    # 解析回應並建立裝置列表
    devices = []
    
    # 遍歷所有已回應的封包
    # answered 是一個串列,每個元素是 (發送的封包, 接收的封包) 的元組
    for sent, received in answered:
        # 提取回應中的 IP 位址與 MAC 位址
        # psrc 是協定來源位址(IP)
        # hwsrc 是硬體來源位址(MAC)
        devices.append({
            'ip': received.psrc,
            'mac': received.hwsrc
        })
    
    return devices

def display_scan_results(devices):
    """
    以格式化方式顯示掃描結果
    
    參數:
        devices: 裝置資訊的字典串列
    """
    if not devices:
        print('未發現任何裝置')
        return
    
    print(f'\n發現 {len(devices)} 個裝置:\n')
    print(f'{"IP 位址":<20} {"MAC 位址":<20}')
    print('-' * 40)
    
    for device in devices:
        print(f'{device["ip"]:<20} {device["mac"]:<20}')

# 使用範例
if __name__ == '__main__':
    # 定義要掃描的 IP 範圍
    # 192.168.1.0/24 表示 192.168.1.0 到 192.168.1.255
    target_range = '192.168.1.0/24'
    
    print(f'開始掃描網路範圍: {target_range}')
    print('這可能需要幾秒鐘的時間...\n')
    
    # 執行掃描
    discovered_devices = scan_network(target_range)
    
    # 顯示結果
    display_scan_results(discovered_devices)
@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100

|掃描程式|
start

:輸入 IP 範圍\n(例如 192.168.1.0/24);

:建立 ARP 請求封包;
note right
  詢問「誰擁有此 IP?」
end note

:建立以太網路廣播幀;
note right
  目標 MAC: ff:ff:ff:ff:ff:ff
end note

:組合封包\n(Ether / ARP);

:廣播到區域網路;

|網路上的裝置|
fork
  :裝置 A 檢查 IP;
  if (IP 符合?) then (是)
    :回傳 ARP 回應;
  else (否)
    :忽略請求;
  endif
fork again
  :裝置 B 檢查 IP;
  if (IP 符合?) then (是)
    :回傳 ARP 回應;
  else (否)
    :忽略請求;
  endif
fork again
  :裝置 C 檢查 IP;
  if (IP 符合?) then (是)
    :回傳 ARP 回應;
  else (否)
    :忽略請求;
  endif
end fork

|掃描程式|
:收集所有回應;

:解析 IP 與 MAC 位址;

:顯示掃描結果;

stop

@enduml

網路掃描工具具有雙面性,它可以被安全管理員用於網路清查與安全稽核,也可能被攻擊者用於偵察與資訊收集。因此在使用這類工具時,必須確保已取得適當的授權,並且只在自己管理的網路環境中使用。未經授權的網路掃描在許多國家可能觸犯法律。

從實務角度來看,Python 在資訊安全領域提供了完整的工具鏈。從應用層的密碼安全到程式碼層的混淆防護,再到網路層的掃描監控,開發者可以使用統一的程式語言來實現各種安全機制。這種一致性不僅降低了學習成本,也讓安全工具能夠更容易地整合到現有的 Python 應用程式中。

然而工具本身並不能保證安全,更重要的是建立正確的安全思維。密碼雜湊需要配合適當的鹽值與強大的演算法,程式碼混淆需要配合授權驗證與防篡改機制,網路掃描需要配合完整的安全策略與存取控制。只有將這些技術手段與安全架構設計、威脅建模、持續監控等實務結合,才能建構真正安全可靠的系統。

展望未來,隨著量子運算的發展,現有的密碼學演算法可能面臨挑戰。Python 社群也在積極跟進後量子密碼學的研究,預計未來會有更多支援抗量子攻擊演算法的函式庫出現。在程式碼防護方面,基於硬體的可信執行環境與安全飛地技術也逐漸成熟,可能為 Python 應用提供更強的保護。無論技術如何演進,安全防護的核心原則不變:縱深防禦、最小權限、持續監控與快速回應。