2026年3月22日

碼農二三事之加密~JWT

 前言:

在我百寶箱中,JWT不常用,但幾乎每個實做都用了,因為JWT基本上已經嵌入在.NET中開箱即用了。😁

說明:

我一般會形容JWT是鑰匙,你通過了認證並授權後,就發給你一把鑰匙,拿著鑰匙就有權限進行操作,這把鑰匙可以有時效,也可以沒有時效,端看需求。
JWT是標準,有標準格式,它標準格式分成3段,每段用.隔開,第二段和第三段是重點,第二段放了所有資訊,JWT標準中規範了必要和可選的資訊,必要的包括sub(subject), iss(issuer), exp(expired), nbf(NotBeFore), iat(IssueAt)。
其中必要中的必要是exp,這個標記了JWT的到期時間。
JWT沒有明確規定時間是哪種Timestamp,但一般用UTC。

第三段則是第二段的簽章,用加密算法把第二段用特定密碼算一個簽章放第三段,因此第二段 + 密碼計算簽章後,必定等於第三段,如果不等於,就是被竄改了。第三段的JWT規範是BASE64。

目前的登入Token,幾乎都用JWT,.NET c# 直接做好JWT中間件,套用即可,基本上是業界標準了。
JWT的第二段資訊顯然是關鍵,第二段資訊稱為Payload,是json用BASE64編碼後的字串。
這裡畫重點,是BASE64編碼後的字串,這表示它沒有加密,JWT不具備加密功能,只具備簽章功能,能夠驗證JWT是否合法沒被竄改,而根據合法JWT,則可以判斷是否過期,並根據sub(Subject)判斷持有者是不是本人,根據iss(Issuer)判斷簽發者是不是授權簽發者。

好~說完了~但JWT故事還沒結束,有沒有特殊用法?

特殊用法:

既然我們知道JWT是根據第二段的Payload紀錄資訊,Payload本身是json,那我們完全可以自行設計非標準的JWT格式,對吧!?
在c#/Java中,我們只需要建立一個Model,我一般取名為AuthToken,裡面參考JWT放入sub,iss,exp,iat,nbf。

使用時
var token = new AuthToken() {
    sub = "{user}",
    iss = "{BestServer}",
    exp = now + 1440 timestamp,
    iat = now timestamp,
    nbf = now - 5secs timestamp
}
var SpecifyJWT = AESCipher.Encrypt(token.toJson());

打完收工。
當然,檢查一樣,反過來,先用AESCipher解密,接著依序檢查 exp, nbf 。
一個具有加密效果的AccessToken就完成了,如果原本用c# JWT中間件,那只要實做一個自己的中間件替換上去,就完成高強度客製化的認證Token設計了。

JWT用法:

實務上,我常用JWT系列,因為它是分散式的,不受限Session,任何後端都能檢核。
但它可以有幾種變種:
  1. 後端統一用一台認證服務簽發和檢核JWT,所有服務都先把Client端JWT送來檢核,變成認證授權與服務分離
  2. 後端服務都使用同一個Jwt工具包進行簽發和檢核
  3. 資料庫中設計黑名單,特定JWT寫入黑名單後失效,新認證不予簽發JWT
  4. 資料庫中設計白名單,所有JWT簽發後都寫入資料庫,檢核時查找資料庫,如果使用者異常,從資料庫中刪除或註記這筆JWT,新認證不予簽發JWT,這種做法等同於把JWT當作AccessToken使用,差別是,AccessToken較短,通常是隨機字串,沒有隱含資訊,JWT則比較長,但有隱含資訊

碼農二三事之加密~非對稱加密~ECC(橢圓曲線加密)

前言:

在我百寶箱中,最近新入的是ECC(橢圓曲線加密),它可以做為RSA非對稱加密的平替。
ECC的特點是,它是開放的,不是公司產品。
ECC相關的實做和過程學習,基本上完全是AI實做,但因為要進入百寶箱,我有查核和測試過程式碼。

程式碼:

這份程式碼裡面包括多個程式語言的ECDH和ECIES實做,也是我百寶箱中新加入的工具。

