1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
6
7use crate::internal::core::AccessPolicy;
8use base64::Engine;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize, Deserialize)]
13pub struct BridgeRequest {
14 pub method: String,
16 pub params: BridgeParams,
18}
19
20#[derive(Debug, Default, Serialize, Deserialize)]
35pub struct BridgeParams {
36 #[serde(default)]
38 pub data: String,
39 #[serde(default)]
41 pub access_policy: AccessPolicy,
42 #[serde(default)]
44 pub app_name: String,
45 #[serde(default)]
47 pub key_label: String,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub rp_id: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub rp_name: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub user_id_b64: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub user_name: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub user_display_name: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub credential_id_b64: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub client_data_b64: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub timeout_ms: Option<u32>,
83}
84
85impl BridgeParams {
86 #[must_use]
91 pub fn effective_access_policy(&self) -> AccessPolicy {
92 self.access_policy
93 }
94
95 #[must_use]
98 pub fn new(
99 data: String,
100 access_policy: AccessPolicy,
101 app_name: String,
102 key_label: String,
103 ) -> Self {
104 Self {
105 data,
106 access_policy,
107 app_name,
108 key_label,
109 rp_id: None,
110 rp_name: None,
111 user_id_b64: None,
112 user_name: None,
113 user_display_name: None,
114 credential_id_b64: None,
115 client_data_b64: None,
116 timeout_ms: None,
117 }
118 }
119}
120
121#[derive(Debug, Serialize, Deserialize)]
125pub struct WebauthnMakeCredentialResult {
126 pub credential_id_b64: String,
128 pub public_key_x_hex: String,
130 pub public_key_y_hex: String,
132 pub authenticator_data_b64: String,
136 pub resident: bool,
140}
141
142#[derive(Debug, Serialize, Deserialize)]
145pub struct WebauthnAssertionResult {
146 pub signature_der_b64: String,
148 pub authenticator_data_b64: String,
150 pub flags: u8,
152 pub counter: u32,
155}
156
157#[derive(Debug, Serialize, Deserialize)]
159pub struct BridgeResponse {
160 pub result: Option<String>,
162 pub error: Option<String>,
164}
165
166impl BridgeResponse {
167 pub fn success(data: &str) -> Self {
169 BridgeResponse {
170 result: Some(data.to_string()),
171 error: None,
172 }
173 }
174
175 pub fn error(msg: &str) -> Self {
177 BridgeResponse {
178 result: None,
179 error: Some(msg.to_string()),
180 }
181 }
182
183 pub fn ok() -> Self {
185 BridgeResponse {
186 result: Some(String::new()),
187 error: None,
188 }
189 }
190
191 pub fn require_result(&self, operation: &str) -> crate::internal::core::Result<&str> {
193 if let Some(error) = &self.error {
194 return Err(crate::internal::core::Error::KeyOperation {
195 operation: operation.into(),
196 detail: error.clone(),
197 });
198 }
199 self.result
200 .as_deref()
201 .ok_or_else(|| crate::internal::core::Error::KeyOperation {
202 operation: operation.into(),
203 detail: "bridge response missing result payload".into(),
204 })
205 }
206
207 pub fn require_ok(&self, operation: &str) -> crate::internal::core::Result<()> {
209 let _unused = self.require_result(operation)?;
210 Ok(())
211 }
212
213 pub fn decode_result(&self, operation: &str) -> crate::internal::core::Result<Vec<u8>> {
215 decode_data(self.require_result(operation)?)
216 }
217}
218
219pub fn encode_data(data: &[u8]) -> String {
221 base64::engine::general_purpose::STANDARD.encode(data)
222}
223
224pub fn decode_data(encoded: &str) -> crate::internal::core::Result<Vec<u8>> {
226 base64::engine::general_purpose::STANDARD
227 .decode(encoded)
228 .map_err(|e| crate::internal::core::Error::Serialization(format!("base64 decode: {e}")))
229}
230
231#[cfg(test)]
232#[allow(clippy::unwrap_used, clippy::panic)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn bridge_request_serde_roundtrip() {
238 let request = BridgeRequest {
239 method: "encrypt".to_string(),
240 params: BridgeParams::new(
241 "aGVsbG8=".to_string(),
242 AccessPolicy::BiometricOnly,
243 "test-app".to_string(),
244 "cache-key".to_string(),
245 ),
246 };
247 let json = serde_json::to_string(&request).unwrap();
248 let parsed: BridgeRequest = serde_json::from_str(&json).unwrap();
249 assert_eq!(parsed.method, "encrypt");
250 assert_eq!(parsed.params.data, "aGVsbG8=");
251 assert_eq!(parsed.params.access_policy, AccessPolicy::BiometricOnly);
252 assert_eq!(parsed.params.app_name, "test-app");
253 assert_eq!(parsed.params.key_label, "cache-key");
254 }
255
256 #[test]
257 fn bridge_request_defaults_for_missing_fields() {
258 let json = r#"{"method":"init","params":{}}"#;
259 let parsed: BridgeRequest = serde_json::from_str(json).unwrap();
260 assert_eq!(parsed.method, "init");
261 assert_eq!(parsed.params.data, "");
262 assert_eq!(parsed.params.access_policy, AccessPolicy::None);
263 assert_eq!(parsed.params.app_name, "");
264 assert_eq!(parsed.params.key_label, "");
265 }
266
267 #[test]
268 fn bridge_request_ignores_legacy_biometric_field() {
269 let json = r#"{"method":"encrypt","params":{"biometric":true,"access_policy":"none","app_name":"a","key_label":"k"}}"#;
274 let parsed: BridgeRequest = serde_json::from_str(json).unwrap();
275 assert_eq!(parsed.params.access_policy, AccessPolicy::None);
276 assert_eq!(parsed.params.effective_access_policy(), AccessPolicy::None);
277 }
278
279 #[test]
280 fn bridge_response_success_construction() {
281 let resp = BridgeResponse::success("c29tZSBkYXRh");
282 assert_eq!(resp.result, Some("c29tZSBkYXRh".to_string()));
283 assert!(resp.error.is_none());
284 }
285
286 #[test]
287 fn bridge_response_error_construction() {
288 let resp = BridgeResponse::error("something went wrong");
289 assert!(resp.result.is_none());
290 assert_eq!(resp.error, Some("something went wrong".to_string()));
291 }
292
293 #[test]
294 fn bridge_response_ok_construction() {
295 let resp = BridgeResponse::ok();
296 assert_eq!(resp.result, Some(String::new()));
297 assert!(resp.error.is_none());
298 }
299
300 #[test]
301 fn bridge_response_serde_roundtrip() {
302 let resp = BridgeResponse::success("dGVzdA==");
303 let json = serde_json::to_string(&resp).unwrap();
304 let parsed: BridgeResponse = serde_json::from_str(&json).unwrap();
305 assert_eq!(parsed.result, Some("dGVzdA==".to_string()));
306 assert!(parsed.error.is_none());
307 }
308
309 #[test]
310 fn encode_decode_roundtrip_empty() {
311 let data = b"";
312 let encoded = encode_data(data);
313 let decoded = decode_data(&encoded).unwrap();
314 assert_eq!(decoded, data);
315 }
316
317 #[test]
318 fn encode_decode_roundtrip_small() {
319 let data = b"hello, world!";
320 let encoded = encode_data(data);
321 let decoded = decode_data(&encoded).unwrap();
322 assert_eq!(decoded, data);
323 }
324
325 #[test]
326 fn encode_decode_roundtrip_large() {
327 let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
328 let encoded = encode_data(&data);
329 let decoded = decode_data(&encoded).unwrap();
330 assert_eq!(decoded, data);
331 }
332
333 #[test]
334 fn decode_data_rejects_invalid_base64() {
335 let result = decode_data("not valid base64!!!");
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn bridge_request_all_methods() {
341 for method in &["init", "encrypt", "decrypt", "destroy", "delete"] {
342 let request = BridgeRequest {
343 method: (*method).to_string(),
344 params: BridgeParams::new(
345 String::new(),
346 AccessPolicy::None,
347 "test".to_string(),
348 "default".to_string(),
349 ),
350 };
351 let json = serde_json::to_string(&request).unwrap();
352 let parsed: BridgeRequest = serde_json::from_str(&json).unwrap();
353 assert_eq!(parsed.method, *method);
354 }
355 }
356
357 #[test]
358 fn bridge_response_success_with_empty_result() {
359 let resp = BridgeResponse::ok();
360 assert_eq!(resp.result, Some(String::new()));
361 assert!(resp.error.is_none());
362
363 let json = serde_json::to_string(&resp).unwrap();
365 let parsed: BridgeResponse = serde_json::from_str(&json).unwrap();
366 assert_eq!(parsed.result, Some(String::new()));
367 }
368
369 #[test]
370 fn bridge_response_error_preserves_message() {
371 let msg = "TPM device not found: error code 0x8028000F";
372 let resp = BridgeResponse::error(msg);
373 assert_eq!(resp.error.as_deref(), Some(msg));
374 assert!(resp.result.is_none());
375
376 let json = serde_json::to_string(&resp).unwrap();
377 let parsed: BridgeResponse = serde_json::from_str(&json).unwrap();
378 assert_eq!(parsed.error.as_deref(), Some(msg));
379 }
380
381 #[test]
382 fn encode_decode_binary_data_with_null_bytes() {
383 let data: Vec<u8> = vec![0x00, 0x01, 0x00, 0xFF, 0x00, 0xFE];
384 let encoded = encode_data(&data);
385 let decoded = decode_data(&encoded).unwrap();
386 assert_eq!(decoded, data);
387 }
388
389 #[test]
390 fn encode_decode_large_data_1mb() {
391 let data: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
392 let encoded = encode_data(&data);
393 let decoded = decode_data(&encoded).unwrap();
394 assert_eq!(decoded, data);
395 }
396
397 #[test]
398 fn decode_data_invalid_base64_returns_error() {
399 let result = decode_data("!!!not-base64!!!");
400 assert!(result.is_err());
401 let err_msg = format!("{}", result.unwrap_err());
402 assert!(err_msg.contains("base64"));
403 }
404
405 #[test]
406 fn decode_data_empty_string_returns_empty_vec() {
407 let result = decode_data("").unwrap();
408 assert!(result.is_empty());
409 }
410
411 #[test]
412 fn bridge_params_default_values() {
413 let json = r#"{}"#;
414 let params: BridgeParams = serde_json::from_str(json).unwrap();
415 assert_eq!(params.data, "");
416 assert_eq!(params.access_policy, AccessPolicy::None);
417 assert_eq!(params.app_name, "");
418 assert_eq!(params.key_label, "");
419 }
420
421 #[cfg(target_os = "macos")]
422 #[test]
423 fn find_bridge_returns_none_on_macos() {
424 let result = crate::internal::bridge::find_bridge("sshenc");
426 assert!(result.is_none());
427 }
428
429 #[test]
430 fn bridge_params_biometric_only_access_policy() {
431 let json = r#"{"access_policy":"biometric_only","app_name":"t","key_label":"k"}"#;
432 let params: BridgeParams = serde_json::from_str(json).unwrap();
433 assert_eq!(
434 params.effective_access_policy(),
435 AccessPolicy::BiometricOnly
436 );
437 }
438
439 #[test]
440 fn bridge_params_any_access_policy() {
441 let json = r#"{"access_policy":"any","app_name":"t","key_label":"k"}"#;
442 let params: BridgeParams = serde_json::from_str(json).unwrap();
443 assert_eq!(params.effective_access_policy(), AccessPolicy::Any);
444 }
445
446 #[test]
447 fn bridge_params_wire_format_omits_biometric_field() {
448 let params = BridgeParams::new(
455 String::new(),
456 AccessPolicy::BiometricOnly,
457 "app".into(),
458 "key".into(),
459 );
460 let json = serde_json::to_string(¶ms).unwrap();
461 assert!(!json.contains("\"biometric\""));
462 assert!(json.contains("\"access_policy\":\"biometric_only\""));
463 }
464
465 #[test]
466 fn bridge_response_require_result_rejects_null() {
467 let resp = BridgeResponse {
468 result: None,
469 error: None,
470 };
471 let err = resp.require_result("test_op").unwrap_err();
472 assert!(err.to_string().contains("missing result payload"));
473 }
474
475 #[test]
476 fn bridge_response_require_result_rejects_error() {
477 let resp = BridgeResponse::error("boom");
478 let err = resp.require_result("test_op").unwrap_err();
479 assert!(err.to_string().contains("boom"));
480 }
481
482 #[test]
483 fn bridge_response_decode_result_works() {
484 let resp = BridgeResponse::success("aGVsbG8=");
485 let data = resp.decode_result("test_op").unwrap();
486 assert_eq!(data, b"hello");
487 }
488
489 #[test]
490 fn bridge_response_require_ok_succeeds_on_ok() {
491 let resp = BridgeResponse::ok();
492 assert!(resp.require_ok("test").is_ok());
493 }
494
495 #[test]
496 fn bridge_response_require_ok_rejects_null() {
497 let resp = BridgeResponse {
498 result: None,
499 error: None,
500 };
501 let err = resp.require_ok("test").unwrap_err();
502 assert!(err.to_string().contains("missing result payload"));
503 }
504
505 #[test]
506 fn bridge_response_require_ok_rejects_error() {
507 let resp = BridgeResponse::error("fail");
508 let err = resp.require_ok("test").unwrap_err();
509 assert!(err.to_string().contains("fail"));
510 }
511
512 #[test]
513 fn bridge_response_decode_result_empty_string() {
514 let resp = BridgeResponse::ok();
515 let data = resp.decode_result("test").unwrap();
516 assert!(data.is_empty());
517 }
518
519 #[test]
520 fn bridge_response_decode_result_rejects_invalid_base64() {
521 let resp = BridgeResponse::success("not-valid-base64!!!");
522 let err = resp.decode_result("test").unwrap_err();
523 assert!(err.to_string().contains("base64"));
524 }
525
526 #[test]
527 fn effective_access_policy_with_all_variants() {
528 let params = BridgeParams::new(String::new(), AccessPolicy::Any, "a".into(), "k".into());
529 assert_eq!(params.effective_access_policy(), AccessPolicy::Any);
530
531 let params = BridgeParams::new(
532 String::new(),
533 AccessPolicy::PasswordOnly,
534 "a".into(),
535 "k".into(),
536 );
537 assert_eq!(params.effective_access_policy(), AccessPolicy::PasswordOnly);
538
539 let params = BridgeParams::new(String::new(), AccessPolicy::None, "a".into(), "k".into());
540 assert_eq!(params.effective_access_policy(), AccessPolicy::None);
541 }
542
543 #[test]
544 fn bridge_params_roundtrip_preserves_all_fields() {
545 let original = BridgeParams::new(
546 "dGVzdA==".into(),
547 AccessPolicy::BiometricOnly,
548 "my-app".into(),
549 "my-key".into(),
550 );
551 let json = serde_json::to_string(&original).unwrap();
552 let parsed: BridgeParams = serde_json::from_str(&json).unwrap();
553 assert_eq!(parsed.data, "dGVzdA==");
554 assert_eq!(parsed.access_policy, AccessPolicy::BiometricOnly);
555 assert_eq!(
556 parsed.effective_access_policy(),
557 AccessPolicy::BiometricOnly
558 );
559 assert_eq!(parsed.app_name, "my-app");
560 assert_eq!(parsed.key_label, "my-key");
561 }
562}