Chapter 21. Passwords & Secrets

image

image

Passwords & Secrets is a notepad-style app that you can protect with a master password. Therefore, it’s a great app for storing a variety of passwords and other secrets that you don’t want getting into the wrong hands. The note-taking functionality is top-notch, supporting

→ Auto-save, which makes jotting down notes fast and easy

→ Quick previews of each note

→ The ability to customize each note’s background/foreground colors and text size

→ The ability to email your notes

On top of this, the data in each note is encrypted with 256-bit Advanced Encryption Standard (AES) encryption to keep prying eyes from discovering the data. This encryption is done based on the master password, so it’s important that the user never forgets their password! There is no way for the app to retrieve the data without it, as the app does not store the password for security reasons.

To make management of the master password as easy as possible, Passwords & Secrets supports specifying and showing a password hint. It also enables you to change your password (but only if you know the current password).

Basic Cryptography

Silverlight’s System.Security.Cryptography namespace contains quite a bit of functionality for cryptographic tasks. This app wraps the necessary pieces of functionality from this namespace in order to expose an easy-to-use Crypto class. This class exposes two simple methods—Encrypt and Decrypt—that accept the decrypted/encrypted data along with a password to use as the basis for the encryption and decryption. Listing 21.1 contains the implementation.

Listing 21.1 Crypto.cs—The Crypto Class That Exposes Simple Encrypt and Decrypt Methods


using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace WindowsPhoneApp
{
  public static class Crypto
  {
    public static string Encrypt(string data, string password)
    {
      if (data == null)
        return null;

      using (SymmetricAlgorithm algorithm = GetAlgorithm(password))
      using (MemoryStream memoryStream = new MemoryStream())
      using (CryptoStream cryptoStream = new CryptoStream(
        memoryStream, algorithm.CreateEncryptor(), CryptoStreamMode.Write))
      {
        // Convert the original data to bytes then write them to the CryptoStream
        byte[] buffer = Encoding.UTF8.GetBytes(data);
        cryptoStream.Write(buffer, 0, buffer.Length);
        cryptoStream.FlushFinalBlock();
        // Convert the encrypted bytes back into a string
        return Convert.ToBase64String(memoryStream.ToArray());
      }
    }

    public static string Decrypt(string data, string password)
    {
      if (data == null)
        return null;

      using (SymmetricAlgorithm algorithm = GetAlgorithm(password))
      using (MemoryStream memoryStream = new MemoryStream())
      using (CryptoStream cryptoStream = new CryptoStream(
        memoryStream, algorithm.CreateDecryptor(), CryptoStreamMode.Write))
      {
        // Convert the encrypted string to bytes then write them
        // to the CryptoStream
        byte[] buffer = Convert.FromBase64String(data);
        cryptoStream.Write(buffer, 0, buffer.Length);
        cryptoStream.FlushFinalBlock();
        // Convert the original data back to a string
        buffer = memoryStream.ToArray();
        return Encoding.UTF8.GetString(buffer, 0, buffer.Length);
      }
    }

    // Hash the input data with a salt, typically used for storing a password
    public static string Hash(string data)
    {
      // Convert the data to bytes
      byte[] dataBytes = Encoding.UTF8.GetBytes(data);

      // Create a new array with the salt bytes followed by the data bytes
      byte[] allBytes = new byte[Settings.Salt.Value.Length + dataBytes.Length];
      // Copy the salt at the beginning
      Settings.Salt.Value.CopyTo(allBytes, 0);
      // Copy the data after the salt
      dataBytes.CopyTo(allBytes, Settings.Salt.Value.Length);

      // Compute the hash for the combined set of bytes
      byte[] hash = new SHA256Managed().ComputeHash(allBytes);

      // Convert the bytes into a string
      return Convert.ToBase64String(hash);
    }

    public static byte[] GenerateNewSalt(int length)
    {
      Byte[] bytes = new Byte[length];
      // Fill the array with random bytes, using a cryptographic
      // random number generator (RNG)
      new RNGCryptoServiceProvider().GetBytes(bytes);
      return bytes;
    }

    static SymmetricAlgorithm GetAlgorithm(string password)
    {
      // Use the Advanced Encryption Standard (AES) algorithm
      AesManaged algorithm = new AesManaged();

      // Derive an encryption key from the password
      Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password,
        Settings.Salt.Value);

      // Initialize, converting the two values in bits to bytes (dividing by 8)
      algorithm.Key = bytes.GetBytes(algorithm.KeySize / 8);
      algorithm.IV = bytes.GetBytes(algorithm.BlockSize / 8);

      return algorithm;
    }
  }
}


→ Both Encrypt and Decrypt call a GetAlgorithm helper method (defined at the end of the file) to get started. The returned algorithm can create an encryptor or a decryptor, which is passed to a crypto stream that is used to drive the encryption/decryption work.

→ In Encrypt, the input string is converted to bytes based on a UTF8 encoding. These bytes can then be written to the crypto stream to perform the encryption. The encrypted bytes are retrieved by using the ToArray method on the underlying memory stream used by the crypto stream. These bytes are converted back to a stream using Base64 encoding, which is a common approach for representing binary data in a string.

