1use std::io::BufRead;
27use std::path::{Path, PathBuf};
28
29pub const PEM_WALLET_FILE_NAME: &str = "ewallet.pem";
31pub const P12_WALLET_FILE_NAME: &str = "ewallet.p12";
33pub const SSO_WALLET_FILE_NAME: &str = "cwallet.sso";
35
36#[derive(thiserror::Error)]
38#[non_exhaustive]
39pub enum WalletError {
40 #[error("wallet file is missing")]
42 FileMissing(String),
43 #[error("failed to read wallet file: {source}")]
45 Io {
46 path: String,
47 #[source]
48 source: std::io::Error,
49 },
50 #[error("failed to parse wallet PEM: {0}")]
52 Pem(String),
53 #[error("wallet contained no certificates")]
55 NoCertificates,
56 #[error("cwallet.sso parse error: {0}")]
58 Sso(String),
59 #[error(
63 "cwallet.sso support is not enabled in this build; convert the wallet \
64 to ewallet.pem"
65 )]
66 SsoNotEnabled,
67 #[error("PKCS#12 wallet parse error: {0}")]
71 Pkcs12(String),
72 #[error("wallet private key decryption failed: {0}")]
76 KeyDecrypt(String),
77 #[error(
82 "wallet {format} is encrypted and requires a wallet password; supply \
83 wallet_password (or use an auto-login cwallet.sso or unencrypted \
84 ewallet.pem wallet)"
85 )]
86 PasswordRequired { format: &'static str },
87 #[error("wallet format {format} is not supported by this thin build")]
90 UnsupportedFormat { format: &'static str },
91}
92
93impl std::fmt::Debug for WalletError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 const REDACTED_PATH: &str = "***redacted***";
96 let redacted = |_: &String| REDACTED_PATH;
97 match self {
98 Self::FileMissing(path) => f.debug_tuple("FileMissing").field(&redacted(path)).finish(),
99 Self::Io { path, source } => f
100 .debug_struct("Io")
101 .field("path", &redacted(path))
102 .field("source", source)
103 .finish(),
104 Self::Pem(message) => f.debug_tuple("Pem").field(message).finish(),
105 Self::NoCertificates => f.write_str("NoCertificates"),
106 Self::Sso(message) => f.debug_tuple("Sso").field(message).finish(),
107 Self::SsoNotEnabled => f.write_str("SsoNotEnabled"),
108 Self::Pkcs12(message) => f.debug_tuple("Pkcs12").field(message).finish(),
109 Self::KeyDecrypt(message) => f.debug_tuple("KeyDecrypt").field(message).finish(),
110 Self::PasswordRequired { format } => f
111 .debug_struct("PasswordRequired")
112 .field("format", format)
113 .finish(),
114 Self::UnsupportedFormat { format } => f
115 .debug_struct("UnsupportedFormat")
116 .field("format", format)
117 .finish(),
118 }
119 }
120}
121
122#[derive(Debug, Clone, Default)]
124pub struct WalletContents {
125 pub ca_certificates: Vec<Vec<u8>>,
127 pub client_cert_chain: Vec<Vec<u8>>,
130 pub client_private_key: Option<Vec<u8>>,
133}
134
135impl WalletContents {
136 #[must_use]
139 pub fn has_client_identity(&self) -> bool {
140 !self.client_cert_chain.is_empty() && self.client_private_key.is_some()
141 }
142}
143
144#[must_use]
156pub fn resolve_wallet_dir(
157 wallet_location: Option<&str>,
158 tns_admin: Option<&str>,
159) -> Option<PathBuf> {
160 if let Some(loc) = wallet_location {
161 if !loc.is_empty() && !loc.eq_ignore_ascii_case("SYSTEM") {
162 return Some(PathBuf::from(loc));
163 }
164 if loc.eq_ignore_ascii_case("SYSTEM") {
166 return None;
167 }
168 }
169 tns_admin.filter(|s| !s.is_empty()).map(PathBuf::from)
170}
171
172#[must_use]
174pub fn pem_wallet_path(dir: &Path) -> PathBuf {
175 dir.join(PEM_WALLET_FILE_NAME)
176}
177
178#[must_use]
180pub fn p12_wallet_path(dir: &Path) -> PathBuf {
181 dir.join(P12_WALLET_FILE_NAME)
182}
183
184#[must_use]
186pub fn sso_wallet_path(dir: &Path) -> PathBuf {
187 dir.join(SSO_WALLET_FILE_NAME)
188}
189
190pub fn parse_ewallet_pem(
212 pem: &[u8],
213 wallet_password: Option<&str>,
214) -> Result<WalletContents, WalletError> {
215 if pem_contains_legacy_encryption(pem) {
219 return Err(WalletError::KeyDecrypt(
220 "legacy OpenSSL PEM encryption (Proc-Type: 4,ENCRYPTED) is not \
221 supported; re-export the key as PKCS#8 with \
222 `openssl pkcs8 -topk8` (optionally encrypted, then supply \
223 wallet_password)"
224 .to_string(),
225 ));
226 }
227
228 let mut reader = std::io::BufReader::new(pem);
229 let mut contents = WalletContents::default();
230 let mut all_certs: Vec<Vec<u8>> = Vec::new();
231 let mut keys: Vec<Vec<u8>> = Vec::new();
232
233 loop {
234 match rustls_pemfile::read_one(&mut reader) {
235 Ok(Some(item)) => match item {
236 rustls_pemfile::Item::X509Certificate(der) => {
237 all_certs.push(der.as_ref().to_vec());
238 }
239 rustls_pemfile::Item::Pkcs8Key(der) => {
240 keys.push(der.secret_pkcs8_der().to_vec());
241 }
242 rustls_pemfile::Item::Pkcs1Key(der) => {
243 keys.push(der.secret_pkcs1_der().to_vec());
244 }
245 rustls_pemfile::Item::Sec1Key(der) => {
246 keys.push(der.secret_sec1_der().to_vec());
247 }
248 _ => {}
251 },
252 Ok(None) => break,
253 Err(e) => return Err(WalletError::Pem(e.to_string())),
254 }
255 }
256
257 if all_certs.is_empty() {
258 return Err(WalletError::NoCertificates);
259 }
260
261 if keys.is_empty() {
263 let encrypted_blocks = extract_encrypted_key_pem_blocks(pem);
264 if !encrypted_blocks.is_empty() {
265 let Some(password) = wallet_password else {
266 return Err(WalletError::PasswordRequired {
267 format: PEM_WALLET_FILE_NAME,
268 });
269 };
270 let block = &encrypted_blocks[0];
274 keys.push(decrypt_encrypted_pem_key(block, password)?);
275 }
276 }
277
278 contents.ca_certificates = all_certs.clone();
281
282 if let Some(key) = keys.into_iter().next() {
286 contents.client_cert_chain = all_certs;
287 contents.client_private_key = Some(key);
288 }
289
290 Ok(contents)
291}
292
293pub fn parse_ewallet_p12(
307 data: &[u8],
308 wallet_password: Option<&str>,
309) -> Result<WalletContents, WalletError> {
310 let Some(password) = wallet_password else {
311 return Err(WalletError::PasswordRequired {
312 format: P12_WALLET_FILE_NAME,
313 });
314 };
315 super::pfx::parse_pfx(data, password.as_bytes())
316}
317
318fn extract_encrypted_key_pem_blocks(pem: &[u8]) -> Vec<String> {
320 const BEGIN: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
321 const END: &str = "-----END ENCRYPTED PRIVATE KEY-----";
322 let text = String::from_utf8_lossy(pem);
323 let mut blocks = Vec::new();
324 let mut rest: &str = &text;
325 while let Some(start) = rest.find(BEGIN) {
326 let Some(end_rel) = rest[start..].find(END) else {
327 break;
328 };
329 let stop = start + end_rel + END.len();
330 blocks.push(rest[start..stop].to_string());
331 rest = &rest[stop..];
332 }
333 blocks
334}
335
336fn decrypt_encrypted_pem_key(block: &str, password: &str) -> Result<Vec<u8>, WalletError> {
339 let (label, doc) = der::Document::from_pem(block)
340 .map_err(|e| WalletError::Pem(format!("ENCRYPTED PRIVATE KEY block: {e}")))?;
341 if label != "ENCRYPTED PRIVATE KEY" {
342 return Err(WalletError::Pem(format!(
343 "expected ENCRYPTED PRIVATE KEY PEM label, got {label}"
344 )));
345 }
346 super::pfx::decrypt_encrypted_private_key_info(doc.as_bytes(), password.as_bytes())
347}
348
349fn pem_contains_legacy_encryption(pem: &[u8]) -> bool {
351 let mut reader = std::io::BufReader::new(pem);
352 let mut line = String::new();
353 while let Ok(n) = reader.read_line(&mut line) {
354 if n == 0 {
355 break;
356 }
357 if line.contains("Proc-Type: 4,ENCRYPTED") {
358 return true;
359 }
360 line.clear();
361 }
362 false
363}
364
365pub fn parse_pem_certificates(reader: &mut dyn BufRead) -> Vec<Vec<u8>> {
370 rustls_pemfile::certs(reader)
371 .filter_map(Result::ok)
372 .map(|der| der.as_ref().to_vec())
373 .collect()
374}
375
376pub fn read_ewallet_pem(
383 dir: &Path,
384 wallet_password: Option<&str>,
385) -> Result<WalletContents, WalletError> {
386 let path = pem_wallet_path(dir);
387 if !path.exists() {
388 return Err(WalletError::FileMissing(path.display().to_string()));
389 }
390 let bytes = std::fs::read(&path).map_err(|source| WalletError::Io {
391 path: path.display().to_string(),
392 source,
393 })?;
394 parse_ewallet_pem(&bytes, wallet_password)
395}
396
397pub fn read_ewallet_p12(
404 dir: &Path,
405 wallet_password: Option<&str>,
406) -> Result<WalletContents, WalletError> {
407 let path = p12_wallet_path(dir);
408 if !path.exists() {
409 return Err(WalletError::FileMissing(path.display().to_string()));
410 }
411 let bytes = std::fs::read(&path).map_err(|source| WalletError::Io {
412 path: path.display().to_string(),
413 source,
414 })?;
415 parse_ewallet_p12(&bytes, wallet_password)
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn resolve_prefers_explicit_location() {
424 let dir = resolve_wallet_dir(Some("/wallets/db1"), Some("/etc/tns"));
425 assert_eq!(dir, Some(PathBuf::from("/wallets/db1")));
426 }
427
428 #[test]
429 fn resolve_system_means_no_wallet() {
430 assert_eq!(resolve_wallet_dir(Some("SYSTEM"), Some("/etc/tns")), None);
431 assert_eq!(resolve_wallet_dir(Some("system"), None), None);
432 }
433
434 #[test]
435 fn resolve_falls_back_to_tns_admin() {
436 assert_eq!(
437 resolve_wallet_dir(None, Some("/etc/tns")),
438 Some(PathBuf::from("/etc/tns"))
439 );
440 }
441
442 #[test]
443 fn resolve_none_when_nothing_set() {
444 assert_eq!(resolve_wallet_dir(None, None), None);
445 assert_eq!(resolve_wallet_dir(Some(""), None), None);
446 }
447
448 #[test]
449 fn parse_rejects_empty_pem() {
450 let err = parse_ewallet_pem(b"", None).unwrap_err();
451 assert!(matches!(err, WalletError::NoCertificates));
452 }
453
454 #[test]
455 fn wallet_errors_redact_paths_in_display_and_debug() {
456 let sensitive_path = "/private/wallet/ewallet.pem";
457 let err = WalletError::FileMissing(sensitive_path.to_string());
458 assert!(!format!("{err}").contains(sensitive_path));
459 assert!(!format!("{err:?}").contains(sensitive_path));
460
461 let err = WalletError::Io {
462 path: sensitive_path.to_string(),
463 source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
464 };
465 assert!(!format!("{err}").contains(sensitive_path));
466 assert!(!format!("{err:?}").contains(sensitive_path));
467 }
468}