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 platform;
19#[allow(missing_docs)]
20pub mod policies;
21#[allow(missing_docs)]
22pub mod tool;
23mod uniffi_support;
24#[allow(missing_docs)]
25pub mod vault;
26
27#[cfg(target_os = "android")]
28mod android_support;
29
30use crypto::CryptoClient;
31use error::{Error, Result};
32pub use log_callback::LogCallback;
33use platform::PlatformClient;
34pub use platform::{
35    AcquiredCookie, BootstrapConfig, ServerCommunicationConfig, ServerCommunicationConfigClient,
36    ServerCommunicationConfigRepository, SsoCookieVendorConfig,
37};
38use tool::{ExporterClient, GeneratorClients, ImporterClient, SendClient, SshClient};
39use vault::VaultClient;
40
41#[allow(missing_docs)]
42#[derive(uniffi::Object)]
43pub struct Client(pub(crate) bitwarden_pm::PasswordManagerClient);
44
45#[uniffi::export(async_runtime = "tokio")]
46impl Client {
47    /// Initialize a new instance of the SDK client
48    #[uniffi::constructor]
49    pub fn new(
50        token_provider: Arc<dyn ClientManagedTokens>,
51        settings: Option<ClientSettings>,
52    ) -> Self {
53        init_logger(None, None);
54        setup_error_converter();
55
56        #[cfg(target_os = "android")]
57        android_support::init();
58
59        Self(bitwarden_pm::PasswordManagerClient::new_with_client_tokens(
60            settings,
61            token_provider,
62        ))
63    }
64
65    /// Crypto operations
66    pub fn crypto(&self) -> CryptoClient {
67        CryptoClient(self.0.crypto())
68    }
69
70    /// Returns the key-management state bridge client used to register a
71    /// host-supplied storage implementation.
72    pub fn km_state_bridge(
73        &self,
74    ) -> bitwarden_core::key_management::state_bridge::StateBridgeClient {
75        self.0.0.km_state_bridge()
76    }
77
78    /// Returns the user-crypto-management sub-client (PIN settings, key rotation, etc).
79    pub fn user_crypto_management(
80        &self,
81    ) -> bitwarden_user_crypto_management::UserCryptoManagementClient {
82        self.0.user_crypto_management()
83    }
84
85    /// Key management operations that run on every sync.
86    pub fn crypto_sync_handler(&self) -> bitwarden_crypto_sync_handler::CryptoSyncHandlerClient {
87        self.0.crypto_sync_handler()
88    }
89
90    /// Vault item operations
91    pub fn vault(&self) -> VaultClient {
92        VaultClient(self.0.vault())
93    }
94
95    #[allow(missing_docs)]
96    pub fn platform(&self) -> PlatformClient {
97        PlatformClient(self.0.0.clone())
98    }
99
100    /// Generator operations
101    pub fn generators(&self) -> GeneratorClients {
102        GeneratorClients(self.0.generator())
103    }
104
105    /// Exporters
106    pub fn exporters(&self) -> ExporterClient {
107        ExporterClient(self.0.exporters())
108    }
109
110    /// Importers
111    pub fn importers(&self) -> ImporterClient {
112        ImporterClient(self.0.importers())
113    }
114
115    /// Sends operations
116    pub fn sends(&self) -> SendClient {
117        SendClient(self.0.sends())
118    }
119
120    /// SSH operations
121    pub fn ssh(&self) -> SshClient {
122        SshClient()
123    }
124
125    /// Random-number generation operations
126    pub fn random(&self) -> bitwarden_random::SdkRandomNumberClient {
127        bitwarden_random::SdkRandomNumberClient::new()
128    }
129
130    /// Auth operations
131    pub fn auth(&self) -> AuthClient {
132        AuthClient(self.0.0.clone())
133    }
134
135    /// Whether the client is in Gov Mode.
136    pub fn gov_mode(&self) -> bool {
137        self.0.0.gov_mode()
138    }
139
140    /// Policy operations
141    pub fn policies(&self) -> policies::PoliciesClient {
142        use bitwarden_policies::PoliciesClientExt;
143        policies::PoliciesClient(self.0.0.policies())
144    }
145
146    /// Test method, echoes back the input
147    pub fn echo(&self, msg: String) -> String {
148        msg
149    }
150
151    /// Test method, calls http endpoint
152    pub async fn http_get(&self, url: String) -> Result<String> {
153        let client = self.0.0.internal.get_http_client();
154        let res = client
155            .get(&url)
156            .send()
157            .await
158            .map_err(|e| Error::Api(e.into()))?;
159
160        res.text().await.map_err(|e| Error::Api(e.into()))
161    }
162}
163
164static INIT: Once = Once::new();
165
166/// Log level for SDK logging
167#[derive(uniffi::Enum)]
168pub enum LogLevel {
169    /// Most verbose: all trace, debug, info, warn, and error messages
170    Trace,
171    /// Verbose: debug, info, warn, and error messages
172    Debug,
173    /// Default: info, warn, and error messages
174    Info,
175    /// Only warn and error messages
176    Warn,
177    /// Only error messages
178    Error,
179}
180
181impl LogLevel {
182    fn as_str(&self) -> &'static str {
183        match self {
184            LogLevel::Trace => "trace",
185            LogLevel::Debug => "debug",
186            LogLevel::Info => "info",
187            LogLevel::Warn => "warn",
188            LogLevel::Error => "error",
189        }
190    }
191}
192
193/// Initialize the SDK logger
194///
195/// This function should be called once before creating any SDK clients.
196/// It initializes the tracing infrastructure for the SDK and optionally
197/// registers a callback to receive log events.
198///
199/// # Parameters
200/// - `callback`: Optional callback to receive SDK log events. Pass `None` to use only platform
201///   loggers (oslog on iOS, logcat on Android).
202/// - `level`: Optional log level. Defaults to `Info` if not specified. Can be overridden by
203///   `RUST_LOG` environment variable at runtime or compile time.
204///
205/// # Example
206/// ```kotlin
207/// // Initialize with callback and trace-level logging before creating clients
208/// initLogger(FlightRecorderCallback(), LogLevel.TRACE)
209/// val client = Client(tokenProvider, settings)
210/// ```
211///
212/// # Notes
213/// - This function can only be called once - subsequent calls are ignored
214/// - If not called explicitly, logging is auto-initialized when first client is created
215/// - Platform loggers (oslog/logcat) are always enabled regardless of callback
216#[uniffi::export]
217pub fn init_logger(callback: Option<Arc<dyn LogCallback>>, level: Option<LogLevel>) {
218    use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _};
219
220    INIT.call_once(|| {
221        // the log level prioritization is determined by:
222        //    1. if RUST_LOG is detected at runtime
223        //    2. if RUST_LOG is provided at compile time
224        //    3. the level parameter passed by the caller
225        //    4. default to INFO
226        let level = level.as_ref().map(|l| l.as_str()).unwrap_or("info");
227        let filter = EnvFilter::builder()
228            .with_default_directive(
229                option_env!("RUST_LOG")
230                    .unwrap_or(level)
231                    .parse()
232                    .expect("should provide valid log level at compile time."),
233            )
234            .from_env_lossy();
235
236        let fmtlayer = tracing_subscriber::fmt::layer()
237            .with_ansi(true)
238            .with_file(true)
239            .with_line_number(true)
240            .with_target(true)
241            .pretty();
242
243        // Build base registry once instead of duplicating per-platform
244        let registry = tracing_subscriber::registry().with(fmtlayer).with(filter);
245
246        // Conditionally add callback layer if provided
247        // Use Option to avoid type incompatibility between Some/None branches
248        let callback_layer = callback.map(log_callback::CallbackLayer::new);
249        let registry = registry.with(callback_layer);
250        #[cfg(target_os = "ios")]
251        {
252            const TAG: &str = "com.8bit.bitwarden";
253            registry
254                .with(tracing_oslog::OsLogger::new(TAG, "default"))
255                .init();
256        }
257
258        #[cfg(target_os = "android")]
259        {
260            const TAG: &str = "com.bitwarden.sdk";
261            registry
262                .with(
263                    tracing_android::layer(TAG)
264                        .expect("initialization of android logcat tracing layer"),
265                )
266                .init();
267        }
268
269        #[cfg(not(any(target_os = "android", target_os = "ios")))]
270        {
271            registry.init();
272        }
273        #[cfg(feature = "dangerous-crypto-debug")]
274        tracing::warn!(
275            "Dangerous crypto debug features are enabled. THIS MUST NOT BE USED IN PRODUCTION BUILDS!!"
276        );
277    });
278}
279
280/// Setup the error converter to ensure conversion errors don't cause panics
281/// Check [`bitwarden_uniffi_error`] for more details
282fn setup_error_converter() {
283    bitwarden_uniffi_error::set_error_to_uniffi_error(|e| {
284        crate::error::BitwardenError::Conversion(e.to_string()).into()
285    });
286}
287#[cfg(test)]
288mod tests {
289    use std::sync::Mutex;
290
291    use super::*;
292    // Mock token provider for testing
293    #[derive(Debug)]
294    struct MockTokenProvider;
295
296    #[async_trait::async_trait]
297    impl ClientManagedTokens for MockTokenProvider {
298        async fn get_access_token(&self) -> Option<String> {
299            Some("mock_token".to_string())
300        }
301    }
302    /// Mock LogCallback implementation for testing
303    struct TestLogCallback {
304        logs: Arc<Mutex<Vec<(String, String, String)>>>,
305    }
306    impl LogCallback for TestLogCallback {
307        fn on_log(&self, level: String, target: String, message: String) -> Result<()> {
308            self.logs
309                .lock()
310                .expect("Failed to lock logs mutex")
311                .push((level, target, message));
312            Ok(())
313        }
314    }
315
316    // Log callback unit tests only test happy path because running this with
317    // Once means we get one registered callback per test run. There are
318    // other tests written as integration tests in the /tests/ folder that
319    // assert more specific details.
320    #[test]
321    fn test_callback_receives_logs() {
322        let logs = Arc::new(Mutex::new(Vec::new()));
323        let callback = Arc::new(TestLogCallback { logs: logs.clone() });
324
325        // Initialize logger with callback before creating client
326        init_logger(Some(callback), None);
327
328        // Create client
329        let _client = Client::new(Arc::new(MockTokenProvider), None);
330
331        // Trigger a log
332        tracing::info!("test message from SDK");
333
334        // Verify callback received it
335        let captured = logs.lock().expect("Failed to lock logs mutex");
336        assert!(!captured.is_empty(), "Callback should receive logs");
337
338        // Find our specific test log (there may be other SDK logs during init)
339        let test_log = captured
340            .iter()
341            .find(|(_, _, msg)| msg.contains("test message"))
342            .expect("Should find our test log message");
343
344        assert_eq!(test_log.0, "INFO");
345        assert!(test_log.2.contains("test message"));
346    }
347}