ys memos

Blog

Rustで簡単なJWTライブラリを実装してみた ~3. base64~


rust

2024/08/31


  1. 概要と使用例
  2. プロジェクト設定
  3. base64関連
  4. 署名アルゴリズム関連
  5. ヘッダ定義
  6. JWT本体の実装
  7. JWT本体のTest

本Partでは、base64関連の実装を行う。

JWTでは、base64を多用するので、base64エンコード・デコードを気軽に利用できるように、クレート内でラップして利用する。


src/jwt/base64.rs
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};

use crate::error::Result;

pub(crate) struct Base64;
impl Base64 {
    pub fn encode<T: AsRef<[u8]>>(input: T) -> String {
        BASE64_URL_SAFE_NO_PAD.encode(input)
    }

    pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>> {
        Ok(BASE64_URL_SAFE_NO_PAD.decode(input)?)
    }

    /// Serialize a value to a base64 string
    pub fn serialize<T: serde::ser::Serialize>(input: &T) -> Result<String> {
        let serialized = serde_json::to_string(input)?;
        Ok(Base64::encode(serialized))
    }

    /// Deserialize a value from a base64 string
    pub fn deserialize<T: serde::de::DeserializeOwned>(input: &str) -> Result<T> {
        let decoded = Base64::decode(input)?;
        Ok(serde_json::from_slice(decoded.as_slice())?)
    }
}

#[cfg(test)]
mod tests {
    use crate::{jwt::header::Header, Alg};

    use super::*;

    #[test]
    fn decode_encode() -> Result<()> {
        let cases = vec![
            (
                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
                r#"{"alg":"HS256","typ":"JWT"}"#,
            ),
            (
                "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
                r#"{"sub":"1234567890","name":"John Doe","iat":1516239022}"#,
            ),
        ];
        for (input, expected) in cases {
            let decoded = String::from_utf8(Base64::decode(input)?)?;
            println!("decoded: {:?}", decoded);
            assert_eq!(decoded, expected);
            let reencoded = Base64::encode(decoded);
            assert_eq!(input, reencoded);
        }
        Ok(())
    }

    #[test]
    fn deserialize_serialize() -> Result<()> {
        let input = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; // {"alg":"HS256","typ":"JWT"}
        let v: Header = Base64::deserialize(input)?;
        matches!(v.alg, Alg::HS256);
        let reencoded = Base64::serialize(&v)?;
        assert_eq!(input, reencoded);

        Ok(())
    }
}


base64クレートを使う。

実装しているクレート内での利用は、Base64構造体と名前をつけ、ラップする。

あえてラップしているのは、

  • paddingなどの設定を変えたくなったときの変更を一元化する
  • クレート内での利便性のため、crate::error::Resultを返すようにする
  • serdeとの連携の簡素な記述のための serialize/deserializeメソッドを追加する

という目的がある。

use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};

use crate::error::Result;

pub(crate) struct Base64;

まずはエンコード・デコードを書いておく。ジェネリクスは、多様な型を受け入れられるようにしておく。

前述の通り、Resultを返すようにしておく。

impl Base64 {
    pub fn encode<T: AsRef<[u8]>>(input: T) -> String {
        BASE64_URL_SAFE_NO_PAD.encode(input)
    }

    pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>> {
        Ok(BASE64_URL_SAFE_NO_PAD.decode(input)?)
    }

Header, Payloadとbase64の連携を簡素に記述するために、

  • serialize(): serdeでシリアライズしたものをbase64エンコードする
  • deserialize(): base64デコードしたものをserdeでデシリアライズする

という関連関数を実装しておく。

Base64::serialize()のように使うので、間違えることはないと思うが、念の為doccommentを書き、base64 encodeが行われることを明示しておく。(deserialize()も同様)

    /// Serialize a value to a base64 string
    pub fn serialize<T: serde::ser::Serialize>(input: &T) -> Result<String> {
        let serialized = serde_json::to_string(input)?;
        Ok(Base64::encode(serialized))
    }

    /// Deserialize a value from a base64 string
    pub fn deserialize<T: serde::de::DeserializeOwned>(input: &str) -> Result<T> {
        let decoded = Base64::decode(input)?;
        Ok(serde_json::from_slice(decoded.as_slice())?)
    }
}

Base64の利用方法の例示を兼ね、テストを書いておく。

encode/decodeserialize/deserializeの組に分けてテストを書いている。 これは、変換・逆変換の対でテストすることで、(値を元に戻せるという観点での)実装の正しさを確認する目的を兼ねている。

Headerという構造体は現時点では未定義だが、ここには serdeSerialize/Deserializeを実装しておくことを想定している。

#[cfg(test)]
mod tests {
    use crate::{jwt::header::Header, Alg};

    use super::*;

    #[test]
    fn decode_encode() -> Result<()> {
        let cases = vec![
            (
                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
                r#"{"alg":"HS256","typ":"JWT"}"#,
            ),
            (
                "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
                r#"{"sub":"1234567890","name":"John Doe","iat":1516239022}"#,
            ),
        ];
        for (input, expected) in cases {
            let decoded = String::from_utf8(Base64::decode(input)?)?;
            println!("decoded: {:?}", decoded);
            assert_eq!(decoded, expected);
            let reencoded = Base64::encode(decoded);
            assert_eq!(input, reencoded);
        }
        Ok(())
    }

    #[test]
    fn deserialize_serialize() -> Result<()> {
        let input = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; // {"alg":"HS256","typ":"JWT"}
        let v: Header = Base64::deserialize(input)?;
        matches!(v.alg, Alg::HS256);
        let reencoded = Base64::serialize(&v)?;
        assert_eq!(input, reencoded);

        Ok(())
    }
}


関連タグを探す