offline_protocol/file_store/
key.rs1use base64::Engine as _;
4use zeroize::Zeroizing;
5
6pub const STORE_KEY_BYTES: usize = 32;
8
9#[non_exhaustive]
11#[derive(Debug, thiserror::Error)]
12pub enum StoreKeyError {
13 #[error("no store key is configured: {0}")]
15 Missing(String),
16 #[error("the configured store key is not usable: {0}")]
18 Malformed(String),
19 #[error("the store key source failed: {0}")]
21 Unavailable(String),
22}
23
24pub trait StoreKeyProvider {
36 fn store_key(&self) -> Result<Zeroizing<[u8; STORE_KEY_BYTES]>, StoreKeyError>;
38}
39
40pub struct StaticStoreKey(Zeroizing<[u8; STORE_KEY_BYTES]>);
42
43impl StaticStoreKey {
44 pub fn new(key: [u8; STORE_KEY_BYTES]) -> Self {
46 Self(Zeroizing::new(key))
47 }
48
49 pub fn from_slice(key: &[u8]) -> Result<Self, StoreKeyError> {
52 let key: [u8; STORE_KEY_BYTES] = key.try_into().map_err(|_| {
53 StoreKeyError::Malformed(format!(
54 "a store key is {STORE_KEY_BYTES} bytes, got {}",
55 key.len()
56 ))
57 })?;
58 let key = Zeroizing::new(key);
59 refuse_all_zero(&key)?;
60 Ok(Self(key))
61 }
62}
63
64impl std::fmt::Debug for StaticStoreKey {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.write_str("StaticStoreKey(<redacted>)")
67 }
68}
69
70impl StoreKeyProvider for StaticStoreKey {
71 fn store_key(&self) -> Result<Zeroizing<[u8; STORE_KEY_BYTES]>, StoreKeyError> {
72 refuse_all_zero(&self.0)?;
73 Ok(self.0.clone())
74 }
75}
76
77#[derive(Debug, Clone)]
85pub struct EnvStoreKey {
86 variable: String,
87}
88
89impl EnvStoreKey {
90 pub const DEFAULT_VARIABLE: &'static str = "OFFLINE_PROTOCOL_STORE_KEY";
92
93 pub fn new(variable: impl Into<String>) -> Self {
95 Self {
96 variable: variable.into(),
97 }
98 }
99}
100
101impl Default for EnvStoreKey {
102 fn default() -> Self {
103 Self::new(Self::DEFAULT_VARIABLE)
104 }
105}
106
107impl StoreKeyProvider for EnvStoreKey {
108 fn store_key(&self) -> Result<Zeroizing<[u8; STORE_KEY_BYTES]>, StoreKeyError> {
109 key_from_variable(&self.variable, std::env::var(&self.variable))
110 }
111}
112
113fn key_from_variable(
117 variable: &str,
118 value: Result<String, std::env::VarError>,
119) -> Result<Zeroizing<[u8; STORE_KEY_BYTES]>, StoreKeyError> {
120 let raw = match value {
121 Ok(raw) => Zeroizing::new(raw),
122 Err(std::env::VarError::NotPresent) => {
123 return Err(StoreKeyError::Missing(format!(
124 "the environment variable {variable} is not set"
125 )))
126 }
127 Err(std::env::VarError::NotUnicode(_)) => {
128 return Err(StoreKeyError::Malformed(format!(
129 "the environment variable {variable} is not valid text"
130 )))
131 }
132 };
133 let decoded = decode_key_text(raw.trim()).ok_or_else(|| {
134 StoreKeyError::Malformed(format!(
135 "{variable} must be {STORE_KEY_BYTES} bytes as 64 hex digits or as base64"
136 ))
137 })?;
138 StaticStoreKey::from_slice(&decoded)?.store_key()
139}
140
141fn decode_key_text(text: &str) -> Option<Zeroizing<Vec<u8>>> {
142 if text.len() == 2 * STORE_KEY_BYTES && text.bytes().all(|b| b.is_ascii_hexdigit()) {
143 return hex::decode(text).ok().map(Zeroizing::new);
144 }
145 let engines = [
146 base64::engine::general_purpose::STANDARD,
147 base64::engine::general_purpose::STANDARD_NO_PAD,
148 ];
149 engines
150 .iter()
151 .find_map(|engine| engine.decode(text).ok())
152 .filter(|bytes| bytes.len() == STORE_KEY_BYTES)
153 .map(Zeroizing::new)
154}
155
156fn refuse_all_zero(key: &[u8; STORE_KEY_BYTES]) -> Result<(), StoreKeyError> {
157 if key.iter().all(|&b| b == 0) {
158 return Err(StoreKeyError::Malformed(
159 "an all-zero store key is an unset buffer, not a key".to_string(),
160 ));
161 }
162 Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn from_env(
173 variable: &str,
174 value: Option<&str>,
175 ) -> Result<Zeroizing<[u8; STORE_KEY_BYTES]>, StoreKeyError> {
176 key_from_variable(
177 variable,
178 value
179 .map(str::to_string)
180 .ok_or(std::env::VarError::NotPresent),
181 )
182 }
183
184 #[test]
185 fn hex_and_base64_both_decode_to_the_same_key() {
186 let key: Vec<u8> = (1u8..=32).collect();
187 let hex_text = hex::encode(&key);
188 let b64 = base64::engine::general_purpose::STANDARD.encode(&key);
189 let b64_no_pad = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&key);
190
191 for (variable, text) in [
192 ("OP_TEST_STORE_KEY_HEX", hex_text.clone()),
193 ("OP_TEST_STORE_KEY_HEX_UPPER", hex_text.to_uppercase()),
194 ("OP_TEST_STORE_KEY_B64", b64),
195 ("OP_TEST_STORE_KEY_B64_NOPAD", b64_no_pad),
196 ("OP_TEST_STORE_KEY_PADDED", format!(" {hex_text}\n")),
197 ] {
198 let got =
199 from_env(variable, Some(&text)).unwrap_or_else(|err| panic!("{variable}: {err}"));
200 assert_eq!(got.as_slice(), key.as_slice(), "{variable}");
201 }
202 }
203
204 #[test]
205 fn an_absent_short_long_or_zero_key_is_refused() {
206 assert!(matches!(
207 from_env("OP_TEST_STORE_KEY_ABSENT", None),
208 Err(StoreKeyError::Missing(_))
209 ));
210 for (variable, text) in [
211 ("OP_TEST_STORE_KEY_SHORT", hex::encode([7u8; 31])),
212 ("OP_TEST_STORE_KEY_LONG", hex::encode([7u8; 33])),
213 ("OP_TEST_STORE_KEY_ZERO", hex::encode([0u8; 32])),
214 ("OP_TEST_STORE_KEY_EMPTY", String::new()),
215 (
216 "OP_TEST_STORE_KEY_WORDS",
217 "correct horse battery staple".to_string(),
218 ),
219 ] {
220 assert!(
221 matches!(
222 from_env(variable, Some(&text)),
223 Err(StoreKeyError::Malformed(_))
224 ),
225 "{variable} accepted"
226 );
227 }
228 }
229
230 #[test]
232 fn an_unset_variable_is_missing() {
233 assert!(matches!(
234 EnvStoreKey::new("OFFLINE_PROTOCOL_TEST_STORE_KEY_NEVER_SET").store_key(),
235 Err(StoreKeyError::Missing(_))
236 ));
237 }
238
239 #[test]
240 fn a_static_key_is_length_checked_and_redacted() {
241 assert!(StaticStoreKey::from_slice(&[1u8; 31]).is_err());
242 assert!(StaticStoreKey::from_slice(&[0u8; 32]).is_err());
243 assert!(StaticStoreKey::new([0u8; 32]).store_key().is_err());
244 let key = StaticStoreKey::from_slice(&[0xabu8; 32]).expect("key");
245 assert_eq!(format!("{key:?}"), "StaticStoreKey(<redacted>)");
246 assert_eq!(*key.store_key().expect("key"), [0xabu8; 32]);
247 }
248}