vboot/ui: implement to_dev transition flow

Introduce three new action hooks:

- vb2_screen_info.init
  Init function runs once when changing to the screen.

- vb2_screen_info.action
  Action function runs repeatedly while on the screen.

- vb2_menu_item:
  Action function takes precedence over target screen if non-NULL.

Create the VB2_SCREEN_RECOVERY_TO_DEV screen, and add a
keyboard shortcut to get to that screen directly when in
manual recovery mode: Ctrl+D.

The TO_DEV screen repeatedly checks for the correct physical
verification state.  When that state is triggered, it switches
to dev mode and reboots.  The trigger depends on physical
presence type:

- PHYSICAL_PRESENCE_KEYBOARD: wait for ENTER key on the
  confirm button, pressed by internal keyboard

- !PHYSICAL_PRESENCE_KEYBOARD: wait for the physical presence
  button (recovery or power) to be pressed and released

- SPACE character also cancels in order to preserve prior
  behaviour

Note that currently there is no way to exit developer mode
once it has been enabled.

BUG=b:146399181
TEST=make clean && make runtests
BRANCH=none

Change-Id: If3ff248d98859d530c3a24524618c6282a5ac5b5
Signed-off-by: Joel Kitching <kitching@google.com>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/2168072
Reviewed-by: Joel Kitching <kitching@chromium.org>
Commit-Queue: Joel Kitching <kitching@chromium.org>
Tested-by: Joel Kitching <kitching@chromium.org>
diff --git a/firmware/2lib/2ui.c b/firmware/2lib/2ui.c
index 3c54ecc..6f90135 100644
--- a/firmware/2lib/2ui.c
+++ b/firmware/2lib/2ui.c
@@ -132,29 +132,42 @@
 
 	menu_item = &ui->state.screen->items[ui->state.selected_item];
 
-	VB2_DEBUG("Select <%s> menu item <%s>\n",
-		  ui->state.screen->name, menu_item->text);
-
-	if (menu_item->target) {
-		VB2_DEBUG("Changing to target screen %#x for menu item <%s>\n",
-			  menu_item->target, menu_item->text);
+	if (menu_item->action) {
+		VB2_DEBUG("Menu item <%s> run action\n", menu_item->text);
+		return menu_item->action(ui);
+	} else if (menu_item->target) {
+		VB2_DEBUG("Menu item <%s> to target screen %#x\n",
+			  menu_item->text, menu_item->target);
 		return change_screen(ui, menu_item->target);
 	}
 
-	VB2_DEBUG("No target screen for menu item <%s>\n", menu_item->text);
-
+	VB2_DEBUG("Menu item <%s> no action or target screen\n",
+		  menu_item->text);
 	return VB2_REQUEST_UI_CONTINUE;
 }
 
 /**
  * Return back to the previous screen.
  */
-vb2_error_t menu_back_action(struct vb2_ui_context *ui)
+vb2_error_t vb2_ui_back_action(struct vb2_ui_context *ui)
 {
 	/* TODO(kitching): Return to previous screen instead of root screen. */
 	return change_screen(ui, ui->root_screen->id);
 }
 
+/**
+ * Context-dependent keyboard shortcut Ctrl+D.
+ *
+ * - Manual recovery mode: Change to dev mode transition screen.
+ * - Developer mode: Boot from internal disk (TODO).
+ */
+vb2_error_t ctrl_d_action(struct vb2_ui_context *ui)
+{
+	if (vb2_allow_recovery(ui->ctx))
+		return change_screen(ui, VB2_SCREEN_RECOVERY_TO_DEV);
+	return VB2_REQUEST_UI_CONTINUE;
+}
+
 /*****************************************************************************/
 /* Action lookup tables */
 
@@ -165,7 +178,9 @@
 	{ VB_BUTTON_VOL_UP_SHORT_PRESS, 	menu_up_action },
 	{ VB_BUTTON_VOL_DOWN_SHORT_PRESS, 	menu_down_action },
 	{ VB_BUTTON_POWER_SHORT_PRESS, 		menu_select_action },