Decrypt starts with the Base64-encoded string and converts it to bytes to be written to the crypto stream. It then uses the underlying memory stream’s ToArray method to convert the decrypted UTF8 bytes back into a string.

→ The Hash function computes a SHA256 (Secure Hash Algorithm with a 256-bit digest) cryptographic hash of the input string prepended with a random “salt.” This is sometimes called a salted hash. This app calls this method in order to store a salted hash of the password rather than the password itself, for extra security. After all, if a hacker got a hold of the data in isolated storage, the encryption would be pointless if the password were stored along with it in plain text!

GenerateNewSalt simply produces a random byte array of the desired length. Rather than using the simple Random class used in other apps, this method uses RNGCryptoServiceProvider, a higher-quality pseudo-random number generator that is more appropriate to use in cryptographic applications. As shown in the next section, this app calls this method only once, and only the first time the app is run. It stores the randomly generated salt in isolated storage and then uses that for all future encryption, decryption, and hashing.

GetAlgorithm constructs the only built-in encryption algorithm, AesManaged, which is the AES symmetric algorithm. The algorithm needs to be initialized with a secret key and an initialization vector (IV), so this is handled by the Rfc2898DeriveBytes instance.

Rfc2898DeriveBytes is an implementation of a password-based key derivation function—PBKDF2. This uses the password and a random “salt” value, and applies a pseudorandom function based on a SHA1 hash function many times (1000 by default). All this makes the password much harder to crack.

→ The default value of AesManaged’s KeySize property is also its maximum supported value: 256. This means that the key is 256-bits long, which is why this process is called 256-bit encryption.

The LoginControl User Control

With the Crypto class in place, we can create a login control that handles all the user interaction needed for the app’s master password. The LoginControl user control used by this app is shown in Figure 21.1. It has three different modes:

→ The new user mode, in which the user must choose their master password for the first time

→ The normal login mode, in which the user must enter their previously chosen password

→ The change password mode, in which the user can change their password (after entering their existing password)

Figure 21.1 The three modes of the LoginControl user control in action.

image

Listing 21.2 contains the XAML for this control.

Listing 21.2 LoginControl.xaml—The User Interface for the LoginControl User Control


<UserControl x:Class="WindowsPhoneApp.LoginControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WindowsPhoneApp"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">
  <Grid Background="{StaticResource PhoneBackgroundBrush}">

    <!-- A dim accent-colored padlock image -->
    <Rectangle Fill="{StaticResource PhoneAccentBrush}" Width="300" Height="364"
               VerticalAlignment="Bottom" HorizontalAlignment="Right"
               Margin="{StaticResource PhoneMargin}" Opacity=".5">
      <Rectangle.OpacityMask>
        <ImageBrush ImageSource="Images/lock.png"/>
      </Rectangle.OpacityMask>
    </Rectangle>

    <ScrollViewer>
      <Grid>
        <!-- This panel is used for both New User and Change Password modes -->
        <StackPanel x:Name="ChangePasswordPanel" Visibility="Collapsed"
                    Margin="{StaticResource PhoneMargin}">

          <!-- Welcome! -->
          <TextBlock x:Name="WelcomeTextBlock" Visibility="Collapsed"
            Margin="{StaticResource PhoneHorizontalMargin}" TextWrapping="Wrap">
          <Run FontWeight="Bold">Welcome!</Run>
          <LineBreak/>
          Choose a password that you'll remember.  There is no way to recover ...
          </TextBlock>

          <!-- Old password -->
          <TextBlock Text="Old password" x:Name="OldPasswordLabel"
                     Style="{StaticResource LabelStyle}"/>
          <PasswordBox x:Name="OldPasswordBox" KeyUp="PasswordBox_KeyUp"/>

          <!-- New password -->
          <TextBlock Text="New password" Style="{StaticResource LabelStyle}"/>
          <PasswordBox x:Name="NewPasswordBox" KeyUp="PasswordBox_KeyUp"/>

          <!-- Confirm new password -->
          <TextBlock Text="Type new password again"
                     Style="{StaticResource LabelStyle}"/>
          <PasswordBox x:Name="ConfirmNewPasswordBox" KeyUp="PasswordBox_KeyUp"/>

          <!-- Password hint -->
          <TextBlock Text="Password hint (optional)"
                     Style="{StaticResource LabelStyle}"/>
          <TextBox x:Name="PasswordHintTextBox" InputScope="Text"
                   KeyUp="PasswordBox_KeyUp"/>

          <Button Content="ok" Click="OkButton_Click" MinWidth="226"
                  HorizontalAlignment="Left" Margin="0,12,0,0"
                  local:Tilt.IsEnabled="True"/>
        </StackPanel>

        <!-- This panel is used only for the Normal Login mode -->
        <StackPanel x:Name="NormalLoginPanel" Visibility="Collapsed"
                    Margin="{StaticResource PhoneMargin}">
          <TextBlock Text="Enter your password"
                     Style="{StaticResource LabelStyle}"/>
          <PasswordBox x:Name="NormalLoginPasswordBox" KeyUp="PasswordBox_KeyUp"/>
          <Button Content="ok" Click="OkButton_Click" MinWidth="226"
                  HorizontalAlignment="Left" local:Tilt.IsEnabled="True"/>
        </StackPanel>
      </Grid>
    </ScrollViewer>
  </Grid>
