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/// Dev-only debug-capability tree, exposed to the mobile bindings. Compiled only
15/// under the `debug-capabilities` feature; never ship in production.
16#[cfg(feature = "debug-capabilities")]
17pub mod debug;
18#[allow(missing_docs)]
19pub mod error;
20mod log_callback;
21#[allow(missing_docs)]
22pub mod managed_settings;
23#[allow(missing_docs)]
24pub mod platform;
25#[allow(missing_docs)]
26pub mod policies;
27#[allow(missing_docs)]
28pub mod tools;
29mod uniffi_support;
30#[allow(missing_docs)]
31pub mod vault;
32
33#[cfg(target_os = "android")]
34mod android_support;
35
36use crypto::CryptoClient;
37use error::{Error, Result};
38pub use log_callback::LogCallback;
39pub use managed_settings::ManagedSettingsBindingClient;
40use platform::PlatformClient;
41pub use platform::{
42    AcquiredCookie, BootstrapConfig, ServerCommunicationConfig, ServerCommunicationConfigClient,
43    ServerCommunicationConfigRepository, SsoCookieVendorConfig,
44};
45use tools::{ExporterClient, GeneratorClients, ImporterClient, SendClient, SshClient};
46use vault::VaultClient;
47
48#[allow(missing_docs)]
49#[derive(uniffi::Object)]
50pub struct Client(pub(crate) bitwarden_pm::PasswordManagerClient);
51
52#[uniffi::export(async_runtime = "tokio")]
53impl Client {
54    /// Initialize a new instance of the SDK client
55    ///
56    /// `managed_settings` is the host-owned handle onto the operating system's Unified Endpoint
57    /// Management profile. The client shares its profile, so profiles pushed after construction
58    /// are visible here. Pass a fresh `ManagedSettingsBindingClient` where the host has no UEM
59    /// source.
60    #[uniffi::constructor]
61    pub fn new(
62        token_provider: Arc<dyn ClientManagedTokens>,
63        settings: Option<ClientSettings>,
64        managed_settings: Arc<ManagedSettingsBindingClient>,
65    ) -> Self {
66        init_logger(None, None, true);
67        setup_error_converter();
68
69        #[cfg(target_os = "android")]
70        android_support::init();
71
72        Self(bitwarden_pm::PasswordManagerClient::new_with_client_tokens(
73            settings,
74            token_provider,
75            &managed_settings.0,
76        ))
77    }
78
79    /// Crypto operations
80    pub fn crypto(&self) -> CryptoClient {
81        CryptoClient(self.0.crypto())
82    }
83
84    /// Returns the key-management state bridge client used to register a
85    /// host-supplied storage implementation.
86    pub fn km_state_bridge(
87        &self,
88    ) -> bitwarden_core::key_management::state_bridge::StateBridgeClient {
89        self.0.0.km_state_bridge()
90    }
91
92    /// Returns the user-crypto-management sub-client (PIN settings, key rotation, etc).
93    pub fn user_crypto_management(
94        &self,
95    ) -> bitwarden_user_crypto_management::UserCryptoManagementClient {
96        self.0.user_crypto_management()
97    }
98
99    /// Unlock related operations.
100    pub fn unlock(&self) -> bitwarden_unlock::UnlockClient {
101        self.0.unlock()
102    }
103
104    /// Key management operations that run on every sync.
105    pub fn crypto_sync_handler(&self) -> bitwarden_crypto_sync_handler::CryptoSyncHandlerClient {
106        self.0.crypto_sync_handler()
107    }
108
109    /// Vault item operations
110    pub fn vault(&self) -> VaultClient {
111        VaultClient(self.0.vault())
112    }
113
114    /// Collection related operations.
115    ///
116    /// This is registered directly on the top-level client in addition to being nested under
117    /// [`vault`](Self::vault). Once mobile clients have migrated to this accessor, the nested one
118    /// will be removed.
119    pub fn collections(&self) -> vault::collections::CollectionsClient {
120        vault::collections::CollectionsClient(self.0.collections())
121    }
122
123    #[allow(missing_docs)]
124    pub fn platform(&self) -> PlatformClient {
125        PlatformClient(self.0.0.clone())
126    }
127
128    /// Generator operations
129    pub fn generators(&self) -> GeneratorClients {
130        GeneratorClients(self.0.generator())
131    }
132
133    /// Exporters
134    pub fn exporters(&self) -> ExporterClient {
135        ExporterClient(self.0.exporters())
136    }
137
138    /// Importers
139    pub fn importers(&self) -> ImporterClient {
140        ImporterClient(self.0.importers())
141    }
142
143    /// Sends operations
144    pub fn sends(&self) -> SendClient {
145        SendClient(self.0.sends())
146    }
147
148    /// Send sync handler operations
149    pub fn send_sync_handler(&self) -> bitwarden_send::SendSyncHandlerClient {
150        self.0.send_sync_handler()
151    }
152
153    /// SSH operations
154    pub fn ssh(&self) -> SshClient {
155        SshClient()
156    }
157
158    /// Random-number generation operations
159    pub fn random(&self) -> bitwarden_random::SdkRandomNumberClient {
160        bitwarden_random::SdkRandomNumberClient::new()
161    }
162
163    /// Auth operations
164    pub fn auth(&self) -> AuthClient {
165        AuthClient(self.0.0.clone())
166    }
167
168    /// Whether the client is in Gov Mode.
169    pub fn gov_mode(&self) -> bool {
170        self.0.0.gov_mode()
171    }
172
173    /// Policy operations
174    pub fn policies(&self) -> policies::PoliciesClient {
175        use bitwarden_policies::PoliciesClientExt;
176        policies::PoliciesClient(self.0.0.policies())
177    }
178
179    /// Test method, echoes back the input
180    pub fn echo(&self, msg: String) -> String {
181        msg
182    }
183
184    /// Test method, calls http endpoint
185    pub async fn http_get(&self, url: String) -> Result<String> {
186        let client = self.0.0.internal.get_http_client();
187        let res = client
188            .get(&url)
189            .send()
190            .await
191            .map_err(|e| Error::Api(e.into()))?;
192
193        res.text().await.map_err(|e| Error::Api(e.into()))
194    }
195}
196
197#[cfg(feature = "debug-capabilities")]
198#[uniffi::export]
199impl Client {
200    /// Dev-only debug capabilities that reach past the public API into internal
201    /// state, for automated tooling. Available only when built with the
202    /// `debug-capabilities` feature, which must never be enabled in production.
203    pub fn debug(&self) -> debug::DebugClient {
204        debug::DebugClient(self.0.debug())
205    }
206}
207
208static INIT: Once = Once::new();
209
210/// Log level for SDK logging
211#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
212pub enum LogLevel {
213    /// Very detailed diagnostic information
214    Trace,
215    /// Diagnostic information for debugging
216    Debug,
217    /// Informational message
218    Info,
219    /// Potential problem that doesn't prevent the operation from completing
220    Warn,
221    /// Failure of an operation
222    Error,
223}
224
225impl LogLevel {
226    fn as_str(&self) -> &'static str {
227        match self {
228            LogLevel::Trace => "trace",
229            LogLevel::Debug => "debug",
230            LogLevel::Info => "info",
231            LogLevel::Warn => "warn",
232            LogLevel::Error => "error",
233        }
234    }
235}
236
237impl From<&tracing::Level> for LogLevel {
238    fn from(level: &tracing::Level) -> Self {
239        match *level {
240            tracing::Level::TRACE => LogLevel::Trace,
241            tracing::Level::DEBUG => LogLevel::Debug,
242            tracing::Level::INFO => LogLevel::Info,
243            tracing::Level::WARN => LogLevel::Warn,
244            tracing::Level::ERROR => LogLevel::Error,
245        }
246    }
247}
248
249/// Initialize the SDK logger
250///
251/// This function should be called once before creating any SDK clients.
252/// It initializes the tracing infrastructure for the SDK and optionally
253/// registers a callback to receive log events.
254///
255/// # Parameters
256/// - `callback`: Optional callback to receive SDK log events. Pass `None` to use only platform
257///   loggers (oslog on iOS, logcat on Android).
258/// - `level`: Optional minimum log level; events below it are dropped. Defaults to `Info` if not
259///   specified. Can be overridden by the `RUST_LOG` environment variable at runtime or compile
260///   time.
261/// - `platform_logger`: Whether the SDK writes log events to the platform logger (oslog on iOS,
262///   logcat on Android, stdout elsewhere). Defaults to `true`. Disable it when `callback` already
263///   forwards events there, otherwise every event is logged twice.
264///
265/// # Example
266/// ```kotlin
267/// // Initialize with callback and trace-level logging before creating clients
268/// initLogger(FlightRecorderCallback(), LogLevel.TRACE)
269/// val client = Client(tokenProvider, settings, ManagedSettingsBindingClient())
270/// ```
271///
272/// # Notes
273/// - This function can only be called once - subsequent calls are ignored
274/// - If not called explicitly, logging is auto-initialized when first client is created
275#[uniffi::export(default(platform_logger = true))]
276pub fn init_logger(
277    callback: Option<Arc<dyn LogCallback>>,
278    level: Option<LogLevel>,
279    platform_logger: bool,
280) {
281    use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _};
282
283    INIT.call_once(|| {
284        // the log level prioritization is determined by:
285        //    1. if RUST_LOG is detected at runtime
286        //    2. if RUST_LOG is provided at compile time
287        //    3. the level parameter passed by the caller
288        //    4. default to INFO
289        let level = level.as_ref().map(|l| l.as_str()).unwrap_or("info");
290        let filter = EnvFilter::builder()
291            .with_default_directive(
292                option_env!("RUST_LOG")
293                    .unwrap_or(level)
294                    .parse()
295                    .expect("should provide valid log level at compile time."),
296            )
297            .from_env_lossy();
298
299        // `Option<L>` is a `Layer`, so disabled sinks are attached as `None`
300        let fmtlayer = platform_logger.then(|| {
301            tracing_subscriber::fmt::layer()
302                .with_ansi(true)
303                .with_file(true)
304                .with_line_number(true)
305                .with_target(true)
306                .pretty()
307        });
308
309        // Build base registry once instead of duplicating per-platform
310        let registry = tracing_subscriber::registry().with(fmtlayer).with(filter);
311
312        // Conditionally add callback layer if provided
313        // Use Option to avoid type incompatibility between Some/None branches
314        let callback_layer = callback.map(log_callback::CallbackLayer::new);
315        let registry = registry.with(callback_layer);
316        #[cfg(target_os = "ios")]
317        {
318            const TAG: &str = "com.8bit.bitwarden";
319            registry
320                .with(platform_logger.then(|| tracing_oslog::OsLogger::new(TAG, "default")))
321                .init();
322        }
323
324        #[cfg(target_os = "android")]
325        {
326            const TAG: &str = "com.bitwarden.sdk";
327            registry
328                .with(platform_logger.then(|| {
329                    tracing_android::layer(TAG)
330                        .expect("initialization of android logcat tracing layer")
331                }))
332                .init();
333        }
334
335        #[cfg(not(any(target_os = "android", target_os = "ios")))]
336        {
337            registry.init();
338        }
339        #[cfg(feature = "dangerous-crypto-debug")]
340        tracing::warn!(
341            "Dangerous crypto debug features are enabled. THIS MUST NOT BE USED IN PRODUCTION BUILDS!!"
342        );
343    });
344}
345
346/// Setup the error converter to ensure conversion errors don't cause panics
347/// Check [`bitwarden_uniffi_error`] for more details
348fn setup_error_converter() {
349    bitwarden_uniffi_error::set_error_to_uniffi_error(|e| {
350        crate::error::BitwardenError::Conversion(e.to_string()).into()
351    });
352}
353#[cfg(test)]
354mod tests {
355    use std::sync::Mutex;
356
357    use super::*;
358    // Mock token provider for testing
359    #[derive(Debug)]
360    struct MockTokenProvider;
361
362    #[async_trait::async_trait]
363    impl ClientManagedTokens for MockTokenProvider {
364        async fn get_access_token(&self) -> Option<String> {
365            Some("mock_token".to_string())
366        }
367    }
368    /// Mock LogCallback implementation for testing
369    struct TestLogCallback {
370        logs: Arc<Mutex<Vec<(LogLevel, String, String)>>>,
371    }
372    impl LogCallback for TestLogCallback {
373        fn on_log(&self, level: LogLevel, target: String, message: String) -> Result<()> {
374            self.logs
375                .lock()
376                .expect("Failed to lock logs mutex")
377                .push((level, target, message));
378            Ok(())
379        }
380    }
381
382    // Log callback unit tests only test happy path because running this with
383    // Once means we get one registered callback per test run. There are
384    // other tests written as integration tests in the /tests/ folder that
385    // assert more specific details.
386    #[test]
387    fn test_callback_receives_logs() {
388        let logs = Arc::new(Mutex::new(Vec::new()));
389        let callback = Arc::new(TestLogCallback { logs: logs.clone() });
390
391        // Initialize logger with callback before creating client
392        init_logger(Some(callback), None, true);
393
394        // Create client
395        let _client = Client::new(
396            Arc::new(MockTokenProvider),
397            None,
398            Arc::new(ManagedSettingsBindingClient::new()),
399        );
400
401        // Trigger a log
402        tracing::info!("test message from SDK");
403
404        // Verify callback received it
405        let captured = logs.lock().expect("Failed to lock logs mutex");
406        assert!(!captured.is_empty(), "Callback should receive logs");
407
408        // Find our specific test log (there may be other SDK logs during init)
409        let test_log = captured
410            .iter()
411            .find(|(_, _, msg)| msg.contains("test message"))
412            .expect("Should find our test log message");
413
414        assert_eq!(test_log.0, LogLevel::Info);
415        assert!(test_log.2.contains("test message"));
416    }
417}