https://gitlab.com/ycfunet/my_ecdh_ecies_code_test

說明:

ECC包括3種,ECDH、ECIES、ECDSA,其中ECDSA是簽章用的,這裡不談,ECIES是我們要用的,但它是ECDH的擴展,因此要先從ECDH說起。
  • ECDH:
ECDH邏輯是,我有我的公鑰和私鑰,你有你的公鑰和私鑰,我用「我的私鑰+你的公鑰」可以生成共享金鑰,你用「你的私鑰+我的公鑰」可以生成相同的共享金鑰,沒錯,這就是神奇的地方,「我的私鑰+你的公鑰」,「你的私鑰+我的公鑰」都可以生成相同金鑰。那麼這個共享金鑰就可以拿來當AES金鑰加密和解密資料。

 

ECDH使用時,不會直接給共享金鑰,而是雙方會把公鑰給對方,加密和解密前,才會組合出共享金鑰後,再用共享金鑰以AES加密解密資料,所以ECDH又稱為「交換金鑰加密系統」。
  • ECIES:
ECIES可以做為RSA的平替,它是以ECDH為基礎的擴展。
使用概念上,一樣是接收端先建立一組公鑰和私鑰,然後把公鑰給對方,自己保留私鑰。

但在邏輯上,如何套入ECDH?
ECIES引入「臨時金鑰」的概念,我(後稱接收端)會先產生一組公私鑰,公鑰提供,私鑰保存。
發送端拿著我的公鑰,在每次傳送資料時,都先產生一組公私鑰,這組公私鑰就稱為「臨時金鑰」,發送端會用「臨時金鑰的私鑰+我的公鑰」「產生共享金鑰」,用這組共享金鑰以AES加密資料。

 

還記得AES提到加鹽的IV技巧嗎?這裡一樣的行為,發送端會把加密後的資料,在最前面加上「臨時金鑰的公鑰」,因此整個資料變成
臨時金鑰的公鑰 + AES編碼後密文

接收端收到後,用「我的私鑰 + 臨時金鑰的公鑰」產生共享金鑰後,用共享金鑰以AES解密密文。
因為臨時金鑰的公鑰長度不一定,AI在最前面加上4 Bytes,記載臨時金鑰的字元數,所以完整資料變成 
臨時金鑰的公鑰長度數值(4Bytes) + 臨時金鑰的公鑰 + AES密文

 因為這樣的設計,儘管加密解密流程比較複雜,但包裝後可以完全平替RSA的非對稱加解密,接收端只要產生一組公私鑰,公鑰提供後,不論是臨時金鑰、AES加解密,其實都包裝在Encrypt和Decrypt裡面了。
RSA是公司產品,同時也有專利,我懷疑ECIES是為了規避RSA專利而用ECDH流程設計出的標準。


碼農二三事之加密~非對稱加密~RSA

 前言:

在我百寶箱中,使用頻率也很高的,就是RSA加解密。

程式碼:

這份程式碼裡面包括多個程式語言的RSA實做,也是我自己常用的百寶箱工具。

https://gitlab.com/ycfunet/my_rsa_code_test

說明:

RSA包括幾種功能,這裡只說加解密,不說憑證、簽章。
AES是對稱式加解密,就是固定用密碼把字串加密和解密,但一個情境是,我不能讓人知道我的密碼(金鑰),但我要能讓別人能加密,我能解密,這就是非對稱加解密,常見的是RSA。

RSA是RSA公司的產品,因為是公司產品,使用簡單,不用管細節,是業界標準。

RSA就3個Function:
  • GenerateKey: 產生一組公私鑰,公鑰給別人加密,私鑰自己留著解密。
  • Encrypt: 用公鑰加密明文成密文。
  • Decrypt: 用私鑰解密密文成明文。
咦...就這樣...對...就這樣,簡單吧 🤣。

公私鑰格式說明:

比較要注意的知識點是公私鑰的格式,一般有2種,PEM和XML。
c#用XML,其他通常用PEM,PEM以 -----BEGIN...----- 和 -----END...----- 當作開頭標記和結尾標記
另外,在Java中,Java要求的格式是PEM去除標記,只有Key的內容。


