Skip to content

Commit 41e192d

Browse files
authored
Merge pull request #77 from nventive/dev/thla/feat-add-tests-implementation
feat: Add Tests implementation
2 parents 9bd2238 + 73e1390 commit 41e192d

11 files changed

Lines changed: 355 additions & 177 deletions

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Biometry Service
1+
# Biometry Service
22

33
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE) ![Version](https://img.shields.io/nuget/v/BiometryService?style=flat-square) ![Downloads](https://img.shields.io/nuget/dt/BiometryService?style=flat-square)
44

@@ -100,7 +100,7 @@ This library offers a simple contract to use the biometry across Android, iOS an
100100

101101
## Features
102102

103-
### Platform Compatibilities
103+
### Platform Compatibilities
104104

105105
The `IBiometryService` has severals methods.
106106

@@ -109,11 +109,27 @@ As of now, this is the list of features available per platform.
109109
| Methods | iOS | Android | WinUI | UWP |
110110
| ---------------- | :-----: | :-----: | :-----: | :-----: |
111111
| `GetCapability` |||||
112-
| `ScanBiometry` |||||
112+
| `ScanBiometry` |||||
113113
| `Encrypt` |||||
114114
| `Decrypt` |||||
115115
| `Remove` |||||
116116

117+
### Tests
118+
119+
It's also possible to use a fake implementation of `IBiometryService` named `FakeBiometryService` for testing purposes only.
120+
121+
This fake implementation doesn't actually encrypt anything, the key and value pairs are stored in memory.
122+
123+
The fake implementation behavior can be customized by using constructor parameters.
124+
125+
``` csharp
126+
var fakeBiometryService = new FakeBiometryService
127+
{
128+
biometryType: BiometryType.None,
129+
isBiometryEnabled: false,
130+
isPasscodeSet: false
131+
};
132+
```
117133

118134
### Error Handling
119135

@@ -199,4 +215,4 @@ This project is licensed under the Apache 2.0 license - see the
199215
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the process for
200216
contributing to this project.
201217

202-
Be mindful of our [Code of Conduct](CODE_OF_CONDUCT.md).
218+
Be mindful of our [Code of Conduct](CODE_OF_CONDUCT.md).

src/BiometryService.Abstractions/IBiometryService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public interface IBiometryService
5959
/// <summary>
6060
/// Removes the ecrypted value in the platform secure storage.
6161
/// </summary>
62-
/// <param name="keyName"></param>
62+
/// <param name="keyName">The name of the Key.</param>
63+
/// <exception cref="BiometryException">Thrown for general biometry errors.</exception>
6364
void Remove(string keyName);
6465
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Logging.Abstractions;
5+
6+
namespace BiometryService;
7+
8+
/// <summary>
9+
/// Represents the base class for <see cref="IBiometryService"/> implementation.
10+
/// </summary>
11+
public abstract class BaseBiometryService : IBiometryService
12+
{
13+
/// <summary>
14+
/// The logger.
15+
/// </summary>
16+
protected readonly ILogger Logger;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="BaseBiometryService" /> class.
20+
/// </summary>
21+
/// <param name="loggerFactory">The logger factory.</param>
22+
public BaseBiometryService(ILoggerFactory loggerFactory = null)
23+
{
24+
Logger = loggerFactory?.CreateLogger<IBiometryService>() ?? NullLogger<IBiometryService>.Instance;
25+
}
26+
27+
/// <inheritdoc/>
28+
public abstract Task<string> Decrypt(CancellationToken ct, string keyName);
29+
30+
/// <inheritdoc/>
31+
public abstract Task Encrypt(CancellationToken ct, string keyName, string keyValue);
32+
33+
/// <inheritdoc/>
34+
public abstract Task<BiometryCapabilities> GetCapabilities(CancellationToken ct);
35+
36+
/// <inheritdoc/>
37+
public abstract void Remove(string keyName);
38+
39+
/// <inheritdoc/>
40+
public abstract Task ScanBiometry(CancellationToken ct);
41+
42+
/// <summary>
43+
/// Validates biometry capabilities and throw the right exception if they aren't valide.
44+
/// </summary>
45+
/// <param name="ct"><see cref="CancellationToken"/>.</param>
46+
/// <returns><see cref="Task"/>.</returns>
47+
/// <exception cref="BiometryException">.</exception>
48+
protected async Task ValidateBiometryCapabilities(CancellationToken ct)
49+
{
50+
if (Logger.IsEnabled(LogLevel.Debug))
51+
{
52+
Logger.LogDebug("Validating biometry capabilities.");
53+
}
54+
55+
var biometryCapabilities = await GetCapabilities(ct);
56+
if (!biometryCapabilities.IsEnabled)
57+
{
58+
var reason = biometryCapabilities.IsSupported ? BiometryExceptionReason.NotEnrolled : BiometryExceptionReason.Unavailable;
59+
var message = biometryCapabilities.IsSupported ? "Biometrics are not enrolled on this device" : "Biometry is not available on this device";
60+
61+
throw new BiometryException(reason, message);
62+
}
63+
64+
if (Logger.IsEnabled(LogLevel.Information))
65+
{
66+
Logger.LogDebug("Biometry capabilities have been successfully validated.");
67+
}
68+
}
69+
}

src/BiometryService/BiometryService.Android.cs

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ namespace BiometryService;
2323
/// <summary>
2424
/// Implementation of the <see cref="IBiometryService" /> for Android.
2525
/// </summary>
26-
public sealed partial class BiometryService : IBiometryService
26+
public sealed class BiometryService : BaseBiometryService
2727
{
2828
private const string ANDROID_KEYSTORE = "AndroidKeyStore"; // Android constant, cannot be changed.
2929
private const string CIPHER_NAME = "AES/CBC/PKCS7Padding";
3030
private const string PREFERENCE_NAME = "BiometricPreferences";
3131

3232
private readonly FragmentActivity _activity;
3333
private readonly Func<BiometricPrompt.PromptInfo> _promptInfoBuilder;
34-
private readonly ILogger _logger;
3534
private readonly Context _applicationContext;
3635
private readonly BiometricManager _biometricManager;
3736
private readonly KeyStore _keyStore;
@@ -44,11 +43,14 @@ public sealed partial class BiometryService : IBiometryService
4443
/// <param name="fragmentActivity"><see cref="FragmentActivity"/>.</param>
4544
/// <param name="promptInfoBuilder">Biometry configuration.</param>
4645
/// <param name="loggerFactory"><see cref="ILoggerFactory"/>.</param>
47-
public BiometryService(FragmentActivity fragmentActivity, Func<BiometricPrompt.PromptInfo> promptInfoBuilder, ILoggerFactory loggerFactory = null)
46+
public BiometryService(
47+
FragmentActivity fragmentActivity,
48+
Func<BiometricPrompt.PromptInfo> promptInfoBuilder,
49+
ILoggerFactory loggerFactory = null
50+
) : base(loggerFactory)
4851
{
4952
_activity = fragmentActivity ?? throw new ArgumentNullException(nameof(fragmentActivity));
5053
_promptInfoBuilder = promptInfoBuilder ?? throw new ArgumentNullException(nameof(promptInfoBuilder));
51-
_logger = loggerFactory?.CreateLogger<IBiometryService>() ?? NullLogger<IBiometryService>.Instance;
5254

5355
_applicationContext = Application.Context;
5456
_biometricManager = BiometricManager.From(_applicationContext);
@@ -58,7 +60,7 @@ public BiometryService(FragmentActivity fragmentActivity, Func<BiometricPrompt.P
5860
}
5961

6062
/// <inheritdoc/>
61-
public Task<BiometryCapabilities> GetCapabilities(CancellationToken ct)
63+
public override Task<BiometryCapabilities> GetCapabilities(CancellationToken ct)
6264
{
6365
var biometryType = BiometryType.None;
6466

@@ -79,17 +81,17 @@ public Task<BiometryCapabilities> GetCapabilities(CancellationToken ct)
7981
}
8082

8183
/// <inheritdoc/>
82-
public async Task ScanBiometry(CancellationToken ct)
84+
public override async Task ScanBiometry(CancellationToken ct)
8385
{
8486
await AuthenticateBiometry(ct);
8587
}
8688

8789
/// <inheritdoc/>
88-
public async Task Encrypt(CancellationToken ct, string keyName, string value)
90+
public override async Task Encrypt(CancellationToken ct, string keyName, string value)
8991
{
90-
if (_logger.IsEnabled(LogLevel.Debug))
92+
if (Logger.IsEnabled(LogLevel.Debug))
9193
{
92-
_logger.LogDebug($"Encrypting the fingerprint for the key '{keyName}'.");
94+
Logger.LogDebug($"Encrypting the fingerprint for the key '{keyName}'.");
9395
}
9496

9597
await ValidateBiometryCapabilities(ct);
@@ -104,9 +106,9 @@ public async Task Encrypt(CancellationToken ct, string keyName, string value)
104106
iv.CopyTo(bytes, 0);
105107
encryptedData.CopyTo(bytes, iv.Length);
106108

107-
if (_logger.IsEnabled(LogLevel.Information))
109+
if (Logger.IsEnabled(LogLevel.Information))
108110
{
109-
_logger.LogInformation($"Succcessfully encrypted the fingerprint for the key'{keyName}'.");
111+
Logger.LogInformation($"Succcessfully encrypted the fingerprint for the key'{keyName}'.");
110112
}
111113

112114
var encodedData = Base64.EncodeToString(bytes, Base64Flags.NoWrap);
@@ -115,11 +117,11 @@ public async Task Encrypt(CancellationToken ct, string keyName, string value)
115117
}
116118

117119
/// <inheritdoc/>
118-
public async Task<string> Decrypt(CancellationToken ct, string keyName)
120+
public override async Task<string> Decrypt(CancellationToken ct, string keyName)
119121
{
120-
if (_logger.IsEnabled(LogLevel.Debug))
122+
if (Logger.IsEnabled(LogLevel.Debug))
121123
{
122-
_logger.LogDebug($"Decrypting the fingerprint for the key '{keyName}'.");
124+
Logger.LogDebug($"Decrypting the fingerprint for the key '{keyName}'.");
123125
}
124126

125127
await ValidateBiometryCapabilities(ct);
@@ -156,26 +158,42 @@ public async Task<string> Decrypt(CancellationToken ct, string keyName)
156158
var result = await AuthenticateBiometry(ct, crypto);
157159
var decryptedData = result.CryptoObject.Cipher.DoFinal(buffer);
158160

159-
if (_logger.IsEnabled(LogLevel.Information))
161+
if (Logger.IsEnabled(LogLevel.Information))
160162
{
161-
_logger.LogInformation($"Succcessfully decrypted the fingerprint for the key '{keyName}'.");
163+
Logger.LogInformation($"Succcessfully decrypted the fingerprint for the key '{keyName}'.");
162164
}
163165

164166
return Encoding.ASCII.GetString(decryptedData);
165167
}
166168

167169
/// <inheritdoc/>
168-
public void Remove(string keyName)
170+
public override void Remove(string keyName)
169171
{
170-
var sharedpref = _applicationContext.GetSharedPreferences(PREFERENCE_NAME, FileCreationMode.Private);
171-
sharedpref.Edit().Remove(keyName).Apply();
172+
try
173+
{
174+
var sharedpref = _applicationContext.GetSharedPreferences(PREFERENCE_NAME, FileCreationMode.Private);
175+
sharedpref.Edit().Remove(keyName).Apply();
176+
177+
if (Logger.IsEnabled(LogLevel.Debug))
178+
{
179+
Logger.LogDebug("The key '{keyName}' has been successfully removed.", keyName);
180+
}
181+
}
182+
catch (System.Exception)
183+
{
184+
if (Logger.IsEnabled(LogLevel.Debug))
185+
{
186+
Logger.LogDebug("The key '{keyName}' has not been successfully removed.", keyName);
187+
}
188+
throw new BiometryException(BiometryExceptionReason.Failed, $"Something went wrong while removing the key '{keyName}'.");
189+
}
172190
}
173191

174192
private async Task<BiometricPrompt.AuthenticationResult> AuthenticateBiometry(CancellationToken ct, BiometricPrompt.CryptoObject crypto = null)
175193
{
176-
if (_logger.IsEnabled(LogLevel.Debug))
194+
if (Logger.IsEnabled(LogLevel.Debug))
177195
{
178-
_logger.LogDebug($"Start authenticating the user biometry.");
196+
Logger.LogDebug($"Start authenticating the user biometry.");
179197
}
180198

181199
// TODO: Refactor this. Why are we doing a version check? Could we juste use the parameter used by the user isntead of BiometricManager.Authenticators.BiometricStrong?
@@ -194,9 +212,9 @@ public void Remove(string keyName)
194212
return await PromptBiometryAuthentication(ct, crypto);
195213
}
196214

197-
if (_logger.IsEnabled(LogLevel.Error))
215+
if (Logger.IsEnabled(LogLevel.Error))
198216
{
199-
_logger.LogError($"The device cannot authenticate with biometry.");
217+
Logger.LogError($"The device cannot authenticate with biometry.");
200218
}
201219

202220
var reason = BiometryExceptionReason.Failed;
@@ -228,7 +246,7 @@ public void Remove(string keyName)
228246
_authenticationCompletionSource = new TaskCompletionSource<BiometricPrompt.AuthenticationResult>();
229247

230248
// Prepare and show UI.
231-
var callback = new AuthenticationCallback(_authenticationCompletionSource, _logger);
249+
var callback = new AuthenticationCallback(_authenticationCompletionSource, Logger);
232250
var executor = ContextCompat.GetMainExecutor(_applicationContext);
233251
var biometricPrompt = new BiometricPrompt(_activity, executor, callback);
234252

@@ -260,9 +278,9 @@ public void Remove(string keyName)
260278

261279
if (authenticationTask.IsCompletedSuccessfully)
262280
{
263-
if (_logger.IsEnabled(LogLevel.Information))
281+
if (Logger.IsEnabled(LogLevel.Information))
264282
{
265-
_logger.LogInformation($"Successfully authenticated and processed the biometric).");
283+
Logger.LogInformation($"Successfully authenticated and processed the biometric).");
266284
}
267285

268286
return authenticationTask.Result;
@@ -287,9 +305,9 @@ private BiometricPrompt.CryptoObject CreateCryptoObject(string keyName)
287305
_keyStore.DeleteEntry(keyName);
288306
}
289307

290-
if (_logger.IsEnabled(LogLevel.Debug))
308+
if (Logger.IsEnabled(LogLevel.Debug))
291309
{
292-
_logger.LogDebug($"Generating a symmetric pair (key name: '{keyName}').");
310+
Logger.LogDebug($"Generating a symmetric pair (key name: '{keyName}').");
293311
}
294312

295313
var keygen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, ANDROID_KEYSTORE);
@@ -304,9 +322,9 @@ private BiometricPrompt.CryptoObject CreateCryptoObject(string keyName)
304322

305323
keygen.GenerateKey();
306324

307-
if (_logger.IsEnabled(LogLevel.Information))
325+
if (Logger.IsEnabled(LogLevel.Information))
308326
{
309-
_logger.LogInformation($"Successfully generated a symmetric pair (key name: '{keyName}').");
327+
Logger.LogInformation($"Successfully generated a symmetric pair (key name: '{keyName}').");
310328
}
311329

312330
cipher.Init(CipherMode.EncryptMode, _keyStore.GetKey(keyName, null));
@@ -328,26 +346,26 @@ private BiometricPrompt.CryptoObject GetCryptoObject(string keyName, byte[] iv =
328346
}
329347
catch (KeyPermanentlyInvalidatedException)
330348
{
331-
if (_logger.IsEnabled(LogLevel.Error))
349+
if (Logger.IsEnabled(LogLevel.Error))
332350
{
333-
_logger.LogError($"Key '{keyName}' has been permanently invalidated.");
351+
Logger.LogError($"Key '{keyName}' has been permanently invalidated.");
334352
}
335353

336354
_keyStore.DeleteEntry(keyName);
337355

338-
if (_logger.IsEnabled(LogLevel.Information))
356+
if (Logger.IsEnabled(LogLevel.Information))
339357
{
340-
_logger.LogInformation($"Permanently invalidated key '{keyName}' has been removed successfully.");
358+
Logger.LogInformation($"Permanently invalidated key '{keyName}' has been removed successfully.");
341359
}
342360

343361
throw new BiometryException(BiometryExceptionReason.KeyInvalidated, "Something went wrong while generating the CryptoObject used to decrypt.");
344362
}
345363
}
346364
else
347365
{
348-
if (_logger.IsEnabled(LogLevel.Error))
366+
if (Logger.IsEnabled(LogLevel.Error))
349367
{
350-
_logger.LogError($"Key '{keyName}' not found.");
368+
Logger.LogError($"Key '{keyName}' not found.");
351369
}
352370
throw new BiometryException(BiometryExceptionReason.KeyInvalidated, $"Key '{keyName}' not found.");
353371
}
@@ -356,19 +374,19 @@ private BiometricPrompt.CryptoObject GetCryptoObject(string keyName, byte[] iv =
356374
private class AuthenticationCallback : BiometricPrompt.AuthenticationCallback
357375
{
358376
private readonly TaskCompletionSource<BiometricPrompt.AuthenticationResult> _tcs;
359-
private readonly ILogger _logger;
377+
private readonly ILogger Logger;
360378

361379
public AuthenticationCallback(TaskCompletionSource<BiometricPrompt.AuthenticationResult> tcs, ILogger logger)
362380
{
363381
_tcs = tcs;
364-
_logger = logger;
382+
Logger = logger;
365383
}
366384

367385
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result)
368386
{
369-
if (_logger.IsEnabled(LogLevel.Information))
387+
if (Logger.IsEnabled(LogLevel.Information))
370388
{
371-
_logger.LogInformation("User attempt to use biometry succeeded.");
389+
Logger.LogInformation("User attempt to use biometry succeeded.");
372390
}
373391

374392
_tcs.TrySetResult(result);
@@ -379,9 +397,9 @@ public override void OnAuthenticationFailed()
379397
// This methods is called after an attempt to use biometry.
380398
// It does not means that it will close the prompt yet.
381399

382-
if (_logger.IsEnabled(LogLevel.Warning))
400+
if (Logger.IsEnabled(LogLevel.Warning))
383401
{
384-
_logger.LogWarning("User attempt to use biometry failed.");
402+
Logger.LogWarning("User attempt to use biometry failed.");
385403
}
386404
}
387405

0 commit comments

Comments
 (0)