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