</UserControl>


Notes:

→ This control uses a password box wherever a password should be entered. A password box is just like a text box, except that it displays each character as a circle (after a brief moment in which you see the letter you just typed). This matches the behavior of password entry in all the built-in apps. Instead of a Text property, it has a Password property.

→ The opacity mask trick from Chapter 17, “Pick a Card Magic Trick,” is used to give an image of a padlock the color of the current theme’s accent color. It’s also given 50% opacity, so it blends into the background a bit more.

Listing 21.3 contains the code-behind.

Listing 21.3 LoginControl.xaml.cs—The Code-Behind for the LoginControl User Control


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WindowsPhoneApp
{
  public partial class LoginControl : UserControl
  {
    // A custom event
    public event EventHandler Closed;

    public LoginControl()
    {
      InitializeComponent();

      // Update the UI depending on which of the three modes we're in
      if (Settings.HashedPassword.Value == null)
      {
        // The "new user" mode
        this.WelcomeTextBlock.Visibility = Visibility.Visible;
        this.OldPasswordLabel.Visibility = Visibility.Collapsed;
        this.OldPasswordBox.Visibility = Visibility.Collapsed;
        this.ChangePasswordPanel.Visibility = Visibility.Visible;
      }
      else if (CurrentContext.IsLoggedIn)
      {
        // The "change password" mode
        this.ChangePasswordPanel.Visibility = Visibility.Visible;
      }
      else
      {
        // The "normal login" mode
        this.NormalLoginPanel.Visibility = Visibility.Visible;
      }
    }

    void OkButton_Click(object sender, RoutedEventArgs e)
    {
      string currentHashedPassword = Settings.HashedPassword.Value;

      if (currentHashedPassword != null && !CurrentContext.IsLoggedIn)
      {
        // We're in "normal login" mode

        // If the hash of the attempted password matches the stored hash,
        // then we know the user entered the correct password.
        if (Crypto.Hash(this.NormalLoginPasswordBox.Password)
            != currentHashedPassword)
        {
          MessageBox.Show("", "Incorrect password", MessageBoxButton.OK);
          return;
        }

        // Keep the unencrypted password in-memory,
        // only until this app is deactivated/closed
        CurrentContext.Password = this.NormalLoginPasswordBox.Password;
      }
      else
      {
        // We're in "new user" or "change password" mode

        // For "change password," be sure that the old password is correct
        if (CurrentContext.IsLoggedIn && Crypto.Hash(this.OldPasswordBox.Password)
             != currentHashedPassword)
        {
          MessageBox.Show("", "Incorrect old password", MessageBoxButton.OK);
          return;
        }

        // Now validate the new password
        if (this.NewPasswordBox.Password != this.ConfirmNewPasswordBox.Password)
        {
          MessageBox.Show("The two passwords don't match.  Please try again.",
                          "Oops!", MessageBoxButton.OK);
          return;
        }

        string newPassword = this.NewPasswordBox.Password;

        if (newPassword == null || newPassword.Length == 0)
        {
          MessageBox.Show("The password cannot be empty.  Please try again.",
                          "Nice try!", MessageBoxButton.OK);
          return;
        }

        // Store a hash of the password so we can check for the correct
        // password in future logins without storing the actual password
        Settings.HashedPassword.Value = Crypto.Hash(newPassword);

        // Store the password hint as plain text
        Settings.PasswordHint.Value = this.PasswordHintTextBox.Text;

        // Keep the unencrypted password in-memory,
        // only until this app is deactivated/closed
        CurrentContext.Password = newPassword;

        // If there already was a password, we must decrypt all data with the old
        // password (then re-encrypt it with the new password) while we still
        // know the old password! Otherwise the data will be unreadable!
        if (currentHashedPassword != null)
        {
          // Each item in the NotesList setting has an EncryptedContent property
          // that must be processed
          for (int i = 0; i < Settings.NotesList.Value.Count; i++)
          {
            // Encrypt with the new password the data that is decrypted
            // with the old password
            Settings.NotesList.Value[i].EncryptedContent =
              Crypto.Encrypt(
                Crypto.Decrypt(Settings.NotesList.Value[i].EncryptedContent,
                this.OldPasswordBox.Password),
                newPassword
              );
          }
        }
      }

      CurrentContext.IsLoggedIn = true;
      Close();
    }

    void PasswordBox_KeyUp(object sender, KeyEventArgs e)
    {
      // Allow the Enter key to cycle between text boxes and to press the ok
      // button when on the last text box
      if (e.Key == Key.Enter)
      {
        if (sender == this.PasswordHintTextBox ||
            sender == this.NormalLoginPasswordBox)
          OkButton_Click(sender, e);
        else if (sender == this.OldPasswordBox)
          this.NewPasswordBox.Focus();
        else if (sender == this.NewPasswordBox)
          this.ConfirmNewPasswordBox.Focus();
        else if (sender == this.ConfirmNewPasswordBox)
          this.PasswordHintTextBox.Focus();
      }
    }

    public void Close()
    {
      if (this.Visibility == Visibility.Collapsed)
        return; // Already closed

      // Clear all
      this.OldPasswordBox.Password = "";
      this.NewPasswordBox.Password = "";
      this.ConfirmNewPasswordBox.Password = "";
      this.NormalLoginPasswordBox.Password = "";
      this.PasswordHintTextBox.Text = "";

      // Close by becoming invisible
      this.Visibility = Visibility.Collapsed;

      // Raise the event
      if (this.Closed != null)
        this.Closed(this, EventArgs.Empty);
    }
  }
}


