密碼是一種最常用來驗證身份的機制,例如在登入使用者帳號時,常會搭配密碼一起輸入,以確保存取這個帳號的人是帳號的擁有人或是代理人。由於密碼就像是身份證一樣的東西,因此不能太簡單,太簡單就會容易被人猜到,或是被電腦程式破解。在撰寫程式的時候,我們常會需要檢查、驗證及儲存使用者輸入的密碼,或是產生出密碼以供使用者或是系統本身使用。若是使用Rust程式語言的話,要如何處理密碼的相關功能呢?



Passwords

「Passwords」是筆者開發的套件,提供四大功能:密碼產生(Generator)、密碼雜湊(Hasher)、密碼分析(Analyzer)和密碼評分(Scorer)。

Crates.io

Cargo.toml

passwords = "*"

使用方法

密碼產生

使用use關鍵字來將passwords這個crate底下的PasswordGenerator結構體給引用到當前的程式範圍下。PasswordGenerator結構體擁有以下幾個欄位,可以控制其實體產生密碼的規則:

  • length:密碼長度。
  • numbers:是否要包含數字(0123456789)。
  • lowercase_letters:是否要包含小寫英文字母(abcdefghijklmnopqrstuvwxyz)。
  • uppercase_letters:是否要包含大寫英文字母(ABCDEFGHIJKLMNOPQRSTUVWXYZ)。
  • symbols:是否要包含符號(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)。
  • spaces:是否要包含空白字元( )。
  • exclude_similar_characters:見底下說明。
  • strict:是否每種啟用的字元都要包含。

由於0(零)、O(大寫歐)、o(小寫歐)和ilI1|以及"'`很像,在某些特定字體下甚至完全無法將它們區分。所以PasswordGenerator結構體還有提供exclude_similar_characters欄位,可以不用以上列舉的易混淆字元來產生密碼。

PasswordGenerator結構體產生出來的實體有兩個主要的方法,generate_one方法會產生出一組密碼,generate會產生出多組密碼。

舉例來說:

use passwords::PasswordGenerator;

let pg = PasswordGenerator {
       length: 8,
       numbers: true,
       lowercase_letters: true,
       uppercase_letters: true,
       symbols: true,
       spaces: true,
       exclude_similar_characters: true,
       strict: true,
};

println!("{}", pg.generate_one().unwrap());
println!("{:?}", pg.generate(5).unwrap());

PasswordGenerator結構體有支援Fluent Interface,用法如下:

use passwords::PasswordGenerator;

let pg = PasswordGenerator::new().length(8).numbers(true).lowercase_letters(true).uppercase_letters(true).symbols(true).spaces(true).exclude_similar_characters(true).strict(true);

println!("{}", pg.generate_one().unwrap());
println!("{:?}", pg.generate(5).unwrap());

generate方法有針對產生多組密碼進行優化,不要重複使用generate_one方法來產生密碼。如果我們沒有辦法事先確定好密碼的數量,可以使用PasswordGenerator結構實體提供的try_iter方法來建立PasswordGeneratorIter結構實體,它有實作Iterator特性,且可以更有效率地重複產生密碼,因為它不像PasswordGenerator結構實體在每次呼叫方法來產生密碼前都要建立新的字元池(Character Pool)。

use passwords::PasswordGenerator;

let pgi = PasswordGenerator::new().try_iter().unwrap();

println!("{}", pgi.generate_one());
println!("{:?}", pgi.generate(5));
use passwords::PasswordGenerator;

let mut pgi = PasswordGenerator::new().try_iter().unwrap();

println!("{}", pgi.next().unwrap());
println!("{}", pgi.next().unwrap());
密碼雜湊

若需要將密碼雜湊之後儲存下來,可以啟用「Passwords」套件的crypto特色,Cargo.toml設定檔的寫法如下:

[dependencies.passwords]
version = "*"
features = ["crypto"]

crypto特色啟用之後,就可以使用use關鍵字來將passwords這個crate底下的hasher模組給引用到當前的程式範圍下。hasher模組有6個函數,gen_salt函數可以產生出16個位元組的鹽;bcrypt函數可以利用bcrypt演算法來計算密碼明文的雜湊值;identify_bcrypt函數可以用來判斷一個密碼明文在雜湊之後是否和一個已知的bcrypt雜湊值是吻合的;bcrypt_format函數可以利用bcrypt演算法來計算密碼明文的雜湊值,並且將結果格式化成MCF(Modular Crypt Format);identify_bcrypt_format函數可以用來判斷一個密碼明文在雜湊之後是否和一個已知的MCF是吻合的;get_password_with_null_terminated_byte函數可以用來將密碼明文轉成以\0結尾的資料(以符合bcrypt的規定)。

