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