use std::collections::BTreeMap;
use std::fmt;
use std::sync::{Arc, Mutex};
use serde::de::{self, Deserializer};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
pub struct SecretString {
inner: String,
}
impl SecretString {
pub fn new(s: impl Into<String>) -> Self {
Self { inner: s.into() }
}
pub fn from_owned(s: String) -> Self {
Self { inner: s }
}
pub fn as_str(&self) -> &str {
&self.inner
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl Clone for SecretString {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl Default for SecretString {
fn default() -> Self {
Self {
inner: String::new(),
}
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecretString(<redacted, {} bytes>)", self.inner.len())
}
}
impl Serialize for SecretString {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.inner)
}
}
impl<'de> Deserialize<'de> for SecretString {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer).map_err(de::Error::custom)?;
Ok(Self::from_owned(raw))
}
}
impl Drop for SecretString {
fn drop(&mut self) {
let bytes = unsafe { self.inner.as_bytes_mut() };
for b in bytes {
*b = 0;
}
}
}
pub trait TokenBook: Send + Sync + fmt::Debug {
fn get(&self, peer_url: &str) -> Option<SecretString>;
fn set(&mut self, peer_url: &str, token: SecretString);
fn delete(&mut self, peer_url: &str);
}
#[derive(Debug, Default)]
pub struct InMemoryTokenBook {
inner: BTreeMap<String, SecretString>,
}
impl InMemoryTokenBook {
pub fn new() -> Self {
Self::default()
}
pub fn from_map<I, K, V>(pairs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let mut book = Self::new();
for (k, v) in pairs {
book.set(&k.into(), SecretString::new(v));
}
book
}
}
impl TokenBook for InMemoryTokenBook {
fn get(&self, peer_url: &str) -> Option<SecretString> {
self.inner.get(&normalize(peer_url)).cloned()
}
fn set(&mut self, peer_url: &str, token: SecretString) {
self.inner.insert(normalize(peer_url), token);
}
fn delete(&mut self, peer_url: &str) {
self.inner.remove(&normalize(peer_url));
}
}
#[derive(Clone)]
pub struct SharedTokenBook {
inner: Arc<Mutex<Box<dyn TokenBook>>>,
}
impl SharedTokenBook {
pub fn new(book: impl TokenBook + 'static) -> Self {
Self {
inner: Arc::new(Mutex::new(Box::new(book))),
}
}
pub fn from_boxed(book: Box<dyn TokenBook>) -> Self {
Self {
inner: Arc::new(Mutex::new(book)),
}
}
pub fn get(&self, peer_url: &str) -> Option<SecretString> {
let guard = self.inner.lock().ok()?;
guard.get(peer_url)
}
pub fn set(&self, peer_url: &str, token: SecretString) {
if let Ok(mut guard) = self.inner.lock() {
guard.set(peer_url, token);
}
}
pub fn delete(&self, peer_url: &str) {
if let Ok(mut guard) = self.inner.lock() {
guard.delete(peer_url);
}
}
}
impl Default for SharedTokenBook {
fn default() -> Self {
Self::new(InMemoryTokenBook::new())
}
}
impl fmt::Debug for SharedTokenBook {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SharedTokenBook").finish_non_exhaustive()
}
}
fn normalize(peer_url: &str) -> String {
peer_url.trim_end_matches('/').to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_memory_get_set_delete() {
let mut book = InMemoryTokenBook::new();
assert!(book.get("https://a:8443").is_none());
book.set("https://a:8443", SecretString::new("tok-a"));
assert_eq!(book.get("https://a:8443").unwrap().as_str(), "tok-a");
book.delete("https://a:8443");
assert!(book.get("https://a:8443").is_none());
}
#[test]
fn in_memory_trailing_slash_normalized() {
let mut book = InMemoryTokenBook::new();
book.set("https://a:8443/", SecretString::new("tok"));
assert_eq!(book.get("https://a:8443").unwrap().as_str(), "tok");
}
#[test]
fn from_map_populates_entries() {
let book =
InMemoryTokenBook::from_map([("https://a:8443", "tok-a"), ("https://b:8443", "tok-b")]);
assert_eq!(book.get("https://a:8443").unwrap().as_str(), "tok-a");
assert_eq!(book.get("https://b:8443").unwrap().as_str(), "tok-b");
}
#[test]
fn shared_book_is_clone_cheap() {
let shared = SharedTokenBook::default();
shared.set("https://a:8443", SecretString::new("t"));
let shared2 = shared.clone();
assert_eq!(shared2.get("https://a:8443").unwrap().as_str(), "t");
shared.delete("https://a:8443");
assert!(shared2.get("https://a:8443").is_none());
}
#[test]
fn debug_does_not_leak_secret() {
let s = SecretString::new("hunter2");
let dbg = format!("{s:?}");
assert!(!dbg.contains("hunter2"));
}
}