DEV Community

Cover image for Lock the Device, Not the Page: App Lock, Biometric, Screen Guard, and the Vault
NuvyntraLabs
NuvyntraLabs

Posted on

Lock the Device, Not the Page: App Lock, Biometric, Screen Guard, and the Vault

A banking screen that hides itself with IsVisible is still in the recents thumbnail. A Face ID call on one button does not lock the app two minutes after the user switches to Messages. A token in Preferences is not a file in a vault.

Each of those is a different package. This post wires the five that cover the device: the lock timer, a one-shot prompt, the screenshot flag, small secrets, and encrypted files.

Job Package Version
Background, timer, cover, unlock Plugin.Maui.AppLock 1.0.7
One Face ID, fingerprint, or device PIN prompt Plugin.Maui.BiometricPlus 1.0.1
Recents thumbnail and screenshots Plugin.Maui.ScreenGuard 1.0.1
A short secret with expiry Plugin.Maui.SecureStoragePlus 1.0.8
An encrypted file Plugin.Maui.FileVault 1.0.8

All five are MIT licensed. AppLock, BiometricPlus, ScreenGuard, and FileVault target Android and iOS. SecureStoragePlus also targets Mac Catalyst and Windows. Access and refresh tokens, 401 refresh, and multi-device revoke stay on Plugin.Maui.SecureSession. SecureSession persists through SecureStoragePlus. It is not a second lock timer.

The timer and the cover

AppLock owns the sequence: the app backgrounds, LockAfter elapses, the app returns, the user authenticates, the app unlocks. Face ID and the device PIN are the unlock step.

dotnet add package Plugin.Maui.AppLock --version 1.0.7
Enter fullscreen mode Exit fullscreen mode
using Plugin.Maui.AppLock;

builder
    .UseMauiApp<App>()
    .UseAppLock(options =>
    {
        options.LockAfter = TimeSpan.FromMinutes(2);
        options.AllowBiometric = true;
        options.AllowDevicePin = true;
        options.AutoPromptOnResume = true;
    });
Enter fullscreen mode Exit fullscreen mode

TimeSpan.Zero locks as soon as the app backgrounds. LockOnStart = true also starts locked after a cold start. UseAppLock hooks Android pause/resume and iOS background/activate. Without the generic host, call NotifyBackground() and NotifyForeground() yourself.

Swap the window to a cover while the app is locked:

appLock.StateChanged += (_, e) =>
{
    window.Page = e.Current is AppLockState.Locked or AppLockState.Authenticating
        ? new AppLockPage(appLock)
        : mainPage;
};
Enter fullscreen mode Exit fullscreen mode

RequireAuthenticationAsync() is the gate before the app is usable. A balance screen that must prompt even when the timer has not elapsed uses AppLockPromptMode.Always:

var result = await AppLock.RequireAuthenticationAsync(AppLockPromptMode.Always);
if (!result.Succeeded)
    return;

ShowBalance();
Enter fullscreen mode Exit fullscreen mode

A failed or cancelled prompt returns AppLockAuthResult and leaves the app locked. It does not throw. If the automatic resume prompt throws, AuthenticationCompleted still fires with a failed result so the cover stays up.

Android API 23+. USE_BIOMETRIC is merged from the package. Enroll a fingerprint, face, or device PIN before you test. When AllowDevicePin is true, the system prompt includes the device credential. iOS needs NSFaceIDUsageDescription when biometrics are allowed. AllowDevicePin uses LAPolicy.DeviceOwnerAuthentication.

samples/Plugin.Maui.AppLock.Sample is a small vault: enable the lock, change the grace period, lock now, and step up on "View balance".

One prompt, without a timer

BiometricPlus answers a single question: is this the enrolled user, right now? The NuGet id is Plugin.Maui.BiometricPlus because nuget.org already reserved Plugin.Maui.Biometric. The namespace stays Plugin.Maui.Biometric.

dotnet add package Plugin.Maui.BiometricPlus --version 1.0.1
Enter fullscreen mode Exit fullscreen mode
using Plugin.Maui.Biometric;

builder.UseBiometric(o =>
{
    o.AllowDeviceCredential = true;
    o.DefaultReason = "Unlock";
});

var availability = await Biometric.Current.GetAvailabilityAsync();
var result = await Biometric.Current.AuthenticateAsync(
    new BiometricRequest { Reason = "Unlock payroll" });
Enter fullscreen mode Exit fullscreen mode

GetAvailabilityAsync returns Available, NotEnrolled, or NotSupported. Android uses AndroidX BiometricPrompt (API 23+). Declare USE_BIOMETRIC and USE_FINGERPRINT on the host. iOS uses LAContext. Face ID needs NSFaceIDUsageDescription. Touch ID and the device passcode do not.

Use this package for a button. Use AppLock when the whole window has to disappear after background.

The recents thumbnail

ScreenGuard is the window flag. It is not the lock, and it is not file encryption.

dotnet add package Plugin.Maui.ScreenGuard --version 1.0.1
Enter fullscreen mode Exit fullscreen mode
using Plugin.Maui.ScreenGuard;

builder.UseScreenGuard(o => o.ProtectOnStart = false);

ScreenGuard.SetProtected(balancePage, true);
Enter fullscreen mode Exit fullscreen mode

Protect and Unprotect are reference-counted. SetProtected(page, true) ties a hold to one page. Android sets FLAG_SECURE: the recents thumbnail is blank, and screenshots are blocked. No extra manifest permission.

