1use std::str::FromStr;
2
3use crate::SecretToken;
4use vitaminc::protected::OpaqueDebug;
5
6const ACCESS_KEY_PREFIX: &str = "CSAK";
8
9#[derive(OpaqueDebug)]
36pub struct AccessKey(SecretToken);
37
38impl AccessKey {
39 pub(crate) fn into_secret_token(self) -> SecretToken {
41 self.0
42 }
43}
44
45impl FromStr for AccessKey {
49 type Err = InvalidAccessKey;
50
51 fn from_str(s: &str) -> Result<Self, Self::Err> {
52 let rest = s
53 .strip_prefix(ACCESS_KEY_PREFIX)
54 .ok_or(InvalidAccessKey::MissingPrefix)?;
55
56 let (id, secret) = rest.split_once('.').ok_or(InvalidAccessKey::MissingDot)?;
57
58 if id.is_empty() {
59 return Err(InvalidAccessKey::EmptyKeyId);
60 }
61 if secret.is_empty() {
62 return Err(InvalidAccessKey::EmptySecret);
63 }
64
65 Ok(Self(SecretToken::new(s)))
66 }
67}
68
69#[derive(Debug, thiserror::Error)]
71pub enum InvalidAccessKey {
72 #[error("access key must start with \"{ACCESS_KEY_PREFIX}\"")]
74 MissingPrefix,
75 #[error("access key must contain a \".\" separator")]
77 MissingDot,
78 #[error("access key ID must not be empty")]
80 EmptyKeyId,
81 #[error("access key secret must not be empty")]
83 EmptySecret,
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn valid_key() {
92 let key: AccessKey =
93 "CSAKT4ZMT2AUPXI7TCD2.ZAQRW2BWXP3Z6SHR4YG2TP3N35LLU46ZAWLR3BL5WUR4IIGA"
94 .parse()
95 .unwrap();
96 assert_eq!(
97 key.0.as_str(),
98 "CSAKT4ZMT2AUPXI7TCD2.ZAQRW2BWXP3Z6SHR4YG2TP3N35LLU46ZAWLR3BL5WUR4IIGA"
99 );
100 }
101
102 #[test]
103 fn missing_prefix() {
104 let err = "key_id.key_secret".parse::<AccessKey>().unwrap_err();
105 assert!(matches!(err, InvalidAccessKey::MissingPrefix));
106 }
107
108 #[test]
109 fn missing_dot() {
110 let err = "CSAKnodot".parse::<AccessKey>().unwrap_err();
111 assert!(matches!(err, InvalidAccessKey::MissingDot));
112 }
113
114 #[test]
115 fn empty_key_id() {
116 let err = "CSAK.secret".parse::<AccessKey>().unwrap_err();
117 assert!(matches!(err, InvalidAccessKey::EmptyKeyId));
118 }
119
120 #[test]
121 fn empty_secret() {
122 let err = "CSAKid.".parse::<AccessKey>().unwrap_err();
123 assert!(matches!(err, InvalidAccessKey::EmptySecret));
124 }
125
126 #[test]
127 fn empty_string() {
128 let err = "".parse::<AccessKey>().unwrap_err();
129 assert!(matches!(err, InvalidAccessKey::MissingPrefix));
130 }
131
132 #[test]
133 fn into_secret_token() {
134 let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap();
135 let secret = key.into_secret_token();
136 assert_eq!(secret.as_str(), "CSAKmyKeyId.myKeySecret");
137 }
138
139 #[test]
140 fn debug_does_not_leak() {
141 let key: AccessKey = "CSAKid.secret".parse().unwrap();
142 let debug = format!("{key:?}");
143 assert!(!debug.contains("secret"));
144 assert!(
145 debug.contains("AccessKey") && debug.contains("***"),
146 "debug should hide secret: {debug}"
147 );
148 }
149}