電子公文傳輸系統安全-進展一

“我美式”组發表於2024-05-18

上週任務完成情況

  • 每人撰寫並提交一份讀書筆記
  • 小組撰寫並提交一份加固計劃書、一份安全性設計方案
  • 小組撰寫並提交一份系統安全設計報告
  • 小組討論原公文傳輸系統的問題不足和改進方向
  • 小組討論新公文傳輸系統的修改方案和預期效果
  • 每人自行學習國密標準、演算法知識和設計方法

原公文傳輸系統的問題不足和改進方向

  • 使用者密碼儲存存在未加密問題,需要改進密碼儲存方案
  • 使用者公私鑰對存在未加密問題,需要重新商討金鑰儲存方案
def register(request):
    if request.method == 'POST':
        id = request.POST['id']
        username_up = request.POST['username_up']
        email = request.POST['email']
        password_up = request.POST['password_up']

        priKey = PrivateKey()
        pubKey = priKey.publicKey()

        new_user = UserProfile.objects.create(
            id=id,
            username_up=username_up,
            email=email,
            password_up=password_up,
            public_key=pubKey.toString(compressed=False),  # 儲存公鑰
            private_key=priKey.toString(), # 儲存私鑰
            avatar='avatars/default_avatar.png'
        )
        new_user.save()

        LogData = Log.objects.create(
            username = new_user.username_up,
            documentname = "無",
            operation = f'使用者{new_user.username_up}於{timezone.now()}註冊了賬號。'
        )
        LogData.save()

        # 新增成功訊息
        messages.success(request, '註冊成功,請登入。')
        time.sleep(3)
        return redirect('login')

  • 問題分析

    • 使用者密碼儲存未加密問題:註冊過程中,使用者的密碼(password_up)直接以明文形式儲存在資料庫中。這樣做會使得使用者的敏感資訊暴露在資料庫中,一旦資料庫洩露,使用者的密碼將被直接獲取,存在安全風險。
    • 使用者公私鑰對未加密問題: 註冊過程中生成了使用者的公私鑰對(pubKeypriKey),並將其以明文形式儲存在資料庫中。同樣地,這樣做會導致使用者的私鑰等敏感資訊被直接暴露在資料庫中,存在洩露風險。
  • 為了解決這些問題,應該採取以下措施:

    • 密碼加密儲存:應該對使用者的密碼進行加密處理,通常採用的方法是使用雜湊演算法對密碼進行雜湊處理,然後再儲存雜湊值而不是明文密碼。Django提供了內建的密碼雜湊演算法和驗證器,應該使用make_password方法對密碼進行加密處理。
    • 公私鑰對加密儲存:不應該將使用者的公私鑰對以明文形式儲存在資料庫中。通常情況下,應該將私鑰加密後再儲存,而公鑰可以公開儲存。私鑰應該由使用者自行保管,不應該儲存在資料庫中。如果需要在服務端使用使用者的私鑰,應該採取適當的加密方式進行儲存,確保私鑰在資料庫中是安全的。

使用者密碼儲存方案

為實現使用者密碼的安全儲存,我們小組現討論出如下幾種方案:

基於加鹽bcrypt演算法的使用者密碼安全儲存方案

加鹽雜湊儲存
  • 雜湊演算法:使用雜湊演算法(如SHA256、SHA512)將密碼轉換成固定長度的摘要,確保密碼不會以明文形式儲存,且雜湊過程不可逆。
  • 加鹽(Salt):在進行雜湊運算前,為每個密碼新增一個隨機生成的鹽值。鹽值的作用是即使兩個使用者使用相同的密碼,最終儲存的雜湊結果也會不同。
鹽的生成策略
  • 鹽值長度:鹽值長度必須足夠長,以防止攻擊者透過預先生成的查詢表進行攻擊。推薦使用較長的鹽值,例如16位元組或更長。
  • 生成方法:使用加密安全偽隨機生成器(Cryptographically Secure Pseudo-Random Number Generator, CSPRNG)生成鹽值,確保其隨機性和安全性。
