facebook_signed_request/
lib.rs

1use base64::{engine::general_purpose, Engine as _};
2use hmac::{Hmac, Mac as _};
3use serde::de::DeserializeOwned;
4use sha2::Sha256;
5
6type HmacSha256 = Hmac<Sha256>;
7
8#[cfg(feature = "with-data-deletion-callback")]
9pub mod data_deletion_callback;
10#[cfg(feature = "with-fb-login-deauth-callback")]
11pub mod fb_login_deauth_callback;
12#[cfg(feature = "with-ig-basic-display-data-deletion-request")]
13pub mod ig_basic_display_data_deletion_request;
14#[cfg(feature = "with-ig-basic-display-deauth-callback")]
15pub mod ig_basic_display_deauth_callback;
16
17pub const NORMALLY_ALGORITHM: &str = "HMAC-SHA256";
18
19pub trait Payload: DeserializeOwned {
20    fn algorithm(&self) -> Option<&str> {
21        None
22    }
23}
24
25/// [Official doc](https://developers.facebook.com/docs/games/gamesonfacebook/login#parsingsr)
26pub fn parse<T: Payload>(signed_request: &str, app_secret: &str) -> Result<T, ParseError> {
27    let mut signed_request_split = signed_request.split('.');
28    let encoded_sig = signed_request_split
29        .next()
30        .ok_or(ParseError::EncodedSignatureMissing)?;
31    let payload = signed_request_split
32        .next()
33        .ok_or(ParseError::PayloadMissing)?;
34    if signed_request_split.next().is_some() {
35        return Err(ParseError::SignedRequestInvalid);
36    }
37
38    let sig = general_purpose::URL_SAFE_NO_PAD
39        .decode(encoded_sig)
40        .map_err(ParseError::EncodedSignatureBase64DecodeFailed)?;
41    let data = general_purpose::URL_SAFE_NO_PAD
42        .decode(payload)
43        .map_err(ParseError::EncodedSignatureBase64DecodeFailed)?;
44
45    let data: T = serde_json::from_slice(&data).map_err(ParseError::PayloadJsonDecodeFailed)?;
46
47    let algorithm = data.algorithm().unwrap_or(NORMALLY_ALGORITHM);
48
49    let expected_sig = match algorithm {
50        NORMALLY_ALGORITHM => hmac_sha256_payload(payload.as_bytes(), app_secret)
51            .map_err(|_| ParseError::SignatureCalculateFailed)?,
52        _ => return Err(ParseError::AlgorithmUnknown(algorithm.to_owned())),
53    };
54
55    if sig != expected_sig {
56        return Err(ParseError::SignatureMismatch);
57    }
58
59    Ok(data)
60}
61
62#[derive(thiserror::Error, Debug)]
63pub enum ParseError {
64    #[error("EncodedSignatureMissing")]
65    EncodedSignatureMissing,
66    #[error("PayloadMissing")]
67    PayloadMissing,
68    #[error("SignedRequestInvalid")]
69    SignedRequestInvalid,
70    #[error("EncodedSignatureBase64DecodeFailed {0}")]
71    EncodedSignatureBase64DecodeFailed(base64::DecodeError),
72    #[error("PayloadBase64DecodeFailed {0}")]
73    PayloadBase64DecodeFailed(base64::DecodeError),
74    #[error("PayloadJsonDecodeFailed {0}")]
75    PayloadJsonDecodeFailed(serde_json::Error),
76    #[error("AlgorithmUnknown {0}")]
77    AlgorithmUnknown(String),
78    #[error("SignatureCalculateFailed")]
79    SignatureCalculateFailed,
80    #[error("SignatureMismatch")]
81    SignatureMismatch,
82}
83
84// $ echo -n "value" | openssl sha256 -hmac "key"
85// (stdin)= 90fbfcf15e74a36b89dbdb2a721d9aecffdfdddc5c83e27f7592594f71932481
86fn hmac_sha256_payload(payload_bytes: &[u8], app_secret: &str) -> Result<Vec<u8>, String> {
87    let mut hmac =
88        HmacSha256::new_from_slice(app_secret.as_bytes()).map_err(|err| err.to_string())?;
89    hmac.update(payload_bytes);
90    let hmac_result = hmac.finalize().into_bytes();
91    let sig = hmac_result.to_vec();
92
93    Ok(sig)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    use serde::Deserialize;
101
102    #[test]
103    fn test_parse() {
104        #[derive(Deserialize)]
105        struct MyPayload {
106            user_id: String,
107            algorithm: String,
108            issued_at: u64,
109        }
110        impl Payload for MyPayload {
111            fn algorithm(&self) -> Option<&str> {
112                Some(&self.algorithm)
113            }
114        }
115
116        // echo -n '{"user_id":"0","algorithm":"HMAC-SHA256","issued_at":1624244156}' | base64 | tr '+/' '-_' | tr -d '='
117        // echo -n 'eyJ1c2VyX2lkIjoiMCIsImFsZ29yaXRobSI6IkhNQUMtU0hBMjU2IiwiaXNzdWVkX2F0IjoxNjI0MjQ0MTU2fQ' | openssl sha256 -hmac "key" -binary | base64 | tr '+/' '-_' | tr -d '='
118        let signed_request = "Mf_s6nTb38UYqioBmPqu0Ewm9souPZB9I2fIGwV729U.eyJ1c2VyX2lkIjoiMCIsImFsZ29yaXRobSI6IkhNQUMtU0hBMjU2IiwiaXNzdWVkX2F0IjoxNjI0MjQ0MTU2fQ";
119
120        match parse::<MyPayload>(signed_request, "key") {
121            Ok(payload) => {
122                assert_eq!(payload.user_id, "0");
123                assert_eq!(payload.algorithm, "HMAC-SHA256");
124                assert_eq!(payload.issued_at, 1624244156);
125            }
126            Err(err) => panic!("{}", err),
127        }
128    }
129
130    #[test]
131    fn test_hmac_sha256_payload() {
132        assert_eq!(
133            hex::encode(hmac_sha256_payload(b"value", "key").unwrap()),
134            "90fbfcf15e74a36b89dbdb2a721d9aecffdfdddc5c83e27f7592594f71932481"
135        );
136    }
137}