開發系統時,偶爾需要隨機產生符合複雜度要求(包含大小寫英數字加符號... 等)密碼的需求,例如:新帳號初始密碼、重設密碼... 等,這篇來聊聊這類程式寫法。

以 .NET 為例,.NET Framework 與 .NET 8+ 都內建 API 能直接實現。

.NET Framwork 有 System.Web.Security.Membership.GeneratePassword(Int32, Int32)

.NET 8+ 則可使用 System.Security.Cryptography.RandomNumberGenerator.GetString(ReadOnlySpan<char> choices, int length) 從一串字元陣列中隨機挑選組成指定長度字串:

var availableCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
string password = System.Security.Cryptography.RandomNumberGenerator.GetString(availableCharacters, length: 16);
Console.WriteLine(password);  // 例如: "9k@#mL2$xQ7!vW5&"

為什麼不用 System.Random 產生亂數就好,要用專屬隨機數產生器?

在資訊安全中,隨機數的品質往往決定其密碼學安全性,一旦不夠隨機或可被預測,代表攻擊者只需嘗試可能出現的組合,破解難度會降低。System.Random 並不符合密碼學對隨機性的嚴格要求,不信的話請看以下示範,以 .NET Framework 為例,Random 預設是用 Environment.TickCount 當種子,故同時用多 Thread 跑 new Random() 的種子相同,產生的隨機數也相同:

.NET Core 的 Random 有所改良,使用 OS 的偽隨機數生成器 PRNG (BCrypt on Windows, OpenSSL on *nix),但仍存可被預測問題,也就是「使用相同種子產生的一連串隨機數都是固定的」,因此透過破解程式資料或猜測種子可預測其產生的隨機數。(對這個議題有興趣,這篇文章有示範:Randomness in .NET)

為獲得完全隨機性,密碼學用的 PRNG 需要一個稱為隨機數生成器 (RNG) 的「熵 (entropy)」來源 (通常來自環境) 以及一個加密演算法 (PRNG) 產生高品質隨機位元。.NET 提供的最新解決方案便是 RandomNumberGenerator (.NET Framework 則是用 RNGCryptoServiceProvider,它在 .NET 8+ 已棄用)。這些隨機數生成 API 基於 Windows BCryptGenRandom (舊版 Windows 為 CryptGenRandom) 及 Linux OpenSSL RAND_bytes,使用 CTR_DRBG 及 OpenSSL DRBG 演算法,符合 NIST SP 800-90A、FIPS 140-2/3 標準,可安心服用。

回到正題,若我們要隨機生成一個符合指定複雜度的密碼,該怎麼做?

  1. 選用符合資安要求的隨機數生成器
  2. 決定密碼長度
  3. 決定密碼字元的組成,要有幾個大寫、幾個小寫、幾個符號,餘下的位數用數字填補
  4. 至於要選用哪些英文字母跟符號,這裡面是有學問的 (如果你在意使用者體驗的話... 資安:那是啥?能吃嗎?)

昨天我有在 FB 分享一個有趣問題:隨機生成複雜密碼 + 不適當無襯線字體 = 整人遊戲

若這類包含英文跟符號的高複雜隨機密碼會透過 Email 或其他電子形式傳送,而使用者是使用肉眼識別再手工輸入方式,如何顯示密碼文字由客戶端軟體決定,若用到不適當字體,便會發生分不清數字 1、大寫 I 、小寫 L 及直線 | 符號,混淆數字 0 與大寫 O 的狀況,明明給了密碼,使用者卻得本著暴力破解精神嘗試不同可能。因此,我主張隨機生成密碼時,避開易混淆的字元,目前想到的有 1, l, I, |,O 及 0。RandomNumberGenerator.GetString() 允許我們提供選字元清單讓隨機組合,因此實作起來一點都不難。

以下是我想到的簡單實作,排除易混淆字元,指定總長度及大寫、小寫、符號的字元數,生成一段指定長度及指定複雜度,隨機性符合 NIST 標準的隨機密碼:

using System.Security.Cryptography;
using System.Text;

for (int i = 0; i < 100; i++)
{
    Console.Write(GenComplexPasswd(8, 1, 1, 1) + "  ");
    if (i % 5 == 4)
        Console.WriteLine();
}

// 移除大寫 I、大寫 O
const string upCasePool = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; 
// 移除小寫 l
const string lowCasePool = "abcdefghijkmnopqrstuvwxyz";
// 選取十個形狀差異較大的符號
const string symbolPool = "!@#$%^&+=?";
// 移除數字 1 跟 0
const string numberPool = "23456789";

string GenComplexPasswd(int length, int minUpCaseLen, int minLowCaseLen, int minSymbolLen)
{
    var pwd = new StringBuilder();
    if (minUpCaseLen > 0)
        pwd.Append(RandomNumberGenerator.GetString(upCasePool, minUpCaseLen));
    if (minLowCaseLen > 0)
        pwd.Append(RandomNumberGenerator.GetString(lowCasePool, minLowCaseLen));
    if (minSymbolLen > 0)
        pwd.Append(RandomNumberGenerator.GetString(symbolPool, minSymbolLen));
    pwd.Append(RandomNumberGenerator.GetString(numberPool, length - pwd.Length));
    return new string(pwd.ToString().ToCharArray().OrderBy(c => Guid.NewGuid()).ToArray());
}

生成的密碼會像這樣:

以上做法的長度與複雜度可以自由調整,在安全性與友善度間取得平衡。想友善一點,就讓數字比重高一點或只加大寫字元;不然可增加大小寫與符號位數,辛苦使用者來讓天秤向資安傾斜。我個人覺得,在有加上時效性與錯誤次數上限的前題下,這類隨機密碼的性質會接近簡訊驗證碼,複雜度要求可以放寬。但實務上該怎麼做,通常也要參酌資安/稽查人員的看法,沒有標準答案。

Explains how to generate secure, complex passwords in .NET using cryptographic RNGs, avoiding predictable randomness and confusing characters, balancing security and usability with practical, customizable implementations.


Comments

Be the first to post a comment

Post a comment