max / alloy
1 file changed,
+344 insertions,
-15 deletions
| @@ -79,7 +79,16 @@ | |||
| 79 | 79 | /// The disk comes first because it is the one that can be wrong in a way | |
| 80 | 80 | /// nothing later recovers from, and because a user who cannot see their disk in | |
| 81 | 81 | /// the list should find that out before typing anything. | |
| 82 | - | const STEPS: [Step; 4] = [Step::Disk, Step::Hostname, Step::Account, Step::Summary]; | |
| 82 | + | /// Encryption sits after the account rather than beside the disk it applies to. | |
| 83 | + | /// Both screens that take a secret are then adjacent, so the passphrases are | |
| 84 | + | /// typed in one stretch, and the review screen stays last. | |
| 85 | + | const STEPS: [Step; 5] = [ | |
| 86 | + | Step::Disk, | |
| 87 | + | Step::Hostname, | |
| 88 | + | Step::Account, | |
| 89 | + | Step::Encryption, | |
| 90 | + | Step::Summary, | |
| 91 | + | ]; | |
| 83 | 92 | ||
| 84 | 93 | /// Which question the wizard is on. | |
| 85 | 94 | /// | |
| @@ -91,6 +100,7 @@ | |||
| 91 | 100 | Disk, | |
| 92 | 101 | Hostname, | |
| 93 | 102 | Account, | |
| 103 | + | Encryption, | |
| 94 | 104 | Summary, | |
| 95 | 105 | } | |
| 96 | 106 | ||
| @@ -100,6 +110,7 @@ | |||
| 100 | 110 | Self::Disk => "select a disk", | |
| 101 | 111 | Self::Hostname => "name this machine", | |
| 102 | 112 | Self::Account => "create your account", | |
| 113 | + | Self::Encryption => "encrypt the disk", | |
| 103 | 114 | Self::Summary => "review and install", | |
| 104 | 115 | } | |
| 105 | 116 | } | |
| @@ -107,7 +118,7 @@ | |||
| 107 | 118 | /// Whether this step takes typing, which decides if the shell keeps | |
| 108 | 119 | /// claiming `q` as quit while it is on screen. | |
| 109 | 120 | const fn types(self) -> bool { | |
| 110 | - | matches!(self, Self::Hostname | Self::Account) | |
| 121 | + | matches!(self, Self::Hostname | Self::Account | Self::Encryption) | |
| 111 | 122 | } | |
| 112 | 123 | } | |
| 113 | 124 | ||
| @@ -130,6 +141,16 @@ | |||
| 130 | 141 | const SLOT_TIMEZONE: usize = 1; | |
| 131 | 142 | const HOSTNAME_SLOTS: usize = 2; | |
| 132 | 143 | ||
| 144 | + | /// Which slot of the encryption step has focus. | |
| 145 | + | /// | |
| 146 | + | /// The checkbox leads because it decides whether the two fields under it mean | |
| 147 | + | /// anything, and a screen whose first slot is a field the answer may discard | |
| 148 | + | /// reads backwards. | |
| 149 | + | const SLOT_ENCRYPT: usize = 0; | |
| 150 | + | const FIELD_PASSPHRASE: usize = 1; | |
| 151 | + | const FIELD_PASSPHRASE_CONFIRM: usize = 2; | |
| 152 | + | const ENCRYPT_SLOTS: usize = 3; | |
| 153 | + | ||
| 133 | 154 | /// Longest username `useradd` accepts. | |
| 134 | 155 | const USERNAME_MAX: usize = 32; | |
| 135 | 156 | ||
| @@ -190,6 +211,23 @@ | |||
| 190 | 211 | Ok(()) | |
| 191 | 212 | } | |
| 192 | 213 | ||
| 214 | + | /// Check the disk passphrase pair. | |
| 215 | + | /// | |
| 216 | + | /// Separate from [`validate_password`] only so the complaints name the right | |
| 217 | + | /// thing: a screen that asks for a disk passphrase and answers "a password is | |
| 218 | + | /// required" is talking about a different field. The no-minimum stance above | |
| 219 | + | /// applies here for the same reason, and the pair still has to agree — more so, | |
| 220 | + | /// since a typo in this one is not recoverable by logging in. | |
| 221 | + | fn validate_passphrase(passphrase: &str, confirm: &str) -> Result<(), String> { | |
| 222 | + | if passphrase.is_empty() { | |
| 223 | + | return Err("a passphrase is required to encrypt the disk".into()); | |
| 224 | + | } | |
| 225 | + | if passphrase != confirm { | |
| 226 | + | return Err("the passphrases do not match".into()); | |
| 227 | + | } | |
| 228 | + | Ok(()) | |
| 229 | + | } | |
| 230 | + | ||
| 193 | 231 | /// The hostname an install gets if the user does not change it. | |
| 194 | 232 | /// | |
| 195 | 233 | /// Matches `etc/hostname` in the image, so the installer's default and the | |
| @@ -364,6 +402,19 @@ | |||
| 364 | 402 | /// not to do. Left off, the install sets no timezone at all and the system | |
| 365 | 403 | /// comes up UTC, which the hostname pane says on screen. | |
| 366 | 404 | pub locate_timezone: bool, | |
| 405 | + | /// Whether the install encrypts the disk with LUKS. | |
| 406 | + | /// | |
| 407 | + | /// The passphrase is deliberately not here, for the same reason the account | |
| 408 | + | /// password is not: it stays in its [`TextField`] until the plan is built. | |
| 409 | + | /// | |
| 410 | + | /// `Default` gives `false` here while the checkbox the user sees starts | |
| 411 | + | /// ticked, which is the one place in this struct where the derived default | |
| 412 | + | /// is not the offered one. It is safe only because the summary cannot be | |
| 413 | + | /// reached without passing the encryption step, and that step writes this | |
| 414 | + | /// field whichever way it went. Nothing should read it before then: an | |
| 415 | + | /// unset value here reads as "no encryption", which is the wrong way for | |
| 416 | + | /// this particular question to fail. | |
| 417 | + | pub encrypt: bool, | |
| 367 | 418 | } | |
| 368 | 419 | ||
| 369 | 420 | /// Where the installer mounts the target's root filesystem to configure it. | |
| @@ -1735,6 +1786,13 @@ | |||
| 1735 | 1786 | locate_timezone: bool, | |
| 1736 | 1787 | /// Which of the three account fields has focus. | |
| 1737 | 1788 | fields: FocusRing, | |
| 1789 | + | passphrase: TextField, | |
| 1790 | + | passphrase_confirm: TextField, | |
| 1791 | + | /// Which of the three encryption-step slots has focus. | |
| 1792 | + | crypt: FocusRing, | |
| 1793 | + | /// The encryption checkbox, until the step is confirmed and it becomes an | |
| 1794 | + | /// answer. Starts ticked: Alloy encrypts unless told not to. | |
| 1795 | + | encrypt: bool, | |
| 1738 | 1796 | answers: Answers, | |
| 1739 | 1797 | error: Option<String>, | |
| 1740 | 1798 | /// The install, once it has been confirmed and started. | |
| @@ -1771,6 +1829,10 @@ | |||
| 1771 | 1829 | machine: FocusRing::new(HOSTNAME_SLOTS), | |
| 1772 | 1830 | locate_timezone: false, | |
| 1773 | 1831 | fields: FocusRing::new(ACCOUNT_FIELDS), | |
| 1832 | + | passphrase: TextField::new(), | |
| 1833 | + | passphrase_confirm: TextField::new(), | |
| 1834 | + | crypt: FocusRing::new(ENCRYPT_SLOTS), | |
| 1835 | + | encrypt: true, | |
| 1774 | 1836 | answers: Answers::default(), | |
| 1775 | 1837 | error: None, | |
| 1776 | 1838 | running: None, | |
| @@ -1944,22 +2006,119 @@ | |||
| 1944 | 2006 | Flow::Continue | |
| 1945 | 2007 | } | |
| 1946 | 2008 | ||
| 2009 | + | /// The passphrase field the encryption step's focus ring is on, if it is on | |
| 2010 | + | /// one. `None` means the checkbox has focus, which is not a field. | |
| 2011 | + | fn focused_crypt_field(&mut self) -> Option<&mut TextField> { | |
| 2012 | + | match self.crypt.current() { | |
| 2013 | + | FIELD_PASSPHRASE => Some(&mut self.passphrase), | |
| 2014 | + | FIELD_PASSPHRASE_CONFIRM => Some(&mut self.passphrase_confirm), | |
| 2015 | + | _ => None, | |
| 2016 | + | } | |
| 2017 | + | } | |
| 2018 | + | ||
| 2019 | + | /// Take the encryption answer, if the passphrase it needs is answerable. | |
| 2020 | + | /// | |
| 2021 | + | /// Turning encryption off skips the pair entirely rather than validating | |
| 2022 | + | /// empty fields: with no LUKS volume there is nothing for a passphrase to | |
| 2023 | + | /// open, and complaining about a blank field the user deliberately left | |
| 2024 | + | /// blank is how a screen teaches people to ignore it. | |
| 2025 | + | fn encrypt_disk(&mut self) -> Flow { | |
| 2026 | + | if self.encrypt | |
| 2027 | + | && let Err(message) = | |
| 2028 | + | validate_passphrase(self.passphrase.value(), self.passphrase_confirm.value()) | |
| 2029 | + | { | |
| 2030 | + | self.error = Some(message); | |
| 2031 | + | // The confirm, for the same reason the account step picks it: a | |
| 2032 | + | // mismatch is far more often a typo in the second of the pair. | |
| 2033 | + | self.crypt.focus(if self.passphrase.value().is_empty() { | |
| 2034 | + | FIELD_PASSPHRASE | |
| 2035 | + | } else { | |
| 2036 | + | FIELD_PASSPHRASE_CONFIRM | |
| 2037 | + | }); | |
| 2038 | + | return Flow::Continue; | |
| 2039 | + | } | |
| 2040 | + | ||
| 2041 | + | self.answers.encrypt = self.encrypt; | |
| 2042 | + | self.error = None; | |
| 2043 | + | self.steps.advance(); | |
| 2044 | + | Flow::Continue | |
| 2045 | + | } | |
| 2046 | + | ||
| 2047 | + | /// Keys for the encryption step: a checkbox and the passphrase pair. | |
| 2048 | + | /// | |
| 2049 | + | /// Same shape as the hostname step's checkbox and the account step's | |
| 2050 | + | /// fields, because it is both of them on one screen. | |
| 2051 | + | fn edit_encryption(&mut self, key: KeyEvent) -> Flow { | |
| 2052 | + | match key.code { | |
| 2053 | + | KeyCode::Tab | KeyCode::Down => self.crypt.next(), | |
| 2054 | + | KeyCode::BackTab | KeyCode::Up => self.crypt.prev(), | |
| 2055 | + | KeyCode::Enter => return self.encrypt_disk(), | |
| 2056 | + | KeyCode::Char(' ') if self.crypt.current() == SLOT_ENCRYPT => { | |
| 2057 | + | self.encrypt = !self.encrypt; | |
| 2058 | + | } | |
| 2059 | + | // Typing into the checkbox edits nothing, the same rule the hostname | |
| 2060 | + | // step follows. The fields below it stay typable while encryption is | |
| 2061 | + | // off so that turning it back on does not lose what was already | |
| 2062 | + | // entered; only the answer gates on the box. | |
| 2063 | + | _ if self.crypt.current() == SLOT_ENCRYPT => {} | |
| 2064 | + | KeyCode::Char(c) => { | |
| 2065 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2066 | + | field.insert(c); | |
| 2067 | + | } | |
| 2068 | + | } | |
| 2069 | + | KeyCode::Backspace => { | |
| 2070 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2071 | + | field.backspace(); | |
| 2072 | + | } | |
| 2073 | + | } | |
| 2074 | + | KeyCode::Delete => { | |
| 2075 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2076 | + | field.delete(); | |
| 2077 | + | } | |
| 2078 | + | } | |
| 2079 | + | KeyCode::Left => { | |
| 2080 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2081 | + | field.left(); | |
| 2082 | + | } | |
| 2083 | + | } | |
| 2084 | + | KeyCode::Right => { | |
| 2085 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2086 | + | field.right(); | |
| 2087 | + | } | |
| 2088 | + | } | |
| 2089 | + | KeyCode::Home => { | |
| 2090 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2091 | + | field.home(); | |
| 2092 | + | } | |
| 2093 | + | } | |
| 2094 | + | KeyCode::End => { | |
| 2095 | + | if let Some(field) = self.focused_crypt_field() { | |
| 2096 | + | field.end(); | |
| 2097 | + | } | |
| 2098 | + | } | |
| 2099 | + | _ => {} | |
| 2100 | + | } | |
| 2101 | + | Flow::Continue | |
| 2102 | + | } | |
| 2103 | + | ||
| 1947 | 2104 | /// One labelled field line, masked if it holds a password. | |
| 1948 | 2105 | /// | |
| 1949 | 2106 | /// Masking happens here rather than in [`TextField`] so the field stays a | |
| 1950 | 2107 | /// plain text buffer: a widget that knows how to hide itself would have to | |
| 1951 | 2108 | /// be trusted to hide itself everywhere, and this is the only place that | |
| 1952 | 2109 | /// draws one. | |
| 2110 | + | /// | |
| 2111 | + | /// Focus arrives as a bool rather than a slot index because two screens | |
| 2112 | + | /// draw these lines now, against two different [`FocusRing`]s, and a slot | |
| 2113 | + | /// number only means something next to the ring it indexes. | |
| 1953 | 2114 | fn field_line<'a>( | |
| 1954 | - | &self, | |
| 1955 | 2115 | theme: &Theme, | |
| 1956 | 2116 | label: &'a str, | |
| 1957 | 2117 | field: &TextField, | |
| 1958 | - | slot: usize, | |
| 2118 | + | focused: bool, | |
| 1959 | 2119 | masked: bool, | |
| 1960 | 2120 | width: u16, | |
| 1961 | 2121 | ) -> Line<'a> { | |
| 1962 | - | let focused = self.fields.is_focused(slot); | |
| 1963 | 2122 | let (before, under, after) = field.split(); | |
| 1964 | 2123 | ||
| 1965 | 2124 | let (mut before, under, mut after) = if masked { | |
| @@ -2027,11 +2186,39 @@ | |||
| 2027 | 2186 | "The account you will log in with. It can become root with run0.", | |
| 2028 | 2187 | )), | |
| 2029 | 2188 | Line::default(), | |
| 2030 | - | self.field_line(theme, "username", &self.username, FIELD_USERNAME, false, w), | |
| 2031 | - | self.field_line(theme, "password", &self.password, FIELD_PASSWORD, true, w), | |
| 2032 | - | self.field_line(theme, "confirm", &self.confirm, FIELD_CONFIRM, true, w), | |
| 2189 | + | Self::field_line( | |
| 2190 | + | theme, | |
| 2191 | + | "username", | |
| 2192 | + | &self.username, | |
| 2193 | + | self.fields.is_focused(FIELD_USERNAME), | |
| 2194 | + | false, | |
| 2195 | + | w, | |
| 2196 | + | ), | |
| 2197 | + | Self::field_line( | |
| 2198 | + | theme, | |
| 2199 | + | "password", | |
| 2200 | + | &self.password, | |
| 2201 | + | self.fields.is_focused(FIELD_PASSWORD), | |
| 2202 | + | true, | |
| 2203 | + | w, | |
| 2204 | + | ), | |
| 2205 | + | Self::field_line( | |
| 2206 | + | theme, | |
| 2207 | + | "confirm", | |
| 2208 | + | &self.confirm, | |
| 2209 | + | self.fields.is_focused(FIELD_CONFIRM), | |
| 2210 | + | true, | |
| 2211 | + | w, | |
| 2212 | + | ), | |
| 2033 | 2213 | Line::default(), | |
| 2034 | - | self.field_line(theme, "ssh key", &self.pubkey, FIELD_PUBKEY, false, w), | |
| 2214 | + | Self::field_line( | |
| 2215 | + | theme, | |
| 2216 | + | "ssh key", | |
| 2217 | + | &self.pubkey, | |
| 2218 | + | self.fields.is_focused(FIELD_PUBKEY), | |
| 2219 | + | false, | |
| 2220 | + | w, | |
| 2221 | + | ), | |
| 2035 | 2222 | ]; | |
| 2036 | 2223 | ||
| 2037 | 2224 | // Said here rather than only at the summary, because this is where the | |
| @@ -2060,6 +2247,91 @@ | |||
| 2060 | 2247 | frame.render_widget(Paragraph::new(lines), area); | |
| 2061 | 2248 | } | |
| 2062 | 2249 | ||
| 2250 | + | /// The encryption pane: the checkbox, then the passphrase pair it governs. | |
| 2251 | + | fn render_encryption(&self, frame: &mut Frame, area: Rect, theme: &Theme) { | |
| 2252 | + | let w = area.width; | |
| 2253 | + | let boxed = self.crypt.is_focused(SLOT_ENCRYPT); | |
| 2254 | + | let box_glyph = if self.encrypt { "[x]" } else { "[ ]" }; | |
| 2255 | + | ||
| 2256 | + | let mut lines = vec![ | |
| 2257 | + | Line::from(text::muted( | |
| 2258 | + | theme, | |
| 2259 | + | "Encrypt this disk, so pulling it out of the machine reveals nothing.", | |
| 2260 | + | )), | |
| 2261 | + | Line::default(), | |
| 2262 | + | // Same focus vocabulary as the hostname checkbox: reversed block on | |
| 2263 | + | // the box, and the label dims with it. | |
| 2264 | + | Line::from(vec![ | |
| 2265 | + | if boxed { | |
| 2266 | + | Span::styled( | |
| 2267 | + | format!("{box_glyph} "), | |
| 2268 | + | Style::default() | |
| 2269 | + | .fg(theme.content_primary) | |
| 2270 | + | .add_modifier(Modifier::REVERSED), | |
| 2271 | + | ) | |
| 2272 | + | } else { | |
| 2273 | + | text::muted(theme, format!("{box_glyph} ")) | |
| 2274 | + | }, | |
| 2275 | + | if boxed { | |
| 2276 | + | text::bold(theme, "encrypt this disk") | |
| 2277 | + | } else { | |
| 2278 | + | text::muted(theme, "encrypt this disk") | |
| 2279 | + | }, | |
| 2280 | + | ]), | |
| 2281 | + | Line::default(), | |
| 2282 | + | ]; | |
| 2283 | + | ||
| 2284 | + | if self.encrypt { | |
| 2285 | + | lines.push(Self::field_line( | |
| 2286 | + | theme, | |
| 2287 | + | "passphrase", | |
| 2288 | + | &self.passphrase, | |
| 2289 | + | self.crypt.is_focused(FIELD_PASSPHRASE), | |
| 2290 | + | true, | |
| 2291 | + | w, | |
| 2292 | + | )); | |
| 2293 | + | lines.push(Self::field_line( | |
| 2294 | + | theme, | |
| 2295 | + | "confirm", | |
| 2296 | + | &self.passphrase_confirm, | |
| 2297 | + | self.crypt.is_focused(FIELD_PASSPHRASE_CONFIRM), | |
| 2298 | + | true, | |
| 2299 | + | w, | |
| 2300 | + | )); | |
| 2301 | + | lines.push(Line::default()); | |
| 2302 | + | // What the passphrase is for, said where it is chosen. The machine | |
| 2303 | + | // unlocks itself from the TPM in the ordinary case, so a user who | |
| 2304 | + | // is not told this will reasonably conclude the passphrase went | |
| 2305 | + | // unused and forget it, which is exactly when they need it. | |
| 2306 | + | for line in [ | |
| 2307 | + | "asked for at boot only when the TPM cannot open the disk,", | |
| 2308 | + | "so it is the one you will almost never type and must not", | |
| 2309 | + | "lose. A recovery phrase is generated as a third way in.", | |
| 2310 | + | ] { | |
| 2311 | + | lines.push(Line::from(text::muted( | |
| 2312 | + | theme, | |
| 2313 | + | format!("{:LABEL_WIDTH$} {line}", ""), | |
| 2314 | + | ))); | |
| 2315 | + | } | |
| 2316 | + | } else { | |
| 2317 | + | // The consequence, stated where it is chosen and not only on the | |
| 2318 | + | // summary. This is the one answer on any pane that cannot be | |
| 2319 | + | // revisited after the install: encryption is set up while the disk | |
| 2320 | + | // is partitioned, and no update can retrofit it. | |
| 2321 | + | for line in [ | |
| 2322 | + | "Anyone who takes this disk can read everything on it.", | |
| 2323 | + | "Encryption cannot be added later without reinstalling.", | |
| 2324 | + | ] { | |
| 2325 | + | lines.push(Line::from(Span::styled( | |
| 2326 | + | format!(" {line}"), | |
| 2327 | + | Severity::Warn.style(theme), | |
| 2328 | + | ))); | |
| 2329 | + | } | |
| 2330 | + | } | |
| 2331 | + | ||
| 2332 | + | frame.render_widget(Paragraph::new(lines), area); | |
| 2333 | + | } | |
| 2334 | + | ||
| 2063 | 2335 | /// The summary pane: the answers, then the commands they produce. | |
| 2064 | 2336 | /// | |
| 2065 | 2337 | /// Showing the actual argv is the point. docs/CONSOLE.md commits the | |
| @@ -2109,6 +2381,25 @@ | |||
| 2109 | 2381 | " no ssh access until one is added".into(), | |
| 2110 | 2382 | ), | |
| 2111 | 2383 | }, | |
| 2384 | + | // Named whichever way it went, like the timezone below, and for a | |
| 2385 | + | // sharper reason: this is the only answer on the screen that the | |
| 2386 | + | // installed machine can never be talked out of. An unencrypted | |
| 2387 | + | // install says so here in the same words the pane used. | |
| 2388 | + | if self.answers.encrypt { | |
| 2389 | + | row( | |
| 2390 | + | theme, | |
| 2391 | + | "encryption", | |
| 2392 | + | "on", | |
| 2393 | + | " TPM, passphrase, recovery phrase".into(), | |
| 2394 | + | ) | |
| 2395 | + | } else { | |
| 2396 | + | row( | |
| 2397 | + | theme, | |
| 2398 | + | "encryption", | |
| 2399 | + | "off", | |
| 2400 | + | " cannot be added without reinstalling".into(), | |
| 2401 | + | ) | |
| 2402 | + | }, | |
| 2112 | 2403 | // Named on the summary whichever way it went. "UTC" is a decision | |
| 2113 | 2404 | // the install is about to make, and a review screen that only | |
| 2114 | 2405 | // listed the answers someone changed would hide the defaults it is | |
| @@ -2390,6 +2681,11 @@ | |||
| 2390 | 2681 | hint("enter", "next"), | |
| 2391 | 2682 | ], | |
| 2392 | 2683 | Step::Account => vec![hint("tab", "field"), hint("enter", "next")], | |
| 2684 | + | Step::Encryption => vec![ | |
| 2685 | + | hint("tab", "field"), | |
| 2686 | + | hint("space", "toggle"), | |
| 2687 | + | hint("enter", "next"), | |
| 2688 | + | ], | |
| 2393 | 2689 | Step::Summary => vec![hint("enter", "install")], | |
| 2394 | 2690 | }; | |
| 2395 | 2691 | if !self.steps.is_first() { | |
| @@ -2423,6 +2719,7 @@ | |||
| 2423 | 2719 | match self.step() { | |
| 2424 | 2720 | Step::Hostname => return self.render_hostname(frame, inner, theme), | |
| 2425 | 2721 | Step::Account => return self.render_account(frame, inner, theme), | |
| 2722 | + | Step::Encryption => return self.render_encryption(frame, inner, theme), | |
| 2426 | 2723 | Step::Summary => return self.render_summary(frame, inner, theme), | |
| 2427 | 2724 | Step::Disk => {} | |
| 2428 | 2725 | } | |
| @@ -2460,6 +2757,7 @@ | |||
| 2460 | 2757 | match self.step() { | |
| 2461 | 2758 | Step::Hostname => return self.edit_hostname(key), | |
| 2462 | 2759 | Step::Account => return self.edit_account(key), | |
| 2760 | + | Step::Encryption => return self.edit_encryption(key), | |
| 2463 | 2761 | Step::Summary => { | |
| 2464 | 2762 | if key.code == KeyCode::Enter { | |
| 2465 | 2763 | return self.confirm_install(); | |
| @@ -2772,6 +3070,10 @@ | |||
| 2772 | 3070 | machine: FocusRing::new(HOSTNAME_SLOTS), | |
| 2773 | 3071 | locate_timezone: false, | |
| 2774 | 3072 | fields: FocusRing::new(ACCOUNT_FIELDS), | |
| 3073 | + | passphrase: TextField::new(), | |
| 3074 | + | passphrase_confirm: TextField::new(), | |
| 3075 | + | crypt: FocusRing::new(ENCRYPT_SLOTS), | |
| 3076 | + | encrypt: true, | |
| 2775 | 3077 | answers: Answers::default(), | |
| 2776 | 3078 | error: None, | |
| 2777 | 3079 | running: None, | |
| @@ -3029,14 +3331,14 @@ | |||
| 3029 | 3331 | } | |
| 3030 | 3332 | ||
| 3031 | 3333 | #[test] | |
| 3032 | - | fn a_complete_account_advances_to_the_summary() { | |
| 3334 | + | fn a_complete_account_advances_to_the_encryption() { | |
| 3033 | 3335 | let (mut view, mut log) = at_account(); | |
| 3034 | 3336 | fill_account(&mut view, "max", "hunter2", "hunter2"); | |
| 3035 | 3337 | ||
| 3036 | 3338 | view.handle(KeyEvent::from(KeyCode::Enter), &mut log); | |
| 3037 | 3339 | ||
| 3038 | 3340 | assert_eq!(view.answers.username.as_deref(), Some("max")); | |
| 3039 | - | assert_eq!(view.step(), Step::Summary); | |
| 3341 | + | assert_eq!(view.step(), Step::Encryption); | |
| 3040 | 3342 | } | |
| 3041 | 3343 | ||
| 3042 | 3344 | // Tab moves between fields; typing has to land in whichever one has focus. | |
| @@ -3080,7 +3382,7 @@ | |||
| 3080 | 3382 | ||
| 3081 | 3383 | view.handle(KeyEvent::from(KeyCode::Enter), &mut log); | |
| 3082 | 3384 | ||
| 3083 | - | assert_eq!(view.step(), Step::Summary); | |
| 3385 | + | assert_eq!(view.step(), Step::Encryption); | |
| 3084 | 3386 | assert_eq!(view.answers.username.as_deref(), Some("max")); | |
| 3085 | 3387 | } | |
| 3086 | 3388 | ||
| @@ -3149,13 +3451,165 @@ | |||
| 3149 | 3451 | assert_eq!(view.username.value(), "q"); | |
| 3150 | 3452 | } | |
| 3151 | 3453 | ||
| 3152 | - | // ---- the summary and the confirm ---- | |
| 3454 | + | // ---- the encryption step ---- | |
| 3153 | 3455 | ||
| 3154 | - | fn at_summary() -> (InstallView, CommandLog) { | |
| 3456 | + | fn at_encryption() -> (InstallView, CommandLog) { | |
| 3155 | 3457 | let (mut view, mut log) = at_account(); | |
| 3156 | 3458 | fill_account(&mut view, "max", "hunter2", "hunter2"); | |
| 3157 | 3459 | view.handle(KeyEvent::from(KeyCode::Enter), &mut log); | |
| 3158 | - | assert_eq!(view.step(), Step::Summary, "fixture stalled on the account"); | |
| 3460 | + | assert_eq!( | |
| 3461 | + | view.step(), | |
| 3462 | + | Step::Encryption, | |
| 3463 | + | "fixture stalled on the account" | |
| 3464 | + | ); | |
| 3465 | + | (view, log) | |
| 3466 | + | } | |
| 3467 | + | ||
| 3468 | + | /// Fill the passphrase pair, leaving focus on the confirm. | |
| 3469 | + | fn fill_passphrase(view: &mut InstallView, passphrase: &str, confirm: &str) { | |
| 3470 | + | let mut log = CommandLog::new(); | |
| 3471 | + | view.crypt.focus(FIELD_PASSPHRASE); | |
| 3472 | + | type_into(view, passphrase, &mut log); | |
| 3473 | + | view.crypt.focus(FIELD_PASSPHRASE_CONFIRM); | |
| 3474 | + | type_into(view, confirm, &mut log); | |
| 3475 | + | } | |
| 3476 | + | ||
| 3477 | + | // The default is the decision for almost every machine, so it is the one | |
| 3478 | + | // worth pinning. Encryption is chosen while the disk is partitioned and no | |
| 3479 | + | // update can add it afterwards, which makes an off-by-default installer a | |
| 3480 | + | // fleet of permanently unencrypted machines. | |
| 3481 | + | #[test] | |
| 3482 | + | fn encryption_is_on_before_anyone_touches_the_box() { | |
| 3483 | + | let (view, _log) = at_encryption(); | |
| 3484 | + | assert!(view.encrypt, "the installer offered to skip encryption"); | |
| 3485 | + | } | |
| 3486 | + | ||
| 3487 | + | #[test] |
Lines truncated