Skip to main content

smart_id_rust_client/models/
interaction.rs

1use crate::error::Result;
2use crate::error::SmartIdClientError;
3use base64::Engine;
4use base64::engine::general_purpose::STANDARD;
5use serde::{Deserialize, Serialize};
6use serde_with::skip_serializing_none;
7use sha2::{Digest, Sha256, Sha384, Sha512};
8use strum_macros::AsRefStr;
9use crate::models::signature::HashingAlgorithm;
10
11/// Interaction Flow
12///
13/// This enum represents the different types of interaction flows that can be started on the user's device.
14/// Each variant corresponds to a specific interaction type.
15///
16/// # Variants
17///
18/// * `DisplayTextAndPIN` - Displays a text message and prompts the user to enter a PIN.
19/// * `ConfirmationMessage` - Displays a confirmation message with confirm and cancel buttons, then prompts the user to enter a pin.
20/// * `VerificationCodeChoice` - Prompts the user to choose a verification code, then enter a pin.
21/// * `ConfirmationMessageAndVerificationCodeChoice` - Displays a confirmation message and prompts the user to choose a verification code.
22#[skip_serializing_none]
23#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default, AsRefStr)]
24#[serde(rename_all = "camelCase")]
25#[strum(serialize_all = "camelCase")]
26#[non_exhaustive]
27pub enum InteractionFlow {
28    #[default]
29    DisplayTextAndPIN,
30    ConfirmationMessage,
31    VerificationCodeChoice,
32    ConfirmationMessageAndVerificationCodeChoice,
33}
34
35/// Represents different types of interactions that can be started on the users device
36///
37/// There are limitations on which interactions can be used with which request types.
38/// For device link flows, the following interactions are allowed:
39/// - DisplayTextAndPIN with display_text_60
40/// - ConfirmationMessage with display_text_200
41///
42/// For notificaiton flows, the following interactions are allowed:
43/// - VerificationCodeChoice with display_text_60
44/// - ConfirmationMessageAndVerificationCodeChoice with display_text_200
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(tag = "type", rename_all = "camelCase")]
47#[non_exhaustive]
48pub enum Interaction {
49    #[serde(rename_all = "camelCase")]
50    DisplayTextAndPIN { display_text_60: String },
51    #[serde(rename_all = "camelCase")]
52    ConfirmationMessage { display_text_200: String },
53    #[serde(rename_all = "camelCase")]
54    VerificationCodeChoice { display_text_60: String },
55    #[serde(rename_all = "camelCase")]
56    ConfirmationMessageAndVerificationCodeChoice { display_text_200: String },
57}
58
59impl Interaction {
60    pub fn validate_text_length(&self) -> Result<()> {
61        match self {
62            Interaction::DisplayTextAndPIN { display_text_60 } => {
63                if display_text_60.len() > 60 {
64                    return Err(SmartIdClientError::InvalidInteractionParametersException(
65                        "Display text must be 60 characters or less",
66                    ));
67                }
68            }
69            Interaction::ConfirmationMessage { display_text_200 } => {
70                if display_text_200.len() > 200 {
71                    return Err(SmartIdClientError::InvalidInteractionParametersException(
72                        "Display text must be 200 characters or less",
73                    ));
74                }
75            }
76            Interaction::VerificationCodeChoice { display_text_60 } => {
77                if display_text_60.len() > 60 {
78                    return Err(SmartIdClientError::InvalidInteractionParametersException(
79                        "Display text must be 60 characters or less",
80                    ));
81                }
82            }
83            Interaction::ConfirmationMessageAndVerificationCodeChoice { display_text_200 } => {
84                if display_text_200.len() > 200 {
85                    return Err(SmartIdClientError::InvalidInteractionParametersException(
86                        "Display text must be 200 characters or less",
87                    ));
88                }
89            }
90        }
91        Ok(())
92    }
93}
94
95/// Pulled from https://sk-eid.github.io/smart-id-documentation/rp-api/interactions.html
96pub fn encode_interactions_base_64(interactions: &Vec<Interaction>) -> Result<String> {
97    let interactions_json = serde_json::to_string(&interactions)
98        .map_err(|e| SmartIdClientError::SerializationError(e.to_string()))?;
99    let base64_encoded =
100        base64::engine::general_purpose::STANDARD.encode(interactions_json.as_bytes());
101    Ok(base64_encoded)
102}
103
104pub fn hash_encode_digest(digest: &str, hashing_algorithm: &HashingAlgorithm) -> Result<String> {
105    let digest_bytes = STANDARD.decode(digest).map_err(
106        |e| SmartIdClientError::InvalidDigestException(format!("Failed to decode digest from base 64: {}", e)),
107    )?;
108
109    let hash_bytes = match hashing_algorithm {
110        HashingAlgorithm::sha_256 => {
111            let mut hasher = Sha256::new();
112            hasher.update(digest_bytes);
113            hasher.finalize().to_vec()
114        },
115        HashingAlgorithm::sha_384 => {
116            let mut hasher = Sha384::new();
117            hasher.update(digest_bytes);
118            hasher.finalize().to_vec()
119        },
120        HashingAlgorithm::sha_512 => {
121            let mut hasher = Sha512::new();
122            hasher.update(digest_bytes);
123            hasher.finalize().to_vec()
124        },
125        HashingAlgorithm::sha3_256 => {
126            let mut hasher = sha3::Sha3_256::new();
127            hasher.update(digest_bytes);
128            hasher.finalize().to_vec()
129        },
130        HashingAlgorithm::sha3_384 => {
131            let mut hasher = sha3::Sha3_384::new();
132            hasher.update(digest_bytes);
133            hasher.finalize().to_vec()
134        },
135        HashingAlgorithm::sha3_512 => {
136            let mut hasher = sha3::Sha3_512::new();
137            hasher.update(digest_bytes);
138            hasher.finalize().to_vec()
139        },
140    };
141    
142    Ok(STANDARD.encode(hash_bytes))
143}
144
145// region: Interaction Tests
146#[cfg(test)]
147mod interaction_tests {
148    use super::*;
149    use serde_json;
150    use tracing_test::traced_test;
151
152    #[traced_test]
153    #[tokio::test]
154    async fn test_serializing_display_text_and_pin() {
155        let interaction = Interaction::DisplayTextAndPIN {
156            display_text_60: "Hello, World!".to_string(),
157        };
158        let serialized = serde_json::to_string(&interaction).unwrap();
159        assert_eq!(
160            serialized,
161            "{\"type\":\"displayTextAndPIN\",\"displayText60\":\"Hello, World!\"}"
162        );
163    }
164
165    #[traced_test]
166    #[tokio::test]
167    async fn test_validate_text_length_display_text_and_pin() {
168        let valid_interaction = Interaction::DisplayTextAndPIN {
169            display_text_60: "Valid text".to_string(),
170        };
171        assert!(valid_interaction.validate_text_length().is_ok());
172
173        let invalid_interaction = Interaction::DisplayTextAndPIN {
174            display_text_60: "This text is way too long and should cause an error because it exceeds the 60 character limit.".to_string(),
175        };
176        assert!(invalid_interaction.validate_text_length().is_err());
177    }
178
179    #[traced_test]
180    #[tokio::test]
181    async fn test_validate_text_length_confirmation_message() {
182        let valid_interaction = Interaction::ConfirmationMessage {
183            display_text_200: "Valid text".to_string(),
184        };
185        assert!(valid_interaction.validate_text_length().is_ok());
186
187        let invalid_interaction = Interaction::ConfirmationMessage {
188            display_text_200: "This text is way too long and should cause an error because it exceeds the 200 character limit. This text is way too long and should cause an error because it exceeds the 200 character limit. This text is way too long and should cause an error because it exceeds the 200 character limit.".to_string(),
189        };
190        assert!(invalid_interaction.validate_text_length().is_err());
191    }
192
193    #[traced_test]
194    #[tokio::test]
195    async fn test_validate_text_length_verification_code_choice() {
196        let valid_interaction = Interaction::VerificationCodeChoice {
197            display_text_60: "Valid text".to_string(),
198        };
199        assert!(valid_interaction.validate_text_length().is_ok());
200
201        let invalid_interaction = Interaction::VerificationCodeChoice {
202            display_text_60: "This text is way too long and should cause an error because it exceeds the 60 character limit.".to_string(),
203        };
204        assert!(invalid_interaction.validate_text_length().is_err());
205    }
206
207    #[traced_test]
208    #[tokio::test]
209    async fn test_validate_text_length_confirmation_message_and_verification_code_choice() {
210        let valid_interaction = Interaction::ConfirmationMessageAndVerificationCodeChoice {
211            display_text_200: "Valid text".to_string(),
212        };
213        assert!(valid_interaction.validate_text_length().is_ok());
214
215        let invalid_interaction = Interaction::ConfirmationMessageAndVerificationCodeChoice {
216            display_text_200: "This text is way too long and should cause an error because it exceeds the 200 character limit. This text is way too long and should cause an error because it exceeds the 200 character limit. This text is way too long and should cause an error because it exceeds the 200 character limit.".to_string(),
217        };
218        assert!(invalid_interaction.validate_text_length().is_err());
219    }
220
221    // Based on the examples provided in https://sk-eid.github.io/smart-id-documentation/rp-api/interactions.html
222    #[traced_test]
223    #[tokio::test]
224    async fn test_confirmation_interaction_base64_encoding() {
225        let interaction = Interaction::ConfirmationMessage {
226            display_text_200: "Longer description of the transaction context".to_string(),
227        };
228        let encoded = encode_interactions_base_64(&vec![interaction]).unwrap();
229        assert_eq!(encoded, "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvbmdlciBkZXNjcmlwdGlvbiBvZiB0aGUgdHJhbnNhY3Rpb24gY29udGV4dCJ9XQ==");
230    }
231
232    // Based on the examples provided in https://sk-eid.github.io/smart-id-documentation/rp-api/interactions.html
233    #[traced_test]
234    #[tokio::test]
235    async fn test_display_text_and_pin_base64_encoding() {
236        let interaction = Interaction::DisplayTextAndPIN {
237            display_text_60: "Log in to mobile banking app".to_string(),
238        };
239        let encoded = encode_interactions_base_64(&vec![interaction]).unwrap();
240        assert_eq!(encoded, "W3sidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IkxvZyBpbiB0byBtb2JpbGUgYmFua2luZyBhcHAifV0=");
241    }
242
243    // Based on the examples provided in https://sk-eid.github.io/smart-id-documentation/rp-api/interactions.html
244    #[traced_test]
245    #[tokio::test]
246    async fn test_multi_interaction_base64_encoding() {
247        let interactions = vec![
248            Interaction::ConfirmationMessage {
249                display_text_200: "Longer description of the transaction context".to_string(),
250            },
251            Interaction::DisplayTextAndPIN {
252                display_text_60: "Short description of the transaction context".to_string(),
253            },
254        ];
255        let encoded = encode_interactions_base_64(&interactions).unwrap();
256        assert_eq!(encoded, "W3sidHlwZSI6ImNvbmZpcm1hdGlvbk1lc3NhZ2UiLCJkaXNwbGF5VGV4dDIwMCI6IkxvbmdlciBkZXNjcmlwdGlvbiBvZiB0aGUgdHJhbnNhY3Rpb24gY29udGV4dCJ9LHsidHlwZSI6ImRpc3BsYXlUZXh0QW5kUElOIiwiZGlzcGxheVRleHQ2MCI6IlNob3J0IGRlc2NyaXB0aW9uIG9mIHRoZSB0cmFuc2FjdGlvbiBjb250ZXh0In1d");
257    }
258}
259
260// endregion: Device Link Tests