biometric_storage 0.3.4+6

  • Readme
  • Changelog
  • Example
  • Installing
  • 88

biometric_storage #

Pub

Encrypted file store, optionally secured by biometric lock for Android, iOS and MacOS.

Meant as a way to store small data in a hardware encrypted fashion. E.g. to store passwords, secret keys, etc. but not massive amounts of data.

On android uses androidx uses KeyStore and on iOS LocalAuthentication with KeyChain.

Check out AuthPass Password Manager for a app which makes heavy use of this plugin.

Versions #

  • Use 0.2.x for legacy plugin format for iOS and android support.
  • Use 0.3.x-beta for the new platforms pubspec format which also supports mac OS.

Getting Started #

Android #

  • Requirements:
    • Android: API Level >= 23

    • MainActivity must extend FlutterFragmentActivity

    • Theme for the main activity must use Theme.AppCompat thme. (Otherwise there will be crases on Android < 29) For example:

      AndroidManifest.xml:

      <activity
      android:name=".MainActivity"
      android:launchMode="singleTop"
      android:theme="@style/LaunchTheme"
      

      xml/styles.xml:

          <style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
          <!-- Show a splash screen on the activity. Automatically removed when
               Flutter draws its first frame -->
          <item name="android:windowBackground">@drawable/launch_background</item>
      
          <item name="android:windowNoTitle">true</item>
          <item name="android:windowActionBar">false</item>
          <item name="android:windowFullscreen">true</item>
          <item name="android:windowContentOverlay">@null</item>
      </style>
      

iOS #

https://developer.apple.com/documentation/localauthentication/logging_a_user_into_your_app_with_face_id_or_touch_id

  • include the NSFaceIDUsageDescription key in your app’s Info.plist file
  • Requires at least iOS 9

Mac OS #

  • include the NSFaceIDUsageDescription key in your app’s Info.plist file
  • enable keychain sharing and signing. (not sure why this is required. but without it You will probably see an error like:

    SecurityError, Error while writing data: -34018: A required entitlement isn't present.

  • Requires at least Mac OS 10.12

Resources #

0.3.4+6 #

0.3.4+5 #

0.3.4+4 #

  • Android: fix PromptInfo deserialization with minification.
  • Android: add proguard setting to fix protobuf exceptions.

0.3.4+2 #

  • Android: updated dependencies to androidx.security, biometric, gradle tools.

0.3.4+1 #

  • Android: on error send stack trace to flutter. also fixed a couple of warnings.

0.3.4 #

  • Android: allow customization of the PromptInfo (labels, buttons, etc). @patrickhammond

0.3.3 #

0.3.2 #

  • android: fingerprint failures don't cancel the dialog, so don't trigger error callback. #2 (fixes crash)

0.3.1 #

  • Use android v2 plugin API.

0.3.0-beta.2 #

  • Use new plugin format for Mac OS format. Not compatible with flutter 1.9.x

0.2.2+2 #

  • Use legacy plugin platforms structure to be compatible with flutter stable.

0.2.2+1 #

  • fixed home page link, updated example README.

0.2.2 #

  • Android: Use codegen instead of reflection for json serialization. (Fixes bug that options aren't assed in correctly due to minification)

0.2.1 #

  • Android: Fix for having multiple files with different configurations.
  • Correctly handle UserCanceled events.
  • Define correct default values on dart side (10 seconds validity timeout).

0.2.0 #

  • MacOS Support

0.1.0 #

  • iOS Support
  • Support for non-authenticated storage (ie. secure/encrypted storage, without extra biometric authenticatiton prompts)
  • delete()'ing files.

0.0.1 - Initial release #

  • Android Support.

example/lib/main.dart

import 'dart:io';

import 'package:biometric_storage/biometric_storage.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.');
  _setTargetPlatformForDesktop();
  runApp(MyApp());
}

