1use serde::{Serialize, Deserialize};
2use base64_url;
3use serde_json;
4use hmac;
5use hmac::{Mac, HmacCore};
6use sha2::Sha256;
7use std::fmt::{Display, Formatter};
8use std::result;
9use std::str::{from_utf8, Utf8Error};
10use base64_url::base64::DecodeError;
11use hmac::digest::{CtOutput, InvalidLength};
12use hmac::digest::core_api::CoreWrapper;
13
14static HEADER: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
15static HEADER_LENGTH: usize = HEADER.len();
16static SIGNATURE_LENGTH: usize = 43;
17static MIN_TOKEN_LENGTH: usize = HEADER_LENGTH + SIGNATURE_LENGTH + 3;
18
19#[derive(Debug)]
20pub enum Error{
21 JsonError(serde_json::error::Error),
22 InvalidKeyLength(InvalidLength),
23 Base64UrlDecodeError(DecodeError),
24 Utf8Error(Utf8Error),
25 InvalidHeader,
26 InvalidChecksum,
27 TooShort
28}
29
30pub type Result<T> = result::Result<T, Error>;
31
32impl Display for Error {
33 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
34 match &*self{
35 Self::JsonError(error) => error.fmt(f),
36 Self::Base64UrlDecodeError(error) => error.fmt(f),
37 Self::InvalidKeyLength(error) => error.fmt(f),
38 Self::Utf8Error(error) => error.fmt(f),
39 Self::InvalidHeader => f.write_str("unsupported header values"),
40 Self::InvalidChecksum => f.write_str("checksum does not match"),
41 Self::TooShort => f.write_str("token is too short")
42 }
43 }
44}
45
46impl std::error::Error for Error{}
47
48impl From<Utf8Error> for Error{
49 fn from(value: Utf8Error) -> Self {
50 Self::Utf8Error(value)
51 }
52}
53
54impl From<DecodeError> for Error{
55 fn from(value: DecodeError) -> Self {
56 Self::Base64UrlDecodeError(value)
57 }
58}
59
60impl From<serde_json::error::Error> for Error{
61 fn from(value: serde_json::Error) -> Self {
62 Self::JsonError(value)
63 }
64}
65
66impl From<InvalidLength> for Error{
67 fn from(value: InvalidLength) -> Self {
68 Self::InvalidKeyLength(value)
69 }
70}
71
72fn calc_checksum(secret: &[u8], value: &[u8]) -> Result<CtOutput<CoreWrapper<HmacCore<Sha256>>>>{
73 let mut hasher = hmac::Hmac::<Sha256>::new_from_slice(secret)?;
74 hasher.update(value);
75 Ok(hasher.finalize())
76}
77
78fn body_with_header<T>(claims: &T) -> Result<String> where T: Serialize {
79 let serialized = base64_url::encode(serde_json::to_string(&claims)?.as_bytes());
80 let mut output = String::with_capacity(serialized.len() + MIN_TOKEN_LENGTH);
81 output.push_str(HEADER);
82 output.push('.');
83 output.push_str(serialized.as_str());
84 Ok(output)
85}
86
87pub fn parse<T>(secret: &[u8], token: &str) -> Result<T> where T: for<'a> Deserialize<'a> {
115 let len = token.len();
116 if len < MIN_TOKEN_LENGTH{
117 return Err(Error::TooShort);
118 }
119 let bytes = token.as_bytes();
120 if &bytes[..HEADER_LENGTH] != HEADER.as_bytes() {
121 return Err(Error::InvalidHeader)
122 }
123 let sig_offset = len - SIGNATURE_LENGTH;
124 let checksum = calc_checksum(secret, &bytes[..sig_offset - 1])?;
125 let signature = base64_url::decode(from_utf8(&bytes[sig_offset..])?)?;
126 if checksum.into_bytes().as_slice() != signature.as_slice(){
127 return Err(Error::InvalidChecksum);
128 }
129 Ok(serde_json::from_slice::<T>(
130 base64_url::decode(&bytes[HEADER_LENGTH + 1 .. sig_offset - 1])?.as_slice()
131 )?)
132}
133
134pub fn create<T>(secret: &[u8], claims: &T) -> Result<String> where T: Serialize {
163 let mut main = body_with_header(claims)?;
164 let hash = base64_url::encode(
165 calc_checksum(secret, main.as_bytes())?.into_bytes().as_slice()
166 );
167 main.push('.');
168 main.push_str(hash.as_str());
169 Ok(main)
170}
171
172#[cfg(test)]
173mod tests {
174 use crate::{create, parse, Error};
175 use serde::{Deserialize, Serialize};
176
177 static SERIALIZED: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.6ekn8MWtOmVT6FMqbAlVQQmretopbWpef_lHV9dYMf4";
178 static BAD_CHECKSUM: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.azXPRJHeWcZ_B5aHtA98gsnowX5gifvMJX2hoH_4YPs";
179 static NAME: &str = "John Doe";
180 static SECRET: &str = "I'm a secret";
181
182 #[derive(Serialize)]
183 struct OutClaims{
184 sub: String,
185 name: String,
186 admin: bool
187 }
188
189 #[derive(Deserialize)]
190 struct InClaims{
191 name: String
192 }
193
194 #[test]
195 fn can_create() {
196 let test = OutClaims{
197 sub: "1234567890".to_string(),
198 name: NAME.to_string(),
199 admin: true
200 };
201 match create(SECRET.as_bytes(), &test){
202 Ok(token) => {
203 assert_eq!(token.as_str(), SERIALIZED)
204 },
205 Err(error) => panic!("{}", error)
206 }
207 }
208
209 #[test]
210 fn can_parse(){
211 match parse::<InClaims>(SECRET.as_bytes(), SERIALIZED){
212 Ok(claims) => assert_eq!(claims.name, NAME),
213 Err(error) => panic!("{}", error)
214 }
215 }
216
217 #[test]
218 fn recognizes_bad_checksum(){
219 match parse::<InClaims>(SECRET.as_bytes(), BAD_CHECKSUM){
220 Ok(_) => assert!(false, "Should not recognize checksum"),
221 Err(error) => match error {
222 Error::InvalidChecksum => return,
223 _ => assert!(false, "Should have produced Error::InvalidChecksum")
224 }
225 }
226 }
227}