waterui_form/
secure.rs

1//! Secure form components for handling sensitive data.
2//!
3//! This module provides utilities for handling sensitive form data such as
4//! passwords and other secrets with automatic memory zeroing for security.
5
6use core::{fmt::Debug, str::FromStr};
7
8use alloc::string::{String, ToString};
9use nami::Binding;
10use waterui_core::{AnyView, View, configurable, layout::StretchAxis};
11use zeroize::Zeroize;
12
13/// A wrapper type for securely handling sensitive string data.
14#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct Secure(String);
16
17impl Debug for Secure {
18    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
19        f.write_str("Secure(****)")
20    }
21}
22
23impl FromStr for Secure {
24    type Err = core::convert::Infallible;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        Ok(Self(s.to_string()))
28    }
29}
30
31impl Secure {
32    /// Creates a new Secure value from a string.
33    ///
34    /// # Arguments
35    ///
36    /// * `value` - The string value to secure
37    ///
38    /// # Returns
39    ///
40    /// A new Secure instance wrapping the provided string.
41    #[must_use]
42    pub const fn new(value: String) -> Self {
43        Self(value)
44    }
45
46    /// Returns the inner string as a string slice.
47    ///
48    /// # Returns
49    ///
50    /// A reference to the inner string data.
51    #[must_use]
52    pub fn expose(&self) -> &str {
53        &self.0
54    }
55
56    /// Sets the value of the secure string.
57    ///
58    /// # Arguments
59    ///
60    /// * `value` - The new string value
61    pub fn set(&mut self, value: String) {
62        self.0.zeroize();
63        self.0 = value;
64    }
65
66    /// Hashes the secure string using bcrypt.
67    ///
68    /// # Returns
69    ///
70    /// A bcrypt hash of the inner string data.
71    #[allow(clippy::missing_panics_doc)] // bcrypt::hash never panics
72    #[must_use]
73    pub fn hash(&self) -> String {
74        bcrypt::hash(self.expose(), bcrypt::DEFAULT_COST).expect("Failed to hash password")
75    }
76}
77
78// Ensure the inner string is zeroed out when dropped
79impl Drop for Secure {
80    fn drop(&mut self) {
81        self.0.zeroize();
82    }
83}
84
85/// Configuration for a secure field component.
86#[derive(Debug)]
87pub struct SecureFieldConfig {
88    /// The label view displayed for the secure field.
89    pub label: AnyView,
90    /// The binding to the secure value being edited.
91    pub value: Binding<Secure>,
92}
93
94configurable!(
95    /// A secure text entry field for passwords and sensitive data.
96    ///
97    /// SecureField masks input and securely stores values with automatic memory zeroing.
98    ///
99    /// # Layout Behavior
100    ///
101    /// SecureField **expands horizontally** to fill available space, but has a fixed height.
102    /// In an `HStack`, it will take up all remaining width after other views are sized.
103    //
104    // ═══════════════════════════════════════════════════════════════════════════
105    // INTERNAL: Layout Contract for Backend Implementers
106    // ═══════════════════════════════════════════════════════════════════════════
107    //
108
109    // Height: Fixed intrinsic (platform-determined)
110    // Width: Reports minimum usable width, expands during layout phase
111    //
112    // Same layout behavior as TextField.
113    //
114    // ═══════════════════════════════════════════════════════════════════════════
115    //
116    SecureField,
117    SecureFieldConfig,
118    StretchAxis::Horizontal
119);
120
121impl SecureField {
122    /// Creates a new `SecureField` instance.
123    ///
124    /// # Arguments
125    ///
126    /// * `label` - A view representing the label for the secure field.
127    /// * `value` - A binding to the `Secure` value that the field will edit.
128    ///
129    /// # Returns
130    ///
131    /// A new `SecureField` instance configured with the provided label and value binding.
132    #[must_use]
133    pub fn new(label: impl View, value: &Binding<Secure>) -> Self {
134        Self(SecureFieldConfig {
135            label: AnyView::new(label),
136            value: value.clone(),
137        })
138    }
139
140    /// Sets the label for the secure field.
141    ///
142    /// # Arguments
143    ///
144    /// * `label` - A view representing the new label for the secure field.
145    ///
146    /// # Returns
147    ///
148    /// A new `SecureField` instance with the updated label.
149    #[must_use]
150    pub fn label(self, label: impl View) -> Self {
151        let mut config = self.0;
152        config.label = AnyView::new(label);
153        Self(config)
154    }
155}
156
157/// Creates a new `SecureField` instance.
158/// See [`SecureField::new`] for more details.
159#[must_use]
160pub fn secure(label: impl View, value: &Binding<Secure>) -> SecureField {
161    SecureField::new(label, value)
162}