/// If the current platform is desktop, override the default platform to
/// a supported platform (iOS for macOS, Android for Linux and Windows).
/// Otherwise, do nothing.
void _setTargetPlatformForDesktop() {
  TargetPlatform targetPlatform;
  if (Platform.isLinux || Platform.isWindows) {
    targetPlatform = TargetPlatform.android;
  }
  _logger.info('targetPlatform: $targetPlatform');
  if (targetPlatform != null) {
    debugDefaultTargetPlatformOverride = targetPlatform;
  }
}

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 {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final String baseName = 'default';
  BiometricStorageFile _authStorage;
  BiometricStorageFile _storage;
  BiometricStorageFile _customPrompt;
  BiometricStorageFile _noConfirmation;

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

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

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

  Future<CanAuthenticateResponse> _checkAuthenticate() async {
    final response = await BiometricStorage().canAuthenticate();
    _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:'),
            RaisedButton(
              child: const Text('init'),
              onPressed: () async {
                _logger.finer('Initializing $baseName');
                if ((await _checkAuthenticate()) !=
                    CanAuthenticateResponse.success) {
                  _logger.severe(
                      'Unable to use authenticate. Unable to getting storage.');
                  return;
                }
                _authStorage = await BiometricStorage().getStorage(
                    '${baseName}_authenticated',
                    options: StorageFileInitOptions(
                        authenticationValidityDurationSeconds: 30));
                _storage = await BiometricStorage()
                    .getStorage('${baseName}_unauthenticated',
                        options: StorageFileInitOptions(
                          authenticationRequired: false,
                        ));
                _customPrompt = await BiometricStorage().getStorage(
                    '${baseName}_customPrompt',
                    options: StorageFileInitOptions(
                        authenticationValidityDurationSeconds: 30),
                    androidPromptInfo: const AndroidPromptInfo(
                      title: 'Custom title',
                      subtitle: 'Custom subtitle',
                      description: 'Custom description',
                      negativeButton: 'Nope!',
                    ));
                _noConfirmation = await BiometricStorage().getStorage(
                    '${baseName}_customPrompt',
                    options: StorageFileInitOptions(
                        authenticationValidityDurationSeconds: 30),
                    androidPromptInfo: const AndroidPromptInfo(
                      confirmationRequired: false,
                    ));
                setState(() {});
                _logger.info('initiailzed $baseName');
              },
            ),
            ...(_authStorage == null
                ? []
                : [
                    const Text('Biometric Authentication',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _authStorage,
                        writeController: _writeController),
                    const Divider(),
                    const Text('Unauthenticated',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _storage,
                        writeController: _writeController),
                    const Divider(),
                    const Text('Custom Authentication Prompt (Android)',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _customPrompt,
                        writeController: _writeController),
                    const Divider(),
                    const Text('No Confirmation Prompt (Android)',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _noConfirmation,
                        writeController: _writeController),
                  ]),
            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(
                  child: Container(
                    padding: const EdgeInsets.all(16),
                    child: Text(
                      logMessages.log.toString(),
                    ),
                  ),
                  reverse: true,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

  final BiometricStorageFile storageFile;
  final TextEditingController writeController;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[
        RaisedButton(
          child: const Text('read'),
          onPressed: () async {
            _logger.fine('reading from ${storageFile.name}');
            final result = await storageFile.read();
            _logger.fine('read: {$result}');
          },
        ),
        RaisedButton(
          child: const Text('write'),
          onPressed: () async {
            _logger.fine('Going to write...');
            await storageFile
                .write(' [${DateTime.now()}] ${writeController.text}');
            _logger.info('Written content.');
          },
        ),
        RaisedButton(
          child: const Text('delete'),
          onPressed: () async {
            _logger.fine('deleting...');
            await storageFile.delete();
            _logger.info('Deleted.');
          },
        ),
      ],
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  biometric_storage: ^0.3.4+6

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:biometric_storage/biometric_storage.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
76
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
88
Learn more about scoring.

We analyzed this package on Jul 2, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.13
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform linux

Because of import path [package:biometric_storage/biometric_storage.dart] that declares support for platforms: android, ios, macos

Package does not support Flutter platform web

Because of import path [package:biometric_storage/biometric_storage.dart] that declares support for platforms: android, ios, macos

Package does not support Flutter platform windows

Because of import path [package:biometric_storage/biometric_storage.dart] that declares support for platforms: android, ios, macos

Package not compatible with SDK dart

because of import path [biometric_storage] that is in a package requiring null.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
logging >=0.10.0 <1.0.0 0.11.4
Transitive dependencies
collection 1.14.12 1.14.13
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8
Dev dependencies
flutter_test
pedantic >=1.7.0 <2.0.0