1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Encode RSA's Public Key as a Magic Public Key
//!
//! This implementation has been reverse-engineered from Mastodon's implementation, since no
//! documentation for the Magic Public Key format could be found online (Maybe I didn't look hard
//! enough).
//!
//! ### Examples
//! From private key
//! ```rust
//! # let mut rng = rand::thread_rng();
//! # let private_key = rsa::RSAPrivateKey::new(&mut rng, 2048).unwrap();
//! use rsa_magic_public_key::AsMagicPublicKey;
//! let string = private_key.as_magic_public_key();
//! ```
//! From public key
//! ```rust
//! # let mut rng = rand::thread_rng();
//! # let private_key = rsa::RSAPrivateKey::new(&mut rng, 2048).unwrap();
//! # let public_key = private_key.to_public_key();
//! use rsa_magic_public_key::AsMagicPublicKey;
//! let string = public_key.as_magic_public_key();
//! ```
//! Parsing
//! ```rust
//! # use rsa_magic_public_key::AsMagicPublicKey;
//! # let mut rng = rand::thread_rng();
//! # let private_key = rsa::RSAPrivateKey::new(&mut rng, 2048).unwrap();
//! # let magic_public_key = private_key.as_magic_public_key();
//! use rsa::RSAPublicKey;
//! use rsa_magic_public_key::FromMagicPublicKey;
//! let public_key = RSAPublicKey::from_magic_public_key(&magic_public_key).unwrap();
//! ```
use base64::{decode_config, encode_config, URL_SAFE};
use num_bigint_dig::BigUint;
use rsa::{PublicKey, RSAPublicKey};
use thiserror::Error;

/// Helper trait to add functionality to RSA types
pub trait AsMagicPublicKey {
    /// Produce a magic-public-key formatted string
    fn as_magic_public_key(&self) -> String;
}

/// Helper trait to add functionality to RSA types
pub trait FromMagicPublicKey {
    /// Parse a type from a magic-public-key formatted string
    fn from_magic_public_key(magic_public_key: &str) -> Result<Self, KeyError>
    where
        Self: Sized;
}

#[derive(Debug, Error)]
/// Parsing errors
pub enum KeyError {
    /// The magic-public-key is not properly formatted
    #[error("The provided key is malformed")]
    Malformed,

    /// The magic-public-key is not RSA
    #[error("The provided key is of the wrong kind")]
    Kind,
}

impl<T> AsMagicPublicKey for T
where
    T: PublicKey,
{
    fn as_magic_public_key(&self) -> String {
        let n = encode_config(&self.n().to_bytes_be(), URL_SAFE);
        let e = encode_config(&self.e().to_bytes_be(), URL_SAFE);

        format!("RSA.{}.{}", n, e)
    }
}

impl FromMagicPublicKey for RSAPublicKey {
    fn from_magic_public_key(magic_public_key: &str) -> Result<Self, KeyError> {
        let mut iter = magic_public_key.split('.');
        let kind = iter.next().ok_or(KeyError::Malformed)?;

        match kind {
            "RSA" => (),
            _ => return Err(KeyError::Kind),
        };

        let n = iter.next().ok_or(KeyError::Malformed)?;
        let e = iter.next().ok_or(KeyError::Malformed)?;

        let n = decode_config(n, URL_SAFE)?;
        let e = decode_config(e, URL_SAFE)?;

        let n = BigUint::from_bytes_be(&n);
        let e = BigUint::from_bytes_be(&e);

        RSAPublicKey::new(n, e).map_err(|_| KeyError::Malformed)
    }
}

impl From<base64::DecodeError> for KeyError {
    fn from(_: base64::DecodeError) -> Self {
        KeyError::Malformed
    }
}

#[cfg(test)]
mod tests {
    use crate::{AsMagicPublicKey, FromMagicPublicKey};
    use rsa::{RSAPrivateKey, RSAPublicKey};

    #[test]
    fn can_complete_cycle() {
        let mut rng = rand::thread_rng();
        let rsa = RSAPrivateKey::new(&mut rng, 2048).unwrap();

        let string = rsa.as_magic_public_key();

        let res = RSAPublicKey::from_magic_public_key(&string);

        assert!(res.is_ok());
    }

    #[test]
    fn asonix_key_is_valid() {
        key_is_valid("RSA.wEvEsHqM3twoC2F3KYMQ9YOialfVQX4StkLvhLUwFv8qpmY7ZSHHl2TWpnzlo5iWS5Pi2vC41HUGYz9XT5G74IUOyuIkjTL1FIcPJDcUFCzQjN3QZcHLPJPJVNOOOEiOk8__paOyrqJTq9ikcJDMJ8KTWQgk1leOxUVEN5uaQ-p9IBFbXC76-RqabfEoqLZagVMDSOfeC2uR9xZ1q5HkFveRTGs84QLR7FJVvx078nszx4UQGnmP0M-0sOeRJGK17IoJmhaok1XBpP6XFQ45vYeIRiaFj0Pc9GNISCW70dVXKMhv-K07orQJm6PwP8USyhq4tLkq6tcPbGRqEk3ZXw==.AQAB");
    }

    #[test]
    fn sir_boops_key_is_valid() {
        key_is_valid("RSA.vwDujxmxoYHs64MyVB3LG5ZyBxV3ufaMRBFu42bkcTpISq1WwZ-3Zb6CI8zOO-nM-Q2llrVRYjZa4ZFnOLvMTq_Kf-Zf5wy2aCRer88gX-MsJOAtItSi412y0a_rKOuFaDYLOLeTkRvmGLgZWbsrZJOp-YWb3zQ5qsIOInkc5BwI172tMsGeFtsnbNApPV4lrmtTGaJ8RiM8MR7XANBOfOHggSt1-eAIKGIsCmINEMzs1mG9D75xKtC_sM8GfbvBclQcBstGkHAEj1VHPW0ch6Bok5_QQppicyb8UA1PAA9bznSFtKlYE4xCH8rlCDSDTBRtdnBWHKcj619Ujz4Qaw==.AQAB");
    }

    fn key_is_valid(key: &str) {
        let res = RSAPublicKey::from_magic_public_key(key);

        assert!(res.is_ok());

        assert_eq!(res.unwrap().as_magic_public_key(), key);
    }
}