1#![doc = include_str!("../README.md")]
2
3uniffi::setup_scaffolding!();
4
5use std::sync::{Arc, Once};
6
7use auth::AuthClient;
8use bitwarden_core::{ClientSettings, auth::ClientManagedTokens};
9
10#[allow(missing_docs)]
11pub mod auth;
12#[allow(missing_docs)]
13pub mod crypto;
14#[allow(missing_docs)]
15pub mod error;
16mod log_callback;
17#[allow(missing_docs)]
18pub mod managed_settings;
19#[allow(missing_docs)]
20pub mod platform;
21#[allow(missing_docs)]
22pub mod policies;
23#[allow(missing_docs)]
24pub mod tools;
25mod uniffi_support;
26#[allow(missing_docs)]
27pub mod vault;
28
29#[cfg(target_os = "android")]
30mod android_support;
31
32use crypto::CryptoClient;
33use error::{Error, Result};
34pub use log_callback::LogCallback;
35pub use managed_settings::ManagedSettingsBindingClient;
36use platform::PlatformClient;
37pub use platform::{
38 AcquiredCookie, BootstrapConfig, ServerCommunicationConfig, ServerCommunicationConfigClient,
39 ServerCommunicationConfigRepository, SsoCookieVendorConfig,
40};
41use tools::{ExporterClient, GeneratorClients, ImporterClient, SendClient, SshClient};
42use vault::VaultClient;
43
44#[allow(missing_docs)]
45#[derive(uniffi::Object)]
46pub struct Client(pub(crate) bitwarden_pm::PasswordManagerClient);
47
48#[uniffi::export(async_runtime = "tokio")]
49impl Client {
50 #[uniffi::constructor]
57 pub fn new(
58 token_provider: Arc<dyn ClientManagedTokens>,
59 settings: Option<ClientSettings>,
60 managed_settings: Arc<ManagedSettingsBindingClient>,
61 ) -> Self {
62 init_logger(None, None, true);
63 setup_error_converter();
64
65 #[cfg(target_os = "android")]
66 android_support::init();
67
68 Self(bitwarden_pm::PasswordManagerClient::new_with_client_tokens(
69 settings,
70 token_provider,
71 &managed_settings.0,
72 ))
73 }
74
75 pub fn crypto(&self) -> CryptoClient {
77 CryptoClient(self.0.crypto())
78 }
79
80 pub fn km_state_bridge(
83 &self,
84 ) -> bitwarden_core::key_management::state_bridge::StateBridgeClient {
85 self.0.0.km_state_bridge()
86 }
87
88 pub fn user_crypto_management(
90 &self,
91 ) -> bitwarden_user_crypto_management::UserCryptoManagementClient {
92 self.0.user_crypto_management()
93 }
94
95 pub fn crypto_sync_handler(&self) -> bitwarden_crypto_sync_handler::CryptoSyncHandlerClient {
97 self.0.crypto_sync_handler()
98 }
99
100 pub fn vault(&self) -> VaultClient {
102 VaultClient(self.0.vault())
103 }
104
105 pub fn collections(&self) -> vault::collections::CollectionsClient {
111 vault::collections::CollectionsClient(self.0.collections())
112 }
113
114 #[allow(missing_docs)]
115 pub fn platform(&self) -> PlatformClient {
116 PlatformClient(self.0.0.clone())
117 }
118
119 pub fn generators(&self) -> GeneratorClients {
121 GeneratorClients(self.0.generator())
122 }
123
124 pub fn exporters(&self) -> ExporterClient {
126 ExporterClient(self.0.exporters())
127 }
128
129 pub fn importers(&self) -> ImporterClient {
131 ImporterClient(self.0.importers())
132 }
133
134 pub fn sends(&self) -> SendClient {
136 SendClient(self.0.sends())
137 }
138
139 pub fn send_sync_handler(&self) -> bitwarden_send::SendSyncHandlerClient {
141 self.0.send_sync_handler()
142 }
143
144 pub fn ssh(&self) -> SshClient {
146 SshClient()
147 }
148
149 pub fn random(&self) -> bitwarden_random::SdkRandomNumberClient {
151 bitwarden_random::SdkRandomNumberClient::new()
152 }
153
154 pub fn auth(&self) -> AuthClient {
156 AuthClient(self.0.0.clone())
157 }
158
159 pub fn gov_mode(&self) -> bool {
161 self.0.0.gov_mode()
162 }
163
164 pub fn policies(&self) -> policies::PoliciesClient {
166 use bitwarden_policies::PoliciesClientExt;
167 policies::PoliciesClient(self.0.0.policies())
168 }
169
170 pub fn echo(&self, msg: String) -> String {
172 msg
173 }
174
175 pub async fn http_get(&self, url: String) -> Result<String> {
177 let client = self.0.0.internal.get_http_client();
178 let res = client
179 .get(&url)
180 .send()
181 .await
182 .map_err(|e| Error::Api(e.into()))?;
183
184 res.text().await.map_err(|e| Error::Api(e.into()))
185 }
186}
187
188static INIT: Once = Once::new();
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
192pub enum LogLevel {
193 Trace,
195 Debug,
197 Info,
199 Warn,
201 Error,
203}
204
205impl LogLevel {
206 fn as_str(&self) -> &'static str {
207 match self {
208 LogLevel::Trace => "trace",
209 LogLevel::Debug => "debug",
210 LogLevel::Info => "info",
211 LogLevel::Warn => "warn",
212 LogLevel::Error => "error",
213 }
214 }
215}
216
217impl From<&tracing::Level> for LogLevel {
218 fn from(level: &tracing::Level) -> Self {
219 match *level {
220 tracing::Level::TRACE => LogLevel::Trace,
221 tracing::Level::DEBUG => LogLevel::Debug,
222 tracing::Level::INFO => LogLevel::Info,
223 tracing::Level::WARN => LogLevel::Warn,
224 tracing::Level::ERROR => LogLevel::Error,
225 }
226 }
227}
228
229#[uniffi::export(default(platform_logger = true))]
256pub fn init_logger(
257 callback: Option<Arc<dyn LogCallback>>,
258 level: Option<LogLevel>,
259 platform_logger: bool,
260) {
261 use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _};
262
263 INIT.call_once(|| {
264 let level = level.as_ref().map(|l| l.as_str()).unwrap_or("info");
270 let filter = EnvFilter::builder()
271 .with_default_directive(
272 option_env!("RUST_LOG")
273 .unwrap_or(level)
274 .parse()
275 .expect("should provide valid log level at compile time."),
276 )
277 .from_env_lossy();
278
279 let fmtlayer = platform_logger.then(|| {
281 tracing_subscriber::fmt::layer()
282 .with_ansi(true)
283 .with_file(true)
284 .with_line_number(true)
285 .with_target(true)
286 .pretty()
287 });
288
289 let registry = tracing_subscriber::registry().with(fmtlayer).with(filter);
291
292 let callback_layer = callback.map(log_callback::CallbackLayer::new);
295 let registry = registry.with(callback_layer);
296 #[cfg(target_os = "ios")]
297 {
298 const TAG: &str = "com.8bit.bitwarden";
299 registry
300 .with(platform_logger.then(|| tracing_oslog::OsLogger::new(TAG, "default")))
301 .init();
302 }
303
304 #[cfg(target_os = "android")]
305 {
306 const TAG: &str = "com.bitwarden.sdk";
307 registry
308 .with(platform_logger.then(|| {
309 tracing_android::layer(TAG)
310 .expect("initialization of android logcat tracing layer")
311 }))
312 .init();
313 }
314
315 #[cfg(not(any(target_os = "android", target_os = "ios")))]
316 {
317 registry.init();
318 }
319 #[cfg(feature = "dangerous-crypto-debug")]
320 tracing::warn!(
321 "Dangerous crypto debug features are enabled. THIS MUST NOT BE USED IN PRODUCTION BUILDS!!"
322 );
323 });
324}
325
326fn setup_error_converter() {
329 bitwarden_uniffi_error::set_error_to_uniffi_error(|e| {
330 crate::error::BitwardenError::Conversion(e.to_string()).into()
331 });
332}
333#[cfg(test)]
334mod tests {
335 use std::sync::Mutex;
336
337 use super::*;
338 #[derive(Debug)]
340 struct MockTokenProvider;
341
342 #[async_trait::async_trait]
343 impl ClientManagedTokens for MockTokenProvider {
344 async fn get_access_token(&self) -> Option<String> {
345 Some("mock_token".to_string())
346 }
347 }
348 struct TestLogCallback {
350 logs: Arc<Mutex<Vec<(LogLevel, String, String)>>>,
351 }
352 impl LogCallback for TestLogCallback {
353 fn on_log(&self, level: LogLevel, target: String, message: String) -> Result<()> {
354 self.logs
355 .lock()
356 .expect("Failed to lock logs mutex")
357 .push((level, target, message));
358 Ok(())
359 }
360 }
361
362 #[test]
367 fn test_callback_receives_logs() {
368 let logs = Arc::new(Mutex::new(Vec::new()));
369 let callback = Arc::new(TestLogCallback { logs: logs.clone() });
370
371 init_logger(Some(callback), None, true);
373
374 let _client = Client::new(
376 Arc::new(MockTokenProvider),
377 None,
378 Arc::new(ManagedSettingsBindingClient::new()),
379 );
380
381 tracing::info!("test message from SDK");
383
384 let captured = logs.lock().expect("Failed to lock logs mutex");
386 assert!(!captured.is_empty(), "Callback should receive logs");
387
388 let test_log = captured
390 .iter()
391 .find(|(_, _, msg)| msg.contains("test message"))
392 .expect("Should find our test log message");
393
394 assert_eq!(test_log.0, LogLevel::Info);
395 assert!(test_log.2.contains("test message"));
396 }
397}