use crate::error::{FluxError, Result};
use crate::keys::{PrivateKey, PublicKey};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use zeroize::ZeroizeOnDrop;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecretFormat {
Raw,
Base64,
Pem,
Hex,
FilePath,
}
#[derive(ZeroizeOnDrop)]
pub struct EnvSecret {
data: Vec<u8>,
#[zeroize(skip)]
format: SecretFormat,
#[zeroize(skip)]
original: String,
}
impl std::fmt::Debug for EnvSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnvSecret")
.field("format", &self.format)
.field("data_len", &self.data.len())
.field("original_len", &self.original.len())
.finish()
}
}
impl EnvSecret {
pub fn new(data: Vec<u8>, format: SecretFormat, original: String) -> Self {
Self {
data,
format,
original,
}
}
pub fn from_string(value: String) -> Result<Self> {
let format = Self::detect_format(&value);
Self::from_string_with_format(value, format)
}
pub fn from_string_with_format(value: String, format: SecretFormat) -> Result<Self> {
let data = match format {
SecretFormat::Raw => value.as_bytes().to_vec(),
SecretFormat::Base64 => BASE64
.decode(&value)
.map_err(|e| FluxError::invalid_input(format!("Invalid base64: {}", e)))?,
SecretFormat::Hex => {
let clean_value = value.trim();
if clean_value.len() % 2 != 0 {
return Err(FluxError::invalid_input("Hex string must have even length"));
}
let mut result = Vec::with_capacity(clean_value.len() / 2);
for chunk in clean_value.as_bytes().chunks(2) {
let hex_str = std::str::from_utf8(chunk)
.map_err(|_| FluxError::invalid_input("Invalid hex characters"))?;
let byte = u8::from_str_radix(hex_str, 16)
.map_err(|_| FluxError::invalid_input("Invalid hex characters"))?;
result.push(byte);
}
result
}
SecretFormat::Pem => value.as_bytes().to_vec(), SecretFormat::FilePath => {
std::fs::read(&value).map_err(|e| {
FluxError::invalid_input(format!("Cannot read secret file {}: {}", value, e))
})?
}
};
Ok(Self::new(data, format, value))
}
pub fn detect_format(value: &str) -> SecretFormat {
if value.starts_with("-----BEGIN") && value.contains("-----END") {
return SecretFormat::Pem;
}
if value.contains('/') || value.contains('\\') {
return SecretFormat::FilePath;
}
if value.len() > 10 && value.chars().all(|c| c.is_ascii_hexdigit()) {
return SecretFormat::Hex;
}
if value.len() > 10
&& (value.ends_with('=') || value.ends_with("=="))
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
{
return SecretFormat::Base64;
}
SecretFormat::Raw
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn format(&self) -> SecretFormat {
self.format
}
pub fn original(&self) -> &str {
&self.original
}
pub fn as_string(&self) -> Result<String> {
String::from_utf8(self.data.clone())
.map_err(|e| FluxError::invalid_input(format!("Secret contains invalid UTF-8: {}", e)))
}
pub fn as_public_key(&self) -> Result<PublicKey> {
match self.format {
SecretFormat::Pem => {
let pem_str = self.as_string()?;
crate::keys::parsing::parse_public_key_from_str(&pem_str)
}
_ => {
if let Ok(pem_str) = self.as_string() {
if let Ok(key) = crate::keys::parsing::parse_public_key_from_str(&pem_str) {
return Ok(key);
}
}
Err(FluxError::invalid_input(
"Cannot parse secret as public key",
))
}
}
}
pub fn as_private_key(&self) -> Result<PrivateKey> {
match self.format {
SecretFormat::Pem => {
let pem_str = self.as_string()?;
crate::keys::parsing::parse_private_key_from_str(&pem_str)
}
_ => {
if let Ok(pem_str) = self.as_string() {
if let Ok(key) = crate::keys::parsing::parse_private_key_from_str(&pem_str) {
return Ok(key);
}
}
Err(FluxError::invalid_input(
"Cannot parse secret as private key",
))
}
}
}
pub fn to_base64(&self) -> String {
BASE64.encode(&self.data)
}
pub fn to_hex(&self) -> String {
self.data.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn len(&self) -> usize {
self.data.len()
}
}
pub fn load_env_secret(var_name: &str) -> Result<EnvSecret> {
let value = std::env::var(var_name)
.map_err(|_| FluxError::env(format!("Environment variable not found: {}", var_name)))?;
EnvSecret::from_string(value)
}
pub fn load_env_secret_with_format(var_name: &str, format: SecretFormat) -> Result<EnvSecret> {
let value = std::env::var(var_name)
.map_err(|_| FluxError::env(format!("Environment variable not found: {}", var_name)))?;
EnvSecret::from_string_with_format(value, format)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_detection() {
let pem_value = "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END RSA PUBLIC KEY-----";
assert_eq!(EnvSecret::detect_format(pem_value), SecretFormat::Pem);
assert_eq!(
EnvSecret::detect_format("/path/to/file"),
SecretFormat::FilePath
);
assert_eq!(
EnvSecret::detect_format("C:\\path\\to\\file"),
SecretFormat::FilePath
);
let hex_value = "abcdef1234567890abcdef1234567890";
assert_eq!(EnvSecret::detect_format(hex_value), SecretFormat::Hex);
let b64_value = "SGVsbG8gV29ybGQ=";
assert_eq!(EnvSecret::detect_format(b64_value), SecretFormat::Base64);
assert_eq!(EnvSecret::detect_format("hello world"), SecretFormat::Raw);
}
#[test]
fn test_raw_secret() {
let secret =
EnvSecret::from_string_with_format("hello world".to_string(), SecretFormat::Raw)
.unwrap();
assert_eq!(secret.as_bytes(), b"hello world");
assert_eq!(secret.format(), SecretFormat::Raw);
assert_eq!(secret.as_string().unwrap(), "hello world");
}
#[test]
fn test_base64_secret() {
let secret = EnvSecret::from_string_with_format(
"SGVsbG8gV29ybGQ=".to_string(),
SecretFormat::Base64,
)
.unwrap();
assert_eq!(secret.as_bytes(), b"Hello World");
assert_eq!(secret.format(), SecretFormat::Base64);
}
#[test]
fn test_hex_secret() {
let secret = EnvSecret::from_string_with_format(
"48656c6c6f20576f726c64".to_string(),
SecretFormat::Hex,
)
.unwrap();
assert_eq!(secret.as_bytes(), b"Hello World");
assert_eq!(secret.format(), SecretFormat::Hex);
}
#[test]
fn test_secret_conversions() {
let secret = EnvSecret::from_string("Hello World".to_string()).unwrap();
assert_eq!(secret.to_base64(), "SGVsbG8gV29ybGQ=");
assert_eq!(secret.to_hex(), "48656c6c6f20576f726c64");
assert!(!secret.is_empty());
assert_eq!(secret.len(), 11);
}
}