慢雜湊函式
  • bcrypt演算法:採用bcrypt演算法對密碼進行加鹽雜湊儲存。bcrypt演算法透過增加迭代因子(work factor),降低運算速度,延長破解所需時間,從而預防暴力破解和彩虹表攻擊。
  • bcrypt計算後的密文中包含了演算法版本、迭代因子、鹽值以及雜湊串。
    示例:加密後的密碼雜湊值$2a$12$eoL7CAx5FXw8zxTOoVBVVVu8VdLq2G0zbssix3fnhh4wN5Pv8/MEX2,其中$2a$代表bcrypt演算法版本,12$代表迭代因子,接下來的22個字元為鹽值,剩餘部分為加密雜湊串。
使用者登入
  • 使用者輸入登入名和密碼:使用者在登入時輸入登入名和密碼。
  • 獲取儲存的鹽值和雜湊值:系統從資料庫中獲取對應使用者的鹽值和加密後的密碼雜湊值。
  • 加鹽雜湊驗證:使用獲取的鹽值和bcrypt演算法對輸入的密碼進行加密。
  • 比對雜湊值:將加密後的結果與資料庫中儲存的雜湊值進行比對。

透過上述方案,確保了使用者密碼的安全儲存,即使資料庫被洩露,攻擊者也難以透過暴力破解和彩虹表攻擊獲取使用者的實際密碼。

基於隨機替換增強的MD5加鹽雜湊密碼儲存方案

加鹽MD5雜湊
  • 初始雜湊處理:
    • 使用者輸入密碼,系統使用MD5演算法對其進行雜湊計算,生成32位16進位制數的MD5值。
  • 提取和替換:
    • 從生成的MD5值中提取第5至第19位字元。
    • 隨機生成一個15位的字串,用於替換提取出的MD5值部分。
  • 儲存處理:
    • 將替換後的新MD5值儲存在資料庫中。
具體流程示例
  • 使用者註冊:
    • 輸入密碼000000,透過MD5演算法轉換為670b14728ad9902aecba32e22fa4f6bd。
    • 提取第5至第19位,得到14728ad9902ae。
    • 隨機生成字串c11wssfgyyj%swg。
    • 替換提取部分,得到新的MD5值670c11wssfgyyj%swgba32e22fa4f6bd。
    • 將新的MD5值儲存在資料庫中。
  • 使用者登入:
    • 輸入密碼000000,透過MD5演算法轉換為670b14728ad9902aecba32e22fa4f6bd。
    • 提取第5至第19位,得到14728ad9902ae。
    • 使用與註冊時相同的方法生成字串c11wssfgyyj%swg。
    • 替換提取部分,得到新的MD5值670c11wssfgyyj%swgba32e22fa4f6bd。
    • 將新的MD5值與資料庫中儲存的MD5值進行比對,驗證透過。

C語言實現:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/md5.h>

// 生成MD5值
void compute_md5(const char *str, char *md5_result) {
    unsigned char md5_digest[MD5_DIGEST_LENGTH];
    MD5((unsigned char *)str, strlen(str), md5_digest);

    for (int i = 0; i < MD5_DIGEST_LENGTH; i++) {
        sprintf(&md5_result[i*2], "%02x", md5_digest[i]);
    }
}

// 生成隨機字串
void generate_random_string(char *random_string, size_t length) {
    const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    for (size_t i = 0; i < length; i++) {
        int key = rand() % (int)(sizeof(charset) - 1);
        random_string[i] = charset[key];
    }
    random_string[length] = '\0';
}

// 使用者密碼儲存方案
void store_password(const char *password, char *stored_password) {
    char md5_result[MD5_DIGEST_LENGTH * 2 + 1];
    compute_md5(password, md5_result);

    char extracted_part[15 + 1];
    strncpy(extracted_part, md5_result + 4, 15);
    extracted_part[15] = '\0';

    char random_string[15 + 1];
    generate_random_string(random_string, 15);

    strncpy(stored_password, md5_result, 4);
    strncpy(stored_password + 4, random_string, 15);
    strncpy(stored_password + 19, md5_result + 19, 13);
    stored_password[32] = '\0';
}