iOS cannot block a screenshot. The plugin raises CaptureStateChanged and exposes IsCaptured while the screen is recorded. The host supplies the overlay. Treat an iOS "blocked screenshot" as a bug in the write-up, not a feature of this package.

The short secret

SecureStoragePlus sits on MAUI SecureStorage (Keychain, EncryptedSharedPreferences) and adds an AES-256-GCM envelope. The key name is associated data, so a blob cannot be copied under another name. Values can expire. The ciphertext is what gets stored.

dotnet add package Plugin.Maui.SecureStoragePlus --version 1.0.8
Enter fullscreen mode Exit fullscreen mode
builder.UseSecureStoragePlus();

await SecureStoragePlus.Default.SetAsync(
    "session",
    sessionJson,
    SecureStorageOptions.ExpireIn(TimeSpan.FromHours(8)));

var result = await SecureStoragePlus.Default.TryGetAsync("session");
if (result.Expired)
{
    // sign in again
}
Enter fullscreen mode Exit fullscreen mode

GetAsync on an expired key returns null and removes the value. Strings are stored as-is. Other types are JSON:

await SecureStoragePlus.Default.SetAsync("profile", new UserProfile("Ada", 36));
var profile = await SecureStoragePlus.Default.GetAsync<UserProfile>("profile");
Enter fullscreen mode Exit fullscreen mode

After an upgrade, copy named keys out of MAUI SecureStorage once:

await SecureStoragePlus.Default.MigrateFromMauiSecureStorageAsync(
    ["oauth_token", "refresh_token"],
    new MigrationOptions
    {
        RemoveSource = true,
        OverwriteExisting = false,
        StorageOptions = SecureStorageOptions.ExpireIn(TimeSpan.FromDays(14))
    });
Enter fullscreen mode Exit fullscreen mode

On iOS, add a keychain access group for $(AppIdentifierPrefix)$(CFBundleIdentifier) so values survive the way you expect. On Android, if Auto Backup restores preferences onto a device that does not have the original key, catch the failed read and call RemoveAllAsync(resetEncryptionKey: true).

A secret that is a file, not a string, belongs in FileVault.

The file

FileVault stores app-private files as AES-256-GCM ciphertext. The master key lives in the platform secure store. Each file has its own nonce.

dotnet add package Plugin.Maui.FileVault --version 1.0.8
Enter fullscreen mode Exit fullscreen mode
using Plugin.Maui.FileVault;

builder.UseFileVault(options =>
{
    options.DefaultTimeToLive = TimeSpan.FromDays(30);
    options.LockOnBackground = true;
    options.ExcludeFromBackup = true;
});

await FileVault.Current.WriteTextAsync("notes/pin.txt", "1234", new VaultWriteOptions
{
    TimeToLive = TimeSpan.FromHours(12)
});

var pin = await FileVault.Current.ReadTextAsync("notes/pin.txt");
var stats = await FileVault.Current.GetStatisticsAsync();
Enter fullscreen mode Exit fullscreen mode

Device-key vaults unlock on first use. RequirePassphrase = true keeps the vault locked until UnlockAsync. ChangePassphraseAsync rewraps the key. It does not re-encrypt every file.

UseFileVault purges expired and idle files on resume. On background, when LockOnBackground is true, the vault locks. The in-memory master key is cleared either way. Reading an expired file deletes it and throws FileVaultException with FileVaultError.Expired.

RootDirectory must stay inside the host-chosen folder. VaultName cannot climb out with ... DestroyAsync deletes the files, the manifest, and the stored key. Prefer GetStatisticsAsync. The synchronous GetStatistics() gives up after 5 seconds if a write holds the gate.

VideoPipeline Encrypt(key) writes its own AES-256-GCM .vault for one clip. FileVault is the store for documents that outlive that call, with a TTL and a lock when the app backgrounds.

When something looks wrong

What you see What to do
The app switcher still shows the balance LockAfter is still the default grace, or ScreenGuard is not protecting that window. LockAfter = TimeSpan.Zero locks immediately. FLAG_SECURE blanks the Android thumbnail.
Face ID works on a button and the app stays open after background That button is BiometricPlus. The timer and the cover are AppLock.
iOS screenshots still save ScreenGuard does not block them. Handle CaptureStateChanged and show your own overlay.
A session token is still readable after eight hours It was written without SecureStorageOptions.ExpireIn. Expiry is checked on the next read.
A note file opens with no passphrase after background LockOnBackground is false, or this vault is device-key and unlocks on first use. Set RequirePassphrase when the key must not sit in SecureStorage.
Tokens refresh themselves That is SecureSession, composed on SecureStoragePlus. AppLock does not store access tokens.

Where the five sit next to each other

Need Tool
Hide the app after it backgrounds, then unlock AppLock
One prompt on one action BiometricPlus
Blank recents, block Android screenshots ScreenGuard
Expire a string in the platform secure store SecureStoragePlus
Encrypt a file, with TTL and lock FileVault
Login, refresh, logout, biometric unlock of the session Plugin.Maui.SecureSession

Plugin.Fingerprint and similar biometric libraries are the usual one-shot prompt when the app already uses them. MAUI SecureStorage is enough when you do not need expiry, listing, or the extra AES-256-GCM envelope.

Try it

Register AppLock with a two-minute LockAfter, put AppLockPage up while StateChanged says Locked, and call ScreenGuard.SetProtected on the balance page. Put the session string in SecureStoragePlus with an expiry. Put the document in FileVault.


By Niladri

Top comments (0)