uwpwindows-hello

store password with Windows Hello


I want to store and retrieve a password with Windows Hello. The user can choose at login time if he wants to input his password manually, or if he wants to use Windows Hello to unlock (which then retrieves the last used password, and fills it in for the user).

If Windows Hello is setup correctly there are two use cases in the doc.
One to just unlock:

UserConsentVerificationResult consentResult = await UserConsentVerifier.RequestVerificationAsync("userMessage");
if (consentResult.Equals(UserConsentVerificationResult.Verified))
{
   // continue
}

and one to sign a message from the server:

var openKeyResult = await KeyCredentialManager.OpenAsync(AccountId);
if (openKeyResult.Status == KeyCredentialStatus.Success)
{
    var userKey = openKeyResult.Credential;
    var publicKey = userKey.RetrievePublicKey();
    //the message is the challenge from the server
    var signResult = await userKey.RequestSignAsync(message);

    if (signResult.Status == KeyCredentialStatus.Success)
    {
        //the with the private key of the user signed message        
        return signResult.Result;
    }
}

Both is not very useful for my use-case: I want a symmetric way to store and retrieve the password.

My question in short:
Is there a way to symmetrically store data with Windows Hello?

relevant docs:
https://learn.microsoft.com/en-us/windows/uwp/security/microsoft-passport


Solution

  • I have solved this problem by encrypting / decrypting the secret I wanted to store using a password generated with Windows Hello. The password was a signature over a fixed message.

    A complete code example (untested) to illustrate my point:

    const accountId = "A ID for this key";
    const challenge = "sign this challenge using Windows Hello to get a secure string";
    
    public async Task<byte[]> GetSecureEncryptionKey() {
        // if first time; we first need to create a key
        if (firstTime) {
            if (!await this.CreateKeyAsync()) {
                return null;
            }
        }
    
        // get the key using Windows Hellp
        return await this.GetKeyAsync();
    }
    
    private async Task<bool> CreateKeyAsync() {
        if (!await KeyCredentialManager.IsSupportedAsync()) {
            return false;
        }
    
        // if app opened for the first time, we need to create an account first
        var keyCreationResult = await KeyCredentialManager.RequestCreateAsync(AccountId, KeyCredentialCreationOption.ReplaceExisting);
        return keyCreationResult.Status == KeyCredentialStatus.Success);
    }
    
    private async Task<byte[]> GetKeyAsync() {
        var openKeyResult = await KeyCredentialManager.OpenAsync(AccountId);
    
        if (openKeyResult.Status == KeyCredentialStatus.Success)
        {
            // convert our string challenge to be able to sign it 
            var buffer = CryptographicBuffer.ConvertStringToBinary(
                challenge, BinaryStringEncoding.Utf8
            );
            
            // request a sign from the user
            var signResult = await openKeyResult.Credential.RequestSignAsync(buffer);
    
            // if successful, we can use that signature as a key
            if (signResult.Status == KeyCredentialStatus.Success)
            {
                return signResult.Result.ToArray();
            }
        }
    
        return null;
    }
    

    The full source is on github, and shows how I have integrated these concepts into the application.

    However, this abuses cryptographic primitives intended for different purposes, which is very dangerous. Consider looking for a more sound approach before resorting to this workaround.

    Concrete caveats: