biometric_vault 1.0.0 copy "biometric_vault: ^1.0.0" to clipboard
biometric_vault: ^1.0.0 copied to clipboard

Secure storage with optional biometric protection for Android, iOS, and macOS. Linux, Windows, and web fall back to unauthenticated storage.

example/lib/main.dart

import 'dart:io';

import 'package:biometric_vault/biometric_vault.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';

final MemoryAppender logMessages = MemoryAppender();

final _logger = Logger('main');

void main() {
  Logger.root.level = Level.ALL;
  PrintAppender().attachToLogger(Logger.root);
  logMessages.attachToLogger(Logger.root);
  _logger.fine('Application launched. (v2)');
  runApp(const MyApp());
}

class StringBufferWrapper with ChangeNotifier {
  final StringBuffer _buffer = StringBuffer();

  void writeln(String line) {
    _buffer.writeln(line);
    notifyListeners();
  }

  @override
  String toString() => _buffer.toString();
}

class ShortFormatter extends LogRecordFormatter {
  @override
  StringBuffer formatToStringBuffer(LogRecord rec, StringBuffer sb) {
    sb.write(
      '${rec.time.hour}:${rec.time.minute}:${rec.time.second} ${rec.level.name} '
      '${rec.message}',
    );

    if (rec.error != null) {
      sb.write(rec.error);
    }
    // ignore: avoid_as
    final stackTrace =
        rec.stackTrace ??
        (rec.error is Error ? (rec.error as Error).stackTrace : null);
    if (stackTrace != null) {
      sb.write(stackTrace);
    }
    return sb;
  }
}

class MemoryAppender extends BaseLogAppender {
  MemoryAppender() : super(ShortFormatter());

  final StringBufferWrapper log = StringBufferWrapper();

  @override
  void handle(LogRecord record) {
    log.writeln(formatter.format(record));
  }
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  final String baseName = 'default';

  static final _authStorageInitOptions = StorageFileInitOptions();
  static final _customPromptInitOptions = StorageFileInitOptions(
    androidBiometricOnly: false,
    androidAuthenticationValidityDuration: const Duration(seconds: 5),
    darwinBiometricOnly: false,
    darwinTouchIDAuthenticationForceReuseContextDuration: const Duration(
      seconds: 5,
    ),
  );

  BiometricVaultFile? _authStorage;
  BiometricVaultFile? _storage;
  BiometricVaultFile? _customPrompt;

  final TextEditingController _writeController = TextEditingController(
    text: 'Lorem Ipsum',
  );

  @override
  void initState() {
    super.initState();
    logMessages.log.addListener(_logChanged);
  }

  @override
  void dispose() {
    logMessages.log.removeListener(_logChanged);
    super.dispose();
  }

  Future<CanAuthenticateResponse> _checkAuthenticate(
    StorageFileInitOptions? options,
  ) async {
    final response = await BiometricVault().canAuthenticate(options: options);
    _logger.info('checked if authentication was possible: $response');
    return response;
  }

