biometric_vault 1.1.0-dev.1 copy "biometric_vault: ^1.1.0-dev.1" to clipboard
biometric_vault: ^1.1.0-dev.1 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 _silentWritesInitOptions = StorageFileInitOptions(
    silentWrites: true,
  );
  static final _customPromptInitOptions = StorageFileInitOptions(
    androidBiometricOnly: false,
    androidAuthenticationValidityDuration: const Duration(seconds: 5),
    darwinBiometricOnly: false,
    darwinTouchIDAuthenticationForceReuseContextDuration: const Duration(
      seconds: 5,
    ),
  );

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

  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,
                  );
                  _silentWrites = await BiometricVault().getStorage(
                    '${baseName}_silentWrites',
                    options: _silentWritesInitOptions,
                  );
                }
                _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');
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  child: const Text('biometry type'),
                  onPressed: () async {
                    final type = await BiometricVault().biometryType();
                    _logger.info('biometryType: $type');
                  },
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  child: const Text('authenticate'),
                  onPressed: () async {
                    try {
                      await BiometricVault().authenticate(
                        promptInfo: const PromptInfo(
                          iosPromptInfo: DarwinPromptInfo(
                            accessTitle: 'Unlock the example app',
                          ),
                          androidPromptInfo: AndroidPromptInfo(
                            title: 'Unlock the example app',
                          ),
                        ),
                      );
                      _logger.info('authenticate: success');
                    } on BiometricVaultException catch (e) {
                      _logger.info('authenticate failed: $e');
                    }
                  },
                ),
              ],
            ),
            ...?_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(),
                  ]),
            ...?(_silentWrites == null
                ? null
                : [
                    const Text(
                      'Silent writes (write never prompts, read does)',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    StorageActions(
                      storageFile: _silentWrites!,
                      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
0
points
--
downloads

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

unknown (license)

Dependencies

ffi, flutter, flutter_web_plugins, plugin_platform_interface, web, win32

More

Packages that depend on biometric_vault

Packages that implement biometric_vault