use std::io::BufRead;
use std::path::{Path, PathBuf};
pub const PEM_WALLET_FILE_NAME: &str = "ewallet.pem";
pub const P12_WALLET_FILE_NAME: &str = "ewallet.p12";
pub const SSO_WALLET_FILE_NAME: &str = "cwallet.sso";
#[derive(thiserror::Error)]
#[non_exhaustive]
pub enum WalletError {
#[error("wallet file is missing")]
FileMissing(String),
#[error("failed to read wallet file: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to parse wallet PEM: {0}")]
Pem(String),
#[error("wallet contained no certificates")]
NoCertificates,
#[error("cwallet.sso parse error: {0}")]
Sso(String),
#[error(
"cwallet.sso support is not enabled in this build; convert the wallet \
to ewallet.pem"
)]
SsoNotEnabled,
#[error("PKCS#12 wallet parse error: {0}")]
Pkcs12(String),
#[error("wallet private key decryption failed: {0}")]
KeyDecrypt(String),
#[error(
"wallet {format} is encrypted and requires a wallet password; supply \
wallet_password (or use an auto-login cwallet.sso or unencrypted \
ewallet.pem wallet)"
)]
PasswordRequired { format: &'static str },
#[error("wallet format {format} is not supported by this thin build")]
UnsupportedFormat { format: &'static str },
}
impl std::fmt::Debug for WalletError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const REDACTED_PATH: &str = "***redacted***";
let redacted = |_: &String| REDACTED_PATH;
match self {
Self::FileMissing(path) => f.debug_tuple("FileMissing").field(&redacted(path)).finish(),
Self::Io { path, source } => f
.debug_struct("Io")
.field("path", &redacted(path))
.field("source", source)
.finish(),
Self::Pem(message) => f.debug_tuple("Pem").field(message).finish(),
Self::NoCertificates => f.write_str("NoCertificates"),
Self::Sso(message) => f.debug_tuple("Sso").field(message).finish(),
Self::SsoNotEnabled => f.write_str("SsoNotEnabled"),
Self::Pkcs12(message) => f.debug_tuple("Pkcs12").field(message).finish(),
Self::KeyDecrypt(message) => f.debug_tuple("KeyDecrypt").field(message).finish(),
Self::PasswordRequired { format } => f
.debug_struct("PasswordRequired")
.field("format", format)
.finish(),
Self::UnsupportedFormat { format } => f
.debug_struct("UnsupportedFormat")
.field("format", format)
.finish(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WalletContents {
pub ca_certificates: Vec<Vec<u8>>,
pub client_cert_chain: Vec<Vec<u8>>,
pub client_private_key: Option<Vec<u8>>,
}
impl WalletContents {
#[must_use]
pub fn has_client_identity(&self) -> bool {
!self.client_cert_chain.is_empty() && self.client_private_key.is_some()
}
}
#[must_use]
pub fn resolve_wallet_dir(
wallet_location: Option<&str>,
tns_admin: Option<&str>,
) -> Option<PathBuf> {
if let Some(loc) = wallet_location {
if !loc.is_empty() && !loc.eq_ignore_ascii_case("SYSTEM") {
return Some(PathBuf::from(loc));
}
if loc.eq_ignore_ascii_case("SYSTEM") {
return None;
}
}
tns_admin.filter(|s| !s.is_empty()).map(PathBuf::from)
}
#[must_use]
pub fn pem_wallet_path(dir: &Path) -> PathBuf {
dir.join(PEM_WALLET_FILE_NAME)
}
#[must_use]
pub fn p12_wallet_path(dir: &Path) -> PathBuf {
dir.join(P12_WALLET_FILE_NAME)
}
#[must_use]
pub fn sso_wallet_path(dir: &Path) -> PathBuf {
dir.join(SSO_WALLET_FILE_NAME)
}
pub fn parse_ewallet_pem(
pem: &[u8],
wallet_password: Option<&str>,
) -> Result<WalletContents, WalletError> {
if pem_contains_legacy_encryption(pem) {
return Err(WalletError::KeyDecrypt(
"legacy OpenSSL PEM encryption (Proc-Type: 4,ENCRYPTED) is not \
supported; re-export the key as PKCS#8 with \
`openssl pkcs8 -topk8` (optionally encrypted, then supply \
wallet_password)"
.to_string(),
));
}
let mut reader = std::io::BufReader::new(pem);
let mut contents = WalletContents::default();
let mut all_certs: Vec<Vec<u8>> = Vec::new();
let mut keys: Vec<Vec<u8>> = Vec::new();
loop {
match rustls_pemfile::read_one(&mut reader) {
Ok(Some(item)) => match item {
rustls_pemfile::Item::X509Certificate(der) => {
all_certs.push(der.as_ref().to_vec());
}
rustls_pemfile::Item::Pkcs8Key(der) => {
keys.push(der.secret_pkcs8_der().to_vec());
}
rustls_pemfile::Item::Pkcs1Key(der) => {
keys.push(der.secret_pkcs1_der().to_vec());
}
rustls_pemfile::Item::Sec1Key(der) => {
keys.push(der.secret_sec1_der().to_vec());
}
_ => {}
},
Ok(None) => break,
Err(e) => return Err(WalletError::Pem(e.to_string())),
}
}
if all_certs.is_empty() {
return Err(WalletError::NoCertificates);
}
if keys.is_empty() {
let encrypted_blocks = extract_encrypted_key_pem_blocks(pem);
if !encrypted_blocks.is_empty() {
let Some(password) = wallet_password else {
return Err(WalletError::PasswordRequired {
format: PEM_WALLET_FILE_NAME,
});
};
let block = &encrypted_blocks[0];
keys.push(decrypt_encrypted_pem_key(block, password)?);
}
}
contents.ca_certificates = all_certs.clone();
if let Some(key) = keys.into_iter().next() {
contents.client_cert_chain = all_certs;
contents.client_private_key = Some(key);
}
Ok(contents)
}
pub fn parse_ewallet_p12(
data: &[u8],
wallet_password: Option<&str>,
) -> Result<WalletContents, WalletError> {
let Some(password) = wallet_password else {
return Err(WalletError::PasswordRequired {
format: P12_WALLET_FILE_NAME,
});
};
super::pfx::parse_pfx(data, password.as_bytes())
}
fn extract_encrypted_key_pem_blocks(pem: &[u8]) -> Vec<String> {
const BEGIN: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
const END: &str = "-----END ENCRYPTED PRIVATE KEY-----";
let text = String::from_utf8_lossy(pem);
let mut blocks = Vec::new();
let mut rest: &str = &text;
while let Some(start) = rest.find(BEGIN) {
let Some(end_rel) = rest[start..].find(END) else {
break;
};
let stop = start + end_rel + END.len();
blocks.push(rest[start..stop].to_string());
rest = &rest[stop..];
}
blocks
}
fn decrypt_encrypted_pem_key(block: &str, password: &str) -> Result<Vec<u8>, WalletError> {
let (label, doc) = der::Document::from_pem(block)
.map_err(|e| WalletError::Pem(format!("ENCRYPTED PRIVATE KEY block: {e}")))?;
if label != "ENCRYPTED PRIVATE KEY" {
return Err(WalletError::Pem(format!(
"expected ENCRYPTED PRIVATE KEY PEM label, got {label}"
)));
}
super::pfx::decrypt_encrypted_private_key_info(doc.as_bytes(), password.as_bytes())
}
fn pem_contains_legacy_encryption(pem: &[u8]) -> bool {
let mut reader = std::io::BufReader::new(pem);
let mut line = String::new();
while let Ok(n) = reader.read_line(&mut line) {
if n == 0 {
break;
}
if line.contains("Proc-Type: 4,ENCRYPTED") {
return true;
}
line.clear();
}
false
}
pub fn parse_pem_certificates(reader: &mut dyn BufRead) -> Vec<Vec<u8>> {
rustls_pemfile::certs(reader)
.filter_map(Result::ok)
.map(|der| der.as_ref().to_vec())
.collect()
}
pub fn read_ewallet_pem(
dir: &Path,
wallet_password: Option<&str>,
) -> Result<WalletContents, WalletError> {
let path = pem_wallet_path(dir);
if !path.exists() {
return Err(WalletError::FileMissing(path.display().to_string()));
}
let bytes = std::fs::read(&path).map_err(|source| WalletError::Io {
path: path.display().to_string(),
source,
})?;
parse_ewallet_pem(&bytes, wallet_password)
}
pub fn read_ewallet_p12(
dir: &Path,
wallet_password: Option<&str>,
) -> Result<WalletContents, WalletError> {
let path = p12_wallet_path(dir);
if !path.exists() {
return Err(WalletError::FileMissing(path.display().to_string()));
}
let bytes = std::fs::read(&path).map_err(|source| WalletError::Io {
path: path.display().to_string(),
source,
})?;
parse_ewallet_p12(&bytes, wallet_password)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_prefers_explicit_location() {
let dir = resolve_wallet_dir(Some("/wallets/db1"), Some("/etc/tns"));
assert_eq!(dir, Some(PathBuf::from("/wallets/db1")));
}
#[test]
fn resolve_system_means_no_wallet() {
assert_eq!(resolve_wallet_dir(Some("SYSTEM"), Some("/etc/tns")), None);
assert_eq!(resolve_wallet_dir(Some("system"), None), None);
}
#[test]
fn resolve_falls_back_to_tns_admin() {
assert_eq!(
resolve_wallet_dir(None, Some("/etc/tns")),
Some(PathBuf::from("/etc/tns"))
);
}
#[test]
fn resolve_none_when_nothing_set() {
assert_eq!(resolve_wallet_dir(None, None), None);
assert_eq!(resolve_wallet_dir(Some(""), None), None);
}
#[test]
fn parse_rejects_empty_pem() {
let err = parse_ewallet_pem(b"", None).unwrap_err();
assert!(matches!(err, WalletError::NoCertificates));
}
#[test]
fn wallet_errors_redact_paths_in_display_and_debug() {
let sensitive_path = "/private/wallet/ewallet.pem";
let err = WalletError::FileMissing(sensitive_path.to_string());
assert!(!format!("{err}").contains(sensitive_path));
assert!(!format!("{err:?}").contains(sensitive_path));
let err = WalletError::Io {
path: sensitive_path.to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
};
assert!(!format!("{err}").contains(sensitive_path));
assert!(!format!("{err:?}").contains(sensitive_path));
}
}