In the last few articles we have walked through how to put together a complete .NET MAUI cryptography tool. Along the way, I have added code for most of the project folders, with the exception of the root folder, and a folder called “ViewModels”. I’ll tackle the latter in this article.
The code for the AboutPageViewModel
class looks like this:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is the view-model for the <see cref="AboutPage"/> view. /// </summary> public class AboutPageViewModel : ViewModelBase<AboutPageViewModel> { /// <summary> /// This property contains the title for the application. /// </summary> public string Title => $"About: {GetType().Assembly.ReadTitle()}"; /// <summary> /// This property contains the version for the application. /// </summary> public string Version => $"Version: {GetType().Assembly.ReadFileVersion()}"; /// <summary> /// This property contains the description for the application. /// </summary> public string Description => GetType().Assembly.ReadDescription(); /// <summary> /// This property contains the copyright for the application. /// </summary> public string Copyright => GetType().Assembly.ReadCopyright(); /// <summary> /// This constructor creates a new instance of the <see cref="AboutPageViewModel"/> /// class. /// </summary> /// <param name="appSettings">The application settings for the view-model.</param> /// <param name="logger">The logger for the view-model.</param> public AboutPageViewModel( IOptions<AppSettings> appSettings, ILogger<AboutPageViewModel> logger ) : base(appSettings, logger) { } }
The first thing to note is that the AboutPageViewModel
class derives from the ViewModelBase
class. That class, ViewModelBase
, is our common view-model base. We’ll look at that code shortly.
For now, notice that the AboutPageViewModel
class has several properties for the information we want to display on the page. Notice also, that the properties all use the extension methods from the AssemblyExtensions
class. We covered that class in an earlier article. That means we’re actually reading everything from the executable’s attributes. That’s pretty cool, since that means we won’t have to locate and manually change these values if/when anything changes.
The code for the AesPageViewModel
looks like this:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is the view-model for the <see cref="AesPage"/> view. /// </summary> public class AesPageViewModel : CryptoViewModelBase<AesPageViewModel> { /// <summary> /// This constructor creates a new instance of the <see cref="AesPageViewModel"/> /// class. /// </summary> /// <param name="appSettings">The application settings to use for the /// view-model.</param> /// <param name="logger">The logger to use for the view-model.</param> public AesPageViewModel( IOptions<AppSettings> appSettings, ILogger<AesPageViewModel> logger ) : base(appSettings, logger) { } /// <inheritdoc/> protected override void OnEncrypt() { try { // Is there anything to encrypt? if (!string.IsNullOrEmpty(DecryptedText)) { var encryptedBytes = new byte[0]; // Get the password bytes. var passwordBytes = Encoding.UTF8.GetBytes( _appSettings.Value.Keys.Password ); // Get the salt bytes. var saltBytes = Encoding.UTF8.GetBytes( _appSettings.Value.Keys.Salt ); // Create the algorithm using (var alg = Aes.Create()) { // Set the block and key sizes. alg.KeySize = 256; alg.BlockSize = 128; // Derive the ACTUAL crypto key. var key = new Rfc2898DeriveBytes( passwordBytes, saltBytes, (int)_appSettings.Value.Keys.Iterations ); // Generate the key and salt with proper lengths. alg.Key = key.GetBytes(alg.KeySize / 8); alg.IV = key.GetBytes(alg.BlockSize / 8); // Create the encryptor. using (var enc = alg.CreateEncryptor( alg.Key, alg.IV )) { // Create a temporary stream. using (var stream = new MemoryStream()) { // Create a cryptographic stream. using (var cryptoStream = new CryptoStream( stream, enc, CryptoStreamMode.Write )) { // Create a writer using (var writer = new StreamWriter( cryptoStream )) { // Write the bytes. writer.Write( DecryptedText ); } // Get the bytes. encryptedBytes = stream.ToArray(); } } } } // Convert the bytes back to an encoded string. var encryptedValue = Convert.ToBase64String( encryptedBytes ); // Update the UI. EncryptedText = encryptedValue; } else { // Nothing to decrypt! EncryptedText = ""; } } catch (Exception ex) { // Prompt the user. OnErrorRaised( "Failed to encrypt text!", ex ); } } /// <inheritdoc/> protected override void OnDecrypt() { try { // Convert the encrypted value to bytes. var encryptedBytes = Convert.FromBase64String( EncryptedText ?? "" ); // Get the password bytes. var passwordBytes = Encoding.UTF8.GetBytes( _appSettings.Value.Keys.Password ); // Get the salt bytes. var saltBytes = Encoding.UTF8.GetBytes( _appSettings.Value.Keys.Salt ); var plainValue = ""; // Create the algorithm using (var alg = Aes.Create()) { // Set the block and key sizes. alg.KeySize = 256; alg.BlockSize = 128; // Derive the ACTUAL crypto key. var key = new Rfc2898DeriveBytes( passwordBytes, saltBytes, (int)_appSettings.Value.Keys.Iterations ); // Generate the key and salt with proper lengths. alg.Key = key.GetBytes(alg.KeySize / 8); alg.IV = key.GetBytes(alg.BlockSize / 8); // Create the decryptor. using (var dec = alg.CreateDecryptor( alg.Key, alg.IV )) { // Create a temporary stream. using (var stream = new MemoryStream( encryptedBytes )) { // Create a crypto stream. using (var cryptoStream = new CryptoStream( stream, dec, CryptoStreamMode.Read )) { using (var reader = new StreamReader( cryptoStream )) { plainValue = reader.ReadToEnd(); } } } } } // Update the UI. DecryptedText = plainValue; } catch (Exception ex) { // Prompt the user. OnErrorRaised( "Failed to decrypt text!", ex ); } } }
The first thing to note is that the AesPageViewModel
class derives from the CryptoViewModelBase
class. We’ll cover that class shorty. For now, we’ll focus on the AesPageViewModel
class.
There are two methods: OnEncrypt
and OnDecrypt
. Let’s cover OnEncrypt
first. The method starts by checking the DecryptedText
property for text. The DecryptedText
property is inherited from the CryptoViewModelBase
class. Assuming DecryptedText
isn’t empty, we continue by converting the Password
property, from the _appSettings
field, into a byte array. The _appSettings
field is inherited from the ViewModelBase.
Next we convert the Salt
property, on the _appSettings
field, to a byte array.
Next we create an instance of the Aes
class. Aes
is a standard part of .NET and encapsulates the AES cryptographic algorithm. AES wants the key and block sizes to be a certain size so we set that up here, choosing a size of 256 for the Key, and 128 for the block.
Next, we create an instance of the Rfc2898DeriveBytes
class, and feed those two byte arrays into it. Rfc2898DeriveBytes
encapsulates the RFC2898 algorithm, which helps us create encryption keys that are much more secure than simply using a password string, in .NET.
Once the Rfc2898DeriveBytes
class has generated our key, we feed that value (along with a IV value, generated from our SALT), into the AES instance we created earlier. At this point, we’re ready to begin the encryption process.
We start the encryption process by creating an encryptor, (actually an ICryptoTransform
object) from the AES we created earlier. Next, we create a MemoryStream
object to use as a working buffer. Next, we pass that buffer into a CryptoStream
object, which, along with the ICryptoTransform
object, will handle all the heavy lifting involved in the encryption itself. From the outside, CryptoStream
is just a stream, like any other .NET stream, so we need to create a StreamWriter
to work with it. We do that next. Once we’ve wrapped everything up nicely, we then write the value of the DecryptedText
property, into the stream writer, and .NET literally handles everything else for us.
.NET writes the encrypted version of DecryptedText
to our internal buffer. So, we save a reference to those bytes and convert those bytes to a Base64 encoded string, which we finally write into the EncryptedText
property. EncryptedText
is a property we inherited from the CryptoViewModelBase
class.
The OnDecrypt
method does the reverse of what we did in OnEncrypt
. The key creation from the Password
and Salt
properties is the same. But, instead of creating an encryptor from the AES object, we create a decryptor instead. Also, instead of reading from the DecryptedText
property, we read from the EncryptedText
property. Also, instead of creating a StreamWriter
, we create a StreamReader
object, since we read to perform a decryption.
In the end, we wind up with the decrypted value of whatever was in the EncryptedText
property, and we write that value into the DecryptedText
property at the end of the method.
The code for the DataProtectionPageViewModel
class looks like this:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is the view-model for the <see cref="DataProtectionPage"/> view. /// </summary> public class DataProtectionPageViewModel : CryptoViewModelBase<DataProtectionPageViewModel> { /// <summary> /// This constructor creates a new instance of the <see cref="DataProtectionPageViewModel"/> /// class. /// </summary> /// <param name="appSettings">The application settings to use for the /// view-model.</param> /// <param name="logger">The logger to use for the view-model.</param> public DataProtectionPageViewModel( IOptions<AppSettings> appSettings, ILogger<DataProtectionPageViewModel> logger ) : base(appSettings, logger) { } /// <inheritdoc/> protected override void OnEncrypt() { try { // Check for a missing cert. if (string.IsNullOrEmpty(_appSettings.Value.Certs.X509Pem)) { // Warn the user. OnWarningRaised( "A certificate wasn't provided in the settings, " + "which means the keys won't be encrypted, at rest. " + Environment.NewLine + Environment.NewLine + "To ensure the keys are protected, at rest, please " + "provide a certificate, in the settings." ); } // Create a data-protector. var dataProtector = CreateDataProtector(); // Is there anything to encrypt? if (!string.IsNullOrEmpty(DecryptedText)) { // Protect the text. EncryptedText = dataProtector.Protect(DecryptedText ?? ""); } else { // Nothing to encrypt! EncryptedText = ""; } } catch (Exception ex) { // Prompt the user. OnErrorRaised( "Failed to encrypt text!", ex ); } } /// <inheritdoc/> protected override void OnDecrypt() { try { // Create a data-protector. var dataProtector = CreateDataProtector(); // Is there anything to decrypt? if (!string.IsNullOrEmpty(EncryptedText)) { // Unprotect the text. DecryptedText = dataProtector.Unprotect(EncryptedText ?? ""); } else { // Nothing to decrypt! DecryptedText = ""; } } catch (Exception ex) { // Prompt the user. OnErrorRaised( "Failed to decrypt text!", ex ); } } /// <summary> /// This method creates a user configured data-protector. /// </summary> /// <returns>A <see cref="IDataProtector"/> instance.</returns> private IDataProtector CreateDataProtector() { // Get a path to our private folder. var appFolderPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CodeGator", AppDomain.CurrentDomain.FriendlyName.Replace(".", "") ); // Should we create the path? if (!Directory.Exists(appFolderPath)) { // Make sure the path exists. Directory.CreateDirectory(appFolderPath); } // The reason we're creating the data protector here, instead of injecting // it with the .NET DI container is becuase we need to be able to re-create // the object each time the user changes any of the related settings. // Create the data-protector provider. var dataProtectionProvider = DataProtectionProvider.Create( new DirectoryInfo(appFolderPath), config => { // Did the user provide us with an X509 certificate? if (!string.IsNullOrEmpty(_appSettings.Value.Certs.X509Pem)) { // Create the actual cert. var cert = X509Certificate2.CreateFromPem( _appSettings.Value.Certs.X509Pem.AsSpan() ); // Make sure the cert is valid. cert.Verify(); // Use the certificate for the ASP.NET data-protector. config.ProtectKeysWithCertificate(cert); // We don't want the keys changing underneath us. config.DisableAutomaticKeyGeneration(); } }); // Create the data-protector. var dataProtector = dataProtectionProvider.CreateProtector( AppDomain.CurrentDomain.FriendlyName ); // Return the data-protector. return dataProtector; } }
The DataProtectionPageViewModel
class inherits from the CryptoViewModelBase
class, which we’ll cover next.
The class has three methods: OnEncrypt
, OnDecrypt
, and CreateDataProtector
. We’ll cover the CreateDataProtector
method first.
The method starts by creating the path to the application’s appSettings.json file. We then use that path, along with any X509 certificate provided by the user, to create an IDataProtectionProvider
object. We verify the certificate here, so if the value is wrong we should throw an exception to warn the user.
Once we have the IDataProtectionProvider
object, we then use it to create the IDataProtector
instance and return it.
The OnEncrypt
method is similar to the one we looked at before, for the AES page’s view-model. However, this time we’re using the ASP.NET data protector, instead of the .NET AES class, to perform our encryption.
Since we already did the heavy lifting associated with creating a properly configured IDataProtector
object, using the settings from our appSettings.json page, this version of OnEncrypt
is much simpler than the one we looked at before. This version checks to see if there’s anything in the DecryptedText
property. If so, it then calls the Protect
method on the IDataProtector
, and the results are then written to the EncryptedText
property.
It really is that easy – see why so many people like to use the ASP.NET data protector?
The OnDecrypt
method is similarly simple. This time, using the Unprotect
method of the IDataProtector
object to decrypt text from the EncryptedText
property, and write the decrypted version in the DecryptedText
property.
Both the AesPageViewModel
and the DataProtectionPageViewModel
classes derive from a common base class named CryptoViewModelBase
. Here is the code for that class:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is a base implementation of a cryptographic view-model. /// </summary> public abstract class CryptoViewModelBase<T> : ViewModelBase<T> where T : CryptoViewModelBase<T> { /// <summary> /// This field backs the <see cref="DecryptedText"/> property. /// </summary> private string? _decryptedText; /// <summary> /// This field backs the <see cref="EncryptedText"/> property. /// </summary> private string? _encryptedText; /// <summary> /// This property contains clear text to be encrypted. /// </summary> public string? DecryptedText { get { return _decryptedText; } set { _decryptedText = value; OnPropertyChanged(); } } /// <summary> /// This property contains encrypted text to be decrypted. /// </summary> public string? EncryptedText { get { return _encryptedText; } set { _encryptedText = value; OnPropertyChanged(); } } /// <summary> /// This command starts an encryption operation. /// </summary> public ICommand Encrypt { get; set; } /// <summary> /// This command starts an decryption operation. /// </summary> public ICommand Decrypt { get; set; } /// <summary> /// This command copies the encrypted text to the clipboard. /// </summary> public ICommand EncryptCopy { get; set; } /// <summary> /// This command clears the decrypted text. /// </summary> public ICommand DecryptClear { get; set; } /// <summary> /// This constructor creates a new instance of the <see cref="CryptoViewModelBase{T}"/> /// class. /// </summary> /// <param name="appSettings">The application settings to use for the /// view-model.</param> /// <param name="logger">The logger to use for the view-model.</param> protected CryptoViewModelBase( IOptions<AppSettings> appSettings, ILogger<T> logger ) : base(appSettings, logger) { Encrypt = new Command(OnEncrypt); Decrypt = new Command(OnDecrypt); EncryptCopy = new Command(OnEncryptCopy); DecryptClear = new Command(OnDecryptClear); } /// <summary> /// This method performs an encryption operation. /// </summary> protected abstract void OnEncrypt(); /// <summary> /// This method performs a decryption operation. /// </summary> protected abstract void OnDecrypt(); /// <summary> /// This method performs an encrypted copy operation. /// </summary> protected virtual async void OnEncryptCopy() { // Copy the text to the clipboard. await Clipboard.Current.SetTextAsync(_encryptedText) .ConfigureAwait(false); } /// <summary> /// This method clears the encrypted text. /// </summary> protected virtual void OnDecryptClear() { // Clear the text. EncryptedText = ""; } }
As we can see, the class derives from the ViewModelBase
class. We’ll cover that class next. The class contains two properties: DecryptedText
and EncryptedText
. Both properties are bound to controls on the UI. I put the properties here because they are common to both the crypto views. Looking at the property setters, we see the familiar pattern where we update the backing field, and then, fire the PropertyChanged
event, so the associated view(s) will know that the UI probably needs to be updated.
CryptoViewModelBase
also has the following commands: Encrypt
, Decrypt
, EncryptCopy
, and DecryptClear
. These commands are bound to controls, on the UI. They are also wired up to handlers that we’ll cover shortly.
The constructor, for the CryptoViewModelBase
class, creates the various ICommand
instances, and stores the results in the command properties. This is all pretty standard MVVM command ‘stuff’, and it’s how we ensure that, when a button is clicked on the UI, that a corresponding handler method is called, on our view-model.
Speaking of handler methods … The handlers for this class are mostly abstract, since it’s up to any derived class to decide how to perform encryptions, or decryptions. So, for that reason, the OnEncrypt
and OnDecrypt
methods are both marked here as abstract.
The OnEncryptCopy
method is called when the user wants to copy the encrypted text, on the UI, to the system’s clipboard. The OnDecryptClear
method is called when the user wants to clear to lower half of the UI – which typically contains encrypted text.
That’s about it, for CryptoViewModelBase
.
The code for the ViewModelBase
class looks like this:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is a base implementation of a MAUI view-model. /// </summary> public abstract class ViewModelBase<T> : INotifyPropertyChanged where T : ViewModelBase<T> { /// <summary> /// This field contains the shared application settings. /// </summary> internal protected readonly IOptions<AppSettings> _appSettings; /// <summary> /// This field contains the logger for the view-model. /// </summary> internal protected readonly ILogger<T> _logger; /// <summary> /// This event is fired whenever a property changes value. /// </summary> public event PropertyChangedEventHandler? PropertyChanged; /// <summary> /// This event is fired whenever an error occurs. /// </summary> public event ErrorRaisedEventHandler? ErrorRaised; /// <summary> /// This event is fired whenever a warning occurs. /// </summary> public event WarningRaisedEventHandler? WarningRaised; /// <summary> /// This property contains the caption for the application. /// </summary> public string Caption => AppDomain.CurrentDomain.FriendlyName; /// <summary> /// This constructor creates a new instance of the <see cref="ViewModelBase{T}"/> /// class. /// </summary> /// <param name="appSettings">The application settings to use for the /// view-model.</param> /// <param name="logger">The logger to use for the view-model.</param> protected ViewModelBase( IOptions<AppSettings> appSettings, ILogger<T> logger ) { // Save the reference(s). _appSettings = appSettings; _logger = logger; } /// <summary> /// This method raises the <see cref="PropertyChanged"/> event. /// </summary> /// <param name="memberName">Not used - supplied by the compiler.</param> protected virtual void OnPropertyChanged( [CallerMemberName] string memberName = "" ) { // Raise the event. PropertyChanged?.Invoke( this, new PropertyChangedEventArgs(memberName) ); } /// <summary> /// This method raises the <see cref="ErrorRaised"/> event. /// </summary> /// <param name="message">The message for the error.</param> /// <param name="ex">An optional exception.</param> protected virtual void OnErrorRaised( string message, Exception? ex ) { // Raise the event. ErrorRaised?.Invoke( this, new ErrorRaisedArgs() { Message = message, Exception = ex }); } /// <summary> /// This method raises the <see cref="WarningRaised"/> event. /// </summary> /// <param name="message">The message for the error.</param> protected virtual void OnWarningRaised( string message ) { // Raise the event. WarningRaised?.Invoke( this, new WarningRaisedArgs() { Message = message }); } }
This is the base class for all our view-models. It implements the INotifyPropertyChanged
interface, so we can alert view(s) whenever the internal state of our view-model(s) change, at runtime.
The class exposes the following events: PropertyChanged
, ErrorRaised
and WarningRaised
. PropertyChanged
is raised whenever the internal state of the view-model changes. ErrorRaised
is raised whenever a view-model encounters an error that it wants to pass to the UI, for display. Similarly, WarningRaised
is raised whenever a view-model encounters a warning that it wants to pass to the UI, for display.
The Caption
property is there so any popups, on the UI, will have a predictable caption.
The constructor passed in an AppSettings
, and ILogger
instance, from the DI container. The AppSettings
contains the application configuration settings. We covered that topic in an earlier article. The logger is just that, an object that writes to a log.
The OnPropertyChanged
, OnErrorRaised
, and OnWarningRaised
methods are used to raise their corresponding events, at runtime.
That’s it for the ViewModelBase
class.
The code for the SettingsPageViewModel
class looks like this:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is the view-model for the <see cref="SettingsPage"/> view. /// </summary> public class SettingsPageViewModel : ViewModelBase<SettingsPageViewModel> { /// <summary> /// This property contains the (optional) X509 PEM data for the ASP.NET /// data-provider. /// </summary> public string X509Pem { get { return _appSettings.Value.Certs.X509Pem ?? ""; } set { _appSettings.Value.Certs.X509Pem = value; OnPropertyChanged(); } } /// <summary> /// This property contains the password for .NET crypto algorithms. /// </summary> public string Password { get { return _appSettings.Value.Keys.Password; } set { _appSettings.Value.Keys.Password = value; OnPropertyChanged(); } } /// <summary> /// This property contains the SALT for .NET crypto algorithms. /// </summary> public string Salt { get { return _appSettings.Value.Keys.Salt; } set { _appSettings.Value.Keys.Salt = value; OnPropertyChanged(); } } /// <summary> /// This property contains the Rfc2898 iterations for .NET crypto algorithms. /// </summary> public double Iterations { get { return _appSettings.Value.Keys.Iterations; } set { if (0 >= value) { _appSettings.Value.Keys.Iterations = 1; } else if (50000 < value) { _appSettings.Value.Keys.Iterations = 50000; } else { _appSettings.Value.Keys.Iterations = value; } OnPropertyChanged(); OnPropertyChanged(nameof(IterationsLabel)); } } /// <summary> /// This property contains the iterations label .NET crypto algorithms. /// </summary> public string IterationsLabel { get { return $"RFC 2898 Iterations: ({_appSettings.Value.Keys.Iterations:N0})"; } } /// <summary> /// This constructor creates a new instance of the <see cref="SettingsPageViewModel"/> /// class. /// </summary> /// <param name="appSettings">The application settings for the view-model.</param> /// <param name="logger">The logger for the view-model.</param> public SettingsPageViewModel( IOptions<AppSettings> appSettings, ILogger<SettingsPageViewModel> logger ) : base(appSettings, logger) { } /// <inheritdoc/> protected override void OnPropertyChanged( [CallerMemberName] string memberName = "" ) { // Ignore label changes. if (nameof(IterationsLabel) == memberName) { return; } // Write the changes to disk. _appSettings.Value.WriteToDisk(); // Give the base class a chance. base.OnPropertyChanged(memberName); } }
As shown above, the class derives from the ViewModelBase
class. It contains a property for each of the application settings. So, X509Pem
, Password
, Salt
, and Iterations
. IterationsLabel
is, technically, not an application settings but it does make it easier to know how many iterations have been selected on the slider control we bound to the Iterations
property.
The OnPropertyChanged
method, from ViewModelBase
, is overridden, so that we can write the updated AppSettings
value(s) to the disk, whenever a change is made. For that purpose, we call the WriteToDisk
extension method, using the _appSettings
instance we got from the DI container. We covered WriteToDisk
in an earlier article.
That’s it for the SettingsPageViewModel
class.
The very last view-model is called AppShellViewModel
, and it looks like this:
namespace CG.Tools.QuickCrypto.ViewModels; /// <summary> /// This class is the view-model for the <see cref="AppShell"/> view. /// </summary> public class AppShellViewModel : ViewModelBase<AppShellViewModel> { /// <summary> /// This constructor creates a new instance of the <see cref="AppShellViewModel"/> /// class. /// </summary> /// <param name="appSettings">The application settings to use for the /// view-model.</param> /// <param name="logger">The logger to use for the view-model.</param> public AppShellViewModel( IOptions<AppSettings> appSettings, ILogger<AppShellViewModel> logger ) : base(appSettings, logger) { } }
This class derives from ViewModelBase, and serves as a placeholder, in case I ever need a view-model for the application shell.
With all the code we covered, this article has gotten longer than I like. So, for that reason, I’ll cover the last few class in the next article, and wrap everything up for CG.Tools.QuickCrypto. See you then!
Photo by Adi Goldstein on Unsplash