1use qrcode::{EcLevel, QrCode};
2
3#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
4pub struct QrCodeMatrix {
5 pub size: u32,
7 pub modules: String,
10}
11
12#[uniffi::export]
16pub fn encode_text_qr(text: String) -> Option<QrCodeMatrix> {
17 let trimmed = text.trim();
18 if trimmed.is_empty() {
19 return None;
20 }
21 let code = QrCode::with_error_correction_level(trimmed.as_bytes(), EcLevel::M).ok()?;
22 let width = code.width();
23 let cells = code.to_colors();
24 let mut modules = String::with_capacity(width * width);
25 for color in cells {
26 modules.push(if color == qrcode::Color::Dark {
27 '1'
28 } else {
29 '0'
30 });
31 }
32 Some(QrCodeMatrix {
33 size: width as u32,
34 modules,
35 })
36}
37
38#[uniffi::export]
39pub fn is_device_approval_bootstrap(raw: String) -> bool {
40 nostr_identity::parse_nostr_identity_device_approval_bootstrap(&raw, &[])
41 .ok()
42 .flatten()
43 .is_some()
44}
45
46#[cfg(test)]
47mod tests {
48 use super::is_device_approval_bootstrap;
49 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
50 use nostr_identity::{
51 create_nostr_identity_device_approval_request,
52 encode_nostr_identity_device_approval_bootstrap, nostr_identity_device_approval_bootstrap,
53 CreateNostrIdentityDeviceApprovalRequestOptions,
54 };
55 use nostr_sdk::Keys;
56
57 #[test]
58 fn device_approval_bootstrap_rejects_non_bootstrap_inputs() {
59 assert!(!is_device_approval_bootstrap("".into()));
60 assert!(!is_device_approval_bootstrap("npub1plainvalue".into()));
61 assert!(!is_device_approval_bootstrap("https://example.com".into()));
62 assert!(!is_device_approval_bootstrap(
63 "nostr-identity://device-approval/?app_key=npub1legacy".into()
64 ));
65 assert!(!is_device_approval_bootstrap(format!(
66 "{}.{}.not-base64!*",
67 "1".repeat(64),
68 "1".repeat(64)
69 )));
70 }
71
72 #[test]
73 fn device_link_qr_accepts_only_shared_bootstrap() {
74 let device = Keys::generate();
75 let request = Keys::generate();
76 let local = create_nostr_identity_device_approval_request(
77 &device,
78 CreateNostrIdentityDeviceApprovalRequestOptions {
79 request_keys: Some(request),
80 request_secret: Some("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE".to_string()),
81 requested_at: 41,
82 request_type: Some("device_link".to_string()),
83 resources: Vec::new(),
84 expires_at: None,
85 profile_id: None,
86 admin_app_key_pubkey: None,
87 label: Some("Safari on macOS".to_string()),
88 },
89 )
90 .expect("approval request");
91 let bootstrap =
92 nostr_identity_device_approval_bootstrap(&local.request).expect("bootstrap");
93 let encoded = encode_nostr_identity_device_approval_bootstrap(&bootstrap, None)
94 .expect("encode bootstrap");
95 let legacy_full_request = format!(
96 "nostr-identity://device-approval/{}",
97 URL_SAFE_NO_PAD
98 .encode(serde_json::to_vec(&local.request).expect("legacy full request JSON"))
99 );
100
101 assert!(is_device_approval_bootstrap(encoded.clone()));
102 assert!(!is_device_approval_bootstrap(legacy_full_request));
103 assert_eq!(
104 serde_json::to_value(&bootstrap)
105 .expect("bootstrap JSON")
106 .as_object()
107 .expect("bootstrap object")
108 .keys()
109 .cloned()
110 .collect::<std::collections::BTreeSet<_>>(),
111 ["deviceAppKeyNpub", "label", "requestNpub", "requestSecret"]
112 .into_iter()
113 .map(str::to_string)
114 .collect()
115 );
116 assert_eq!(bootstrap.label.as_deref(), Some("Safari on macOS"));
117
118 let prefixed = format!("prefix:{encoded}");
119 assert!(!is_device_approval_bootstrap(prefixed));
120 }
121}