oracledb_protocol/tls/
wallet.rs1use std::io::BufRead;
22use std::path::{Path, PathBuf};
23
24pub const PEM_WALLET_FILE_NAME: &str = "ewallet.pem";
26pub const SSO_WALLET_FILE_NAME: &str = "cwallet.sso";
28
29#[derive(thiserror::Error)]
31#[non_exhaustive]
32pub enum WalletError {
33 #[error("wallet file is missing")]
35 FileMissing(String),
36 #[error("failed to read wallet file: {source}")]
38 Io {
39 path: String,
40 #[source]
41 source: std::io::Error,
42 },
43 #[error("failed to parse wallet PEM: {0}")]
45 Pem(String),
46 #[error("wallet contained no certificates")]
48 NoCertificates,
49 #[error("cwallet.sso parse error: {0}")]
51 Sso(String),
52 #[error(
54 "cwallet.sso support is experimental and not enabled; rebuild with \
55 --features experimental, or convert the wallet to ewallet.pem"
56 )]
57 SsoNotEnabled,
58 #[error("wallet format {format} is not supported by this thin build")]
61 UnsupportedFormat { format: &'static str },
62}
63
64impl std::fmt::Debug for WalletError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 const REDACTED_PATH: &str = "***redacted***";
67 let redacted = |_: &String| REDACTED_PATH;
68 match self {
69 Self::FileMissing(path) => f.debug_tuple("FileMissing").field(&redacted(path)).finish(),
70 Self::Io { path, source } => f
71 .debug_struct("Io")
72 .field("path", &redacted(path))
73 .field("source", source)
74 .finish(),
75 Self::Pem(message) => f.debug_tuple("Pem").field(message).finish(),
76 Self::NoCertificates => f.write_str("NoCertificates"),
77 Self::Sso(message) => f.debug_tuple("Sso").field(message).finish(),
78 Self::SsoNotEnabled => f.write_str("SsoNotEnabled"),
79 Self::UnsupportedFormat { format } => f
80 .debug_struct("UnsupportedFormat")
81 .field("format", format)
82 .finish(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct WalletContents {
90 pub ca_certificates: Vec<Vec<u8>>,
92 pub client_cert_chain: Vec<Vec<u8>>,
95 pub client_private_key: Option<Vec<u8>>,
98}
99
100impl WalletContents {
101 #[must_use]
104 pub fn has_client_identity(&self) -> bool {
105 !self.client_cert_chain.is_empty() && self.client_private_key.is_some()
106 }
107}
108
109#[must_use]
121pub fn resolve_wallet_dir(
122 wallet_location: Option<&str>,
123 tns_admin: Option<&str>,
124) -> Option<PathBuf> {
125 if let Some(loc) = wallet_location {
126 if !loc.is_empty() && !loc.eq_ignore_ascii_case("SYSTEM") {
127 return Some(PathBuf::from(loc));
128 }
129 if loc.eq_ignore_ascii_case("SYSTEM") {
131 return None;
132 }
133 }
134 tns_admin.filter(|s| !s.is_empty()).map(PathBuf::from)
135}
136
137#[must_use]
139pub fn pem_wallet_path(dir: &Path) -> PathBuf {
140 dir.join(PEM_WALLET_FILE_NAME)
141}
142
143#[must_use]
145pub fn sso_wallet_path(dir: &Path) -> PathBuf {
146 dir.join(SSO_WALLET_FILE_NAME)
147}
148
149pub fn parse_ewallet_pem(
167 pem: &[u8],
168 _wallet_password: Option<&str>,
169) -> Result<WalletContents, WalletError> {
170 let mut reader = std::io::BufReader::new(pem);
171 let mut contents = WalletContents::default();
172 let mut all_certs: Vec<Vec<u8>> = Vec::new();
173 let mut keys: Vec<Vec<u8>> = Vec::new();
174 let mut saw_encrypted_key = false;
175
176 loop {
177 match rustls_pemfile::read_one(&mut reader) {
178 Ok(Some(item)) => match item {
179 rustls_pemfile::Item::X509Certificate(der) => {
180 all_certs.push(der.as_ref().to_vec());
181 }
182 rustls_pemfile::Item::Pkcs8Key(der) => {
183 keys.push(der.secret_pkcs8_der().to_vec());
184 }
185 rustls_pemfile::Item::Pkcs1Key(der) => {
186 keys.push(der.secret_pkcs1_der().to_vec());
187 }
188 rustls_pemfile::Item::Sec1Key(der) => {
189 keys.push(der.secret_sec1_der().to_vec());
190 }
191 _ => {}
195 },
196 Ok(None) => break,
197 Err(e) => return Err(WalletError::Pem(e.to_string())),
198 }
199 }
200
201 if keys.is_empty() && pem_contains_encrypted_key(pem) {
205 saw_encrypted_key = true;
206 }
207
208 if all_certs.is_empty() {
209 return Err(WalletError::NoCertificates);
210 }
211
212 contents.ca_certificates = all_certs.clone();
215
216 if let Some(key) = keys.into_iter().next() {
220 contents.client_cert_chain = all_certs;
221 contents.client_private_key = Some(key);
222 } else if saw_encrypted_key {
223 return Err(WalletError::Pem(
224 "wallet private key is encrypted; supply a wallet with an \
225 unencrypted ewallet.pem (orapki ... -auto_login) or use cwallet.sso"
226 .to_string(),
227 ));
228 }
229
230 Ok(contents)
231}
232
233pub fn parse_pem_certificates(reader: &mut dyn BufRead) -> Vec<Vec<u8>> {
238 rustls_pemfile::certs(reader)
239 .filter_map(Result::ok)
240 .map(|der| der.as_ref().to_vec())
241 .collect()
242}
243
244fn pem_contains_encrypted_key(pem: &[u8]) -> bool {
246 let mut reader = std::io::BufReader::new(pem);
247 let mut line = String::new();
248 while let Ok(n) = reader.read_line(&mut line) {
249 if n == 0 {
250 break;
251 }
252 if line.contains("ENCRYPTED PRIVATE KEY") || line.contains("Proc-Type: 4,ENCRYPTED") {
253 return true;
254 }
255 line.clear();
256 }
257 false
258}
259
260pub fn read_ewallet_pem(
267 dir: &Path,
268 wallet_password: Option<&str>,
269) -> Result<WalletContents, WalletError> {
270 let path = pem_wallet_path(dir);
271 if !path.exists() {
272 return Err(WalletError::FileMissing(path.display().to_string()));
273 }
274 let bytes = std::fs::read(&path).map_err(|source| WalletError::Io {
275 path: path.display().to_string(),
276 source,
277 })?;
278 parse_ewallet_pem(&bytes, wallet_password)
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn resolve_prefers_explicit_location() {
287 let dir = resolve_wallet_dir(Some("/wallets/db1"), Some("/etc/tns"));
288 assert_eq!(dir, Some(PathBuf::from("/wallets/db1")));
289 }
290
291 #[test]
292 fn resolve_system_means_no_wallet() {
293 assert_eq!(resolve_wallet_dir(Some("SYSTEM"), Some("/etc/tns")), None);
294 assert_eq!(resolve_wallet_dir(Some("system"), None), None);
295 }
296
297 #[test]
298 fn resolve_falls_back_to_tns_admin() {
299 assert_eq!(
300 resolve_wallet_dir(None, Some("/etc/tns")),
301 Some(PathBuf::from("/etc/tns"))
302 );
303 }
304
305 #[test]
306 fn resolve_none_when_nothing_set() {
307 assert_eq!(resolve_wallet_dir(None, None), None);
308 assert_eq!(resolve_wallet_dir(Some(""), None), None);
309 }
310
311 #[test]
312 fn parse_rejects_empty_pem() {
313 let err = parse_ewallet_pem(b"", None).unwrap_err();
314 assert!(matches!(err, WalletError::NoCertificates));
315 }
316
317 #[test]
318 fn wallet_errors_redact_paths_in_display_and_debug() {
319 let sensitive_path = "/private/wallet/ewallet.pem";
320 let err = WalletError::FileMissing(sensitive_path.to_string());
321 assert!(!format!("{err}").contains(sensitive_path));
322 assert!(!format!("{err:?}").contains(sensitive_path));
323
324 let err = WalletError::Io {
325 path: sensitive_path.to_string(),
326 source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
327 };
328 assert!(!format!("{err}").contains(sensitive_path));
329 assert!(!format!("{err:?}").contains(sensitive_path));
330 }
331}