use std::fmt;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use gotatun::x25519::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
use crate::config::value::WireGuardPublicKey;
pub struct DeviceKey(StaticSecret);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("the stored device key is damaged: it isn't 32 bytes of base64")]
pub struct InvalidKey;
impl DeviceKey {
pub fn generate() -> Self {
let mut bytes = Zeroizing::new([0u8; 32]);
getrandom::fill(bytes.as_mut())
.expect("the operating system's random number generator failed");
Self(StaticSecret::from(*bytes))
}
pub fn public_key(&self) -> WireGuardPublicKey {
WireGuardPublicKey::from_bytes(PublicKey::from(&self.0).to_bytes())
}
pub fn to_base64(&self) -> Zeroizing<String> {
Zeroizing::new(BASE64.encode(self.0.as_bytes()))
}
pub fn from_base64(text: &str) -> Result<Self, InvalidKey> {
let bytes = Zeroizing::new(BASE64.decode(text.trim()).map_err(|_| InvalidKey)?);
let array: Zeroizing<[u8; 32]> =
Zeroizing::new(bytes.as_slice().try_into().map_err(|_| InvalidKey)?);
Ok(Self(StaticSecret::from(*array)))
}
}
impl DeviceKey {
pub(crate) fn duplicate(&self) -> Self {
Self(self.0.clone())
}
pub(crate) fn into_secret(self) -> StaticSecret {
self.0
}
}
impl fmt::Debug for DeviceKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DeviceKey(public: {})", self.public_key())
}
}
#[cfg(test)]
mod tests {
use super::*;
const RFC_PRIVATE: &str = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
const RFC_PUBLIC: &str = "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo=";
#[test]
fn public_key_matches_the_rfc_test_vector() {
let key = DeviceKey::from_base64(RFC_PRIVATE).unwrap();
assert_eq!(key.public_key().to_string(), RFC_PUBLIC);
}
#[test]
fn base64_round_trips() {
let key = DeviceKey::generate();
let again = DeviceKey::from_base64(&key.to_base64()).unwrap();
assert_eq!(again.public_key(), key.public_key());
let with_newline = format!("{}\n", key.to_base64().as_str());
assert!(DeviceKey::from_base64(&with_newline).is_ok());
}
#[test]
fn generated_keys_differ() {
assert_ne!(
DeviceKey::generate().public_key(),
DeviceKey::generate().public_key()
);
}
#[test]
fn damaged_keys_are_rejected() {
for bad in ["", "not base64!", "AAAA", &format!("{RFC_PRIVATE}AAAA")] {
assert_eq!(
DeviceKey::from_base64(bad).unwrap_err(),
InvalidKey,
"{bad}"
);
}
}
#[test]
fn debug_output_never_shows_the_private_key() {
let key = DeviceKey::from_base64(RFC_PRIVATE).unwrap();
let debug = format!("{key:?}");
assert!(!debug.contains(RFC_PRIVATE), "{debug}");
assert!(debug.contains(RFC_PUBLIC), "{debug}");
}
}