1#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
17
18mod tpm;
19
20pub use tpm::{TpmSigningStorage, TpmStorage};
21
22use crate::internal::bridge::BridgeResponse;
23use crate::internal::core::types::AccessPolicy;
24use base64::prelude::*;
25use serde::Deserialize;
26use std::io::{self, BufRead, Write};
27
28#[derive(Debug, Deserialize)]
31pub struct BridgeRequestCompat {
32 pub method: String,
34 #[serde(default)]
36 pub params: BridgeParamsCompat,
37}
38
39#[derive(Debug, Default, Deserialize)]
41pub struct BridgeParamsCompat {
42 #[serde(default)]
44 pub data: String,
45 #[serde(default)]
47 pub access_policy: AccessPolicy,
48 #[serde(default)]
51 pub biometric: bool,
52 #[serde(default)]
54 pub app_name: String,
55 #[serde(default)]
57 pub key_label: String,
58
59 #[serde(default)]
63 pub rp_id: Option<String>,
64 #[serde(default)]
65 pub rp_name: Option<String>,
66 #[serde(default)]
67 pub user_id_b64: Option<String>,
68 #[serde(default)]
69 pub user_name: Option<String>,
70 #[serde(default)]
71 pub user_display_name: Option<String>,
72 #[serde(default)]
73 pub credential_id_b64: Option<String>,
74 #[serde(default)]
75 pub client_data_b64: Option<String>,
76 #[serde(default)]
77 pub timeout_ms: Option<u32>,
78}
79
80impl BridgeParamsCompat {
81 pub fn app_name_or<'param>(&'param self, default: &'param str) -> &'param str {
83 if self.app_name.is_empty() {
84 default
85 } else {
86 &self.app_name
87 }
88 }
89
90 pub fn key_label_or<'param>(&'param self, default: &'param str) -> &'param str {
92 if self.key_label.is_empty() {
93 default
94 } else {
95 &self.key_label
96 }
97 }
98
99 pub fn effective_access_policy(&self) -> AccessPolicy {
102 if self.access_policy != AccessPolicy::None {
103 return self.access_policy;
104 }
105 if self.biometric {
106 return AccessPolicy::BiometricOnly;
107 }
108 AccessPolicy::None
109 }
110}
111
112pub fn handle_request(
115 request: &BridgeRequestCompat,
116 storage: &mut Option<TpmStorage>,
117 signing_storage: &mut Option<TpmSigningStorage>,
118 default_app_name: &str,
119 default_key_label: &str,
120) -> BridgeResponse {
121 let app_name = request.params.app_name_or(default_app_name);
122 let key_label = request.params.key_label_or(default_key_label);
123
124 match request.method.as_str() {
125 "init" => {
126 match TpmStorage::new(
127 app_name,
128 key_label,
129 request.params.effective_access_policy(),
130 ) {
131 Ok(s) => {
132 *storage = Some(s);
133 BridgeResponse::success("ok")
134 }
135 Err(e) => BridgeResponse::error(&format!("init failed: {e}")),
136 }
137 }
138 "encrypt" => {
139 let Some(ref s) = storage else {
140 return BridgeResponse::error("not initialized: call init first");
141 };
142 if request.params.data.is_empty() {
143 return BridgeResponse::error("missing data parameter");
144 }
145 let plaintext = match BASE64_STANDARD.decode(&request.params.data) {
146 Ok(d) => d,
147 Err(e) => {
148 return BridgeResponse::error(&format!("base64 decode error: {e}"));
149 }
150 };
151 match s.encrypt(&plaintext) {
152 Ok(ciphertext) => BridgeResponse::success(&BASE64_STANDARD.encode(&ciphertext)),
153 Err(e) => BridgeResponse::error(&format!("encrypt failed: {e}")),
154 }
155 }
156 "decrypt" => {
157 let Some(ref s) = storage else {
158 return BridgeResponse::error("not initialized: call init first");
159 };
160 if request.params.data.is_empty() {
161 return BridgeResponse::error("missing data parameter");
162 }
163 let ciphertext = match BASE64_STANDARD.decode(&request.params.data) {
164 Ok(d) => d,
165 Err(e) => {
166 return BridgeResponse::error(&format!("base64 decode error: {e}"));
167 }
168 };
169 match s.decrypt(&ciphertext) {
170 Ok(plaintext) => BridgeResponse::success(&BASE64_STANDARD.encode(&plaintext)),
171 Err(e) => BridgeResponse::error(&format!("decrypt failed: {e}")),
172 }
173 }
174 "destroy" | "delete" => match TpmStorage::delete(app_name, key_label) {
175 Ok(()) => {
176 *storage = None;
177 BridgeResponse::success("ok")
178 }
179 Err(e) => BridgeResponse::error(&format!("delete failed: {e}")),
180 },
181 "init_signing" => {
182 match TpmSigningStorage::new(
183 app_name,
184 key_label,
185 request.params.effective_access_policy(),
186 ) {
187 Ok(s) => {
188 *signing_storage = Some(s);
189 BridgeResponse::success("ok")
190 }
191 Err(e) => BridgeResponse::error(&format!("init_signing failed: {e}")),
192 }
193 }
194 "sign" => {
195 let Some(ref s) = signing_storage else {
196 return BridgeResponse::error("signing not initialized: call init_signing first");
197 };
198 if request.params.data.is_empty() {
199 return BridgeResponse::error("missing data parameter");
200 }
201 let data = match BASE64_STANDARD.decode(&request.params.data) {
202 Ok(d) => d,
203 Err(e) => {
204 return BridgeResponse::error(&format!("base64 decode error: {e}"));
205 }
206 };
207 match s.sign(&data) {
208 Ok(signature) => BridgeResponse::success(&BASE64_STANDARD.encode(&signature)),
209 Err(e) => BridgeResponse::error(&format!("sign failed: {e}")),
210 }
211 }
212 "public_key" => {
213 match TpmSigningStorage::public_key_for_app(app_name, key_label) {
223 Ok(pubkey) => BridgeResponse::success(&BASE64_STANDARD.encode(&pubkey)),
224 Err(e) => BridgeResponse::error(&format!("public_key failed: {e}")),
225 }
226 }
227 "list_keys" => {
228 match TpmSigningStorage::list_keys_for_app(app_name) {
235 Ok(keys) => {
236 let json = serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string());
237 BridgeResponse::success(&json)
238 }
239 Err(e) => BridgeResponse::error(&format!("list_keys failed: {e}")),
240 }
241 }
242 "delete_signing" => match TpmSigningStorage::delete(app_name, key_label) {
243 Ok(()) => {
244 *signing_storage = None;
245 BridgeResponse::success("ok")
246 }
247 Err(e) => BridgeResponse::error(&format!("delete_signing failed: {e}")),
248 },
249 "signing_key_exists" => match TpmSigningStorage::key_exists(app_name, key_label) {
255 Ok(exists) => BridgeResponse::success(if exists { "true" } else { "false" }),
256 Err(e) => BridgeResponse::error(&format!("signing_key_exists failed: {e}")),
257 },
258 "webauthn_is_available" => webauthn_is_available_handler(),
265 "webauthn_make_credential" => webauthn_make_credential_handler(&request.params),
266 "webauthn_get_assertion" => webauthn_get_assertion_handler(&request.params),
267 "webauthn_delete_platform_credential" => {
268 webauthn_delete_platform_credential_handler(&request.params)
269 }
270 other => BridgeResponse::error(&format!("unknown method: {other}")),
271 }
272}
273
274#[cfg(target_os = "windows")]
275fn webauthn_is_available_handler() -> BridgeResponse {
276 if crate::internal::windows_webauthn::is_platform_authenticator_available() {
277 BridgeResponse::success("true")
278 } else {
279 BridgeResponse::success("false")
280 }
281}
282
283#[cfg(not(target_os = "windows"))]
284fn webauthn_is_available_handler() -> BridgeResponse {
285 BridgeResponse::success("false")
286}
287
288#[cfg(target_os = "windows")]
289#[allow(clippy::too_many_lines)] fn webauthn_make_credential_handler(params: &BridgeParamsCompat) -> BridgeResponse {
291 use crate::internal::windows_webauthn::{make_credential, MakeCredentialParams};
292
293 let Some(rp_id) = params.rp_id.as_deref() else {
294 return BridgeResponse::error("webauthn_make_credential: missing rp_id");
295 };
296 let rp_name = params.rp_name.as_deref().unwrap_or("sshenc");
297 let Some(user_id_b64) = params.user_id_b64.as_deref() else {
298 return BridgeResponse::error("webauthn_make_credential: missing user_id_b64");
299 };
300 let user_id = match BASE64_STANDARD.decode(user_id_b64) {
301 Ok(v) => v,
302 Err(e) => return BridgeResponse::error(&format!("user_id_b64 decode: {e}")),
303 };
304 let user_name = params.user_name.as_deref().unwrap_or("");
305 let user_display_name = params.user_display_name.as_deref().unwrap_or(user_name);
306 let timeout_ms = params.timeout_ms.unwrap_or(60_000);
307
308 match make_credential(MakeCredentialParams {
309 rp_id,
310 rp_name,
311 user_id: &user_id,
312 user_name,
313 user_display_name,
314 timeout_ms,
315 hwnd: None,
320 }) {
321 Ok(cred) => {
322 let payload = crate::internal::bridge::WebauthnMakeCredentialResult {
323 credential_id_b64: BASE64_STANDARD.encode(&cred.credential_id),
324 public_key_x_hex: hex_lower(&cred.public_key_x),
325 public_key_y_hex: hex_lower(&cred.public_key_y),
326 authenticator_data_b64: BASE64_STANDARD.encode(&cred.authenticator_data),
327 resident: cred.resident,
328 };
329 match serde_json::to_string(&payload) {
330 Ok(s) => BridgeResponse::success(&s),
331 Err(e) => BridgeResponse::error(&format!("serialize make_credential result: {e}")),
332 }
333 }
334 Err(e) => BridgeResponse::error(&format!("webauthn_make_credential: {e}")),
335 }
336}
337
338#[cfg(not(target_os = "windows"))]
339fn webauthn_make_credential_handler(_params: &BridgeParamsCompat) -> BridgeResponse {
340 BridgeResponse::error(
341 "webauthn_make_credential: webauthn.dll is only available on Windows; \
342 this bridge build can't service the request",
343 )
344}
345
346#[cfg(target_os = "windows")]
347fn webauthn_get_assertion_handler(params: &BridgeParamsCompat) -> BridgeResponse {
348 use crate::internal::windows_webauthn::{get_assertion, GetAssertionParams};
349
350 let Some(rp_id) = params.rp_id.as_deref() else {
351 return BridgeResponse::error("webauthn_get_assertion: missing rp_id");
352 };
353 let Some(credential_id_b64) = params.credential_id_b64.as_deref() else {
354 return BridgeResponse::error("webauthn_get_assertion: missing credential_id_b64");
355 };
356 let credential_id = match BASE64_STANDARD.decode(credential_id_b64) {
357 Ok(v) => v,
358 Err(e) => return BridgeResponse::error(&format!("credential_id_b64 decode: {e}")),
359 };
360 let Some(client_data_b64) = params.client_data_b64.as_deref() else {
361 return BridgeResponse::error("webauthn_get_assertion: missing client_data_b64");
362 };
363 let client_data = match BASE64_STANDARD.decode(client_data_b64) {
364 Ok(v) => v,
365 Err(e) => return BridgeResponse::error(&format!("client_data_b64 decode: {e}")),
366 };
367 let timeout_ms = params.timeout_ms.unwrap_or(60_000);
368
369 match get_assertion(GetAssertionParams {
370 rp_id,
371 credential_id: &credential_id,
372 client_data: &client_data,
373 timeout_ms,
374 hwnd: None,
375 }) {
376 Ok(asn) => {
377 let payload = crate::internal::bridge::WebauthnAssertionResult {
378 signature_der_b64: BASE64_STANDARD.encode(&asn.signature_der),
379 authenticator_data_b64: BASE64_STANDARD.encode(&asn.authenticator_data),
380 flags: asn.flags,
381 counter: asn.counter,
382 };
383 match serde_json::to_string(&payload) {
384 Ok(s) => BridgeResponse::success(&s),
385 Err(e) => BridgeResponse::error(&format!("serialize assertion result: {e}")),
386 }
387 }
388 Err(e) => BridgeResponse::error(&format!("webauthn_get_assertion: {e}")),
389 }
390}
391
392#[cfg(not(target_os = "windows"))]
393fn webauthn_get_assertion_handler(_params: &BridgeParamsCompat) -> BridgeResponse {
394 BridgeResponse::error(
395 "webauthn_get_assertion: webauthn.dll is only available on Windows; \
396 this bridge build can't service the request",
397 )
398}
399
400#[cfg(target_os = "windows")]
401fn webauthn_delete_platform_credential_handler(params: &BridgeParamsCompat) -> BridgeResponse {
402 let Some(credential_id_b64) = params.credential_id_b64.as_deref() else {
403 return BridgeResponse::error(
404 "webauthn_delete_platform_credential: missing credential_id_b64",
405 );
406 };
407 let credential_id = match BASE64_STANDARD.decode(credential_id_b64) {
408 Ok(v) => v,
409 Err(e) => return BridgeResponse::error(&format!("credential_id_b64 decode: {e}")),
410 };
411 match crate::internal::windows_webauthn::delete_platform_credential(&credential_id) {
412 Ok(()) => BridgeResponse::success("ok"),
413 Err(e) => BridgeResponse::error(&format!("webauthn_delete_platform_credential: {e}")),
414 }
415}
416
417#[cfg(not(target_os = "windows"))]
418fn webauthn_delete_platform_credential_handler(_params: &BridgeParamsCompat) -> BridgeResponse {
419 BridgeResponse::error(
420 "webauthn_delete_platform_credential: webauthn.dll is only available on Windows",
421 )
422}
423
424#[cfg(target_os = "windows")]
425fn hex_lower(bytes: &[u8]) -> String {
426 let mut s = String::with_capacity(bytes.len() * 2);
427 for b in bytes {
428 s.push_str(&format!("{:02x}", b));
429 }
430 s
431}
432
433#[derive(Debug)]
436pub struct BridgeServer {
437 default_app_name: String,
438 default_key_label: String,
439}
440
441impl BridgeServer {
442 pub fn new(default_app_name: &str, default_key_label: &str) -> Self {
447 Self {
448 default_app_name: default_app_name.to_string(),
449 default_key_label: default_key_label.to_string(),
450 }
451 }
452
453 #[allow(clippy::print_stdout)]
457 pub fn run_stdio(&mut self) -> io::Result<()> {
458 let stdin = io::stdin();
459 let mut stdout = io::stdout().lock();
460 let mut storage: Option<TpmStorage> = None;
461 let mut signing_storage: Option<TpmSigningStorage> = None;
462
463 for line in stdin.lock().lines() {
464 let line = match line {
465 Ok(l) => l,
466 Err(e) => {
467 let resp = BridgeResponse::error(&format!("read error: {e}"));
468 drop(serde_json::to_writer(&mut stdout, &resp));
469 drop(stdout.write_all(b"\n"));
470 drop(stdout.flush());
471 break;
472 }
473 };
474
475 if line.trim().is_empty() {
476 continue;
477 }
478
479 let response = match serde_json::from_str::<BridgeRequestCompat>(&line) {
480 Ok(req) => handle_request(
481 &req,
482 &mut storage,
483 &mut signing_storage,
484 &self.default_app_name,
485 &self.default_key_label,
486 ),
487 Err(e) => BridgeResponse::error(&format!("invalid JSON: {e}")),
488 };
489
490 if serde_json::to_writer(&mut stdout, &response).is_err() {
491 break;
492 }
493 if stdout.write_all(b"\n").is_err() {
494 break;
495 }
496 if stdout.flush().is_err() {
497 break;
498 }
499 }
500
501 Ok(())
502 }
503}
504
505#[cfg(test)]
506#[allow(clippy::unwrap_used, clippy::panic)]
507mod tests {
508 use super::*;
509
510 const TEST_APP_NAME: &str = "test-app";
511 const TEST_KEY_LABEL: &str = "cache-key";
512
513 fn make_request(method: &str, data: &str, access_policy: AccessPolicy) -> BridgeRequestCompat {
514 BridgeRequestCompat {
515 method: method.to_string(),
516 params: BridgeParamsCompat {
517 data: data.to_string(),
518 access_policy,
519 biometric: false,
520 app_name: TEST_APP_NAME.to_string(),
521 key_label: TEST_KEY_LABEL.to_string(),
522 ..BridgeParamsCompat::default()
523 },
524 }
525 }
526
527 fn handle(req: &BridgeRequestCompat, storage: &mut Option<TpmStorage>) -> BridgeResponse {
528 let mut signing_storage = None;
529 handle_request(
530 req,
531 storage,
532 &mut signing_storage,
533 TEST_APP_NAME,
534 TEST_KEY_LABEL,
535 )
536 }
537
538 fn handle_signing(
539 req: &BridgeRequestCompat,
540 signing_storage: &mut Option<TpmSigningStorage>,
541 ) -> BridgeResponse {
542 let mut storage = None;
543 handle_request(
544 req,
545 &mut storage,
546 signing_storage,
547 TEST_APP_NAME,
548 TEST_KEY_LABEL,
549 )
550 }
551
552 #[test]
555 fn parse_init_request() {
556 let json = r#"{"method": "init", "params": {"access_policy": "none"}}"#;
557 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
558 assert_eq!(req.method, "init");
559 assert_eq!(req.params.access_policy, AccessPolicy::None);
560 assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
561 assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
562 }
563
564 #[test]
565 fn parse_init_request_defaults() {
566 let json = r#"{"method": "init", "params": {}}"#;
567 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
568 assert_eq!(req.method, "init");
569 assert_eq!(req.params.access_policy, AccessPolicy::None);
570 assert!(req.params.data.is_empty());
571 assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
572 assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
573 }
574
575 #[test]
576 fn parse_encrypt_request() {
577 let json =
578 r#"{"method": "encrypt", "params": {"data": "aGVsbG8=", "access_policy": "none"}}"#;
579 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
580 assert_eq!(req.method, "encrypt");
581 assert_eq!(req.params.data, "aGVsbG8=");
582 }
583
584 #[test]
585 fn parse_decrypt_request() {
586 let json = r#"{"method": "decrypt", "params": {"data": "Y2lwaGVy"}}"#;
587 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
588 assert_eq!(req.method, "decrypt");
589 assert_eq!(req.params.data, "Y2lwaGVy");
590 }
591
592 #[test]
593 fn parse_destroy_request() {
594 let json = r#"{"method": "destroy", "params": {}}"#;
595 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
596 assert_eq!(req.method, "destroy");
597 assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
598 assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
599 }
600
601 #[test]
602 fn parse_delete_request() {
603 let json = r#"{"method": "delete", "params": {"key_label": "cache-key"}}"#;
604 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
605 assert_eq!(req.method, "delete");
606 assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
607 }
608
609 #[test]
610 fn parse_request_uses_defaults_for_minimal_payloads() {
611 let json = r#"{"method":"init","params":{"access_policy":"biometric_only"}}"#;
612 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
613 assert_eq!(req.params.access_policy, AccessPolicy::BiometricOnly);
614 assert_eq!(req.params.app_name_or(TEST_APP_NAME), TEST_APP_NAME);
615 assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), TEST_KEY_LABEL);
616 }
617
618 #[test]
619 fn parse_request_with_explicit_app_name_and_key_label() {
620 let json = r#"{"method":"init","params":{"app_name":"custom","key_label":"my-key"}}"#;
621 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
622 assert_eq!(req.params.app_name_or(TEST_APP_NAME), "custom");
623 assert_eq!(req.params.key_label_or(TEST_KEY_LABEL), "my-key");
624 }
625
626 #[test]
629 fn serialize_success_response() {
630 let resp = BridgeResponse::success("ok");
631 let json = serde_json::to_string(&resp).unwrap();
632 assert!(json.contains("\"result\":\"ok\""));
633 }
634
635 #[test]
636 fn serialize_error_response() {
637 let resp = BridgeResponse::error("something went wrong");
638 let json = serde_json::to_string(&resp).unwrap();
639 assert!(json.contains("\"error\":\"something went wrong\""));
640 }
641
642 #[test]
645 fn handle_init_creates_storage() {
646 let req = make_request("init", "", AccessPolicy::None);
647 let mut storage = None;
648 let resp = handle(&req, &mut storage);
649 if let Some(err) = &resp.error {
652 assert!(!err.is_empty(), "init error message should not be empty");
653 } else {
654 assert!(
655 resp.result.is_some(),
656 "init should return a result on success"
657 );
658 }
659 }
660
661 #[test]
662 fn handle_destroy_clears_storage() {
663 let req = make_request("destroy", "", AccessPolicy::None);
664 let mut storage = None;
665 let resp = handle(&req, &mut storage);
666 if let Some(err) = &resp.error {
668 assert!(!err.is_empty(), "destroy error message should not be empty");
669 } else {
670 assert!(
671 resp.result.is_some(),
672 "destroy should return a result on success"
673 );
674 }
675 assert!(storage.is_none());
676 }
677
678 #[test]
679 fn handle_delete_clears_storage() {
680 let req = make_request("delete", "", AccessPolicy::None);
681 let mut storage = None;
682 let resp = handle(&req, &mut storage);
683 if let Some(err) = &resp.error {
685 assert!(!err.is_empty(), "delete error message should not be empty");
686 } else {
687 assert!(
688 resp.result.is_some(),
689 "delete should return a result on success"
690 );
691 }
692 assert!(storage.is_none());
693 }
694
695 #[test]
696 fn destroy_and_delete_are_aliases() {
697 let destroy = make_request("destroy", "", AccessPolicy::None);
704 let delete = make_request("delete", "", AccessPolicy::None);
705
706 let mut storage_a = None;
707 let mut storage_b = None;
708 let resp_destroy = handle(&destroy, &mut storage_a);
709 let resp_delete = handle(&delete, &mut storage_b);
710
711 for resp in [&resp_destroy, &resp_delete] {
714 if let Some(err) = &resp.error {
715 assert!(
716 !err.contains("unknown method"),
717 "bridge rejected a supported alias as unknown: {err}"
718 );
719 }
720 }
721 assert_eq!(
724 resp_destroy.error.is_some(),
725 resp_delete.error.is_some(),
726 "destroy/delete disagreed: destroy={resp_destroy:?} delete={resp_delete:?}"
727 );
728 }
729
730 #[test]
731 fn handle_unknown_method() {
732 let req = make_request("bogus", "", AccessPolicy::None);
733 let mut storage = None;
734 let resp = handle(&req, &mut storage);
735 assert!(resp
736 .error
737 .as_deref()
738 .is_some_and(|e| e.contains("unknown method")),);
739 }
740
741 #[test]
742 fn handle_encrypt_without_init() {
743 let req = make_request("encrypt", "aGVsbG8=", AccessPolicy::None);
744 let mut storage = None;
745 let resp = handle(&req, &mut storage);
746 assert!(resp
747 .error
748 .as_deref()
749 .is_some_and(|e| e.contains("not initialized")),);
750 }
751
752 #[test]
753 fn handle_decrypt_without_init() {
754 let req = make_request("decrypt", "Y2lwaGVy", AccessPolicy::None);
755 let mut storage = None;
756 let resp = handle(&req, &mut storage);
757 assert!(resp
758 .error
759 .as_deref()
760 .is_some_and(|e| e.contains("not initialized")),);
761 }
762
763 #[test]
764 fn handle_encrypt_missing_data() {
765 let req = make_request("encrypt", "", AccessPolicy::None);
766 let mut storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
769 let resp = handle(&req, &mut storage);
770 assert!(resp.error.is_some());
771 }
772
773 #[test]
774 fn handle_encrypt_invalid_base64() {
775 let req = make_request("encrypt", "not-valid-base64!!!", AccessPolicy::None);
776 let mut storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
777 let resp = handle(&req, &mut storage);
778 assert!(resp.error.is_some());
779 }
780
781 #[test]
782 fn handle_decrypt_missing_data() {
783 let req = make_request("decrypt", "", AccessPolicy::None);
784 let mut storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
785 let resp = handle(&req, &mut storage);
786 assert!(resp.error.is_some());
787 }
788
789 #[cfg(not(target_os = "windows"))]
790 #[test]
791 fn encrypt_returns_platform_error_on_non_windows() {
792 let storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
793 let result = storage.encrypt(b"hello");
794 assert!(result.is_err());
795 assert!(result.unwrap_err().contains("only supported on Windows"));
796 }
797
798 #[cfg(not(target_os = "windows"))]
799 #[test]
800 fn decrypt_returns_platform_error_on_non_windows() {
801 let storage = TpmStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
802 let result = storage.decrypt(b"hello");
803 assert!(result.is_err());
804 assert!(result.unwrap_err().contains("only supported on Windows"));
805 }
806
807 #[test]
808 fn roundtrip_json_protocol() {
809 let init_json = r#"{"method":"init","params":{"app_name":"test-app","key_label":"cache-key","access_policy":"none"}}"#;
811 let encrypt_json = r#"{"method":"encrypt","params":{"data":"aGVsbG8gd29ybGQ=","app_name":"test-app","key_label":"cache-key","access_policy":"none"}}"#;
812 let destroy_json =
813 r#"{"method":"destroy","params":{"app_name":"test-app","key_label":"cache-key"}}"#;
814
815 let mut storage = None;
816 let mut signing_storage = None;
817
818 let req: BridgeRequestCompat = serde_json::from_str(init_json).unwrap();
820 let resp = handle_request(
821 &req,
822 &mut storage,
823 &mut signing_storage,
824 TEST_APP_NAME,
825 TEST_KEY_LABEL,
826 );
827 if let Some(err) = &resp.error {
828 assert!(!err.is_empty(), "init error message should not be empty");
829 } else {
830 assert!(
831 resp.result.is_some(),
832 "init should return a result on success"
833 );
834 }
835
836 let req: BridgeRequestCompat = serde_json::from_str(encrypt_json).unwrap();
838 let resp = handle_request(
839 &req,
840 &mut storage,
841 &mut signing_storage,
842 TEST_APP_NAME,
843 TEST_KEY_LABEL,
844 );
845 if let Some(err) = &resp.error {
846 assert!(!err.is_empty(), "encrypt error message should not be empty");
847 } else {
848 assert!(
849 resp.result.is_some(),
850 "encrypt should return a result on success"
851 );
852 }
853
854 let req: BridgeRequestCompat = serde_json::from_str(destroy_json).unwrap();
856 let resp = handle_request(
857 &req,
858 &mut storage,
859 &mut signing_storage,
860 TEST_APP_NAME,
861 TEST_KEY_LABEL,
862 );
863 if let Some(err) = &resp.error {
864 assert!(!err.is_empty(), "destroy error message should not be empty");
865 } else {
866 assert!(
867 resp.result.is_some(),
868 "destroy should return a result on success",
869 );
870 }
871 assert!(storage.is_none());
872 }
873
874 #[test]
875 fn invalid_json_produces_error() {
876 let bad_json = "this is not json";
877 let result = serde_json::from_str::<BridgeRequestCompat>(bad_json);
878 assert!(result.is_err());
879 }
880
881 #[test]
884 fn effective_access_policy_none_without_biometric() {
885 let json = r#"{"method":"init","params":{}}"#;
886 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
887 assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
888 }
889
890 #[test]
891 fn effective_access_policy_any() {
892 let json = r#"{"method":"init","params":{"access_policy":"any"}}"#;
893 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
894 assert_eq!(req.params.effective_access_policy(), AccessPolicy::Any);
895 }
896
897 #[test]
898 fn effective_access_policy_password_only() {
899 let json = r#"{"method":"init","params":{"access_policy":"password_only"}}"#;
900 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
901 assert_eq!(
902 req.params.effective_access_policy(),
903 AccessPolicy::PasswordOnly
904 );
905 }
906
907 #[test]
910 fn legacy_payload_with_no_params_defaults_to_none() {
911 let json = r#"{"method":"init","params":{}}"#;
912 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
913 assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
914 assert_eq!(req.params.app_name, "");
915 assert_eq!(req.params.key_label, "");
916 }
917
918 #[test]
921 fn biometric_and_access_policy_coexist_in_json() {
922 let json = r#"{"method":"init","params":{"access_policy":"biometric_only","biometric":true,"app_name":"test","key_label":"k"}}"#;
923 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
924 assert_eq!(
925 req.params.effective_access_policy(),
926 AccessPolicy::BiometricOnly
927 );
928 }
929
930 #[test]
933 fn biometric_true_falls_back_to_biometric_only() {
934 let json =
935 r#"{"method":"init","params":{"biometric":true,"app_name":"a","key_label":"k"}}"#;
936 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
937 assert_eq!(
938 req.params.effective_access_policy(),
939 AccessPolicy::BiometricOnly
940 );
941 }
942
943 #[test]
944 fn legacy_biometric_true_maps_to_biometric_only() {
945 let json = r#"{"method": "init", "params": {"biometric": true}}"#;
946 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
947 assert_eq!(
948 req.params.effective_access_policy(),
949 AccessPolicy::BiometricOnly
950 );
951 }
952
953 #[test]
954 fn legacy_biometric_false_maps_to_none() {
955 let json = r#"{"method": "init", "params": {"biometric": false}}"#;
956 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
957 assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
958 }
959
960 #[test]
961 fn access_policy_takes_precedence_over_biometric() {
962 let json = r#"{"method": "init", "params": {"access_policy": "any", "biometric": true}}"#;
964 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
965 assert_eq!(req.params.effective_access_policy(), AccessPolicy::Any);
966 }
967
968 #[test]
969 fn password_only_takes_precedence_over_biometric() {
970 let json =
971 r#"{"method":"init","params":{"access_policy":"password_only","biometric":true}}"#;
972 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
973 assert_eq!(
974 req.params.effective_access_policy(),
975 AccessPolicy::PasswordOnly,
976 "explicit access_policy should take precedence over legacy biometric field"
977 );
978 }
979
980 #[test]
981 fn empty_params_all_defaults() {
982 let json = r#"{"method":"init","params":{}}"#;
983 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
984 assert_eq!(req.params.effective_access_policy(), AccessPolicy::None);
985 assert_eq!(req.params.data, "");
986 assert_eq!(req.params.app_name, "");
987 assert_eq!(req.params.key_label, "");
988 }
989
990 #[test]
993 fn handle_init_signing_creates_signing_storage() {
994 let req = make_request("init_signing", "", AccessPolicy::None);
995 let mut signing_storage = None;
996 let resp = handle_signing(&req, &mut signing_storage);
997 if let Some(err) = &resp.error {
998 assert!(!err.is_empty(), "init_signing error should not be empty");
999 } else {
1000 assert!(resp.result.is_some(), "init_signing should return a result");
1001 }
1002 }
1003
1004 #[test]
1005 fn handle_sign_without_init_signing() {
1006 let req = make_request("sign", "aGVsbG8=", AccessPolicy::None);
1007 let mut signing_storage = None;
1008 let resp = handle_signing(&req, &mut signing_storage);
1009 assert!(resp
1010 .error
1011 .as_deref()
1012 .is_some_and(|e| e.contains("signing not initialized")),);
1013 }
1014
1015 #[test]
1016 fn handle_public_key_without_init_signing() {
1017 let req = make_request("public_key", "", AccessPolicy::None);
1025 let mut signing_storage = None;
1026 let resp = handle_signing(&req, &mut signing_storage);
1027 if let Some(err) = &resp.error {
1028 assert!(
1029 !err.contains("signing not initialized"),
1030 "public_key should be standalone, not require init_signing: {err}"
1031 );
1032 }
1033 }
1034
1035 #[test]
1036 fn handle_list_keys_without_init_signing() {
1037 let req = make_request("list_keys", "", AccessPolicy::None);
1046 let mut signing_storage = None;
1047 let resp = handle_signing(&req, &mut signing_storage);
1048 if let Some(err) = &resp.error {
1049 assert!(
1050 !err.contains("signing not initialized"),
1051 "list_keys should be standalone, not require init_signing: {err}"
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn handle_sign_missing_data() {
1058 let req = make_request("sign", "", AccessPolicy::None);
1059 let mut signing_storage =
1060 TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).ok();
1061 let resp = handle_signing(&req, &mut signing_storage);
1062 assert!(resp.error.is_some());
1063 }
1064
1065 #[test]
1066 fn handle_delete_signing_clears_signing_storage() {
1067 let req = make_request("delete_signing", "", AccessPolicy::None);
1068 let mut signing_storage = None;
1069 let resp = handle_signing(&req, &mut signing_storage);
1070 if let Some(err) = &resp.error {
1071 assert!(
1072 !err.is_empty(),
1073 "delete_signing error message should not be empty"
1074 );
1075 } else {
1076 assert!(
1077 resp.result.is_some(),
1078 "delete_signing should return a result on success"
1079 );
1080 }
1081 assert!(signing_storage.is_none());
1082 }
1083
1084 #[test]
1085 fn parse_init_signing_request() {
1086 let json = r#"{"method": "init_signing", "params": {"access_policy": "none"}}"#;
1087 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1088 assert_eq!(req.method, "init_signing");
1089 assert_eq!(req.params.access_policy, AccessPolicy::None);
1090 }
1091
1092 #[test]
1093 fn parse_sign_request() {
1094 let json = r#"{"method": "sign", "params": {"data": "aGVsbG8=", "access_policy": "none"}}"#;
1095 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1096 assert_eq!(req.method, "sign");
1097 assert_eq!(req.params.data, "aGVsbG8=");
1098 }
1099
1100 #[test]
1101 fn parse_public_key_request() {
1102 let json = r#"{"method": "public_key", "params": {}}"#;
1103 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1104 assert_eq!(req.method, "public_key");
1105 }
1106
1107 #[test]
1108 fn parse_list_keys_request() {
1109 let json = r#"{"method": "list_keys", "params": {}}"#;
1110 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1111 assert_eq!(req.method, "list_keys");
1112 }
1113
1114 #[test]
1115 fn parse_delete_signing_request() {
1116 let json = r#"{"method": "delete_signing", "params": {"key_label": "cache-key"}}"#;
1117 let req: BridgeRequestCompat = serde_json::from_str(json).unwrap();
1118 assert_eq!(req.method, "delete_signing");
1119 }
1120
1121 #[cfg(not(target_os = "windows"))]
1122 #[test]
1123 fn sign_returns_platform_error_on_non_windows() {
1124 let storage =
1125 TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
1126 let result = storage.sign(b"hello");
1127 assert!(result.is_err());
1128 assert!(result.unwrap_err().contains("only supported on Windows"));
1129 }
1130
1131 #[cfg(not(target_os = "windows"))]
1132 #[test]
1133 fn public_key_returns_platform_error_on_non_windows() {
1134 let storage =
1135 TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
1136 let result = storage.public_key();
1137 assert!(result.is_err());
1138 assert!(result.unwrap_err().contains("only supported on Windows"));
1139 }
1140
1141 #[cfg(not(target_os = "windows"))]
1142 #[test]
1143 fn list_keys_returns_platform_error_on_non_windows() {
1144 let storage =
1145 TpmSigningStorage::new(TEST_APP_NAME, TEST_KEY_LABEL, AccessPolicy::None).unwrap();
1146 let result = storage.list_keys();
1147 assert!(result.is_err());
1148 assert!(result.unwrap_err().contains("only supported on Windows"));
1149 }
1150
1151 #[test]
1154 fn bridge_server_new() {
1155 let server = BridgeServer::new("myapp", "mykey");
1156 assert_eq!(server.default_app_name, "myapp");
1157 assert_eq!(server.default_key_label, "mykey");
1158 }
1159}