  void _logChanged() => setState(() {});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Plugin example app')),
        body: Column(
          children: [
            const Text('Methods:'),
            ElevatedButton(
              child: const Text('init'),
              onPressed: () async {
                _logger.finer('Initializing $baseName');
                final authStorageSupport = await _checkAuthenticate(
                  _authStorageInitOptions,
                );
                if (authStorageSupport == CanAuthenticateResponse.unsupported) {
                  _logger.severe(
                    'Unable to use authenticate. Unable to get storage.',
                  );
                  return;
                }
                final supportsAuthenticated =
                    authStorageSupport == CanAuthenticateResponse.success ||
                    authStorageSupport == CanAuthenticateResponse.statusUnknown;
                if (supportsAuthenticated) {
                  _authStorage = await BiometricVault().getStorage(
                    '${baseName}_authenticated',
                    options: _authStorageInitOptions,
                  );
                }
                _storage = await BiometricVault().getStorage(
                  '${baseName}_unauthenticated',
                  options: StorageFileInitOptions(
                    authenticationRequired: false,
                  ),
                );
                final supportsCustomPrompt = await _checkAuthenticate(
                  _customPromptInitOptions,
                );
                if (supportsCustomPrompt == CanAuthenticateResponse.success) {
                  _customPrompt = await BiometricVault().getStorage(
                    '${baseName}_customPrompt',
                    options: _customPromptInitOptions,
                    promptInfo: const PromptInfo(
                      iosPromptInfo: DarwinPromptInfo(
                        saveTitle: 'Custom save title',
                        accessTitle: 'Custom access title.',
                      ),
                      androidPromptInfo: AndroidPromptInfo(
                        title: 'Custom title',
                        subtitle: 'Custom subtitle',
                        description: 'Custom description',
                        negativeButton: 'Nope!',
                      ),
                    ),
                  );
                }
                setState(() {});
                _logger.info('initiailzed $baseName');
              },
            ),
            ...?_appArmorButton(),
            ...(_authStorage == null
                ? []
                : [
                    const Text(
                      'Biometric Authentication',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    StorageActions(
                      storageFile: _authStorage!,
                      writeController: _writeController,
                    ),
                    const Divider(),
                  ]),
            ...?(_storage == null
                ? null
                : [
                    const Text(
                      'Unauthenticated',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    StorageActions(
                      storageFile: _storage!,
                      writeController: _writeController,
                    ),
                    const Divider(),
                  ]),
            ...?(_customPrompt == null
                ? null
                : [
                    const Text(
                      'Custom Prompts w/ 5s auth validity',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    StorageActions(
                      storageFile: _customPrompt!,
                      writeController: _writeController,
                    ),
                    const Divider(),
                  ]),
            const Divider(),
            TextField(
              decoration: const InputDecoration(
                labelText: 'Example text to write',
              ),
              controller: _writeController,
            ),
            Expanded(
              child: Container(
                color: Colors.white,
                constraints: const BoxConstraints.expand(),
                child: SingleChildScrollView(
                  reverse: true,
                  child: Container(
                    padding: const EdgeInsets.all(16),
                    child: Text(logMessages.log.toString()),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  List<Widget>? _appArmorButton() => kIsWeb || !Platform.isLinux
      ? null
      : [
          ElevatedButton(
            child: const Text('Check App Armor'),
            onPressed: () async {
              if (await BiometricVault().linuxCheckAppArmorError()) {
                _logger.info(
                  'Got an error! User has to authorize us to '
                  'use secret service.',
                );
                _logger.info(
                  'Run: `snap connect biometric-storage-example:password-manager-service`',
                );
              } else {
                _logger.info('all good.');
              }
            },
          ),
        ];
}

class StorageActions extends StatelessWidget {
  const StorageActions({
    super.key,
    required this.storageFile,
    required this.writeController,
  });

  final BiometricVaultFile storageFile;
  final TextEditingController writeController;

  /// Demonstrates exhaustive handling of the sealed exception hierarchy.
  void _handleStorageError(BiometricVaultException e) {
    switch (e) {
      case AuthException(code: AuthExceptionCode.userCanceled):
        _logger.info('User canceled.');
      case AuthException(code: AuthExceptionCode.lockedOut):
        _logger.warning('Biometry is temporarily locked. Try again shortly.');
      case AuthException(code: AuthExceptionCode.lockedOutPermanently):
        _logger.warning(
          'Biometry is locked. Unlock the device with PIN/passcode first.',
        );
      case AuthException(:final code):
        _logger.warning('Could not authenticate: $code ${e.message}');
      case StorageInvalidatedException(:final reason):
        _logger.severe(
          'Stored value is unrecoverable ($reason). '
          'Delete it and store a fresh secret.',
        );
      case BiometricVaultPluginException(:final code):
        _logger.severe('Unexpected plugin error $code: ${e.message}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[
        ElevatedButton(
          key: ValueKey('${storageFile.name}.read'),
          child: const Text('read'),
          onPressed: () async {
            _logger.fine('reading from ${storageFile.name}');
            try {
              final result = await storageFile.read();
              _logger.fine('read: {$result}');
            } on BiometricVaultException catch (e) {
              _handleStorageError(e);
            }
          },
        ),
        ElevatedButton(
          key: ValueKey('${storageFile.name}.write'),
          child: const Text('write'),
          onPressed: () async {
            _logger.fine('Going to write...');
            try {
              await storageFile.write(
                ' [${DateTime.now()}] ${writeController.text}',
              );
              _logger.info('Written content.');
            } on BiometricVaultException catch (e) {
              _handleStorageError(e);
            }
          },
        ),
        ElevatedButton(
          key: ValueKey('${storageFile.name}.delete'),
          child: const Text('delete'),
          onPressed: () async {
            _logger.fine('deleting...');
            await storageFile.delete();
            _logger.info('Deleted.');
          },
        ),
      ],
    );
  }
}
0
likes
160
points
--
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Secure storage with optional biometric protection for Android, iOS, and macOS. Linux, Windows, and web fall back to unauthenticated storage.

Repository (GitHub)
View/report issues

Topics

#biometrics #encryption #storage #security #secure-storage

License

MIT (license)

Dependencies

ffi, flutter, flutter_web_plugins, plugin_platform_interface, web, win32

More

Packages that depend on biometric_vault

Packages that implement biometric_vault