int main() {
    const char *password = "your_password";
    char stored_password[33];

    store_password(password, stored_password);
    printf("Stored password: %s\n", stored_password);

    return 0;
}

使用者登入驗證:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/md5.h>

// 前面函式同上

int verify_password(const char *password, const char *stored_password) {
    char md5_result[MD5_DIGEST_LENGTH * 2 + 1];
    compute_md5(password, md5_result);

    char extracted_part[15 + 1];
    strncpy(extracted_part, md5_result + 4, 15);
    extracted_part[15] = '\0';

    char random_string[15 + 1];
    strncpy(random_string, stored_password + 4, 15);
    random_string[15] = '\0';

    char verification_password[33];
    strncpy(verification_password, md5_result, 4);
    strncpy(verification_password + 4, random_string, 15);
    strncpy(verification_password + 19, md5_result + 19, 13);
    verification_password[32] = '\0';

    return strcmp(verification_password, stored_password) == 0;
}

int main() {
    const char *password = "your_password";
    char stored_password[33];
    store_password(password, stored_password);

    // 模擬使用者登入驗證
    if (verify_password(password, stored_password)) {
        printf("Password verified successfully.\n");
    } else {
        printf("Password verification failed.\n");
    }

    return 0;
}

Python實現:

import hashlib
import random
import string

def compute_md5(password):
    md5_hash = hashlib.md5(password.encode()).hexdigest()
    return md5_hash

def generate_random_string(length):
    characters = string.ascii_letters + string.digits
    random_string = ''.join(random.choice(characters) for _ in range(length))
    return random_string

def store_password(password):
    md5_hash = compute_md5(password)
    
    extracted_part = md5_hash[4:19]
    random_string = generate_random_string(15)
    
    stored_password = md5_hash[:4] + random_string + md5_hash[19:]
    
    return stored_password

# 示例
password = "your_password"
stored_password = store_password(password)
print(f"Stored password: {stored_password}")

使用者登入驗證:

# 前面函式同上

def verify_password(password, stored_password):
    md5_hash = compute_md5(password)
    
    extracted_part = md5_hash[4:19]
    random_string = stored_password[4:19]
    
    verification_password = md5_hash[:4] + random_string + md5_hash[19:]
    
    return verification_password == stored_password

# 示例
password = "your_password"
stored_password = store_password(password)

# 模擬使用者登入驗證
if verify_password(password, stored_password):
    print("Password verified successfully.")
else:
    print("Password verification failed.")

金鑰儲存方案

為了正確管理金鑰,確保秘密資訊不能明存,我們小組討論了多種方案,其中幾種有可行性的如下:

呼叫龍脈智慧鑰匙中的演算法加密金鑰

本方案為最終選定的金鑰儲存方案。
我們可以呼叫龍脈智慧鑰匙中的SM2演算法加密公文傳輸系統中的對稱金鑰,呼叫SM1演算法加密公文傳輸系統中的私鑰。
我們的公文傳輸系統是用Python程式碼的Django框架搭建的,而呼叫龍脈智慧鑰匙的程式碼是C語言的,所以我們需要在Python程式碼中嵌入C語言程式碼,並編譯執行C語言程式碼,以呼叫龍脈智慧鑰匙中的加密演算法加密金鑰。
我們的程式碼需要在Windows環境下執行,所以需要將C語言程式碼編譯成可執行檔案.exe的格式,命令列為:gcc XXX.c -o XXX.exe,我們需要使用-I指定標頭檔案的路徑,使用-L指定動態庫的路徑。

  • 利用龍脈key實現對私鑰的加密
#include "../include/skfapi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define	TRUE	1
#define FALSE	0
#define ERROR_THROW(r) {if((r) != SAR_OK) goto END_OF_FUN;}