Notes:

→ This listing makes use of some of the following settings defined in a separate Settings.cs file:

public static class Settings
{
    // Password-related settings
    public static readonly Setting<byte[]> Salt =
      new Setting<byte[]>("Salt", Crypto.GenerateNewSalt(16));
    public static readonly Setting<string> HashedPassword =
      new Setting<string>("HashedPassword", null);
    public static readonly Setting<string> PasswordHint =
      new Setting<string>("PasswordHint", null);

  // The user's data
  public static readonly Setting<ObservableCollection<Note>> NotesList =
    new Setting<ObservableCollection<Note>>("NotesList",
                                            new ObservableCollection<Note>());

  // User settings
  public static readonly Setting<bool> MakeDefault =
    new Setting<bool>("MakeDefault", false);
  public static readonly Setting<Color> ScreenColor =
    new Setting<Color>("ScreenColor", Color.FromArgb(0xFF, 0xFE, 0xCF, 0x58));
  public static readonly Setting<Color> TextColor =
    new Setting<Color>("TextColor", Colors.Black);
  public static readonly Setting<int> TextSize = new Setting<int>("TextSize",
    22);

  // Temporary state
  public static readonly Setting<int> CurrentNoteIndex =
    new Setting<int>("CurrentNoteIndex", -1);
  public static readonly Setting<Color?> TempScreenColor =
    new Setting<Color?>("TempScreenColor", null);
  public static readonly Setting<Color?> TempTextColor =
    new Setting<Color?>("TempTextColor", null);
}

The salt required by Rfc2898DeriveBytes used by the Crypto class must be at least 8 bytes. With the call to GenerateNewSalt, this app generates a 16-byte salt.

→ In the normal login mode, the control must determine whether the entered password is correct. But the app doesn’t store the user’s password. Instead, it stores a salted hash of the password. Therefore, to validate the entered password, it calls the same Crypto.Hash function and checks if it matches the stored hashed value.

→ Although the unencrypted password is not persisted, it is kept in memory while the app runs so it can decrypt the user’s saved content and encrypt any new content. This is done with the CurrentContext class, defined as follows in CurrentContext.cs:

public static class CurrentContext
{
  public static bool IsLoggedIn = false;
  public static string Password = null;
}

→ In the change password mode, something very important must be done before the old password is forgotten. Everything that has been encrypted with the old password must be decrypted then re-encrypted with the new password. Otherwise, the data would become unreadable because the new password cannot be used to decrypt data that was encrypted with the old password!

→ Inside Close, the Password property of each password box is set to an empty string instead of null because the Password property throws an exception if set to null.

→ You can see that LoginControl is not a general-purpose control but rather tailored to this app. (Although it wouldn’t be hard to generalize it by providing a hook for the consumer to perform the data re-encryption during the password-change process.) It is used in three separate places, shown in the next three sections of this chapter.

The Change Password Page

The change password page, seen previously in Figure 21.1, is nothing more than a page hosting a LoginControl instance. The user can only reach this page when already signed in, so the control is automatically initialized to the “change password” mode thanks to the code in Listing 21.3. Listings 21.4 and 21.5 contain the simple XAML and code-behind for the change password page.

Listing 21.4 ChangePasswordPage.xaml—The User Interface for Password & Secrets’ Change Password Page


<phone:PhoneApplicationPage
    x:Class="WindowsPhoneApp.ChangePasswordPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:local="clr-namespace:WindowsPhoneApp"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="PortraitOrLandscape" shell:SystemTray.IsVisible="True">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- The standard header -->
    <StackPanel Style="{StaticResource PhoneTitlePanelStyle}">
      <TextBlock Text="PASSWORDS &amp; SECRETS"
                 Style="{StaticResource PhoneTextTitle0Style}"/>
      <TextBlock Text="change password"
                 Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>

    <!-- The user control -->
    <local:LoginControl Grid.Row="1" Closed="LoginControl_Closed"/>
  </Grid>
</phone:PhoneApplicationPage>


Listing 21.5 ChangePasswordPage.xaml.cs—The Code-Behind for Password & Secrets’ Change Password Page


using Microsoft.Phone.Controls;

namespace WindowsPhoneApp
{
  public partial class ChangePasswordPage : PhoneApplicationPage
  {
    public ChangePasswordPage()
    {
      InitializeComponent();
    }

    void LoginControl_Closed(object sender, System.EventArgs e)
    {
      if (this.NavigationService.CanGoBack)
        this.NavigationService.GoBack();
    }
  }
}


The Main Page

This app’s main page contains the list of user’s notes, as demonstrated in Figure 21.2. Each one can be tapped to view and/or edit it. A button on the application bar enables adding new notes. But before the list is populated and any of this is shown, the user must enter the correct password. When the user isn’t logged in, the LoginControl covers the entire page except its header, and the application bar doesn’t have the add-note button.

