use std::collections::BTreeMap;
use std::fmt;
use std::future::Future;
use std::io::{Read, Write};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, Zeroizing};
use super::{
MemorySecretProvider, RotationHandle, SecretBytes, SecretError, SecretId, SecretMeta,
SecretProvider,
};
const MAX_FRAME_BYTES: usize = 1_048_576;
const MAX_SECRETS: usize = 256;
const MAX_SECRET_BYTES: usize = 65_536;
pub const PARENT_SECRET_HANDOFF_OPTION: &str = "parent-secret-stdin";
struct BoundedFrame(Zeroizing<Vec<u8>>);
impl Write for BoundedFrame {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
if bytes.len() > MAX_FRAME_BYTES.saturating_sub(self.0.len()) {
return Err(std::io::Error::other("frame exceeds transport bound"));
}
self.0.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
tokio::task_local! {
static PARENT_PROVIDER: Arc<dyn SecretProvider>;
}
pub(super) fn active_parent_provider() -> Option<Arc<dyn SecretProvider>> {
PARENT_PROVIDER.try_with(Arc::clone).ok()
}
pub async fn with_parent_secret_handoff<T>(
handoff: Option<ParentSecretHandoff>,
operation: impl Future<Output = T>,
) -> T {
match handoff {
Some(handoff) => {
let provider: Arc<dyn SecretProvider> = Arc::new(handoff.into_provider());
PARENT_PROVIDER
.scope(
Arc::clone(&provider),
super::with_active_secret_provider(Some(provider), operation),
)
.await
}
None => operation.await,
}
}
pub struct ParentSecretHandoff {
secrets: BTreeMap<SecretId, SecretBytes>,
}
struct ReceivedParentSecrets(MemorySecretProvider);
#[async_trait::async_trait]
impl SecretProvider for ReceivedParentSecrets {
async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
self.0.get(id).await
}
async fn put(&self, _id: &SecretId, _value: SecretBytes) -> Result<(), SecretError> {
Err(SecretError::Unsupported {
provider: self.namespace().into(),
operation: "write_parent_grant",
})
}
async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
Err(SecretError::Unsupported {
provider: self.namespace().into(),
operation: "rotate_parent_grant",
})
}
async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
self.0.list(prefix).await
}
fn namespace(&self) -> &str {
self.0.namespace()
}
fn supports_versions(&self) -> bool {
self.0.supports_versions()
}
fn persists_writes(&self) -> bool {
false
}
}
impl fmt::Debug for ParentSecretHandoff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParentSecretHandoff")
.field("secret_count", &self.secrets.len())
.finish_non_exhaustive()
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Frame {
version: u32,
secrets: Vec<Entry>,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Entry {
id: SecretId,
value: Vec<u8>,
}
impl Drop for Entry {
fn drop(&mut self) {
self.value.zeroize();
}
}
fn invalid(reason: &'static str) -> SecretError {
SecretError::InvalidInput(format!("parent secret handoff: {reason}"))
}
impl ParentSecretHandoff {
pub async fn capture(
provider: &dyn SecretProvider,
ids: impl IntoIterator<Item = SecretId>,
) -> Result<Self, SecretError> {
let mut secrets = BTreeMap::new();
let mut captured_bytes = 0usize;
for id in ids {
if id.namespace.trim().is_empty() || id.name.trim().is_empty() {
return Err(invalid("invalid reference"));
}
if secrets.contains_key(&id) {
return Err(invalid("duplicate reference"));
}
if secrets.len() == MAX_SECRETS {
return Err(invalid("too many references"));
}
let value = provider.get(&id).await?;
if value.len() > MAX_SECRET_BYTES {
return Err(invalid("secret exceeds the transport bound"));
}
captured_bytes = captured_bytes
.saturating_add(id.namespace.len())
.saturating_add(id.name.len())
.saturating_add(value.len());
if captured_bytes > MAX_FRAME_BYTES {
return Err(invalid("selected secrets exceed the transport bound"));
}
secrets.insert(id, value);
}
Ok(Self { secrets })
}
pub fn write_to(&self, mut pipe: impl Write) -> Result<(), SecretError> {
let frame = Frame {
version: 1,
secrets: self
.secrets
.iter()
.map(|(id, value)| Entry {
id: id.clone(),
value: value.with_exposed(<[u8]>::to_vec),
})
.collect(),
};
let mut encoded = BoundedFrame(Zeroizing::new(Vec::new()));
serde_json::to_writer(&mut encoded, &frame)
.map_err(|_| invalid("cannot encode bounded frame"))?;
let bytes = encoded.0;
pipe.write_all(&(bytes.len() as u32).to_be_bytes())
.and_then(|()| pipe.write_all(&bytes))
.and_then(|()| pipe.flush())
.map_err(|_| invalid("cannot write frame"))
}
pub fn read_from(mut pipe: impl Read) -> Result<Self, SecretError> {
let mut header = [0; 4];
pipe.read_exact(&mut header)
.map_err(|_| invalid("missing frame header"))?;
let length = u32::from_be_bytes(header) as usize;
if length == 0 || length > MAX_FRAME_BYTES {
return Err(invalid("invalid frame length"));
}
let mut bytes = Zeroizing::new(vec![0; length]);
pipe.read_exact(&mut bytes)
.map_err(|_| invalid("incomplete frame"))?;
let frame: Frame = serde_json::from_slice(&bytes).map_err(|_| invalid("invalid frame"))?;
if frame.version != 1 || frame.secrets.len() > MAX_SECRETS {
return Err(invalid("unsupported frame"));
}
let mut secrets = BTreeMap::new();
for entry in frame.secrets {
if entry.id.namespace.trim().is_empty()
|| entry.id.name.trim().is_empty()
|| entry.value.len() > MAX_SECRET_BYTES
|| secrets.contains_key(&entry.id)
{
return Err(invalid("invalid or duplicate reference"));
}
secrets.insert(entry.id.clone(), SecretBytes::from(entry.value.as_slice()));
}
Ok(Self { secrets })
}
pub fn into_provider(self) -> impl SecretProvider {
ReceivedParentSecrets(MemorySecretProvider::from_resolved_snapshot(
"parent-handoff",
self.secrets,
))
}
}
#[cfg(test)]
mod tests;