Skip to main content

bitwarden_uniffi/
lib.rs

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    /// Initialize a new instance of the SDK client
51    ///
52    /// `managed_settings` is the host-owned handle onto the operating system's Unified Endpoint
53    /// Management profile. The client shares its profile, so profiles pushed after construction
54    /// are visible here. Pass a fresh `ManagedSettingsBindingClient` where the host has no UEM
55    /// source.
56    #[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    /// Crypto operations
76    pub fn crypto(&self) -> CryptoClient {
77        CryptoClient(self.0.crypto())
78    }
79
80    /// Returns the key-management state bridge client used to register a
81    /// host-supplied storage implementation.
82    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    /// Returns the user-crypto-management sub-client (PIN settings, key rotation, etc).
89    pub fn user_crypto_management(
90        &self,
91    ) -> bitwarden_user_crypto_management::UserCryptoManagementClient {
92        self.0.user_crypto_management()
93    }
94
95    /// Key management operations that run on every sync.
96    pub fn crypto_sync_handler(&self) -> bitwarden_crypto_sync_handler::CryptoSyncHandlerClient {
97        self.0.crypto_sync_handler()
98    }
99
100    /// Vault item operations
101    pub fn vault(&self) -> VaultClient {
102        VaultClient(self.0.vault())
103    }
104
105    /// Collection related operations.
106    ///
107    /// This is registered directly on the top-level client in addition to being nested under
108    /// [`vault`](Self::vault). Once mobile clients have migrated to this accessor, the nested one
109    /// will be removed.
110    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    /// Generator operations
120    pub fn generators(&self) -> GeneratorClients {
121        GeneratorClients(self.0.generator())
122    }
123
124    /// Exporters
125    pub fn exporters(&self) -> ExporterClient {
126        ExporterClient(self.0.exporters())
127    }
128
129    /// Importers
130    pub fn importers(&self) -> ImporterClient {
131        ImporterClient(self.0.importers())
132    }
133
134    /// Sends operations
135    pub fn sends(&self) -> SendClient {
136        SendClient(self.0.sends())
137    }
138
139    /// Send sync handler operations
140    pub fn send_sync_handler(&self) -> bitwarden_send::SendSyncHandlerClient {
141        self.0.send_sync_handler()
142    }
143
144    /// SSH operations
145    pub fn ssh(&self) -> SshClient {
146        SshClient()
147    }
148
149    /// Random-number generation operations
150    pub fn random(&self) -> bitwarden_random::SdkRandomNumberClient {
151        bitwarden_random::SdkRandomNumberClient::new()
152    }
153
154    /// Auth operations
155    pub fn auth(&self) -> AuthClient {
156        AuthClient(self.0.0.clone())
157    }
158
159    /// Whether the client is in Gov Mode.
160    pub fn gov_mode(&self) -> bool {
161        self.0.0.gov_mode()
162    }
163
164    /// Policy operations
165    pub fn policies(&self) -> policies::PoliciesClient {
166        use bitwarden_policies::PoliciesClientExt;
167        policies::PoliciesClient(self.0.0.policies())
168    }
169
170    /// Test method, echoes back the input
171    pub fn echo(&self, msg: String) -> String {
172        msg
173    }
174
175    /// Test method, calls http endpoint
176    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/// Log level for SDK logging
191#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
192pub enum LogLevel {
193    /// Very detailed diagnostic information
194    Trace,
195    /// Diagnostic information for debugging
196    Debug,
197    /// Informational message
198    Info,
199    /// Potential problem that doesn't prevent the operation from completing
200    Warn,
201    /// Failure of an operation
202    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/// Initialize the SDK logger
230///
231/// This function should be called once before creating any SDK clients.
232/// It initializes the tracing infrastructure for the SDK and optionally
233/// registers a callback to receive log events.
234///
235/// # Parameters
236/// - `callback`: Optional callback to receive SDK log events. Pass `None` to use only platform
237///   loggers (oslog on iOS, logcat on Android).
238/// - `level`: Optional minimum log level; events below it are dropped. Defaults to `Info` if not
239///   specified. Can be overridden by the `RUST_LOG` environment variable at runtime or compile
240///   time.
241/// - `platform_logger`: Whether the SDK writes log events to the platform logger (oslog on iOS,
242///   logcat on Android, stdout elsewhere). Defaults to `true`. Disable it when `callback` already
243///   forwards events there, otherwise every event is logged twice.
244///
245/// # Example
246/// ```kotlin
247/// // Initialize with callback and trace-level logging before creating clients
248/// initLogger(FlightRecorderCallback(), LogLevel.TRACE)
249/// val client = Client(tokenProvider, settings, ManagedSettingsBindingClient())
250/// ```
251///
252/// # Notes
253/// - This function can only be called once - subsequent calls are ignored
254/// - If not called explicitly, logging is auto-initialized when first client is created
255#[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        // the log level prioritization is determined by:
265        //    1. if RUST_LOG is detected at runtime
266        //    2. if RUST_LOG is provided at compile time
267        //    3. the level parameter passed by the caller
268        //    4. default to INFO
269        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        // `Option<L>` is a `Layer`, so disabled sinks are attached as `None`
280        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        // Build base registry once instead of duplicating per-platform
290        let registry = tracing_subscriber::registry().with(fmtlayer).with(filter);
291
292        // Conditionally add callback layer if provided
293        // Use Option to avoid type incompatibility between Some/None branches
294        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
326/// Setup the error converter to ensure conversion errors don't cause panics
327/// Check [`bitwarden_uniffi_error`] for more details
328fn 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    // Mock token provider for testing
339    #[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    /// Mock LogCallback implementation for testing
349    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    // Log callback unit tests only test happy path because running this with
363    // Once means we get one registered callback per test run. There are
364    // other tests written as integration tests in the /tests/ folder that
365    // assert more specific details.
366    #[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        // Initialize logger with callback before creating client
372        init_logger(Some(callback), None, true);
373
374        // Create client
375        let _client = Client::new(
376            Arc::new(MockTokenProvider),
377            None,
378            Arc::new(ManagedSettingsBindingClient::new()),
379        );
380
381        // Trigger a log
382        tracing::info!("test message from SDK");
383
384        // Verify callback received it
385        let captured = logs.lock().expect("Failed to lock logs mutex");
386        assert!(!captured.is_empty(), "Callback should receive logs");
387
388        // Find our specific test log (there may be other SDK logs during init)
389        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}