Figure 21.2 A list of notes on the main page, in various colors and sizes.

image

The User Interface

Listing 21.6 contains the XAML for the main page.

Listing 21.6 MainPage.xaml—The User Interface for Password & Secrets’ Main Page


<phone:PhoneApplicationPage
    x:Class="WindowsPhoneApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:local="clr-namespace:WindowsPhoneApp"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="PortraitOrLandscape" shell:SystemTray.IsVisible="True">

  <phone:PhoneApplicationPage.Resources>
    <local:DateConverter x:Key="DateConverter"/>
  </phone:PhoneApplicationPage.Resources>

  <!-- The application bar, with 3 menu items -->
  <phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar>
      <shell:ApplicationBar.MenuItems>
        <shell:ApplicationBarMenuItem Text="show password hint"
                                      Click="PasswordMenuItem_Click"/>
        <shell:ApplicationBarMenuItem Text="instructions"
                                      Click="InstructionsMenuItem_Click"/>
        <shell:ApplicationBarMenuItem Text="about" Click="AboutMenuItem_Click"/>
        <shell:ApplicationBarMenuItem Text="more apps"
                                      Click="MoreAppsMenuItem_Click"/>
      </shell:ApplicationBar.MenuItems>
    </shell:ApplicationBar>
  </phone:PhoneApplicationPage.ApplicationBar>

  <Grid Background="Transparent">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- The standard header -->
    <StackPanel Grid.Row="0"
                Style="{StaticResource PhoneTitlePanelStyle}">
      <TextBlock Text="PASSWORDS &amp; SECRETS"
                 Style="{StaticResource PhoneTextTitle0Style}"/>
    </StackPanel>

    <!-- Show this when there are no notes -->
    <TextBlock Name="NoItemsTextBlock" Grid.Row="1" Text="No notes"
               Visibility="Collapsed" Margin="22,17,0,0"
               Style="{StaticResource PhoneTextGroupHeaderStyle}"/>

    <!-- The list box containing notes -->
    <ListBox x:Name="ListBox" Grid.Row="1" ItemsSource="{Binding}"
             SelectionChanged="ListBox_SelectionChanged">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <StackPanel>
            <!-- The title, in a style matching the note -->
            <Border Background="{Binding ScreenBrush}" Margin="24,0" Width="800"
                    MinHeight="60" local:Tilt.IsEnabled="True">
              <TextBlock Text="{Binding Title}" FontSize="{Binding TextSize}"
                         Foreground="{Binding TextBrush}" Margin="12"
                         VerticalAlignment="Center"/>
            </Border>
            <!-- The modified date -->
            <TextBlock Foreground="{StaticResource PhoneSubtleBrush}"
              Text="{Binding Modified, Converter={StaticResource DateConverter}}"
              Margin="24,0,0,12"/>
          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>

    <!-- The user control -->
    <local:LoginControl x:Name="LoginControl" Grid.Row="1"
Closed="LoginControl_Closed"/>
  </Grid>
</phone:PhoneApplicationPage>


Notes:

→ The ampersand in the app’s title is XML encoded to avoid a XAML parsing error.

→ The LoginControl user control is used as a part of this page, rather than as a separate login page, to ensure a sensible navigation flow. When the user opens the app, logs in, and then sees the data on the main page, pressing the hardware Back button should exit the app, not go back to a login page!

LoginControl doesn’t protect the data simply by visually covering it up; you’ll see in the code-behind that it isn’t populated until after login. And there’s no way for the app to show the data before login because the correct password is needed to properly decrypt the stored notes.

→ The list box’s item template binds to several properties of each note. (The Note class used to represent each one is shown later in this chapter.) The binding to the Modified property uses something called a value converter to change the resultant display. Value converters are discussed next.

Value Converters

In data binding, value converters can morph a source value into a completely different target value. They enable you to plug in custom logic without giving up the benefits of data binding.

Value converters are often used to reconcile a source and target that are different data types. For example, you could change the background or foreground color of an element based on the value of some nonbrush data source, à la conditional formatting in Microsoft Excel. As another example, the toggle switch in the Silverlight for Windows Phone Toolkit leverages a value converter called OnOffConverter that converts the nullable Boolean IsChecked value to an “On” or “Off” string used as its default content.

In Passwords & Secrets, we want to slightly customize the display of each note’s Modified property. Modified is of type DateTimeOffset, so without a value converter applied, it would appear as follows:

12/11/2012 10:18:49 PM -08:00

The -08:00 represents the time zone. It is expressed as an offset from Coordinated Universal Time (UTC).

Our custom value converter strips off the time zone information and the seconds, as that’s more information than we need. It produces a result like the following:

12/11/2010 10:18 PM

Even if Modified were a DateTime instead of a DateTimeOffset, the value converter would still be useful for stripping the seconds value out of the string.

To create a value converter, you must write a class that implements an IValueConverter interface in the System.Windows.Data namespace. This interface has two simple methods—Convert, which is passed the source instance that must be converted to the target instance, and ConvertBack, which does the opposite. Listing 21.7 contains the implementation of the DateConverter value converter used in Listing 21.6.

