From fd024387d710887bd2016658c44d4762a08c791c Mon Sep 17 00:00:00 2001 From: Steffen Eiden Date: Tue, 5 Mar 2024 12:19:22 +0100 Subject: [PATCH] rust/pv: Retrievable secrets support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support retrievable secret for Add-Secret requests. Acked-by: Marc Hartmayer Reviewed-by: Christoph Schlameuss Signed-off-by: Steffen Eiden Signed-off-by: Jan Höppner --- rust/pv/src/crypto.rs | 3 +- rust/pv/src/error.rs | 8 + rust/pv/src/lib.rs | 8 +- rust/pv/src/uvsecret.rs | 1 + rust/pv/src/uvsecret/guest_secret.rs | 399 +++++++++++++++++++++++++-- rust/pv/src/uvsecret/retr_secret.rs | 234 ++++++++++++++++ 6 files changed, 631 insertions(+), 22 deletions(-) create mode 100644 rust/pv/src/uvsecret/retr_secret.rs diff --git a/rust/pv/src/crypto.rs b/rust/pv/src/crypto.rs index 8f11d2b4..ebc85f72 100644 --- a/rust/pv/src/crypto.rs +++ b/rust/pv/src/crypto.rs @@ -29,7 +29,6 @@ pub type Aes256XtsKey = Confidential<[u8; SymKeyType::AES_256_XTS_KEY_LEN]>; /// SHA-512 digest length (in bytes) pub const SHA_512_HASH_LEN: usize = 64; - #[allow(dead_code)] pub(crate) const SHA_256_HASH_LEN: u32 = 32; #[allow(dead_code)] @@ -60,6 +59,8 @@ impl SymKeyType { pub const AES_256_XTS_KEY_LEN: usize = 64; /// AES256-XTS tweak length (in bytes) pub const AES_256_XTS_TWEAK_LEN: usize = 16; + /// AES256 GCM Block length + pub const AES_256_GCM_BLOCK_LEN: usize = 16; /// Returns the tag length of the [`SymKeyType`] if it is an AEAD key pub const fn tag_len(&self) -> Option { diff --git a/rust/pv/src/error.rs b/rust/pv/src/error.rs index 3ba808f2..601b40f0 100644 --- a/rust/pv/src/error.rs +++ b/rust/pv/src/error.rs @@ -109,6 +109,14 @@ pub enum Error { #[error("An ASCII string was expected, but non-ASCII characters were received.")] NonAscii, + #[error("Incorrect {what} for a {kind}. Is: {value}; expected: {exp}")] + RetrInvKey { + what: &'static str, + kind: String, + value: String, + exp: String, + }, + // errors from other crates #[error(transparent)] PvCore(#[from] pv_core::Error), diff --git a/rust/pv/src/lib.rs b/rust/pv/src/lib.rs index ec31b9a4..43375669 100644 --- a/rust/pv/src/lib.rs +++ b/rust/pv/src/lib.rs @@ -104,7 +104,12 @@ pub mod request { /// Reexports some useful OpenSSL symbols pub mod openssl { - pub use openssl::{error::ErrorStack, hash::DigestBytes, pkey, x509}; + pub use openssl::{error::ErrorStack, hash::DigestBytes, nid::Nid, pkey, x509}; + // rust-OpenSSL does not define these NIDs + #[allow(missing_docs)] + pub const NID_ED25519: Nid = Nid::from_raw(openssl_sys::NID_ED25519); + #[allow(missing_docs)] + pub const NID_ED448: Nid = Nid::from_raw(openssl_sys::NID_ED448); } pub use pv_core::request::*; @@ -118,6 +123,7 @@ pub mod secret { asrcb::{AddSecretFlags, AddSecretRequest, AddSecretVersion}, ext_secret::ExtSecret, guest_secret::GuestSecret, + retr_secret::{IbmProtectedKey, RetrievedSecret}, user_data::verify_asrcb_and_get_user_data, }; } diff --git a/rust/pv/src/uvsecret.rs b/rust/pv/src/uvsecret.rs index 343e4b05..c3b43bba 100644 --- a/rust/pv/src/uvsecret.rs +++ b/rust/pv/src/uvsecret.rs @@ -10,4 +10,5 @@ pub mod asrcb; pub mod ext_secret; pub mod guest_secret; +pub mod retr_secret; pub mod user_data; diff --git a/rust/pv/src/uvsecret/guest_secret.rs b/rust/pv/src/uvsecret/guest_secret.rs index 4f1db31c..3bad6d3c 100644 --- a/rust/pv/src/uvsecret/guest_secret.rs +++ b/rust/pv/src/uvsecret/guest_secret.rs @@ -4,20 +4,34 @@ #[allow(unused_imports)] // used for more convenient docstring use super::asrcb::AddSecretRequest; -use crate::assert_size; use crate::{ - crypto::{hash, random_array}, - request::Confidential, - Result, + assert_size, + crypto::{hash, random_array, SymKeyType}, + request::{ + openssl::{NID_ED25519, NID_ED448}, + Confidential, + }, + uv::{ + AesSizes, AesXtsSizes, EcCurves, HmacShaSizes, ListableSecretType, RetrievableSecret, + RetrieveCmd, SecretId, + }, + Error, Result, }; use byteorder::BigEndian; -use openssl::hash::MessageDigest; -use pv_core::uv::{ListableSecretType, SecretId}; +use openssl::{ + hash::MessageDigest, + nid::Nid, + pkey::{Id, PKey, Private}, +}; +use pv_core::static_assert; use serde::{Deserialize, Serialize}; -use std::{convert::TryInto, fmt::Display}; +use std::fmt::Display; use zerocopy::{AsBytes, U16, U32}; const ASSOC_SECRET_SIZE: usize = 32; +/// Maximum size of a plain-text secret payload (8190) +pub(crate) const MAX_SIZE_PLAIN_PAYLOAD: usize = RetrieveCmd::MAX_SIZE - 2; +static_assert!(MAX_SIZE_PLAIN_PAYLOAD == 8190); /// A Secret to be added in [`AddSecretRequest`] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -36,13 +50,60 @@ pub enum GuestSecret { #[serde(skip)] secret: Confidential<[u8; ASSOC_SECRET_SIZE]>, }, + /// Retrievable key + /// + /// Create Retrievables using [`GuestSecret::retrievable`] + /// Secret size is always valid for the type/kind + Retrievable { + /// Retrievable secret type + kind: RetrievableSecret, + /// Name of the secret + name: String, + /// SHA256 hash of [`GuestSecret::RetrievableKey::name`] + id: SecretId, + /// Confidential actual retrievable secret (32 bytes) + #[serde(skip)] + secret: Confidential>, + }, +} + +macro_rules! retr_constructor { + ($(#[$err:meta])* | $(#[$kind:meta])* => $type: ty, $func: ident) => { + /// Create a new + $(#[$kind])* + /// [`GuestSecret::Retrievable`] secret. + /// + /// * `name` - Name of the secret. Will be hashed into a 32 byte id + /// * `secret` - the secret value + /// + /// # Errors + /// + $(#[$err])* + pub fn $func(name: &str, secret: $type) -> Result { + let (kind, secret) = $func(secret)?; + Ok(Self::Retrievable { + kind, + name: name.to_string(), + id: Self::name_to_id(name)?, + secret, + }) + } + }; } impl GuestSecret { + fn name_to_id(name: &str) -> Result { + let id: [u8; SecretId::ID_SIZE] = hash(MessageDigest::sha256(), name.as_bytes())? + .to_vec() + .try_into() + .unwrap(); + Ok(id.into()) + } + /// Create a new [`GuestSecret::Association`]. /// /// * `name` - Name of the secret. Will be hashed into a 32 byte id - /// * `secret` - Value of the secret. Ranom if [`Option::None`] + /// * `secret` - Value of the secret. Random if [`Option::None`] /// /// # Errors /// @@ -51,10 +112,6 @@ impl GuestSecret { where O: Into>, { - let id: [u8; SecretId::ID_SIZE] = hash(MessageDigest::sha256(), name.as_bytes())? - .to_vec() - .try_into() - .unwrap(); let secret = match secret.into() { Some(s) => s, None => random_array()?, @@ -62,16 +119,28 @@ impl GuestSecret { Ok(Self::Association { name: name.to_string(), - id: id.into(), + id: Self::name_to_id(name)?, secret: secret.into(), }) } + retr_constructor!(#[doc = r"This function will return an error if the secret is larger than 8 pages"] + | #[doc = r"plaintext"] => Confidential>, plaintext); + retr_constructor!(#[doc = r"This function will return an error if OpenSSL cannot create a hash or the secret size is invalid"] + | #[doc = r"AES Key"] => Confidential>, aes); + retr_constructor!(#[doc = r"This function will return an error if OpenSSL cannot create a hash or the secret size is invalid"] + | #[doc = r"AES-XTS Key"] => Confidential>, aes_xts); + retr_constructor!(#[doc = r"This function will return an error if OpenSSL cannot create a hash or the secret size is invalid"] + | #[doc = r"HMAC-SHA Key"] => Confidential>, hmac_sha); + 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); + /// Reference to the confidential data pub fn confidential(&self) -> &[u8] { match &self { Self::Null => &[], Self::Association { secret, .. } => secret.value().as_slice(), + Self::Retrievable { secret, .. } => secret.value(), } } @@ -79,7 +148,7 @@ impl GuestSecret { pub(crate) fn auth(&self) -> SecretAuth { match &self { Self::Null => SecretAuth::Null, - // Panic: every non null secret type is listable -> no panic + // Panic: every non null secret type is list-able -> no panic listable => { SecretAuth::Listable(ListableSecretHdr::from_guest_secret(listable).unwrap()) } @@ -92,6 +161,7 @@ impl GuestSecret { // Null is not listable, but the ListableSecretType provides the type constant (1) Self::Null => ListableSecretType::NULL, Self::Association { .. } => ListableSecretType::ASSOCIATION, + Self::Retrievable { kind, .. } => kind.into(), } } @@ -100,6 +170,7 @@ impl GuestSecret { match self { Self::Null => 0, Self::Association { secret, .. } => secret.value().len() as u32, + Self::Retrievable { secret, .. } => secret.value().len() as u32, } } @@ -107,18 +178,157 @@ impl GuestSecret { fn id(&self) -> Option { match self { Self::Null => None, - Self::Association { id, .. } => Some(id.to_owned()), + Self::Association { id, .. } | Self::Retrievable { id, .. } => Some(id.to_owned()), } } } +type RetrKeyInfo = (RetrievableSecret, Confidential>); + +fn extend_to_multiple(mut key: Vec, multiple: usize) -> Confidential> { + match key.len().checked_rem(multiple) { + Some(0) | None => key, + Some(m) => { + key.resize(key.len() + multiple - m, 0); + key + } + } + .into() +} + +/// Get a plain-text key +/// +/// ```none +/// size U16 | payload (0-8190) bytes +/// ``` +fn plaintext(inp: Confidential>) -> Result { + let key_len = inp.value().len(); + if key_len > RetrieveCmd::MAX_SIZE { + return Err(Error::RetrInvKey { + what: "key size", + value: key_len.to_string(), + kind: RetrievableSecret::PlainText.to_string(), + exp: RetrievableSecret::PlainText.expected(), + }); + } + let mut key = Vec::with_capacity(2 + inp.value().len()); + let key_len: U16 = (key_len as u16).into(); + key.extend_from_slice(key_len.as_bytes()); + key.extend_from_slice(inp.value()); + let key = extend_to_multiple(key, SymKeyType::AES_256_GCM_BLOCK_LEN); + + Ok((RetrievableSecret::PlainText, key)) +} + +/// Get an AES-key +fn aes(key: Confidential>) -> Result { + let key_len = key.value().len() as u32; + let bit_size = bitsize(key_len); + match AesSizes::from_bits(bit_size) { + Some(size) => Ok((RetrievableSecret::Aes(size), key)), + None => { + // Use some AES type to get exp sizes and name + let kind = RetrievableSecret::Aes(AesSizes::Bits128); + Err(Error::RetrInvKey { + what: "key size", + value: bit_size.to_string(), + kind: format!("{kind:#}"), + exp: kind.expected(), + }) + } + } +} + +/// Get an AES-XTS-key +fn aes_xts(key: Confidential>) -> Result { + let key_len = key.value().len() as u32; + let bit_size = bitsize(key_len / 2); + match AesXtsSizes::from_bits(bit_size) { + Some(size) => Ok((RetrievableSecret::AesXts(size), key)), + None => { + // Use some AES-XTS type to get exp sizes and name + let kind = RetrievableSecret::AesXts(AesXtsSizes::Bits128); + Err(Error::RetrInvKey { + what: "key size", + value: bit_size.to_string(), + kind: format!("{kind:#}"), + exp: kind.expected(), + }) + } + } +} + +/// Get an HMAC-SHA-key +fn hmac_sha(key: Confidential>) -> Result { + let key_len = key.value().len() as u32; + let size = bitsize(key_len / 2); + match HmacShaSizes::from_sha_size(size) { + Some(size) => Ok((RetrievableSecret::HmacSha(size), key)), + None => { + // Use some HMAC type to get exp sizes and name + let kind = RetrievableSecret::HmacSha(HmacShaSizes::Sha256); + Err(Error::RetrInvKey { + what: "key size", + value: size.to_string(), + kind: format!("{kind:#}"), + exp: kind.expected(), + }) + } + } +} + +/// Get an EC-private-key +fn ec(key: PKey) -> Result { + let (key, nid) = match key.id() { + Id::EC => { + let ec_key = key.ec_key()?; + let key = ec_key.private_key().to_vec(); + let nid = ec_key.group().curve_name().unwrap_or(Nid::UNDEF); + (key, nid) + } + // ED keys are not handled via the EC struct in OpenSSL. + id @ (Id::ED25519 | Id::ED448) => { + let key = key.raw_private_key()?; + let nid = Nid::from_raw(id.as_raw()); + (key, nid) + } + _ => (vec![], Nid::UNDEF), + }; + + let kind = match nid { + Nid::X9_62_PRIME256V1 => EcCurves::Secp256R1, + Nid::SECP384R1 => EcCurves::Secp384R1, + Nid::SECP521R1 => EcCurves::Secp521R1, + NID_ED25519 => EcCurves::Ed25519, + NID_ED448 => EcCurves::Ed448, + nid => { + // Use some EC type to get exp sizes and name + let ec = RetrievableSecret::Ec(EcCurves::Secp521R1); + return Err(Error::RetrInvKey { + what: "curve or format", + kind: format!("{ec:#}"), + value: nid.long_name()?.to_string(), + exp: ec.expected(), + }); + } + }; + + let key = kind.resize_raw_key(key); + Ok((RetrievableSecret::Ec(kind), key.into())) +} + +#[inline(always)] +const fn bitsize(bytesize: u32) -> u32 { + bytesize * 8 +} + impl Display for GuestSecret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Null => write!(f, "Meta"), gs => { let kind: U16 = gs.kind().into(); - let st: ListableSecretType = kind.into(); + let st: ListableSecretType = kind.get().into(); write!(f, "{st}") } } @@ -153,20 +363,24 @@ assert_size!(ListableSecretHdr, 0x30); impl ListableSecretHdr { fn from_guest_secret(gs: &GuestSecret) -> Option { - let id = gs.id()?; Some(Self { res0: 0, kind: gs.kind().into(), secret_len: gs.secret_len().into(), res8: 0, - id, + id: gs.id()?, }) } } #[cfg(test)] mod test { + + use super::HmacShaSizes as HmacSizes; + use super::RetrievableSecret::*; use super::*; + use openssl::ec::{EcGroup, EcKey}; + use pv_core::uv::AesSizes; use serde_test::{assert_tokens, Token}; #[test] @@ -187,8 +401,103 @@ mod test { assert_eq!(secret, exp); } + macro_rules! retr_test { + ($name: ident, $func: ident, $size: expr, $exp_kind: expr) => { + #[test] + fn $name() { + let secret_value = vec![0x11; $size]; + let name = "test retr secret".to_string(); + let secret = GuestSecret::$func(&name, secret_value.clone().into()).unwrap(); + let exp_id = [ + 0x61, 0x2c, 0xd6, 0x3e, 0xa8, 0xf2, 0xc1, 0x15, 0xc1, 0xe, 0x15, 0xb8, 0x8a, + 0x90, 0x16, 0xc1, 0x55, 0xef, 0x9c, 0x7c, 0x2c, 0x8e, 0x56, 0xd0, 0x78, 0x4c, + 0x8a, 0x1d, 0xc9, 0x3a, 0x80, 0xba, + ]; + let exp = GuestSecret::Retrievable { + kind: $exp_kind, + name, + id: exp_id.into(), + secret: secret_value.into(), + }; + assert_eq!(exp, secret); + } + }; + } + + retr_test!(retr_aes_128, aes, 16, Aes(AesSizes::Bits128)); + retr_test!(retr_aes_192, aes, 24, Aes(AesSizes::Bits192)); + retr_test!(retr_aes_256, aes, 32, Aes(AesSizes::Bits256)); + retr_test!(retr_aes_xts_128, aes_xts, 32, AesXts(AesXtsSizes::Bits128)); + retr_test!(retr_aes_xts_256, aes_xts, 64, AesXts(AesXtsSizes::Bits256)); + retr_test!(retr_aes_hmac_256, hmac_sha, 64, HmacSha(HmacSizes::Sha256)); + retr_test!(retr_aes_hmac_512, hmac_sha, 128, HmacSha(HmacSizes::Sha512)); + + #[test] + fn plaintext_no_pad() { + let key = vec![0, 14, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]; + let name = "PLAINTEXT_PAD".to_string(); + let secret = GuestSecret::plaintext(&name, key[2..].to_vec().into()).unwrap(); + let exp_id = [ + 15, 123, 176, 210, 135, 231, 220, 232, 148, 93, 198, 195, 165, 212, 214, 129, 45, 1, + 94, 11, 167, 18, 151, 15, 120, 254, 13, 109, 173, 186, 37, 74, + ]; + let exp = GuestSecret::Retrievable { + kind: PlainText, + name, + id: exp_id.into(), + secret: key.into(), + }; + + assert_eq!(secret, exp); + } + #[test] - fn ap_asc_parse() { + fn plaintext_pad() { + let key = vec![0, 10, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0]; + let name = "PLAINTEXT_PAD".to_string(); + let secret = GuestSecret::plaintext(&name, key[2..12].to_vec().into()).unwrap(); + let exp_id = [ + 15, 123, 176, 210, 135, 231, 220, 232, 148, 93, 198, 195, 165, 212, 214, 129, 45, 1, + 94, 11, 167, 18, 151, 15, 120, 254, 13, 109, 173, 186, 37, 74, + ]; + let exp = GuestSecret::Retrievable { + kind: PlainText, + name, + id: exp_id.into(), + secret: key.into(), + }; + + assert_eq!(secret, exp); + } + + #[track_caller] + fn test_ec(grp: Nid, exp_kind: EcCurves, exp_len: usize) { + let key = match grp { + NID_ED25519 => PKey::generate_ed25519().unwrap(), + NID_ED448 => PKey::generate_ed448().unwrap(), + nid => { + let group = EcGroup::from_curve_name(nid).unwrap(); + let key = EcKey::generate(&group).unwrap(); + PKey::from_ec_key(key).unwrap() + } + }; + let (kind, key) = ec(key).unwrap(); + + assert_eq!(kind, Ec(exp_kind)); + assert_eq!(key.value().len(), exp_len); + } + + #[test] + fn retr_ec() { + test_ec(Nid::X9_62_PRIME256V1, EcCurves::Secp256R1, 32); + test_ec(Nid::SECP384R1, EcCurves::Secp384R1, 48); + test_ec(Nid::SECP521R1, EcCurves::Secp521R1, 80); + test_ec(NID_ED25519, EcCurves::Ed25519, 32); + test_ec(NID_ED448, EcCurves::Ed448, 64); + } + + #[test] + fn asc_parse() { let id = [ 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, @@ -217,6 +526,39 @@ mod test { ); } + #[test] + fn retrievable_parse() { + let id = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, + 0x89, 0xab, 0xcd, 0xef, + ]; + let asc = GuestSecret::Retrievable { + kind: PlainText, + name: "test123".to_string(), + id: id.into(), + secret: vec![].into(), + }; + + assert_tokens( + &asc, + &[ + Token::StructVariant { + name: "GuestSecret", + variant: "Retrievable", + len: 3, + }, + Token::String("kind"), + Token::String("3 (PLAINTEXT)"), + Token::String("name"), + Token::String("test123"), + Token::String("id"), + Token::String("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), + Token::StructVariantEnd, + ], + ); + } + #[test] fn guest_secret_bin_null() { let gs = GuestSecret::Null; @@ -228,7 +570,7 @@ mod test { } #[test] - fn guest_secret_bin_ap() { + fn guest_secret_bin_asoc() { let gs = GuestSecret::Association { name: "test".to_string(), id: [1; 32].into(), @@ -241,4 +583,21 @@ mod test { assert_eq!(exp, gs_bytes_auth.get()); assert_eq!(&[2; 32], gs.confidential()); } + + #[test] + fn guest_secret_bin_retr() { + let gs = GuestSecret::Retrievable { + kind: PlainText, + name: "test".to_string(), + id: [1; 32].into(), + secret: vec![2; 32].into(), + }; + let auth = gs.auth(); + let gs_bytes_auth = auth.get(); + let mut exp = vec![0u8, 0, 0, 3, 0, 0, 0, 0x20, 0, 0, 0, 0, 0, 0, 0, 0]; + exp.extend([1; 32]); + + assert_eq!(exp, gs_bytes_auth); + assert_eq!(&[2; 32], gs.confidential()); + } } diff --git a/rust/pv/src/uvsecret/retr_secret.rs b/rust/pv/src/uvsecret/retr_secret.rs new file mode 100644 index 00000000..5fad016f --- /dev/null +++ b/rust/pv/src/uvsecret/retr_secret.rs @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +// +// Copyright IBM Corp. 2024 + +use crate::{pem::Pem, uvsecret::guest_secret::MAX_SIZE_PLAIN_PAYLOAD, Result}; + +use byteorder::BigEndian; +use log::warn; +use pv_core::{ + request::Confidential, + uv::{ListableSecretType, RetrievableSecret, RetrieveCmd}, +}; +use zerocopy::{FromBytes, U16}; + +/// An IBM Protected Key +/// +/// A protected key, writeable as pem. +/// +/// Will convert into PEM as: +/// ```PEM +///-----BEGIN IBM PROTECTED KEY----- +///kind: +/// +/// +///-----END IBM PROTECTED KEY----- +/// ``` +#[derive(Debug, PartialEq, Eq)] +pub struct IbmProtectedKey { + kind: ListableSecretType, + key: Confidential>, +} + +impl IbmProtectedKey { + /// Get the binary representation of the key. + pub fn data(&self) -> &[u8] { + self.key.value() + } + + /// Converts a [`IbmProtectedKey`] into a vector. + pub fn into_bytes(self) -> Confidential> { + self.key + } + + /// Get the data in PEM format. + /// + /// # Errors + /// + /// This function will return an error if the PEM conversion failed (very unlikely). + pub fn to_pem(&self) -> Result { + Pem::new( + "IBM PROTECTED KEY", + format!("kind: {}", self.kind), + self.key.value(), + ) + } + + fn new(kind: ListableSecretType, key: K) -> Self + where + K: Into>>, + { + Self { + kind, + key: key.into(), + } + } +} + +impl From for RetrievedSecret { + fn from(value: RetrieveCmd) -> Self { + let kind = value.meta_data().stype(); + let key = value.into_key(); + + match kind { + ListableSecretType::Retrievable(RetrievableSecret::PlainText) => { + // Will not run into default, retrieve has a granularity of 16 bytes and 16 bytes is the + // minimum size + let len = U16::::read_from_prefix(key.value()) + .unwrap_or_default() + .get() as usize; + + // Test if the plain text secret has a size: + // 1. len <= 8190 + // 2. first two bytes are max 15 less than buffer-size+2 + // 3. bytes after len + 2 are zero + match len <= MAX_SIZE_PLAIN_PAYLOAD + && key.value().len() - (len + 2) < 15 + && key.value()[len + 2..].iter().all(|c| *c == 0) + { + false => Self::Plaintext(key), + true => Self::Plaintext(key.value()[2..len + 2].to_vec().into()), + } + } + kind => { + match kind { + ListableSecretType::Retrievable(_) => (), + _ => warn!("Retrieved an unretrievable Secret! Will continue; interpreting it as a protected key."), + } + Self::ProtectedKey(IbmProtectedKey::new(kind, key)) + } + } + } +} + +/// A retrieved Secret. +#[derive(Debug, PartialEq, Eq)] +pub enum RetrievedSecret { + /// A plaintext secret + Plaintext(Confidential>), + /// An [`IbmProtectedKey`] + ProtectedKey(IbmProtectedKey), +} + +impl RetrievedSecret { + /// Create a new IBM PROTECTED KEY object + pub fn from_cmd(cmd: RetrieveCmd) -> Self { + cmd.into() + } + + /// Get the binary representation of the key. + pub fn data(&self) -> &[u8] { + match self { + RetrievedSecret::Plaintext(p) => p.value(), + RetrievedSecret::ProtectedKey(p) => p.data(), + } + } + + /// Converts a [`IbmProtectedKey`] into a vector. + pub fn into_bytes(self) -> Confidential> { + match self { + RetrievedSecret::Plaintext(p) => p, + RetrievedSecret::ProtectedKey(p) => p.into_bytes(), + } + } + /// Get the data in PEM format. + /// + /// # Errors + /// + /// This function will return an error if the PEM conversion failed (very unlikely). + pub fn to_pem(&self) -> Result { + match self { + RetrievedSecret::Plaintext(p) => Pem::new("PLAINTEXT SECRET", None, p.value()), + RetrievedSecret::ProtectedKey(p) => p.to_pem(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use pv_core::uv::*; + + fn mk_retr(secret: &[u8]) -> RetrievedSecret { + let entry = SecretEntry::new( + 0, + ListableSecretType::Retrievable(RetrievableSecret::PlainText), + SecretId::default(), + secret.len() as u32, + ); + let mut cmd = RetrieveCmd::from_entry(entry).unwrap(); + cmd.data().unwrap().copy_from_slice(secret); + RetrievedSecret::from_cmd(cmd) + } + + #[test] + fn from_retr_cmd() { + let secret = vec![0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0, 0, 0, 0]; + let prot_key = mk_retr(&secret); + let exp = RetrievedSecret::Plaintext(secret[2..12].to_vec().into()); + assert_eq!(prot_key, exp); + } + + #[test] + fn from_retr_inv_size() { + let secret = vec![0x20; 32]; + let prot_key = mk_retr(&secret); + let exp = RetrievedSecret::Plaintext(secret.into()); + assert_eq!(prot_key, exp); + } + + #[test] + fn from_retr_inv_no_zero_after_end() { + let secret = vec![0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 1, 0, 0, 0]; + let prot_key = mk_retr(&secret); + let exp = RetrievedSecret::Plaintext(secret.into()); + assert_eq!(prot_key, exp); + } + + #[test] + fn from_retr_inv_to_much_padding() { + let secret = vec![ + 0, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + let prot_key = mk_retr(&secret); + let exp = RetrievedSecret::Plaintext(secret.into()); + assert_eq!(prot_key, exp); + } + + #[test] + fn from_retr_0_size() { + let secret = vec![0x00; 32]; + let prot_key = mk_retr(&secret); + let exp = RetrievedSecret::Plaintext(secret.into()); + assert_eq!(prot_key, exp); + } + + #[test] + fn plain_text_pem() { + let exp = "\ + -----BEGIN PLAINTEXT SECRET-----\n\ + ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER\n\ + -----END PLAINTEXT SECRET-----\n"; + let prot = RetrievedSecret::Plaintext(vec![17; 48].into()); + let pem = prot.to_pem().unwrap(); + let pem_str = pem.to_string(); + assert_eq!(pem_str, exp); + } + + #[test] + fn prot_key_pem() { + let exp = "\ + -----BEGIN IBM PROTECTED KEY-----\n\ + kind: AES-128-KEY\n\n\ + ERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERER\n\ + -----END IBM PROTECTED KEY-----\n"; + let prot = IbmProtectedKey::new( + ListableSecretType::Retrievable(RetrievableSecret::Aes(AesSizes::Bits128)), + vec![17; 48], + ); + let pem = prot.to_pem().unwrap(); + let pem_str = pem.to_string(); + assert_eq!(pem_str, exp); + } +}