prefixed_api_key/
prefixed_api_key.rs1use digest::{Digest, FixedOutputReset};
2use std::error::Error;
3use std::fmt;
4use std::fmt::Debug;
5
6#[derive(Debug, PartialEq, Eq)]
7pub enum PrefixedApiKeyError {
8 WrongNumberOfParts(usize),
9}
10
11impl Error for PrefixedApiKeyError {}
12
13impl fmt::Display for PrefixedApiKeyError {
14 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15 write!(f, "{:?}", self)
17 }
18}
19
20pub struct PrefixedApiKey {
25 prefix: String,
26 short_token: String,
27 long_token: String,
28}
29
30impl PrefixedApiKey {
31 pub fn new(prefix: String, short_token: String, long_token: String) -> PrefixedApiKey {
35 PrefixedApiKey {
36 prefix,
37 short_token,
38 long_token,
39 }
40 }
41
42 pub fn prefix(&self) -> &str {
44 &self.prefix
45 }
46
47 pub fn short_token(&self) -> &str {
49 &self.short_token
50 }
51
52 pub fn long_token(&self) -> &str {
54 &self.long_token
55 }
56
57 pub fn long_token_hashed<D: Digest + FixedOutputReset>(&self, digest: &mut D) -> String {
61 Digest::update(digest, self.long_token.clone());
62 hex::encode(digest.finalize_reset())
63 }
64
65 pub fn from_string(pak_string: &str) -> Result<PrefixedApiKey, PrefixedApiKeyError> {
69 let parts: Vec<&str> = pak_string.split('_').collect();
70
71 if parts.len() != 3 {
72 return Err(PrefixedApiKeyError::WrongNumberOfParts(parts.len()));
74 }
75
76 Ok(PrefixedApiKey::new(
77 parts[0].to_owned(),
78 parts[1].to_owned(),
79 parts[2].to_owned(),
80 ))
81 }
82}
83
84impl Debug for PrefixedApiKey {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.debug_struct("PrefixedApiKey")
89 .field("prefix", &self.prefix)
90 .field("short_token", &self.short_token)
91 .field("long_token", &"***")
92 .finish()
93 }
94}
95
96#[allow(clippy::to_string_trait_impl)]
100impl ToString for PrefixedApiKey {
101 fn to_string(&self) -> String {
102 format!("{}_{}_{}", self.prefix, self.short_token, self.long_token)
103 }
104}
105
106impl TryInto<PrefixedApiKey> for &str {
107 type Error = PrefixedApiKeyError;
108
109 fn try_into(self) -> Result<PrefixedApiKey, Self::Error> {
110 PrefixedApiKey::from_string(self)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use sha2::{Digest, Sha256};
117
118 use crate::prefixed_api_key::{PrefixedApiKey, PrefixedApiKeyError};
119
120 #[test]
121 fn to_string_is_expected() {
122 let prefix = "mycompany".to_owned();
123 let short = "abcdefg".to_owned();
124 let long = "bacdegadsa".to_owned();
125 let expected_token = format!("{}_{}_{}", prefix, short, long);
126 let pak = PrefixedApiKey::new(prefix, short, long);
127 assert_eq!(pak.to_string(), expected_token)
128 }
129
130 #[test]
131 fn self_from_string_works() {
132 let pak_string = "mycompany_abcdefg_bacdegadsa";
133 let pak_result = PrefixedApiKey::from_string(pak_string);
134 assert_eq!(pak_result.is_ok(), true);
135 assert_eq!(pak_result.unwrap().to_string(), pak_string);
136 }
137
138 #[test]
139 fn str_into_pak() {
140 let pak_string = "mycompany_abcdefg_bacdegadsa";
141 let pak_result: Result<PrefixedApiKey, _> = pak_string.try_into();
142 assert_eq!(pak_result.is_ok(), true);
143 assert_eq!(pak_result.unwrap().to_string(), pak_string);
144 }
145
146 #[test]
147 fn string_into_pak_via_as_ref() {
148 let pak_string = "mycompany_abcdefg_bacdegadsa".to_owned();
149 let pak_result: Result<PrefixedApiKey, _> = pak_string.as_str().try_into();
150 assert_eq!(pak_result.is_ok(), true);
151 assert_eq!(pak_result.unwrap().to_string(), pak_string);
152 }
153
154 #[test]
155 fn str_into_pak_with_extra_parts() {
156 let pak_string = "mycompany_abcd_efg_bacdegadsa";
157 let pak_result: Result<PrefixedApiKey, _> = pak_string.try_into();
158 assert_eq!(pak_result.is_err(), true);
159 assert_eq!(
160 pak_result.unwrap_err(),
161 PrefixedApiKeyError::WrongNumberOfParts(4)
162 );
163 }
164
165 #[test]
166 fn check_long_token() {
167 let pak_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
168 let hash = "0f01ab6e0833f280b73b2b618c16102d91c0b7c585d42a080d6e6603239a8bee";
169
170 let pak: PrefixedApiKey = pak_string.try_into().unwrap();
171 let mut digest = Sha256::new();
172 assert_eq!(pak.long_token_hashed(&mut digest), hash);
173 }
174
175 #[test]
176 fn check_debug_display_hides_secret_token() {
177 let pak_string = "mycompany_CEUsS4psCmc_BddpcwWyCT3EkDjHSSTRaSK1dxtuQgbjb";
178
179 let pak: PrefixedApiKey = pak_string.try_into().unwrap();
180 let debug_string = format!("{:?}", pak);
181 assert_eq!(debug_string, "PrefixedApiKey { prefix: \"mycompany\", short_token: \"CEUsS4psCmc\", long_token: \"***\" }");
182 }
183}