diff --git a/rust/pv/src/uvsecret/guest_secret.rs b/rust/pv/src/uvsecret/guest_secret.rs index 3bad6d3c..87b58bb8 100644 --- a/rust/pv/src/uvsecret/guest_secret.rs +++ b/rust/pv/src/uvsecret/guest_secret.rs @@ -92,7 +92,8 @@ macro_rules! retr_constructor { } impl GuestSecret { - fn name_to_id(name: &str) -> Result { + /// Hashes the name with sha256 + pub fn name_to_id(name: &str) -> Result { let id: [u8; SecretId::ID_SIZE] = hash(MessageDigest::sha256(), name.as_bytes())? .to_vec() .try_into() @@ -135,6 +136,19 @@ impl GuestSecret { retr_constructor!(#[doc = r"This function will return an error if OpenSSL cannot create a hash or the curve is invalid"] | #[doc = r"EC PRIVATE Key"] => PKey, ec); + /// Use the name as ID, do not hash it + pub fn no_hash_name(&mut self) { + match self { + Self::Null => (), + Self::Association { + name, ref mut id, .. + } + | Self::Retrievable { + name, ref mut id, .. + } => id.clone_from(&SecretId::from_string(name)), + } + } + /// Reference to the confidential data pub fn confidential(&self) -> &[u8] { match &self { diff --git a/rust/pv_core/src/uvdevice/secret_list.rs b/rust/pv_core/src/uvdevice/secret_list.rs index d7c268c9..7c7e63b5 100644 --- a/rust/pv_core/src/uvdevice/secret_list.rs +++ b/rust/pv_core/src/uvdevice/secret_list.rs @@ -11,7 +11,9 @@ use crate::{ use byteorder::{BigEndian, ByteOrder}; use serde::{Deserialize, Serialize, Serializer}; use std::{ - fmt::Display, + cmp::min, + ffi::CStr, + fmt::{Debug, Display, LowerHex, UpperHex}, io::{Cursor, Read, Seek, Write}, mem::size_of, slice::Iter, @@ -35,6 +37,33 @@ impl SecretId { pub fn from(buf: [u8; Self::ID_SIZE]) -> Self { buf.into() } + + /// Create a Id from a string + /// + /// Uses the first 31 bytes from `name` as id + /// Does not hash anything. Byte 32 is the NUL char + pub fn from_string(name: &str) -> Self { + let len = min(name.len(), Self::ID_SIZE - 1); + let mut res = Self::default(); + res.0[0..len].copy_from_slice(&name.as_bytes()[0..len]); + res + } + + /// Tries to represent the Id as printable-ASCII string + pub fn as_ascii(&self) -> Option<&str> { + if let Ok(t) = CStr::from_bytes_until_nul(&self.0) { + if let Ok(t) = t.to_str() { + if !t.is_empty() + && t.chars() + .all(|c| c.is_ascii_whitespace() | c.is_ascii_graphic()) + && self.0[t.len()..].iter().all(|b| *b == 0) + { + return Some(t); + } + } + }; + None + } } impl Serialize for SecretId { @@ -42,8 +71,8 @@ impl Serialize for SecretId { where S: Serializer, { - // calls Display at one point - ser.serialize_str(&self.to_string()) + // calls LowerHex at one point + ser.serialize_str(&format!("{self:#x}")) } } @@ -56,12 +85,36 @@ impl<'de> Deserialize<'de> for SecretId { } } +impl UpperHex for SecretId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "0x")?; + } + for b in self.0 { + write!(f, "{b:02X}")?; + } + Ok(()) + } +} + +impl LowerHex for SecretId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "0x")?; + } + for b in self.0 { + write!(f, "{b:02x}")?; + } + Ok(()) + } +} + impl Display for SecretId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut s = String::with_capacity(32 * 2 + 2); - s.push_str("0x"); - let s = self.0.iter().fold(s, |acc, e| acc + &format!("{e:02x}")); - write!(f, "{s}") + if let Some(s) = self.as_ascii() { + write!(f, "{s} | ")?; + } + write!(f, "{self:#x}") } } @@ -79,7 +132,7 @@ impl AsRef<[u8]> for SecretId { /// A secret in a [`SecretList`] #[repr(C)] -#[derive(Debug, PartialEq, Eq, AsBytes, FromZeroes, FromBytes, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, AsBytes, FromZeroes, FromBytes, Serialize)] pub struct SecretEntry { #[serde(serialize_with = "ser_u16")] index: U16, @@ -153,11 +206,7 @@ impl Display for SecretEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let stype: ListableSecretType = self.stype.get().into(); writeln!(f, "{} {}:", self.index, stype)?; - write!(f, " ")?; - for b in self.id.as_ref() { - write!(f, "{b:02x}")?; - } - Ok(()) + write!(f, " {}", self.id) } } @@ -269,6 +318,11 @@ impl SecretList { self.hdr.total_num_secrets.get() as usize } + /// Find the first [`SecretEntry`] that has the provided [`SecretId`] + pub fn find(&self, id: &SecretId) -> Option { + self.iter().find(|e| e.id() == id.as_ref()).cloned() + } + /// Encodes the list in the same binary format the UV would do pub fn encode(&self, w: &mut T) -> Result<()> { w.write_all(self.hdr.as_bytes())?; @@ -456,7 +510,7 @@ where type Value = [u8; SecretId::ID_SIZE]; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a `32 bytes long hexstring` prepended with 0x") + formatter.write_str("a `32 bytes (=64 character) long hexstring` prepended with 0x") } fn visit_str(self, s: &str) -> Result @@ -464,7 +518,10 @@ where E: serde::de::Error, { if s.len() != SecretId::ID_SIZE * 2 + "0x".len() { - return Err(serde::de::Error::invalid_length(s.len() - 2, &self)); + return Err(serde::de::Error::invalid_length( + s.len().saturating_sub("0x".len()), + &self, + )); } let nb = s.strip_prefix("0x").ok_or_else(|| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(s), &self) @@ -655,4 +712,80 @@ mod test { ], ) } + + #[test] + fn secret_id_display() { + let text = "Fancy secret ID"; + let id = SecretId::from_string(text); + + let exp = + "Fancy secret ID | 0x46616e6379207365637265742049440000000000000000000000000000000000"; + assert_eq!(id.to_string(), exp); + } + + #[test] + fn secret_id_long_name() { + let text = "the most fanciest secret ID you ever seen in the time the universe exists"; + let id = SecretId::from_string(text); + let exp = + "the most fanciest secret ID you | 0x746865206d6f73742066616e63696573742073656372657420494420796f7500"; + assert_eq!(id.to_string(), exp); + } + + #[test] + fn secret_id_no_ascii_name() { + let text = [0; 32]; + let id = SecretId::from(text); + + let exp = "0x0000000000000000000000000000000000000000000000000000000000000000"; + assert_eq!(id.to_string(), exp); + } + + #[test] + fn secret_id_no_ascii_name2() { + let text = [ + 0x25, 0x55, 3, 4, 50, 0, 6, 0, 8, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0xa, 0, 0, 0, 0, 0xf, 0, + 0, 0, 0, 0, 0, 0, 0, + ]; + let id = SecretId::from(text); + assert_eq!(id.as_ascii(), None); + } + + #[test] + fn secret_id_no_ascii_name3() { + let text = [ + 0x25, 0x55, 0, 4, 50, 0, 6, 0, 8, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0xa, 0, 0, 0, 0, 0xf, 0, + 0, 0, 0, 0, 0, 0, 0, + ]; + let id = SecretId::from(text); + assert_eq!(id.as_ascii(), None); + } + + #[test] + fn secret_id_hex() { + let id_str = "Nice Test 123"; + let id = SecretId::from_string(id_str); + + let s = format!("{id:#x}"); + assert_eq!( + s, + "0x4e69636520546573742031323300000000000000000000000000000000000000" + ); + let s = format!("{id:x}"); + assert_eq!( + s, + "4e69636520546573742031323300000000000000000000000000000000000000" + ); + let s = format!("{id:#X}"); + assert_eq!( + s, + "0x4E69636520546573742031323300000000000000000000000000000000000000" + ); + + let s = format!("{id:X}"); + assert_eq!( + s, + "4E69636520546573742031323300000000000000000000000000000000000000" + ); + } } diff --git a/rust/pvsecret/src/cli.rs b/rust/pvsecret/src/cli.rs index 4e747682..d858fc29 100644 --- a/rust/pvsecret/src/cli.rs +++ b/rust/pvsecret/src/cli.rs @@ -141,6 +141,12 @@ pub struct CreateSecretOpt { /// by default. #[arg(long, value_name = "FILE", value_hint = ValueHint::FilePath,)] pub user_sign_key: Option, + + /// Do not hash the name, use it directly as secret ID. + /// + /// Ignored for meta-secrets. + #[arg(long)] + pub use_name: bool, } #[derive(Subcommand, Debug)] @@ -342,6 +348,8 @@ pub enum RetrInpFmt { Yaml, /// Use a hex string. Hex, + /// Use a name-string. Will hash it if no secret with the name found. + Name, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Default)] diff --git a/rust/pvsecret/src/cmd/create.rs b/rust/pvsecret/src/cmd/create.rs index 73089a12..fab37e67 100644 --- a/rust/pvsecret/src/cmd/create.rs +++ b/rust/pvsecret/src/cmd/create.rs @@ -94,7 +94,7 @@ fn read_private_key(buf: &[u8]) -> Result> { fn build_asrcb(opt: &CreateSecretOpt) -> Result { debug!("Build add-secret request"); - let secret = match &opt.secret { + let mut secret = match &opt.secret { AddSecretType::Meta => GuestSecret::Null, AddSecretType::Association { name, @@ -112,6 +112,8 @@ fn build_asrcb(opt: &CreateSecretOpt) -> Result { }; trace!("AddSecret: {secret:x?}"); + opt.use_name.then(|| secret.no_hash_name()); + let mut flags = match &opt.pcf { Some(v) => (&try_parse_u64(v, "pcf")?).into(), None => AddSecretFlags::default(), diff --git a/rust/pvsecret/src/cmd/retr.rs b/rust/pvsecret/src/cmd/retr.rs index 7f7704cc..ad3e91c3 100644 --- a/rust/pvsecret/src/cmd/retr.rs +++ b/rust/pvsecret/src/cmd/retr.rs @@ -17,12 +17,17 @@ use utils::get_writer_from_cli_file_arg; fn retrieve(id: &SecretId) -> Result { let uv = UvDevice::open()?; let secrets = list_uvc(&uv)?; - let secret = secrets - .into_iter() - .find(|s| s.id() == id.as_ref()) + let secret = match secrets.find(id) { + Some(s) => s, + // hash it + try again if it is ASCII-representable + None => match id.as_ascii() { + Some(s) => secrets.find(&GuestSecret::name_to_id(s)?), + None => None, + } .ok_or(anyhow!( "The UV secret-store has no secret with the ID {id}" - ))?; + ))?, + }; info!("Try to retrieve secret at index: {}", secret.index()); debug!("Try to retrieve: {secret:?}"); @@ -43,6 +48,7 @@ pub fn retr(opt: &RetrSecretOptions) -> Result<()> { RetrInpFmt::Hex => { serde_yaml::from_str(&opt.input).context("Cannot parse SecretId information")? } + RetrInpFmt::Name => SecretId::from_string(&opt.input), }; let retr_secret =