rustferry_remote/
secret.rs1use std::{error::Error, fmt, hint::black_box, str};
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Deserializer, Serialize};
5
6const MAX_REFERENCE_NAME_BYTES: usize = 128;
7
8pub struct Secret {
33 bytes: SecretBytes,
34}
35
36impl Secret {
37 pub fn new(value: impl Into<String>) -> Self {
39 Self {
40 bytes: SecretBytes::new(value.into().into_bytes()),
41 }
42 }
43
44 pub fn expose_secret(&self) -> &str {
51 str::from_utf8(self.bytes.expose_secret_bytes())
52 .expect("Secret is constructed from valid UTF-8")
53 }
54
55 pub fn len(&self) -> usize {
57 self.bytes.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.bytes.is_empty()
63 }
64
65 pub fn clear(&mut self) {
67 self.bytes.clear();
68 }
69}
70
71impl From<String> for Secret {
72 fn from(value: String) -> Self {
73 Self::new(value)
74 }
75}
76
77pub struct SecretBytes {
90 bytes: Vec<u8>,
91}
92
93impl SecretBytes {
94 pub fn new(value: impl Into<Vec<u8>>) -> Self {
96 Self {
97 bytes: value.into(),
98 }
99 }
100
101 pub fn expose_secret_bytes(&self) -> &[u8] {
103 &self.bytes
104 }
105
106 pub fn len(&self) -> usize {
108 self.bytes.len()
109 }
110
111 pub fn is_empty(&self) -> bool {
113 self.bytes.is_empty()
114 }
115
116 pub fn clear(&mut self) {
118 wipe(&mut self.bytes);
119 }
120}
121
122impl Drop for SecretBytes {
123 fn drop(&mut self) {
124 self.clear();
125 }
126}
127
128fn wipe(bytes: &mut [u8]) {
129 bytes.fill(0);
130 let _ = black_box(bytes);
133}
134
135#[derive(
137 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
138)]
139#[serde(rename_all = "snake_case")]
140pub enum SecretReferenceKind {
141 Environment,
143 CredentialStore,
145 GithubActions,
147 Worker,
149}
150
151#[derive(Clone, Debug, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
157pub struct SecretReference {
158 kind: SecretReferenceKind,
159 name: String,
160}
161
162impl SecretReference {
163 pub fn new(
170 kind: SecretReferenceKind,
171 name: impl Into<String>,
172 ) -> Result<Self, SecretReferenceError> {
173 let name = name.into();
174 validate_reference_name(kind, &name)?;
175 Ok(Self { kind, name })
176 }
177
178 pub fn kind(&self) -> SecretReferenceKind {
180 self.kind
181 }
182
183 pub fn name(&self) -> &str {
185 &self.name
186 }
187}
188
189#[derive(Deserialize)]
190#[serde(deny_unknown_fields)]
191struct UncheckedSecretReference {
192 kind: SecretReferenceKind,
193 name: String,
194}
195
196impl<'de> Deserialize<'de> for SecretReference {
197 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198 where
199 D: Deserializer<'de>,
200 {
201 let unchecked = UncheckedSecretReference::deserialize(deserializer)?;
202 Self::new(unchecked.kind, unchecked.name).map_err(serde::de::Error::custom)
203 }
204}
205
206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
208pub enum SecretReferenceError {
209 Empty,
211 TooLong {
213 maximum: usize,
215 },
216 InvalidStart,
218 InvalidCharacter {
220 index: usize,
222 },
223 DotSequence,
225}
226
227impl fmt::Display for SecretReferenceError {
228 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match self {
230 Self::Empty => formatter.write_str("secret reference is empty"),
231 Self::TooLong { maximum } => {
232 write!(formatter, "secret reference exceeds {maximum} bytes")
233 }
234 Self::InvalidStart => formatter.write_str("secret reference has an invalid first byte"),
235 Self::InvalidCharacter { index } => {
236 write!(
237 formatter,
238 "secret reference contains an unsafe byte at offset {index}"
239 )
240 }
241 Self::DotSequence => {
242 formatter.write_str("secret reference contains a path-like dot sequence")
243 }
244 }
245 }
246}
247
248impl Error for SecretReferenceError {}
249
250fn validate_reference_name(
251 kind: SecretReferenceKind,
252 name: &str,
253) -> Result<(), SecretReferenceError> {
254 if name.is_empty() {
255 return Err(SecretReferenceError::Empty);
256 }
257 if name.len() > MAX_REFERENCE_NAME_BYTES {
258 return Err(SecretReferenceError::TooLong {
259 maximum: MAX_REFERENCE_NAME_BYTES,
260 });
261 }
262 if name.contains("..") {
263 return Err(SecretReferenceError::DotSequence);
264 }
265
266 let first = name.as_bytes()[0];
267 let valid_start = first.is_ascii_alphabetic() || first == b'_';
268 if !valid_start {
269 return Err(SecretReferenceError::InvalidStart);
270 }
271
272 for (index, byte) in name.bytes().enumerate() {
273 let valid = match kind {
274 SecretReferenceKind::Environment | SecretReferenceKind::GithubActions => {
275 byte.is_ascii_alphanumeric() || byte == b'_'
276 }
277 SecretReferenceKind::CredentialStore | SecretReferenceKind::Worker => {
278 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')
279 }
280 };
281 if !valid {
282 return Err(SecretReferenceError::InvalidCharacter { index });
283 }
284 }
285
286 Ok(())
287}