Listing 21.7 DateConverter.cs—A Value Converter That Customizes the Display of a DateTimeOffset


using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace WindowsPhoneApp
{
  public class DateConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter,
      CultureInfo culture)
    {
      DateTimeOffset date = (DateTimeOffset)value;
      // Return a custom format
      return date.LocalDateTime.ToShortDateString() + " "
           + date.LocalDateTime.ToShortTimeString();
    }

    public object ConvertBack(object value, Type targetType, object parameter,
      CultureInfo culture)
    {
      return DependencyProperty.UnsetValue;
    }
  }
}


The Convert method is called every time the source value changes. It’s given the DateTimeOffset value and returns a string with the date and time in a short format. The ConvertBack method is not needed, as it is only invoked in two-way data binding. Therefore, it returns a dummy value.

Value converters can be applied to any data binding with its Converter parameter. This was done in Listing 21.6 as follows:

<!-- The modified date -->
<TextBlock Foreground="{StaticResource PhoneSubtleBrush}"
  Text="{Binding Modified, Converter={StaticResource DateConverter}}"
  Margin="24,0,0,12"/>

Setting this via StaticResource syntax requires an instance of the converter class to be defined in an appropriate resource dictionary. Listing 21.6 added an instance with the DateConverter key to the page’s resource dictionary:

<phone:PhoneApplicationPage.Resources>
  <local:DateConverter x:Key="DateConverter"/>
</phone:PhoneApplicationPage.Resources>

The Code-Behind

The code-behind for the main page is shown in Listing 21.8.

Listing 21.8 MainPage.xaml.cs—The Code-Behind for Password & Secrets’ Main Page


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace WindowsPhoneApp
{
  public partial class MainPage : PhoneApplicationPage
  {
    IApplicationBarMenuItem passwordMenuItem;

    public MainPage()
    {
      InitializeComponent();
      this.passwordMenuItem = this.ApplicationBar.MenuItems[0]
        as IApplicationBarMenuItem;
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      base.OnNavigatedTo(e);

      // The password menu item is "show password hint" when not logged in,
      // or "change password" when logged in
      if (CurrentContext.IsLoggedIn)
      {
        this.passwordMenuItem.Text = "change password";
        // This is only needed when reactivating app and navigating back to this
        // page from the details page, because going back can instantiate
        // this page in a logged-in state
        this.LoginControl.Close();
      }
      else
      {
        this.passwordMenuItem.Text = "show password hint";
      }

      // Clear the selection so selecting the same item twice in a row will
      // still raise the SelectionChanged event
      Settings.CurrentNoteIndex.Value = -1;
      this.ListBox.SelectedIndex = -1;

      if (Settings.NotesList.Value.Count == 0)
        this.NoItemsTextBlock.Visibility = Visibility.Visible;
      else
        this.NoItemsTextBlock.Visibility = Visibility.Collapsed;
    }

    void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
      if (ListBox.SelectedIndex >= 0)
      {
        // Navigate to the details page for the selected item
        Settings.CurrentNoteIndex.Value = ListBox.SelectedIndex;
        this.NavigationService.Navigate(new Uri("/DetailsPage.xaml",
          UriKind.Relative));
      }
    }

    void LoginControl_Closed(object sender, EventArgs e)
    {
      // Now that we're logged-in, add the "new" button to the application bar
      ApplicationBarIconButton newButton = new ApplicationBarIconButton
      {
        Text = "new",
        IconUri = new Uri("/Shared/Images/appbar.add.png", UriKind.Relative)
      };
      newButton.Click += NewButton_Click;
      this.ApplicationBar.Buttons.Add(newButton);

      // The password menu item is "show password hint" when not logged in,
      // or "change password" when logged in
      this.passwordMenuItem.Text = "change password";

      // Now bind the notes list as the data source for the list box,
      // because its contents can be decrypted
      this.DataContext = Settings.NotesList.Value;
    }

    // Application bar handlers

    void NewButton_Click(object sender, EventArgs e)
    {
      // Create a new note and add it to the top of the list
      Note note = new Note();
      note.Modified = DateTimeOffset.Now;
      note.ScreenColor = Settings.ScreenColor.Value;
      note.TextColor = Settings.TextColor.Value;
      note.TextSize = Settings.TextSize.Value;
      Settings.NotesList.Value.Insert(0, note);

      // "Select" the new note
      Settings.CurrentNoteIndex.Value = 0;

      // Navigate to the details page for the newly created note
      this.NavigationService.Navigate(new Uri("/DetailsPage.xaml",
        UriKind.Relative));
    }

    void PasswordMenuItem_Click(object sender, EventArgs e)
    {
      if (CurrentContext.IsLoggedIn)
      {
        // Change password
        this.NavigationService.Navigate(new Uri("/ChangePasswordPage.xaml",
          UriKind.Relative));
      }
      else
      {
        // Show password hint
        if (Settings.PasswordHint.Value == null ||
            Settings.PasswordHint.Value.Trim().Length == 0)
        {
          MessageBox.Show("Sorry, but there is no hint!", "Password hint",
            MessageBoxButton.OK);
        }
        else
        {
          MessageBox.Show(Settings.PasswordHint.Value, "Password hint",
            MessageBoxButton.OK);
        }
      }
    }

    void InstructionsMenuItem_Click(object sender, EventArgs e)
    {
      this.NavigationService.Navigate(new Uri("/InstructionsPage.xaml",
        UriKind.Relative));
    }

    void AboutMenuItem_Click(object sender, EventArgs e)
    {
      this.NavigationService.Navigate(
        new Uri("/Shared/About/AboutPage.xaml?appName=Passwords %26 Secrets",
          UriKind.Relative));
    }
  }
}