void encrypt_privateKey(const char* private_key)
{
	ULONG ulRslt = SAR_OK;
	HANDLE hdev = NULL;
	HANDLE happ = NULL;
	HANDLE hkey = NULL;
	HANDLE hcont = NULL;
	char   szDevName[256] = { 0 };
	ULONG	ulDevNameLen = 256;
	char	szAppName[256] = { 0 };
	ULONG	ulAppNameLen = 256;
	char	szContName[256] = { 0 };
	ULONG	ulContName = 256;
	char* pUserPin = "12345678";
	ULONG	ulRetryCount = 0;

	BYTE	pbEncrypt[256] = { 0 };
	ULONG   ulEncryptLen = 256;

	BYTE    pbRandom[32] = { 0 };
	BLOCKCIPHERPARAM bp = { 0 };

	int  nDatalen = strlen(private_key);
	char* pContName = szContName;
	char* pdevname = szDevName;
	char* pappname = szAppName;

	ulRslt = SKF_EnumDev(TRUE, szDevName, &ulDevNameLen);
	ERROR_THROW(ulRslt)


		ulRslt = SKF_ConnectDev(pdevname, &hdev);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_EnumApplication(hdev, szAppName, &ulAppNameLen);
	ERROR_THROW(ulRslt)


		ulRslt = SKF_OpenApplication(hdev, pappname, &happ);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_VerifyPIN(happ, USER_TYPE, pUserPin, &ulRetryCount);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_EnumContainer(happ, szContName, &ulContName);
	ERROR_THROW(ulRslt)


		ulRslt = SKF_OpenContainer(happ, pContName, &hcont);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_GenRandom(hdev, pbRandom, 16);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_SetSymmKey(hdev, pbRandom, SGD_SM1_ECB, &hkey);
	ERROR_THROW(ulRslt)

		//bp.PaddingType = 1;
		ulRslt = SKF_EncryptInit(hkey, bp);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_Encrypt(hkey, (BYTE*)private_key, Datalen, pbEncrypt, &ulEncryptLen);
	ERROR_THROW(ulRslt)

		printf("encrypt data ok!\n");

	char* encrypted_key = pbEncrypt;

	return encrypted_key;

END_OF_FUN:
	if (hkey)
		SKF_CloseHandle(hkey);
	if (hcont)
		SKF_CloseContainer(hcont);
	if (happ)
		SKF_CloseApplication(happ);
	if (hdev)
		SKF_DisConnectDev(hdev);
	return 1;
}


  • 解密私鑰
#include "../include/skfapi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define	TRUE	1
#define FALSE	0
#define ERROR_THROW(r) {if((r) != SAR_OK) goto END_OF_FUN;}


void decrypt_privateKey(const char* private_key_encoded)
{
	ULONG ulRslt = SAR_OK;
	HANDLE hdev = NULL;
	HANDLE happ = NULL;
	HANDLE hkey = NULL;
	HANDLE hcont = NULL;
	char   szDevName[256] = { 0 };
	ULONG	ulDevNameLen = 256;
	char	szAppName[256] = { 0 };
	ULONG	ulAppNameLen = 256;
	char	szContName[256] = { 0 };
	ULONG	ulContName = 256;
	char* pUserPin = "12345678";
	ULONG	ulRetryCount = 0;

	BYTE	pbnDcrypt[256] = { 0 };
	ULONG   ulDecryptLen = 256;

	BYTE    pbRandom[32] = { 0 };
	BLOCKCIPHERPARAM bp = { 0 };

	int  nDatalen = strlen(private_key_encoded);
	char* pContName = szContName;
	char* pdevname = szDevName;
	char* pappname = szAppName;

	ulRslt = SKF_EnumDev(TRUE, szDevName, &ulDevNameLen);
	ERROR_THROW(ulRslt)


		ulRslt = SKF_ConnectDev(pdevname, &hdev);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_EnumApplication(hdev, szAppName, &ulAppNameLen);
	ERROR_THROW(ulRslt)


		ulRslt = SKF_OpenApplication(hdev, pappname, &happ);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_VerifyPIN(happ, USER_TYPE, pUserPin, &ulRetryCount);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_EnumContainer(happ, szContName, &ulContName);
	ERROR_THROW(ulRslt)


		ulRslt = SKF_OpenContainer(happ, pContName, &hcont);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_GenRandom(hdev, pbRandom, 16);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_SetSymmKey(hdev, pbRandom, SGD_SM1_ECB, &hkey);
	ERROR_THROW(ulRslt)

		//bp.PaddingType = 1;
		ulRslt = SKF_DecryptInit(hkey, bp);
	ERROR_THROW(ulRslt)

		ulRslt = SKF_Decrypt(hkey, (BYTE*)private_key_encoded, Datalen, pbDncrypt, &ulDecryptLen);
	ERROR_THROW(ulRslt)

		printf("decrypt data ok!\n");

	char* decrypted_key = pbDecrypt;

	return decrypted_key;

END_OF_FUN:
	if (hkey)
		SKF_CloseHandle(hkey);
	if (hcont)
		SKF_CloseContainer(hcont);
	if (happ)
		SKF_CloseApplication(happ);
	if (hdev)
		SKF_DisConnectDev(hdev);
	return 1;
}
  • 加密保護私鑰
