Skip to main content

max / alloy

Answer the recovery gate, so the encrypted path can be driven to its end
Author: Max Johnson <me@maxj.phd> · 2026-09-03 18:42 UTC
Signed with PGP, not checked
Commit: 809bde7bba30b1efc58f678170942d241f7d44fe
Parent: b86a468
1 file changed, +36 insertions, -2 deletions
@@ -20,7 +20,10 @@
20 20 the screen exists to be checked, and a reader cannot check what it does not
21 21 attribute.
22 22 4. **The install completes**, so a skipped step is a step that was genuinely
23 - answered rather than one that was merely hidden.
23 + answered rather than one that was merely hidden. fw12's recipe encrypts, so
24 + this is also the first scenario to drive the encrypted path to its end: it
25 + reads the recovery phrase off the screen and types it back, which is what
26 + the installer waits for before it will say "finished".
24 27
25 28 ## Why the target is an NVMe
26 29
@@ -50,6 +53,34 @@
50 53
51 54 STEP = re.compile(r"step (\d+) of 6")
52 55
56 + # The recovery phrase, as `render_recovery` draws it: eight lowercase words on
57 + # their own indented line, in the action color, between the prose above and the
58 + # field below. Anchored on the shape rather than on a line number, because the
59 + # prose around it is the part likely to be reworded.
60 + PHRASE = re.compile(r"^\s{2,}([a-z]+(?: [a-z]+){7})\s*$", re.M)
61 +
62 +
63 + def finish_encrypted(s, timeout):
64 + """Answer the recovery gate, which only an encrypted install reaches.
65 +
66 + The install is over by the time this screen appears; what it gates is the
67 + installer saying "finished, reboot". A user who reboots without these words
68 + has a machine whose disk dies with its TPM, so the installer will not move
69 + on until they are typed back, and a scenario that stops here would leave the
70 + encrypted path asserted only as far as the last command.
71 +
72 + `install_drive.py` defaults encryption off and never reaches this, which is
73 + why this lives here rather than there."""
74 + s.wait_for(r"Type it back to confirm you have it", timeout)
75 + s.pump(0.5)
76 + screen = s.screen.text()
77 + check("Installation finished" in screen,
78 + "the recovery screen says the install did not finish", s)
79 + hit = PHRASE.search(screen)
80 + check(hit is not None, "no eight-word recovery phrase on the screen that asks for one", s)
81 + s.send(hit.group(1))
82 + s.send(ENTER)
83 +
53 84
54 85 def current_step(s):
55 86 """The step number the title is showing, or None if no title is up."""
@@ -134,7 +165,10 @@
134 165 s.send(ENTER)
135 166
136 167 print("==> installing; this is the long part", flush=True)
137 - ok = wait_for_verdict(s, args.install_timeout)
168 + # Encrypted installs stop on the recovery phrase before they will admit
169 + # to being finished. Answer it, then read the footer as usual.
170 + finish_encrypted(s, args.install_timeout)
171 + ok = wait_for_verdict(s, 120)
138 172 print(s.screen.text())
139 173 if not ok:
140 174 print("\nerror: the installer finished without offering a reboot, "