use std::{error::Error, fmt, hint::black_box, str};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
const MAX_REFERENCE_NAME_BYTES: usize = 128;
pub struct Secret {
bytes: SecretBytes,
}
impl Secret {
pub fn new(value: impl Into<String>) -> Self {
Self {
bytes: SecretBytes::new(value.into().into_bytes()),
}
}
pub fn expose_secret(&self) -> &str {
str::from_utf8(self.bytes.expose_secret_bytes())
.expect("Secret is constructed from valid UTF-8")
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn clear(&mut self) {
self.bytes.clear();
}
}
impl From<String> for Secret {
fn from(value: String) -> Self {
Self::new(value)
}
}
pub struct SecretBytes {
bytes: Vec<u8>,
}
impl SecretBytes {
pub fn new(value: impl Into<Vec<u8>>) -> Self {
Self {
bytes: value.into(),
}
}
pub fn expose_secret_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn clear(&mut self) {
wipe(&mut self.bytes);
}
}
impl Drop for SecretBytes {
fn drop(&mut self) {
self.clear();
}
}
fn wipe(bytes: &mut [u8]) {
bytes.fill(0);
let _ = black_box(bytes);
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "snake_case")]
pub enum SecretReferenceKind {
Environment,
CredentialStore,
GithubActions,
Worker,
}
#[derive(Clone, Debug, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SecretReference {
kind: SecretReferenceKind,
name: String,
}
impl SecretReference {
pub fn new(
kind: SecretReferenceKind,
name: impl Into<String>,
) -> Result<Self, SecretReferenceError> {
let name = name.into();
validate_reference_name(kind, &name)?;
Ok(Self { kind, name })
}
pub fn kind(&self) -> SecretReferenceKind {
self.kind
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct UncheckedSecretReference {
kind: SecretReferenceKind,
name: String,
}
impl<'de> Deserialize<'de> for SecretReference {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let unchecked = UncheckedSecretReference::deserialize(deserializer)?;
Self::new(unchecked.kind, unchecked.name).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecretReferenceError {
Empty,
TooLong {
maximum: usize,
},
InvalidStart,
InvalidCharacter {
index: usize,
},
DotSequence,
}
impl fmt::Display for SecretReferenceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("secret reference is empty"),
Self::TooLong { maximum } => {
write!(formatter, "secret reference exceeds {maximum} bytes")
}
Self::InvalidStart => formatter.write_str("secret reference has an invalid first byte"),
Self::InvalidCharacter { index } => {
write!(
formatter,
"secret reference contains an unsafe byte at offset {index}"
)
}
Self::DotSequence => {
formatter.write_str("secret reference contains a path-like dot sequence")
}
}
}
}
impl Error for SecretReferenceError {}
fn validate_reference_name(
kind: SecretReferenceKind,
name: &str,
) -> Result<(), SecretReferenceError> {
if name.is_empty() {
return Err(SecretReferenceError::Empty);
}
if name.len() > MAX_REFERENCE_NAME_BYTES {
return Err(SecretReferenceError::TooLong {
maximum: MAX_REFERENCE_NAME_BYTES,
});
}
if name.contains("..") {
return Err(SecretReferenceError::DotSequence);
}
let first = name.as_bytes()[0];
let valid_start = first.is_ascii_alphabetic() || first == b'_';
if !valid_start {
return Err(SecretReferenceError::InvalidStart);
}
for (index, byte) in name.bytes().enumerate() {
let valid = match kind {
SecretReferenceKind::Environment | SecretReferenceKind::GithubActions => {
byte.is_ascii_alphanumeric() || byte == b'_'
}
SecretReferenceKind::CredentialStore | SecretReferenceKind::Worker => {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')
}
};
if !valid {
return Err(SecretReferenceError::InvalidCharacter { index });
}
}
Ok(())
}