Notes:

→ The first menu item on the application bar, shown expanded in Figure 21.3, reveals the password hint when the user is logged out and navigates to the change password page when the user is logged in.

Figure 21.3 The expanded application bar menu shows “change password” when the user is logged in.

image

→ As seen earlier, the NotesList collection used as the data context for the list box is not just any collection (like List<Note>); it’s an observable collection:

public static readonly Setting<ObservableCollection<Note>> NotesList =
  new Setting<ObservableCollection<Note>>("NotesList",
                                          new ObservableCollection<Note>());

Observable collections raise a CollectionChanged event whenever any changes occur, such as items being added or removed. Data binding automatically leverages this event to keep the target (the list box, in this page) up-to-date at all times. Thanks to this, Listing 21.8 simply sets the page’s data context to the list and the rest takes care of itself.

The INotifyPropertyChanged Interface

Although the observable collection takes care off additions and deletions being reflected in the list box, each Note item must provide notifications to ensure that item-specific property changes are reflected in the databound list box. Note does this by implementing INotifyPropertyChanged, as shown in Listing 21.9.

Listing 21.9 Note.cs—The Note Class Representing Each Item in the List


using System;
using System.ComponentModel;
using System.Windows.Media;

namespace WindowsPhoneApp
{
  public class Note : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    // A helper method used by the properties
    void OnPropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = this.PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
    }

    string encryptedContent;
    public string EncryptedContent
    {
      get { return this.encryptedContent; }
      set { this.encryptedContent = value;
            OnPropertyChanged("EncryptedContent"); OnPropertyChanged("Title"); }
    }

    DateTimeOffset modified;
    public DateTimeOffset Modified
    {
      get { return this.modified; }
      set { this.modified = value; OnPropertyChanged("Modified"); }
    }

    int textSize;
    public int TextSize
    {
      get { return this.textSize; }
      set { this.textSize = value; OnPropertyChanged("TextSize"); }
    }

    Color screenColor;
    public Color ScreenColor
    {
      get { return this.screenColor; }
      set { this.screenColor = value;
            OnPropertyChanged("ScreenColor"); OnPropertyChanged("ScreenBrush"); }
    }

    Color textColor;
    public Color TextColor
    {
      get { return this.textColor; }
      set { this.textColor = value;
            OnPropertyChanged("TextColor"); OnPropertyChanged("TextBrush"); }
    }

    // Three readonly properties whose value is computed from other properties:

    public Brush ScreenBrush
    {
      get { return new SolidColorBrush(this.ScreenColor); }
    }

    public Brush TextBrush
    {
      get { return new SolidColorBrush(this.TextColor); }
    }

    public string Title
    {
      get
      {
        // Grab the note's content
        string title =
          Crypto.Decrypt(this.EncryptedContent, CurrentContext.Password) ?? "";

        // Don't include more than the first 100 characters, which should be long
        // enough, even in landscape with a small font
        if (title.Length > 100)
          title = title.Substring(0, 100);

        // Fold the remaining content into a single line. We can't use
        // Environment.NewLine because it's , whereas newlines inserted from
        // a text box are just
        return title.Replace(' ', ' ');
      }
    }
  }
}


Notes:

INotifyPropertyChanged has a single member—a PropertyChanged event. If the implementer raises this event at the appropriate time with the name of each property that has changed, data binding takes care of refreshing any targets.

→ The raising of the PropertyChanged event is handled by the OnPropertyChanged helper method. The event handler field is assigned to a handler variable to avoid a potential bug. Without this, if a different thread removed the last handler between the time that the current thread checked for null and performed the invocation, a NullReferenceException would be thrown. (The event handler field becomes null when no more listeners are attached.)

→ Notice that some properties, when changed, raise the PropertyChanged event for an additional property. For example, when EncryptedContent is set to a new value, a PropertyChanged event is raised for the readonly Title property. This is done because the value of Title is based on the value of EncryptedContent, so a change to EncryptedContent may change Title.

The Details Page

The details page, shown in Figure 21.4, appears when the user taps a note in the list box on the main page. This page displays the entire contents of the note and enables the user to edit it, delete it, or email its contents. It also provides access to a per-note settings page that gives control over the note’s colors and text size. Listing 21.10 contains this page’s XAML.

Figure 21.4 The details page, shown for a white-on-red note.

image

Listing 21.10 DetailsPage.xaml—The User Interface for Passwords & Secrets’ Details Page