-	{ VB_KEY_ESC, 			 	menu_back_action },
+	{ VB_KEY_ESC, 			 	vb2_ui_back_action },
+	{ VB_KEY_CTRL('D'),		 	ctrl_d_action },
+	{ ' ',				 	vb2_ui_recovery_to_dev_action },
 };
 
 vb2_error_t (*input_action_lookup(int key))(struct vb2_ui_context *ui)
@@ -192,6 +207,9 @@
 	memset(&ui->state, 0, sizeof(ui->state));
 	ui->state.screen = new_screen_info;
 
+	if (ui->state.screen->init)
+		return ui->state.screen->init(ui);
+
 	return VB2_REQUEST_UI_CONTINUE;
 }
 
@@ -200,7 +218,6 @@
 {
 	struct vb2_ui_context ui;
 	struct vb2_screen_state prev_state;
-	uint32_t key;
 	uint32_t key_flags;
 	vb2_error_t (*action)(struct vb2_ui_context *ui);
 	vb2_error_t rv;
@@ -231,26 +248,38 @@
 					 ui.state.disabled_item_mask);
 		}
 
+		/* Run screen action. */
+		if (ui.state.screen->action) {
+			rv = ui.state.screen->action(&ui);
+			if (rv != VB2_REQUEST_UI_CONTINUE)
+				return rv;
+		}
+
+		/* Grab new keyboard input. */
+		ui.key = VbExKeyboardReadWithFlags(&key_flags);
+		ui.key_trusted = !!(key_flags & VB_KEY_FLAG_TRUSTED_KEYBOARD);
+
 		/* Check for shutdown request. */
