Skip to content

Latest commit

 

History

History

Lesson_Password

In this lesson we would investigate the password VFR element.

For that let's create a driver PasswordForm with the following VFR code (UefiLessonsPkg/PasswordForm/Form.vfr):

#include <Uefi/UefiMultiPhase.h>
#include "Data.h"

formset
  guid     = FORMSET_GUID,
  title    = STRING_TOKEN(FORMSET_TITLE),
  help     = STRING_TOKEN(FORMSET_HELP),

  varstore VARIABLE_STRUCTURE,
    name  = FormData,
    guid  = STORAGE_GUID;

  defaultstore StandardDefault,
    prompt      = STRING_TOKEN(STANDARD_DEFAULT_PROMPT),
    attribute   = 0x0000;

  form
    formid = 1,
    title = STRING_TOKEN(FORMID1_TITLE);

    password
      varid = FormData.Password,
      prompt = STRING_TOKEN(PASSWORD_PROMPT),
      help = STRING_TOKEN(PASSWORD_HELP),
      minsize = PASSWORD_MIN_LEN,
      maxsize = PASSWORD_MAX_LEN,
    endpassword;
  endform;
endformset;

As you can see we can set minimal and maximum size for the password via its minsize/maxsize fields.

The values for these defines and the overall varstore structure as usually we would place to the header file (UefiLessonsPkg/PasswordForm/Data.h):

#ifndef _DATA_H_
#define _DATA_H_

#define FORMSET_GUID  {0xe54b953d, 0x7ddc, 0x455c, {0x8c, 0x1a, 0x59, 0x92, 0xbb, 0xc7, 0x72, 0xc4}}
#define DATAPATH_GUID {0xb289cf9f, 0xf911, 0x41fd, {0x9b, 0xad, 0x2c, 0x92, 0x57, 0x68, 0x86, 0x64}}
#define STORAGE_GUID  {0xe7f2d73c, 0x699a, 0x4606, {0x92, 0xb6, 0xa3, 0x5e, 0x49, 0x27, 0xc4, 0xd4}}

#define PASSWORD_MIN_LEN       6
#define PASSWORD_MAX_LEN       8
#define PASSWORD_STORAGE_SIZE  9


#pragma pack(1)
typedef struct {
  CHAR16 Password[PASSWORD_STORAGE_SIZE];
} VARIABLE_STRUCTURE;
#pragma pack()

#endif

Here you can see that internally the password element is a simple string. Looking ahead we set the size of the array PASSWORD_STORAGE_SIZE = (PASSWORD_MAX_LEN + 1). This way the password string would be stored along with the null-terminator symbol. This way it would be easier to use standard string handling function on the password data.

Now let's compile our application and try to use it.

Unfortunately when we try to access the password element we would get the following error:

password1

So let's add flags = INTERACTIVE to our element. As you know with that flag our element would call EFI_HII_CONFIG_ACCESS_PROTOCOL.Callback() function when we would access the element (open/close its form, try to change value of the element).

In our case the password element is the only element on the form, so all the generated callbacks would be the callbacks generated by it. But as a good tone let's also add key = KEY_PASSWORD to the VFR code:

password
  varid = FormData.Password,
  prompt = STRING_TOKEN(PASSWORD_PROMPT),
  help = STRING_TOKEN(PASSWORD_HELP),
  flags = INTERACTIVE,
  key = KEY_PASSWORD,
  minsize = PASSWORD_MIN_LEN,
  maxsize = PASSWORD_MAX_LEN,
endpassword;

Don't forget to add #define KEY_PASSWORD 0x1234 to the UefiLessonsPkg/PasswordForm/Data.h.

Before diving into the actual callback code let's make a short stop exploring the IFR code of the element:

EFI_IFR_PASSWORD

The password element produces the EFI_IFR_PASSWORD and EFI_IFR_END opcodes: Build/UefiLessonsPkg/RELEASE_GCC5/X64/UefiLessonsPkg/PasswordForm/PasswordForm/DEBUG/Form.lst

    password
>00000058: 08 91 06 00 07 00 34 12 01 00 00 00 04 06 00 08 00
      varid = FormData.Password,
      prompt = STRING_TOKEN(0x0006),
      help = STRING_TOKEN(0x0007),
      flags = INTERACTIVE,
      key = 0x1234,
      minsize = 6,
      maxsize = 8,
    endpassword;
>00000069: 29 02

Here is a definition for the EFI_IFR_PASSWORD:

EFI_IFR_PASSWORD

Summary:
Creates a password question

Prototype:
#define EFI_IFR_PASSWORD_OP 0x08

typedef struct _EFI_IFR_PASSWORD {
  EFI_IFR_OP_HEADER Header;
  EFI_IFR_QUESTION_HEADER Question;
  UINT16 MinSize;
  UINT16 MaxSize;
} EFI_IFR_PASSWORD;

Members:
Header      The sequence that defines the type of opcode as well as the length of the opcode being defined.
            Header.OpCode = EFI_IFR_PASSWORD_OP
