Skip to main content

iris_chat_core/
qr.rs

1use qrcode::{EcLevel, QrCode};
2
3#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
4pub struct QrCodeMatrix {
5    /// Square module count (one side of the matrix).
6    pub size: u32,
7    /// "1" = dark module, "0" = light module. Length == size * size.
8    /// We use a string instead of Vec<bool> to keep the FFI surface cheap.
9    pub modules: String,
10}
11
12/// Render `text` to a QR-code module matrix. Returns a square matrix encoded
13/// as `1`/`0` characters in row-major order. Returns `None` for inputs that
14/// don't fit at the medium error-correction level.
15#[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}