舉例來說:

use passwords::hasher;

let salt = hasher::gen_salt();

let hashed = hasher::bcrypt(10, &salt, "password\0").unwrap();
assert!(unsafe { hasher::identify_bcrypt(10, &salt, "password\0", &hashed) });

let mcf = hasher::bcrypt_format(10, &salt, "password\0").unwrap();
assert!(unsafe { hasher::identify_bcrypt_format("password\0", mcf) });
密碼分析

使用use關鍵字來將passwords這個crate底下的analyzer模組給引用到當前的程式範圍下。analyzer模組有一個函數,那就是analyze函數。這個analyze函數可以輸入一組密碼,將其分析之後會回傳analyzer模組下的AnalyzedPassword結構實體。這個AnalyzedPassword結構實體所帶的密碼分析結果欄位如下:

  • password:移除ASCII控制碼之後的密碼明文。
  • length:密碼中所有字元的數量。
  • spaces_count:密碼中空白字元的數量。
  • numbers_count:密碼中數字的數量。
  • lowercase_letters_count:密碼中小寫英文字母的數量。
  • uppercase_letters_count:密碼中大寫英文字母的數量。
  • symbols_count:密碼中符號的數量。
  • other_characters_count:其它如中文字的字元。
  • consecutive_count:連續重覆出現的字元數量。
  • non_consecutive_count:不連續重覆出現的字元數量。
  • progressive_count:照順序出現的字元數量。

舉例來說:

use passwords::analyzer;

let password = "ZYX[$BCkQB中文}%A_3456]  H(\rg";

let analyzed = analyzer::analyze(password);

assert_eq!("ZYX[$BCkQB中文}%A_3456]  H(g", analyzed.password()); // "\r" was filtered
assert_eq!(26, analyzed.length()); // Characters' length, instead of that of UTF-8 bytes
assert_eq!(2, analyzed.spaces_count()); // Two spaces between "]" and "H"
assert_eq!(4, analyzed.numbers_count()); // Numbers are "3456"
assert_eq!(2, analyzed.lowercase_letters_count()); // Lowercase letters are "k" and "g"
assert_eq!(9, analyzed.uppercase_letters_count()); // Uppercase letters are "ZYX", "BC", "QB", "A" and "H"
assert_eq!(7, analyzed.symbols_count()); // Symbols are "[$", "}%", "_", "]" and "("
assert_eq!(2, analyzed.other_characters_count()); // Other characters are "中文". These characters are usually not included on the rainbow table.
assert_eq!(2, analyzed.consecutive_count()); // Consecutive repeated characters are "  " (two spaces)
assert_eq!(2, analyzed.non_consecutive_count()); // Non-consecutive repeated characters are "B" (appears twice)
assert_eq!(7, analyzed.progressive_count()); // Progressive characters are "ZYX" and "3456". "BC" is not counted, because its length is only 2, not three or more.

如果想要判斷密碼是不是在常用不安全密碼的清單內,可以啟用「Passwords」套件的common-password特色,Cargo.toml設定檔的寫法如下:

[dependencies.passwords]
version = "*"
features = ["common-password"]

common-password特色啟用之後,AnalyzedPassword結構實體就會多出is_common方法可以使用。若is_common方法回傳true,表示該密碼是非常危險的!

密碼評分

使用use關鍵字來將passwords這個crate底下的scorer模組給引用到當前的程式範圍下。scorer模組有一個函數,那就是score函數。這個score函數可以傳入一個,AnalyzedPassword結構實體的參考,來計算其所表示的密碼的分數,分數會直接以f64型別的數值來回傳。

舉例來說:

use passwords::analyzer;
use passwords::scorer;

assert_eq!(62f64, scorer::score(&analyzer::analyze("kq4zpz13")));
assert_eq!(100f64, scorer::score(&analyzer::analyze("ZYX[$BCkQB中文}%A_3456]  H(\rg")));

if cfg!(feature = "common-password") {
    assert_eq!(11.2f64, scorer::score(&analyzer::analyze("feelings"))); // "feelings" is common, so the score is punitively the original divided by 5
} else {
    assert_eq!(56f64, scorer::score(&analyzer::analyze("feelings")));
}

不同分數區間的涵意如下:

  • 0 ~ 20:非常危險。(有機會在數秒內就被破解)
  • 20 ~ 40:危險。
  • 40 ~ 60:很弱。
  • 60 ~ 80:弱。
  • 80 ~ 90:好。
  • 90 ~ 95:強。
  • 95 ~ 99:很強。
  • 99 ~ 100:極強(堅不可破)。