-		key = VbExKeyboardReadWithFlags(&key_flags);
-		if (shutdown_required(ctx, key)) {
+		if (shutdown_required(ctx, ui.key)) {
 			VB2_DEBUG("Shutdown required!\n");
 			return VB2_REQUEST_SHUTDOWN;
 		}
 
 		/* Run input action function if found. */
-		action = input_action_lookup(key);
+		action = input_action_lookup(ui.key);
 		if (action) {
-			ui.key = key;
 			rv = action(&ui);
-			ui.key = 0;
 			if (rv != VB2_REQUEST_UI_CONTINUE)
 				return rv;
-		} else if (key) {
-			VB2_DEBUG("Pressed key %#x, trusted? %d\n", key,
-				  !!(key_flags & VB_KEY_FLAG_TRUSTED_KEYBOARD));
+		} else if (ui.key) {
+			VB2_DEBUG("Pressed key %#x, trusted? %d\n",
+				  ui.key, ui.key_trusted);
 		}
 
+		/* Reset keyboard input. */
+		ui.key = 0;
+		ui.key_trusted = 0;
+
 		/* Run global action function if available. */
 		if (global_action) {
 			rv = global_action(&ui);
diff --git a/firmware/2lib/2ui_screens.c b/firmware/2lib/2ui_screens.c
index 5b235c2..a6fffb5 100644
--- a/firmware/2lib/2ui_screens.c
+++ b/firmware/2lib/2ui_screens.c
@@ -6,7 +6,11 @@
  */
 
 #include "2common.h"
+#include "2misc.h"
+#include "2nvstorage.h"
 #include "2ui.h"
+#include "2ui_private.h"
+#include "vboot_api.h"  /* for VB_KEY_ */
 
 #define MENU_ITEMS(a) \
 	.num_items = ARRAY_SIZE(a), \
@@ -62,6 +66,101 @@
 };
 
 /******************************************************************************/
+/* VB2_SCREEN_RECOVERY_TO_DEV */
+
+#define RECOVERY_TO_DEV_ITEM_CONFIRM 0
+
+vb2_error_t recovery_to_dev_init(struct vb2_ui_context *ui)
+{
+	if (vb2_get_sd(ui->ctx)->flags & VB2_SD_FLAG_DEV_MODE_ENABLED) {
+		VB2_DEBUG("Dev mode already enabled?\n");
+		return vb2_ui_back_action(ui);
+	}
+
+	if (!PHYSICAL_PRESENCE_KEYBOARD && vb2ex_physical_presence_pressed()) {
+		VB2_DEBUG("Presence button stuck?\n");
+		return vb2_ui_back_action(ui);
+	}
+
+	/* Disable "Confirm" button for other physical presence types. */
+	if (!PHYSICAL_PRESENCE_KEYBOARD)
+		ui->state.disabled_item_mask =
+			1 << RECOVERY_TO_DEV_ITEM_CONFIRM;
+
+	return VB2_REQUEST_UI_CONTINUE;
+}
+
+vb2_error_t vb2_ui_recovery_to_dev_action(struct vb2_ui_context *ui)
+{
+	static int pressed_last;
+	int pressed;
+
+	if (ui->state.screen->id != VB2_SCREEN_RECOVERY_TO_DEV) {
+		VB2_DEBUG("Action needs RECOVERY_TO_DEV screen\n");
+		return VB2_REQUEST_UI_CONTINUE;
+	}
+
+	if (ui->key == ' ') {
+		VB2_DEBUG("SPACE means cancel dev mode transition\n");
+		return vb2_ui_back_action(ui);
+	}
+
+	if (PHYSICAL_PRESENCE_KEYBOARD) {
+		if (ui->key != VB_KEY_ENTER &&
+		    ui->key != VB_BUTTON_POWER_SHORT_PRESS)
+			return VB2_REQUEST_UI_CONTINUE;
+		if (!ui->key_trusted) {
+			VB2_DEBUG("Reject untrusted %s confirmation\n",
+				  ui->key == VB_KEY_ENTER ?
+				  "ENTER" : "POWER");
+			return VB2_REQUEST_UI_CONTINUE;
+		}
+	} else {
+		pressed = vb2ex_physical_presence_pressed();
+		if (pressed) {
+			VB2_DEBUG("Physical presence button pressed, "
+				 "awaiting release\n");
+			pressed_last = 1;
+			return VB2_REQUEST_UI_CONTINUE;
+		}
+		if (!pressed_last)
+			return VB2_REQUEST_UI_CONTINUE;
+		VB2_DEBUG("Physical presence button released\n");
+	}
+	VB2_DEBUG("Physical presence confirmed!\n");
+
+	/* Sanity check, should never happen. */
+	if ((vb2_get_sd(ui->ctx)->flags & VB2_SD_FLAG_DEV_MODE_ENABLED) ||
+	    !vb2_allow_recovery(ui->ctx)) {
+		VB2_DEBUG("ERROR: dev transition sanity check failed\n");
+		return VB2_REQUEST_UI_CONTINUE;
+	}
+
+	VB2_DEBUG("Enabling dev mode and rebooting...\n");
+	vb2_enable_developer_mode(ui->ctx);
+	return VB2_REQUEST_REBOOT_EC_TO_RO;
+}
+
+static const struct vb2_menu_item recovery_to_dev_items[] = {
+	[RECOVERY_TO_DEV_ITEM_CONFIRM] = {
+		.text = "Confirm",
+		.action = vb2_ui_recovery_to_dev_action,
+	},
+	{
+		.text = "Cancel",
+		.action = vb2_ui_back_action,
+	},
+};
+
+static const struct vb2_screen_info recovery_to_dev_screen = {
+	.id = VB2_SCREEN_RECOVERY_TO_DEV,
+	.name = "Transition to developer mode",
+	.init = recovery_to_dev_init,
+	.action = vb2_ui_recovery_to_dev_action,
+	MENU_ITEMS(recovery_to_dev_items),
+};
+
+/******************************************************************************/
 /* VB2_SCREEN_RECOVERY_PHONE_STEP1 */
 
 static const struct vb2_screen_info recovery_phone_step1_screen = {
@@ -92,6 +191,7 @@
 	&recovery_broken_screen,
 	&recovery_select_screen,
 	&recovery_invalid_screen,
+	&recovery_to_dev_screen,
 	&recovery_phone_step1_screen,
 	&recovery_disk_step1_screen,
 };
diff --git a/firmware/2lib/include/2api.h b/firmware/2lib/include/2api.h
index 416e2aa..6b51c54 100644
--- a/firmware/2lib/include/2api.h
+++ b/firmware/2lib/include/2api.h
@@ -1181,6 +1181,8 @@
 	VB2_SCREEN_RECOVERY_SELECT		= 0x200,
 	/* Invalid recovery media inserted */
 	VB2_SCREEN_RECOVERY_INVALID		= 0x201,
+	/* Confirm transition to developer mode */
+	VB2_SCREEN_RECOVERY_TO_DEV		= 0x202,
 	/* Recovery using disk */
 	VB2_SCREEN_RECOVERY_DISK_STEP1		= 0x210,
 	VB2_SCREEN_RECOVERY_DISK_STEP2		= 0x211,
diff --git a/firmware/2lib/include/2ui.h b/firmware/2lib/include/2ui.h
index c0e2b63..b575402 100644
--- a/firmware/2lib/include/2ui.h
+++ b/firmware/2lib/include/2ui.h
@@ -14,11 +14,17 @@
 /*****************************************************************************/
 /* Data structures */
 
+struct vb2_ui_context;  /* Forward declaration */
+
 struct vb2_screen_info {
 	/* Screen id */
 	enum vb2_screen id;
 	/* Screen name for printing to console only */
 	const char *name;
+	/* Init function runs once when changing to the screen. */
+	vb2_error_t (*init)(struct vb2_ui_context *ui);
+	/* Action function runs repeatedly while on the screen. */
+	vb2_error_t (*action)(struct vb2_ui_context *ui);
 	/* Number of menu items */
 	uint16_t num_items;
 	/* List of menu items */
@@ -30,6 +36,8 @@
 	const char *text;
 	/* Target screen */
 	enum vb2_screen target;
+	/* Action function takes precedence over target screen if non-NULL. */
+	vb2_error_t (*action)(struct vb2_ui_context *ui);
 };
 
 struct vb2_screen_state {
@@ -44,8 +52,13 @@
 	struct vb2_screen_state state;
 	uint32_t locale_id;
 	uint32_t key;
+	int key_trusted;
 };
 
+vb2_error_t vb2_ui_change_screen(struct vb2_ui_context *ui, enum vb2_screen id);
+vb2_error_t vb2_ui_back_action(struct vb2_ui_context *ui);
+vb2_error_t vb2_ui_recovery_to_dev_action(struct vb2_ui_context *ui);
+
 /**
  * Get info struct of a screen.
  *
diff --git a/firmware/2lib/include/2ui_private.h b/firmware/2lib/include/2ui_private.h
index 0053449..f7c4e24 100644
--- a/firmware/2lib/include/2ui_private.h
+++ b/firmware/2lib/include/2ui_private.h
@@ -28,7 +28,7 @@
 vb2_error_t menu_up_action(struct vb2_ui_context *ui);
 vb2_error_t menu_down_action(struct vb2_ui_context *ui);
 vb2_error_t menu_select_action(struct vb2_ui_context *ui);
-vb2_error_t menu_back_action(struct vb2_ui_context *ui);
+vb2_error_t ctrl_d_action(struct vb2_ui_context *ui);
 vb2_error_t (*input_action_lookup(int key))(struct vb2_ui_context *ui);
 
 vb2_error_t change_screen(struct vb2_ui_context *ui, enum vb2_screen id);
@@ -37,4 +37,7 @@
 
 vb2_error_t try_recovery_action(struct vb2_ui_context *ui);
 
+/* From 2ui_screens.c */
+vb2_error_t recovery_to_dev_init(struct vb2_ui_context *ui);
+
 #endif  /* VBOOT_REFERENCE_2UI_PRIVATE_H_ */
diff --git a/tests/vb2_ui_utility_tests.c b/tests/vb2_ui_utility_tests.c
index e62f6c0..08b537f 100644
--- a/tests/vb2_ui_utility_tests.c
+++ b/tests/vb2_ui_utility_tests.c
@@ -595,11 +595,11 @@
 		screen_state_eq(mock_state, MOCK_SCREEN_MENU, 1, MOCK_IGNORE);
 	}
 
-	/* menu_back_action */
+	/* vb2_ui_back_action */
 	reset_common_data();
 	mock_ui_context.key = VB_KEY_ESC;
-	TEST_EQ(menu_back_action(&mock_ui_context), VB2_REQUEST_UI_CONTINUE,
-		"menu_back_action");
+	TEST_EQ(vb2_ui_back_action(&mock_ui_context), VB2_REQUEST_UI_CONTINUE,
+		"vb2_ui_back_action");
 	screen_state_eq(mock_state, VB2_SCREEN_BLANK, 0, MOCK_IGNORE);
 
 	VB2_DEBUG("...done.\n");