Question    The standard question header
MinSize     The minimum number of characters that can be accepted for this opcode
MaxSize     The maximum number of characters that can be accepted for this opcode

Description:
Creates a password question in the current form.

We've covered EFI_IFR_OP_HEADER and EFI_IFR_QUESTION_HEADER many times, and MinSize/MaxSize are pretty trivial. So there is nothing new for us here. But it is good to know that we understand the underlying IFR code now.

Callback code

Now we need to write an implementation for the EFI_HII_CONFIG_ACCESS_PROTOCOL.CallBack() to correctly handle our password element.

This element is kind of special and UEFI specification even defines a dedicated flowchart about how the browser should communicate with the driver having the password element:

flowchart

The specification also has part two of the flowchart, but currently it seems like is not supported in the edk2 code. The part two is for password elements without the flags = INTERACTIVE, but as you saw earlier the Form Browser simply outputs an error for such cases.

Before we would write the necessary callback code we need to understand when and how the Form Browser will call Callback().

Here is couple of observations regarding the Form Browser:

  • When we open the form Callback() function is called 2 times for the password element, one with the Action=EFI_BROWSER_ACTION_FORM_OPEN argument and one with the Action=EFI_BROWSER_ACTION_RETRIEVE argument, in both cases the passed agument Value would be the string token pointing to the empty string "":
EFI_BROWSER_ACTION_FORM_OPEN (Value="")
EFI_BROWSER_ACTION_RETRIEVE  (Value="")

This is even true when the password has some value, the Form Browser still woudn't pass it to Callback(). This is a security measure.

  • When we close the form Callback() function is called 1 time, also with an empty string:
EFI_BROWSER_ACTION_FORM_CLOSE (Value="")
  • All the rest calls of the Callback() function would happen with the Action=EFI_BROWSER_ACTION_CHANGING argument.

We don't need to do anything special in the EFI_BROWSER_ACTION_FORM_OPEN/EFI_BROWSER_ACTION_RETRIEVE callbacks, so all our handling code can be placed inside the EFI_BROWSER_ACTION_CHANGING action handling:

STATIC
EFI_STATUS
EFIAPI
Callback (
  IN     CONST EFI_HII_CONFIG_ACCESS_PROTOCOL   *This,
  IN     EFI_BROWSER_ACTION                     Action,
  IN     EFI_QUESTION_ID                        QuestionId,
  IN     UINT8                                  Type,
  IN OUT EFI_IFR_TYPE_VALUE                     *Value,
  OUT    EFI_BROWSER_ACTION_REQUEST             *ActionRequest
  )
{
  if ((QuestionId == KEY_PASSWORD) && (Action == EFI_BROWSER_ACTION_CHANGING)) {
     <...>
  }

  return EFI_UNSUPPORTED;
}

Now to the actual logic. According to the UEFI specification password flowchart when you access the password element (i.e. try to change it) first the browser asks if the password is already set. For that the form browser calls the Callback() function with a Value string token pointing to an empty string. In the edk2 code the Form Browser uses Action=EFI_BROWSER_ACTION_CHANGING for that action.

Let's start with a case when the password is not yet set. Then the Callback() code should return EFI_SUCCESS for the above call.

After that the Form Browser prompts user to type new password:

password2

If the input is valid the Form Browser prompts again for password confirmation:

password3

If the user input is incorrect (user has typed password with a length less than password minsize, or user has typed the second time different password) the Form Broswer calls the Callback() function with the Value equal to 0. Keep in mind that since in this case Value is a string token, you shouldn't even try to get an actual string from that with the HiiGetString call.

Here is a flowchart describing possible form browser calls:

   EFI_BROWSER_ACTION_CHANGING  (Value="")
[Callback returns EFI_SUCCESS to say that password is not set yet]
         |
  "Please type in your new password"
         ----------------------------------------------------------------------------
         |                                                                          |
  "Please confirm your new password"                                  "Please enter enough characters"
         |                                                          EFI_BROWSER_ACTION_CHANGING (Token=ZERO)
         |
         ----------------------------------------------------------------------------------------------------------------------------------
         |                                                                          |                                                     |
  EFI_BROWSER_ACTION_CHANGING (Value="<USER INPUT>")                  "Please enter enough characters"                          "Passwords are not the same"
   [Callback sets password and returns EFI_SUCCESS]                 EFI_BROWSER_ACTION_CHANGING (Token=ZERO)                  EFI_BROWSER_ACTION_CHANGING (Token=ZERO)       

Here is a minimal code to support that logic:

EFI_STATUS HandlePasswordInput(EFI_STRING Password)
{
  if (Password[0] == 0) {
    // Form Browser checks if password exists
    if (StrLen(FormStorage.Password) != 0) {
      //TODO
    }
    return EFI_SUCCESS;
  }
  
  // Form Browser sends 'new passwd' to set
  
  if (FormStorage.Password[0] == 0) {
    StrnCpyS(FormStorage.Password, PASSWORD_STORAGE_SIZE, Password, StrLen(Password));
    return EFI_SUCCESS;
  }
}

