2021/11/23

[Rust][TOTP]Rustで2段階認証を実装する

rust

概要

TOTP(Time-Based One Time Password)ベースの2段階認証をサイトに追加することがあったので
調査して実装しました。

‎Google Authenticatorとか‎Authyとかで利用できるよくあるやつです。

google-authenticatorという便利なライブラリがあったので、そちらを利用しています。

実装方法

install

Cargo toml

google-authenticator = "0.3.0"
once_cell = "1.8.0"           # authenticatorのsingleton instance生成用
percent-encoding = "2.1.0"    # qrcode生成用のurl文字列生成用

rust code

ユーザーに表示させたい時

use google_authenticator::GoogleAuthenticator;
use once_cell::sync::OnceCell;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};

static GA_AUTH: OnceCell<GoogleAuthenticator> = OnceCell::new();

fn ga_auth() -> &'static GoogleAuthenticator {
    GA_AUTH.get_or_init(|| GoogleAuthenticator::new())
}

pub fn generate_otp_secret() -> String {
    // random base32
    ga_auth().create_secret(32)
}

pub fn build_otp_url(otp_secret: &str, account_name: &str, issuer_name: &str) -> String {
    let account_name = utf8_percent_encode(account_name, NON_ALPHANUMERIC);
    let issuer_name = utf8_percent_encode(issuer_name, NON_ALPHANUMERIC);
    format!(
        "otpauth://totp/{}?secret={}&issuer={}",
        account_name, otp_secret, issuer_name
    )
}
  • otp_secret がsecretです。これはユーザーが設定する毎に生成して、DBに保存して利用します。
  • otp_url QRcodeなどでアプリに読ませる情報です
    • otpauth://totp/your-app-name:user-email%40gmail.com?secret=BASE32STRING&issuer=your-app-name みたいなURL文字列です

認証用

pub fn verify_otp_code(otp_secret: &str, otp_code: &str) -> bool {
    ga_auth().verify_code(otp_secret, otp_code, 3, 0)
}

test code

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_otp() {
        let otp_secret = generate_otp_secret();
        assert_eq!(otp_secret.len(), 32);
        let otp_url = build_otp_url(otp_secret.as_str(), "[email protected]", "test-app");
        let expected_url = format!(
            "otpauth://totp/test%40example%2Ecom?secret={}&issuer=test%2Dapp",
            otp_secret
        );
        assert_eq!(otp_url, expected_url);
        let otp_code = ga_auth().get_code(otp_secret.as_str(), 0).unwrap();
        assert!(verify_otp_code(otp_secret.as_str(), otp_code.as_str()));
        let otp_code = "000000".to_owned();
        assert!(!verify_otp_code(otp_secret.as_str(), otp_code.as_str()));
    }
}

以上になります。