import subprocess
import ctypes
# 編譯SM1_PrivateKey C 程式碼
compile_command = "gcc -shared -o libsm1.dll sm1_encrypt.c -Llib/windows/lib -lmtoken_gm3000"

# 載入動態連結庫,注意這裡只需要載入編譯後的libsm2.dll,而不需要載入 Ukey 動態連結庫
lib = ctypes.cdll.LoadLibrary('./libsm1.ddl')

# 定義函式返回型別
lib.encrypt_private_key.restype = ctypes.c_char_p

# 呼叫C函式
private_key = "your_private_key_here"
encrypted_private_key = lib.encrypt_private_key(private_key.encode('utf-8'))

# 將加密後的私鑰從位元組串解碼為字串
encrypted_private_key = encrypted_private_key.decode('utf-8')

print("Encrypted private key:", encrypted_private_key)

基於Cache的金鑰安全方案

我們還考慮改變金鑰的硬體儲存位置提高金鑰的安全性。透過將對稱金鑰和私鑰儲存到Cache中,而非記憶體中,防止針對記憶體空間的金鑰提取攻擊,保護金鑰的安全。
我們使用的針對國密演算法的Copker方案,方案框架如下圖所示。

我們採用CAR技術,把Cache當作RAM使用,將所有敏感資料,例如:私鑰、中間狀態、隨機數種子等,在整個儲存期和計算期內鎖定在Cache中處理,而不觸及RAM保證金鑰和其它敏感資料不以明文形態出現在記憶體中。
我們採用兩層金鑰體系,分別為SM4主金鑰和私鑰。SM4主金鑰儲存在CPU的特權暫存器中。私鑰加密後儲存在記憶體中,只在需要使用時解密到Cache中。
我們採用的方案執行金鑰相關操作的過程必須是原子的,在操作過程中不允許產生任何形式的中斷,且在執行操作前會完成棧空間的構造,操作開始時,會將程序的棧切換到提前預留的Cache中的棧空間中,保證所有計算均在Cache中進行、所有敏感資料和中間狀態均儲存在Cache中,避免敏感資料同步到主存。

呼叫bouncycastle庫加密金鑰

利用java包bouncycastle中的國密sm4演算法加密sm2的私鑰,並將加密後的私鑰儲存在資料庫中。但由於bouncycastle為java平臺的庫,我們本次網站部署使用的是python程式碼,因此在兩個語言平臺之間替換不方便,因此不優先考慮使用bouncycastle的庫。

本週計劃

  • 改進密碼儲存方案,解決使用者密碼儲存未加密問題
  • 重新商討金鑰儲存方案,解決使用者公私鑰對存在未加密問題
  • 修改css檔案,美化前端頁面
  • 完善後端資料庫

相關文章