1use core::fmt;
2use core::str::FromStr;
3use hmac::Mac;
4
5#[cfg(feature = "alloc")]
6use alloc::string::String;
7
8type HmacSha1 = hmac::Hmac<sha1::Sha1>;
9type HmacSha256 = hmac::Hmac<sha2::Sha256>;
10type HmacSha512 = hmac::Hmac<sha2::Sha512>;
11
12#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(
16 all(feature = "serde", feature = "alloc"),
17 serde(try_from = "String", into = "String")
18)]
19#[non_exhaustive]
20pub enum Algorithm {
21 #[default]
24 SHA1,
25 SHA256,
28 SHA512,
31 #[cfg(feature = "steam")]
32 #[cfg_attr(docsrs, doc(cfg(feature = "steam")))]
33 #[cfg_attr(feature = "serde", serde(rename = "STEAM"))]
34 Steam,
36}
37
38impl Algorithm {
39 pub const fn as_str(&self) -> &str {
41 match self {
42 Algorithm::SHA1 => "SHA1",
43 Algorithm::SHA256 => "SHA256",
44 Algorithm::SHA512 => "SHA512",
45 #[cfg(feature = "steam")]
46 Algorithm::Steam => "STEAM",
47 }
48 }
49}
50
51impl fmt::Display for Algorithm {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(self.as_str())
54 }
55}
56
57#[cfg(feature = "alloc")]
58#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
59impl From<Algorithm> for String {
60 fn from(value: Algorithm) -> Self {
61 value.as_str().into()
62 }
63}
64
65#[cfg(feature = "alloc")]
66#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
67impl TryFrom<String> for Algorithm {
68 type Error = UnsupportedAlgorithm;
69
70 fn try_from(value: String) -> Result<Self, Self::Error> {
71 Self::from_str(&value)
72 }
73}
74
75impl FromStr for Algorithm {
76 type Err = UnsupportedAlgorithm;
77
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 if s.eq_ignore_ascii_case("SHA1") {
80 Ok(Self::SHA1)
81 } else if s.eq_ignore_ascii_case("SHA256") {
82 Ok(Self::SHA256)
83 } else if s.eq_ignore_ascii_case("SHA512") {
84 Ok(Self::SHA512)
85 } else {
86 #[cfg(feature = "steam")]
87 if s.eq_ignore_ascii_case("STEAM") {
88 return Ok(Self::Steam);
89 }
90
91 Err(UnsupportedAlgorithm {
92 #[cfg(feature = "alloc")]
93 algorithm: s.into(),
94 })
95 }
96 }
97}
98
99impl Algorithm {
100 fn hash<D>(key: &[u8], counter: u64) -> hmac::digest::Output<D>
101 where
102 D: Mac + hmac::digest::KeyInit,
103 {
104 let mut digest = D::new_from_slice(key).unwrap();
105 let data = counter.to_be_bytes();
106 digest.update(&data);
107 digest.finalize().into_bytes()
108 }
109
110 pub(crate) fn sign(&self, key: &[u8], counter: u64) -> impl AsRef<[u8]> {
111 match self {
112 Algorithm::SHA1 => Signature::SHA1(Algorithm::hash::<HmacSha1>(key, counter)),
113 Algorithm::SHA256 => Signature::SHA256(Algorithm::hash::<HmacSha256>(key, counter)),
114 Algorithm::SHA512 => Signature::SHA512(Algorithm::hash::<HmacSha512>(key, counter)),
115 #[cfg(feature = "steam")]
116 Algorithm::Steam => Signature::SHA1(Algorithm::hash::<HmacSha1>(key, counter)),
117 }
118 }
119}
120
121enum Signature {
122 SHA1(hmac::digest::Output<HmacSha1>),
123 SHA256(hmac::digest::Output<HmacSha256>),
124 SHA512(hmac::digest::Output<HmacSha512>),
125}
126
127impl AsRef<[u8]> for Signature {
128 fn as_ref(&self) -> &[u8] {
129 match self {
130 Signature::SHA1(inner) => inner.as_ref(),
131 Signature::SHA256(inner) => inner.as_ref(),
132 Signature::SHA512(inner) => inner.as_ref(),
133 }
134 }
135}
136
137#[derive(PartialEq, Eq)]
138#[non_exhaustive]
139pub struct UnsupportedAlgorithm {
140 #[cfg(feature = "alloc")]
141 algorithm: String,
142}
143
144impl core::fmt::Debug for UnsupportedAlgorithm {
145 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
146 <Self as core::fmt::Display>::fmt(self, f)
147 }
148}
149
150impl core::fmt::Display for UnsupportedAlgorithm {
151 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
152 f.write_str("Unsupported Algorithm")?;
153
154 #[cfg(feature = "alloc")]
155 write!(f, ": {}", self.algorithm)?;
156
157 Ok(())
158 }
159}
160
161impl core::error::Error for UnsupportedAlgorithm {}
162
163#[cfg(test)]
164mod tests {
165 use core::str::FromStr;
166
167 use super::{Algorithm, UnsupportedAlgorithm};
168
169 const ALL_ALGORITHMS: &[Algorithm] = &[
171 Algorithm::SHA1,
172 Algorithm::SHA256,
173 Algorithm::SHA512,
174 #[cfg(feature = "steam")]
175 Algorithm::Steam,
176 ];
177
178 #[test]
179 fn from_str_unsupported() {
180 let algorithm = Algorithm::from_str("not a real algorithm");
181 assert!(matches!(algorithm, Err(UnsupportedAlgorithm { .. })));
182 let error = algorithm.unwrap_err();
183 assert!(format!("{:?}", error).starts_with("Unsupported Algorithm"));
184 }
185
186 #[cfg(feature = "alloc")]
187 #[test]
188 fn to_string_round_trip() {
189 for &alg in ALL_ALGORITHMS {
190 let to_string = String::from(alg);
191 let from_string = Algorithm::try_from(to_string);
192 assert_eq!(from_string, Ok(alg));
193 }
194 }
195
196 #[cfg(feature = "steam")]
199 #[test]
200 fn steam_is_spelled_steam() {
201 assert_eq!(Algorithm::Steam.as_str(), "STEAM");
202 assert_eq!(Algorithm::from_str("STEAM"), Ok(Algorithm::Steam));
203 assert_eq!(Algorithm::from_str("steam"), Ok(Algorithm::Steam));
204 }
205
206 #[cfg(feature = "serde")]
207 #[test]
208 fn serde_repr_is_as_str() {
209 for alg in ALL_ALGORITHMS {
210 let name = alg.as_str();
211
212 #[cfg(feature = "alloc")]
213 serde_test::assert_tokens(alg, &[serde_test::Token::Str(name)]);
214
215 #[cfg(not(feature = "alloc"))]
216 serde_test::assert_tokens(
217 alg,
218 &[serde_test::Token::UnitVariant {
219 name: "Algorithm",
220 variant: name,
221 }],
222 );
223 }
224 }
225
226 #[cfg(all(feature = "serde", feature = "steam"))]
227 #[test]
228 fn serde_deserialize_case_sensitivity() {
229 #[cfg(feature = "alloc")]
230 serde_test::assert_de_tokens(&Algorithm::Steam, &[serde_test::Token::Str("steam")]);
231
232 #[cfg(not(feature = "alloc"))]
233 serde_test::assert_de_tokens_error::<Algorithm>(
234 &[serde_test::Token::UnitVariant {
235 name: "Algorithm",
236 variant: "steam",
237 }],
238 "unknown variant `steam`, expected one of `SHA1`, `SHA256`, `SHA512`, `STEAM`",
239 );
240 }
241
242 #[test]
243 fn signing_test() {
244 for &alg in ALL_ALGORITHMS {
245 let key = "TestSecretSuperSecret".as_bytes();
246 let data = 123456;
247
248 let signature = alg.sign(key, data);
249
250 assert!(!signature.as_ref().is_empty());
251 }
252 }
253}