碼農二三事之加密~AES

前言:

這篇不會跟你解釋AES裡面怎麼XOR、怎麼移位,這篇我想說的是哪幾個規格以及怎麼寫。
在我的百寶箱中,使用頻率最高也最重要的莫過於加密相關,其中AES是重點之一。

程式碼:

這份程式碼裡面包括多個程式語言的AES實做,是我自己常用的百寶箱工具。
裡面的AES程式碼基本上相互驗證過,原則上都能互相正常編碼解碼(除了my_browser_js_test,能編碼,不能解碼)。

https://gitlab.com/ycfunet/my_aes_code_test

說明:

AES是對稱加密演算法,邏輯是你用一個密碼把字串加密,另一個人用同一個密碼把字串解密,密碼就是鑰匙,關鍵字叫金鑰(Key),加密後的資料叫密文,未加密的原始字串叫明文。



AES的核心邏輯是,用固定長度的金鑰,把固定長度的明文加密成密文,反過來,固定長度的密文用固定長度的金鑰解密成明文。
因此,最基本的認知是,金鑰長度固定,明文長度也固定,咦...我使用時,沒聽說明文規定長度啊,有些AES金鑰也沒規定長度啊。
這時引出兩個AES的重點,一個叫金鑰雜湊,一個叫Padding。

因為密碼(金鑰)可能長度不一,為了讓密碼(金鑰)可以是任意長度,通常會用SHA256把密碼進行雜湊,就能取出固定長度的金鑰,而這個長度就跟使用的加密規格相關,例如AES256,就表示256 Bits,SHA也用256 Bits。

Padding翻譯叫填充,剛剛提到加密和解密時,明文必須是固定長度,不固定長度怎麼辦呢?會把明文填充固定字元,讓它長度固定。
Padding有多種填充內容,直覺想到填0,對這是一種,目前常用的是根據空白數量填數字,例如空3格就填0x03 0x03 0x03,這種目前是主流叫做PKCS7Padding。

到這裡,我們知道能用SHA256把密碼雜湊成密文,能用Padding把明文填充成固定長度,這時候會發現,固定的金鑰+固定的明文=固定的密文。

這很OK,但感覺每次的密文都一樣好像怪怪的,有沒有辦法讓它每次密文都不同?這時引出加密常見的重點,加鹽(Salt)。
所謂的加鹽,真的如字面意思,就是加料,在AES中,規範了多種模式,我們只要記得常用的2種,ECB和CBC,ECB就是上面寫的未加鹽的就稱為ECB,加鹽的模式就稱為CBC。
AES-CBC的加鹽是說,CBC規定了一個欄位,稱為IV,這個欄位就是加鹽的鹽巴,AES-CBC加密時將密鑰+IV當密鑰,一起將明文加密,解密時將密鑰+IV當密鑰,一起將密文解密。IV通常是16字元(128Bits)。

因為IV是鹽巴,通常會用亂數產生,那既然是亂數產生,解密時如何知道?
一般會把IV加在密文最前面,變成
IV(16個字)+密文

解密時,先切割出IV和密文,然後把設定IV和手上的密鑰,把密文解密成明文。
IV放密文最前面是慣例,也可以和密文一起放在兩端,傳送時只傳送密文,但既然是加鹽,通常目的是讓密文判讀性更低,放在密文最前面會讓密文每次都不一樣,減少一致性。

最後一個知識點,實作會發現,AES加密後的密文,是Bytes的Binary,這不好處理呀。
在最後要提供密文時,Binary不容易使用,通常會編碼讓它字串化,一般都用BASE64或HEX,這裡有個小小誤區,BASE64有2種,一種是一般的BASE64,但一般的BASE64會有特殊字元,在網頁傳輸時,可能因為HTTP GET不支援的字元,因此有個變形是BASE64 URLEncoding,這兩種BASE64的結果是不同的。

以上就是AES知識點,還是蠻長的,但不用管底層XOR、移位邏輯。

結論:

這次的文章放了一陣子,圖片是Claude-Code (GLM-5/GLM-4.7) 用Excalidraw MCP畫的