<phone:PhoneApplicationPage
    x:Class="WindowsPhoneApp.DetailsPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:local="clr-namespace:WindowsPhoneApp"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="PortraitOrLandscape" shell:SystemTray.IsVisible="True">

  <!-- The application bar, with three buttons -->
  <phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="False">
      <shell:ApplicationBarIconButton Text="delete"
                                      IconUri="/Shared/Images/appbar.delete.png"
                                      Click="DeleteButton_Click"/>
      <shell:ApplicationBarIconButton Text="email"
                                      IconUri="/Shared/Images/appbar.email.png"
                                      Click="EmailButton_Click"/>
      <shell:ApplicationBarIconButton Text="settings"
                                      IconUri="/Shared/Images/appbar.settings.png"
                                      Click="SettingsButton_Click"/>
    </shell:ApplicationBar>
  </phone:PhoneApplicationPage.ApplicationBar>

  <phone:PhoneApplicationPage.Resources>

    <!-- A copy of the text box default style with its border removed and
         background applied differently. Compare with the style in Program Files
         Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml -->
    ...
  </phone:PhoneApplicationPage.Resources>

  <ScrollViewer>
    <Grid>
      <!-- The full-screen text box -->
      <TextBox x:Name="TextBox" InputScope="Text"
               Style="{StaticResource PhoneTextBox}"
               AcceptsReturn="True" TextWrapping="Wrap"
               GotFocus="TextBox_GotFocus" LostFocus="TextBox_LostFocus"/>

      <!-- The user control -->
      <local:LoginControl x:Name="LoginControl" Closed="LoginControl_Closed"/>
    </Grid>
  </ScrollViewer>
</phone:PhoneApplicationPage>


The text box that basically occupies the whole screen is given a custom style that removes its border and ensures the desired background color remains visible whether the text box has focus. The style was created by copying the default style from %ProgramFiles%Microsoft SDKsWindows Phonev7.0DesignSystem.Windows.xaml then making a few tweaks.

Listing 21.11 contains the code-behind for this page.

Listing 21.11 DetailsPage.xaml.cs—The Code-Behind for Passwords & Secrets’ Details Page


using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;

namespace WindowsPhoneApp
{
  public partial class DetailsPage : PhoneApplicationPage
  {
    bool navigatingFrom;
    string initialText = "";

    public DetailsPage()
    {
      InitializeComponent();
      this.Loaded += DetailsPage_Loaded;
    }

    void DetailsPage_Loaded(object sender, RoutedEventArgs e)
    {
      if (CurrentContext.IsLoggedIn)
      {
        // Automatically show the keyboard for new notes.
        // This also gets called when navigating away, hence the extra check
        // to make sure we're only doing this when navigating to the page
        if (this.TextBox.Text.Length == 0 && !this.navigatingFrom)
          this.TextBox.Focus();
      }
    }

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
      this.navigatingFrom = true;
      base.OnNavigatedFrom(e);
      if (this.initialText != this.TextBox.Text)
      {
        // Automatically save the new content
        Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
        n.EncryptedContent =
          Crypto.Encrypt(this.TextBox.Text, CurrentContext.Password) ?? "";
        n.Modified = DateTimeOffset.Now;
      }
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      base.OnNavigatedTo(e);
      if (CurrentContext.IsLoggedIn)
        this.LoginControl.Close();
    }

    void TextBox_GotFocus(object sender, RoutedEventArgs e)
    {
      this.ApplicationBar.IsVisible = false;
    }

    void TextBox_LostFocus(object sender, RoutedEventArgs e)
    {
      this.ApplicationBar.IsVisible = true;
    }

    void LoginControl_Closed(object sender, EventArgs e)
    {
      this.ApplicationBar.IsVisible = true;

      // Show the note's contents
      Note n = Settings.NotesList.Value[Settings.CurrentNoteIndex.Value];
      if (n != null)
      {
        this.TextBox.Background = n.ScreenBrush;
        this.TextBox.Foreground = n.TextBrush;
        this.TextBox.FontSize = n.TextSize;
        this.initialText = this.TextBox.Text =
          Crypto.Decrypt(n.EncryptedContent, CurrentContext.Password) ?? "";
      }
    }

    // Application bar handlers:
    void DeleteButton_Click(object sender, EventArgs e)
    {
      if (MessageBox.Show("Are you sure you want to delete this note?",
         "Delete note?", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
      {
        Settings.NotesList.Value.Remove(
          Settings.NotesList.Value[Settings.CurrentNoteIndex.Value]);

        if (this.NavigationService.CanGoBack)
          this.NavigationService.GoBack();
      }
    }

    void EmailButton_Click(object sender, EventArgs e)
    {
      EmailComposeTask launcher = new EmailComposeTask();
      launcher.Body = this.TextBox.Text;
      launcher.Subject = "Note";
      launcher.Show();
    }

    void SettingsButton_Click(object sender, EventArgs e)
    {
      this.NavigationService.Navigate(new Uri("/SettingsPage.xaml",
        UriKind.Relative));
    }
  }
}


Notes:

→ This page uses a navigatingFrom flag to check whether the page is in the process of navigating away. That’s because the Loaded event gets raised a second time after OnNavigatedFrom, and applying focus to the text box at this time could cause an unwanted flicker from the on-screen keyboard briefly appearing.

→ The code for the settings page linked to this page is shown in the next chapter, because it is identical to the one used by this app!

The Finished Product

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.243.184