STATIC
EFI_STATUS
EFIAPI
Callback (
  IN     CONST EFI_HII_CONFIG_ACCESS_PROTOCOL   *This,
  IN     EFI_BROWSER_ACTION                     Action,
  IN     EFI_QUESTION_ID                        QuestionId,
  IN     UINT8                                  Type,
  IN OUT EFI_IFR_TYPE_VALUE                     *Value,
  OUT    EFI_BROWSER_ACTION_REQUEST             *ActionRequest
  )
{
  EFI_STATUS Status;
  if ((QuestionId == KEY_PASSWORD) && (Action == EFI_BROWSER_ACTION_CHANGING)) {
    if (Value->string == 0) {
      return EFI_UNSUPPORTED;
    }

    EFI_STRING Password = HiiGetString(mHiiHandle, Value->string, "en-US");
    Status = HandlePasswordInput(Password);
    FreePool(Password);
    return Status;
  }

  return EFI_UNSUPPORTED;  
}

Now we can successfully set initial value for the password from the browser. So it it time to investigate the Form Browser Callback() calls for the case when password is already set.

As before, when we try to access the password element, first the browser would ask if the password is already set. But this time we should return an error. We would return EFI_ALREADY_STARTED (other errors are possible, but let's stick with that).

After that the Form Browser will ask user to input the current password.

password4

If it has enough characters the browser will pass the input to the Callback(). The Callback() code should check if the input is equal to the old password. If everything is ok, the code should return EFI_SUCCESS.

After that the Form Browser will ask user to type new password:

password5

And if everything correct to confirm it:

password6

The logic is the same as earlier. If the user input is incorrect (user has typed password with a length less than password minsize, or user typed the second time different password) the Form Broswer calls the Callback() function with the Value equal to 0.

Once again here is a flowchart for better understanding:

  EFI_BROWSER_ACTION_CHANGING  (Value="")
[Callback should return EFI_ALREADY_STARTED to say the password is set]
         |
  "Please type in your password" 
         |
         ----------------------------------------------------------------------------
         |                                                                          |
  EFI_BROWSER_ACTION_CHANGING (Value="<USER INPUT>")                      "Please enter enough characters"
[Callback checks if <USER INPUT> equal to current password]
[and returns EFI_SUCCESS on match, EFI_NOT_READY on fail]
         |
         ---------------------------------------------------------------------------
         |                                                                          |
  "Please type in your new password"                                        "Incorrect password"
         |
         ----------------------------------------------------------------------------
         |                                                                          |
  "Please confirm your new password"                                      "Please enter enough characters"
         |                                                             EFI_BROWSER_ACTION_CHANGING (Token=ZERO)
         |                                                        
         ----------------------------------------------------------------------------------------------------------------------------------
         |                                                                          |                                                     |
  EFI_BROWSER_ACTION_CHANGING (Value="<USER INPUT>")                      "Please enter enough characters"                      "Passwords are not the same"
   [Callback sets password and returns EFI_SUCCESS]                    EFI_BROWSER_ACTION_CHANGING (Token=ZERO)               EFI_BROWSER_ACTION_CHANGING (Token=ZERO)   

Since now the Form Browser can send non-empty string not only to set the new password, but also to verify the old one, we need to understand our place in the flowchart for the correct input handling. For that let's create a global variable BOOLEAN OldPasswordVerified:

  • initially we would set this variable to FALSE,
  • every time the user resets the Form Browser logic typing incorrect data, we would set it to FALSE,
  • when the old password is verified we would set it to TRUE,
  • when the password is updated correctly, we would set it back to FALSE again.

With everything in mind let's update our HandlePasswordInput() code:

BOOLEAN OldPasswordVerified = FALSE;

EFI_STATUS HandlePasswordInput(EFI_STRING Password)
{
  if (Password[0] == 0) {
    // Form Browser checks if password exists
    if (FormStorage.Password[0] != 0) {
      return EFI_ALREADY_STARTED;
    } else {
      return EFI_SUCCESS;
    }
  } else {
    // Form Browser sends password value
    // It can be old password to check or initial/updated password to set

    if (FormStorage.Password[0] == 0) {
      // Set initial password
      StrnCpyS(FormStorage.Password, PASSWORD_STORAGE_SIZE, Password, StrLen(Password));
      return EFI_SUCCESS;
    }

    if (!OldPasswordVerified) {
      // Check old password
      if (StrCmp(Password, FormStorage.Password))
        return EFI_NOT_READY;

      OldPasswordVerified = TRUE;
      return EFI_SUCCESS;
    }

    // Update password
    StrnCpyS(FormStorage.Password, PASSWORD_STORAGE_SIZE, Password, StrLen(Password));
    OldPasswordVerified = FALSE;
    return EFI_SUCCESS;
  }
}

With the following code you can set initial password and also have a possibility update it via the Form Browser.

In the next lesson we would investigate why we shouldn't store password as a plain text in the storage and how we can make our password storage more secure.