From 1aeddfd82ede6eff3da0411de2ac6ac6c65eb631 Mon Sep 17 00:00:00 2001 From: nesquikm Date: Wed, 25 Oct 2023 20:27:54 +0400 Subject: [PATCH] feat: add encrypted storage --- .../melos_flutter_test_encrypted_storage.xml | 7 + .../library/tome_library/tome_library.g.dart | 2 +- .../tome_library/tome_list/tome_list.g.dart | 2 +- packages/encrypted_storage/CHANGELOG.md | 114 ++++++ packages/encrypted_storage/README.md | 102 ++++++ .../encrypted_storage/analysis_options.yaml | 1 + .../lib/encrypted_storage.dart | 5 + .../lib/src/abstract_storage.dart | 65 ++++ .../lib/src/cipher_storage.dart | 69 ++++ .../lib/src/encrypt_helper.dart | 43 +++ .../lib/src/encrypted_storage.dart | 142 ++++++++ .../encrypted_storage/lib/src/storage.dart | 291 ++++++++++++++++ packages/encrypted_storage/pubspec.yaml | 28 ++ .../test/src/cipher_storade_test.dart | 75 ++++ .../test/src/encrypt_helper_test.dart | 108 ++++++ .../test/src/encrypted_storage_test.dart | 312 +++++++++++++++++ .../test/src/storage_test.dart | 328 ++++++++++++++++++ .../test/src/storage_value_test.dart | 18 + pubspec.lock | 76 ++++ pubspec.yaml | 2 + pubspec_overrides.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 23 files changed, 1795 insertions(+), 3 deletions(-) create mode 100644 .idea/runConfigurations/melos_flutter_test_encrypted_storage.xml create mode 100644 packages/encrypted_storage/CHANGELOG.md create mode 100644 packages/encrypted_storage/README.md create mode 100644 packages/encrypted_storage/analysis_options.yaml create mode 100644 packages/encrypted_storage/lib/encrypted_storage.dart create mode 100644 packages/encrypted_storage/lib/src/abstract_storage.dart create mode 100644 packages/encrypted_storage/lib/src/cipher_storage.dart create mode 100644 packages/encrypted_storage/lib/src/encrypt_helper.dart create mode 100644 packages/encrypted_storage/lib/src/encrypted_storage.dart create mode 100644 packages/encrypted_storage/lib/src/storage.dart create mode 100644 packages/encrypted_storage/pubspec.yaml create mode 100644 packages/encrypted_storage/test/src/cipher_storade_test.dart create mode 100644 packages/encrypted_storage/test/src/encrypt_helper_test.dart create mode 100644 packages/encrypted_storage/test/src/encrypted_storage_test.dart create mode 100644 packages/encrypted_storage/test/src/storage_test.dart create mode 100644 packages/encrypted_storage/test/src/storage_value_test.dart diff --git a/.idea/runConfigurations/melos_flutter_test_encrypted_storage.xml b/.idea/runConfigurations/melos_flutter_test_encrypted_storage.xml new file mode 100644 index 0000000..dc1d7cc --- /dev/null +++ b/.idea/runConfigurations/melos_flutter_test_encrypted_storage.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/lib/features/library/tome_library/tome_library.g.dart b/lib/features/library/tome_library/tome_library.g.dart index 3521ae8..683fec7 100644 --- a/lib/features/library/tome_library/tome_library.g.dart +++ b/lib/features/library/tome_library/tome_library.g.dart @@ -6,7 +6,7 @@ part of 'tome_library.dart'; // RiverpodGenerator // ************************************************************************** -String _$tomeLibraryHash() => r'db00bfc27e6602f8c032454ce693f0df1d127d82'; +String _$tomeLibraryHash() => r'5f49749fdb0aa4e3a132290da640727100099ed2'; /// See also [TomeLibrary]. @ProviderFor(TomeLibrary) diff --git a/lib/features/library/tome_library/tome_list/tome_list.g.dart b/lib/features/library/tome_library/tome_list/tome_list.g.dart index 0b4f3b5..b1f6a4e 100644 --- a/lib/features/library/tome_library/tome_list/tome_list.g.dart +++ b/lib/features/library/tome_library/tome_list/tome_list.g.dart @@ -6,7 +6,7 @@ part of 'tome_list.dart'; // RiverpodGenerator // ************************************************************************** -String _$tomeListHash() => r'0bda9eec3622686a1e7180f29b6ca3c8d47a598f'; +String _$tomeListHash() => r'b9359766df6e3d49ad79b064a5e1b421bcee5d48'; /// See also [TomeList]. @ProviderFor(TomeList) diff --git a/packages/encrypted_storage/CHANGELOG.md b/packages/encrypted_storage/CHANGELOG.md new file mode 100644 index 0000000..6c69870 --- /dev/null +++ b/packages/encrypted_storage/CHANGELOG.md @@ -0,0 +1,114 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## 2023-08-30 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`encrypted_storage` - `v0.1.2`](#encrypted_storage---v012) + +--- + +#### `encrypted_storage` - `v0.1.2` + + - **FIX**: flutter test concurrency (#6). + - **FEAT**: use flutter templates repository (#9). + - **DOCS**: add package description (#3). + +## 0.1.2 + + - **FIX**: flutter test concurrency (#6). + - **FEAT**: use flutter templates repository (#9). + - **DOCS**: add package description (#3). + + +## 2023-08-22 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`encrypted_storage` - `v0.1.1`](#encrypted_storage---v011) + +--- + +#### `encrypted_storage` - `v0.1.1` + + - **FIX**: flutter test concurrency (#6). + - **FEAT**: use flutter templates repository (#9). + - **DOCS**: add package description (#3). + +## 0.1.1 + + - **FIX**: flutter test concurrency (#6). + - **FEAT**: use flutter templates repository (#9). + - **DOCS**: add package description (#3). + + +## 2023-07-06 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`encrypted_storage` - `v0.1.0+2`](#encrypted_storage---v0102) + +Packages graduated to a stable release (see pre-releases prior to the stable version for changelog entries): + + - `encrypted_storage` - `v0.1.0+2` + +--- + +#### `encrypted_storage` - `v0.1.0+2` + +## 0.1.0+2 + + - Graduate package to a stable release. See pre-releases prior to this version for changelog entries. + + +## 2023-06-15 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`encrypted_storage` - `v0.1.0-dev.0+2`](#encrypted_storage---v010-dev02) + +--- + +#### `encrypted_storage` - `v0.1.0-dev.0+2` + + - **DOCS**: add package description (#3). + +## 0.1.0-dev.0+2 + + - **DOCS**: add package description (#3). + diff --git a/packages/encrypted_storage/README.md b/packages/encrypted_storage/README.md new file mode 100644 index 0000000..fd60994 --- /dev/null +++ b/packages/encrypted_storage/README.md @@ -0,0 +1,102 @@ +# Encrypted Storage + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: AGPLv3][license_badge]][license_link] + +This package provides a fast and simple way to store and retrieve encrypted data usind [sqflite][sqflite_link]. It uses AES encryption via [encrypt][encrypt_link] to encrypt the data and [flutter_secure_storage][flutter_secure_storage_link] to store key and initialization vector. + +## Installation ๐Ÿ’ป + +**โ— In order to start using Encrypted Storage you must have the [Flutter SDK][flutter_install_link] installed on your machine.** + +Add [encrypted_storage][pubdev_link] to your `pubspec.yaml`: + +```yaml +dependencies: + encrypted_storage: +``` + +## Melos magic ๐Ÿช„ + +Using [melos](https://melos.invertase.dev/) makes it very easy to work with the project, so enjoy. + +You can run any job interactively run running `melos run` and selecting needed case or directly (e.g. `melos run test`). + +### Bootstrap ๐Ÿ + +Melos takes care about dependencies of all packages, including managing of local-generated library version. So, just run: + +``` +melos bs +``` + +### Codegen ๐Ÿฆพ + +This thing will run all code generators for all packages: + +``` +$ melos run codegen +``` + +### Clean up ๐Ÿงน + +Just run commands below to clean all, including build directories and flutter projects. + +``` +melos clean +``` + +### Tests โœ”๏ธ + +You can run all tests at one by running this command. + +``` +melos run test +``` + +### Code ๐Ÿ“Š + +You can run code analysis: + +``` +melos run analyze +``` + +### Code format ๐Ÿ—ƒ๏ธ + +`melos run check-format` will check, `melos run format` will fix dart code formatting. + +``` +melos run check-format +melos run format +``` + +### Prepare to commit ๐Ÿค๐Ÿป + +`melos run check-all` will ckeck, analyze and run all tests. + +``` +melos run check-all +``` + +## Conventional Commits โค๏ธ + +[This magic](https://melos.invertase.dev/guides/automated-releases#versioning) will update version and build our library automatically using commit messages and tags. [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) is a lightweight convention on top of commit messages. + +## Version ๐Ÿท๏ธ + +Package version control is done by melos. It runs by gh action 'Create version PR' ```melos version -a --yes```. + +## Github Secrets ๐Ÿ”‘ + +`BOT_ACCESS_TOKEN`: Personal access token (PAT) used to fetch the repository. We should use PAT and not default GITHUB_TOKEN because ["When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN, with the exception of workflow_dispatch and repository_dispatch, will not create a new workflow run"](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow). We want to trigger a workflow from the workflow (to run tests), so we need to use PAT. This thing is used in `version` workflow. + +[flutter_install_link]: https://docs.flutter.dev/get-started/install +[license_badge]: https://img.shields.io/badge/license-AGPLv3-blue.svg +[license_link]: https://opensource.org/license/agpl-v3/ +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[flutter_secure_storage_link]: https://pub.dev/packages/flutter_secure_storage +[sqflite_link]: https://pub.dev/packages/sqflite +[encrypt_link]: https://pub.dev/packages/encrypt +[pubdev_link]: https://pub.dev/packages/encrypted_storage diff --git a/packages/encrypted_storage/analysis_options.yaml b/packages/encrypted_storage/analysis_options.yaml new file mode 100644 index 0000000..f04c6cf --- /dev/null +++ b/packages/encrypted_storage/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/encrypted_storage/lib/encrypted_storage.dart b/packages/encrypted_storage/lib/encrypted_storage.dart new file mode 100644 index 0000000..4948bc6 --- /dev/null +++ b/packages/encrypted_storage/lib/encrypted_storage.dart @@ -0,0 +1,5 @@ +/// Encrypted storage +library encrypted_storage; + +export 'src/abstract_storage.dart'; +export 'src/encrypted_storage.dart'; diff --git a/packages/encrypted_storage/lib/src/abstract_storage.dart b/packages/encrypted_storage/lib/src/abstract_storage.dart new file mode 100644 index 0000000..3f24bff --- /dev/null +++ b/packages/encrypted_storage/lib/src/abstract_storage.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +/// Default domain name +const String defaultDomain = 'default'; + +/// {@template storage} +/// AbstractStorage: storage interface +/// {@endtemplate} +abstract class AbstractStorage { + /// Clear storage: all records + Future clearAll(); + + /// Clear storage: all records in one domain + Future clearDomain([String? domain = defaultDomain]); + + /// Write the key-value pair. [value] will be written for the [key] in + /// [domain]. + /// If the pair was already existed it will be overwritten if [overwrite] + /// is true (by default) + Future set( + String key, + String value, { + String domain = defaultDomain, + bool overwrite = true, + }); + + /// Write the key-value pair map. [pairs] will be written in [domain]. + /// If the pair was already existed it will be overwritten if [overwrite] + /// is true (by default). Unspecified in [pairs] in db will not be altered + /// or deleted. + Future setDomain( + Map pairs, { + String domain = defaultDomain, + bool overwrite = true, + }); + + /// Delete by [key] from [domain]. + Future delete( + String key, { + String domain = defaultDomain, + }); + + /// Delete by [keys] from [domain]. + Future deleteDomain( + List keys, { + String domain = defaultDomain, + }); + + /// Get value by [key] and [domain]. If not found will return [defaultValue] + Future get( + String key, { + String? defaultValue, + String domain = defaultDomain, + }); + + /// Get key-value pair map from [domain]. + Future> getDomain({ + String domain = defaultDomain, + }); + + /// Get keys from [domain] + Future> getDomainKeys({ + String domain = defaultDomain, + }); +} diff --git a/packages/encrypted_storage/lib/src/cipher_storage.dart b/packages/encrypted_storage/lib/src/cipher_storage.dart new file mode 100644 index 0000000..00c8a99 --- /dev/null +++ b/packages/encrypted_storage/lib/src/cipher_storage.dart @@ -0,0 +1,69 @@ +import 'package:encrypt/encrypt.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const _keyLength = 32; +const _ivLength = 16; + +/// {@template ciper_storage} +/// Ciper storage +/// {@endtemplate} +class CipherStorage { + /// {@macro ciper_storage} + CipherStorage(); + + static const String _keyKey = 'cipher_storage_key'; + static const String _ivKey = 'cipher_storage_iv'; + late FlutterSecureStorage _storage; + late final Key _key; + late final IV _iv; + + /// Init ciper storage + Future init() async { + _storage = FlutterSecureStorage(aOptions: _getAndroidOptions()); + + final rows = await _storage.readAll(); + final key = rows[_keyKey]; + final iv = rows[_ivKey]; + + if (key != null && iv != null) { + _key = keyFromBase64(key); + _iv = ivFromBase64(iv); + + return; + } + + if ((key == null && iv != null) || (key != null && iv == null)) { + throw StateError( + 'key and iv reading error: one of them is null while another is not', + ); + } + + _key = keyFromSecureRandom(); + _iv = ivFromSecureRandom(); + + await _storage.write(key: _keyKey, value: _key.base64); + await _storage.write(key: _ivKey, value: _iv.base64); + } + + /// Get key + Key get key => _key; + + /// Get initialization vector + IV get iv => _iv; + + AndroidOptions _getAndroidOptions() => const AndroidOptions( + encryptedSharedPreferences: true, + ); + + /// Generate key from base64 + static Key keyFromBase64(String key) => Key.fromBase64(key); + + /// Generate initialization vector from base64 + static IV ivFromBase64(String iv) => IV.fromBase64(iv); + + /// Generate a new key + static Key keyFromSecureRandom() => Key.fromSecureRandom(_keyLength); + + /// Generate a new initialization vector + static IV ivFromSecureRandom() => IV.fromSecureRandom(_ivLength); +} diff --git a/packages/encrypted_storage/lib/src/encrypt_helper.dart b/packages/encrypted_storage/lib/src/encrypt_helper.dart new file mode 100644 index 0000000..2ecae66 --- /dev/null +++ b/packages/encrypted_storage/lib/src/encrypt_helper.dart @@ -0,0 +1,43 @@ +import 'package:encrypt/encrypt.dart'; +import 'package:encrypted_storage/src/cipher_storage.dart'; + +/// {@template encrypt_helper} +/// Encrtypt helper +/// {@endtemplate} +class EncryptHelper { + /// {@macro encrypt_helper} + EncryptHelper(this._cipherStorage) + : _encrypter = Encrypter( + AES(_cipherStorage.key), + ); + final CipherStorage _cipherStorage; + final Encrypter _encrypter; + + /// Encrypt [String], return base64-encoded String + String encrypt(String input, [String? iv]) { + return _encrypter + .encrypt( + input, + iv: iv != null ? CipherStorage.ivFromBase64(iv) : _cipherStorage.iv, + ) + .base64; + } + + /// Decrypt base64-encoded String [String], return original String + String decrypt(String input, [String? iv]) { + return _encrypter.decrypt64( + input, + iv: iv != null ? CipherStorage.ivFromBase64(iv) : _cipherStorage.iv, + ); + } + + /// Encrypt [String], return base64-encoded String + String? encryptNullable(String? input, [String? iv]) { + return input != null ? encrypt(input, iv) : null; + } + + /// Decrypt base64-encoded String [String], return original String + String? decryptNullable(String? input, [String? iv]) { + return input != null ? decrypt(input, iv) : null; + } +} diff --git a/packages/encrypted_storage/lib/src/encrypted_storage.dart b/packages/encrypted_storage/lib/src/encrypted_storage.dart new file mode 100644 index 0000000..b8b5f1b --- /dev/null +++ b/packages/encrypted_storage/lib/src/encrypted_storage.dart @@ -0,0 +1,142 @@ +import 'package:encrypted_storage/src/abstract_storage.dart'; +import 'package:encrypted_storage/src/cipher_storage.dart'; +import 'package:encrypted_storage/src/encrypt_helper.dart'; +import 'package:encrypted_storage/src/storage.dart'; + +/// {@template encrypted_storage} +/// Encrypted storage +/// {@endtemplate} +class EncryptedStorage implements AbstractStorage { + /// {@macro encrypted_storage} + EncryptedStorage() + : _storage = Storage(), + _cipherStorage = CipherStorage(); + + final CipherStorage _cipherStorage; + late EncryptHelper _encryptHelper; + final Storage _storage; + + static const String _storageFileName = 'encrypted_storage.db'; + + /// Init encrypted storage + Future init([String dbName = _storageFileName]) async { + await Future.wait([ + _cipherStorage.init(), + _storage.init(dbName), + ]); + _encryptHelper = EncryptHelper(_cipherStorage); + } + + /// Reset storage + /// @visibleForTesting + Future reset([String dbName = _storageFileName]) async { + return _storage.reset(dbName); + } + + @override + Future clearAll() async { + return _storage.clearAll(); + } + + @override + Future clearDomain([String? domain = defaultDomain]) async { + return _storage.clearDomain(domain); + } + + @override + Future delete(String key, {String domain = defaultDomain}) async { + return _storage.delete(_encryptHelper.encrypt(key), domain: domain); + } + + @override + Future deleteDomain( + List keys, { + String domain = defaultDomain, + }) async { + return _storage.deleteDomain( + keys.map((key) => _encryptHelper.encrypt(key)).toList(), + domain: domain, + ); + } + + @override + Future get( + String key, { + String? defaultValue, + String domain = defaultDomain, + }) async { + final storageValue = await _storage.get( + _encryptHelper.encrypt(key), + defaultValue: defaultValue != null + ? StorageValue(_encryptHelper.encrypt(defaultValue), '') + : null, + domain: domain, + ); + + return _encryptHelper.decryptNullable( + storageValue?.value, + storageValue?.iv, + ); + } + + @override + Future> getDomain({String domain = defaultDomain}) async { + final pairs = await _storage.getDomain( + domain: domain, + ); + + return pairs.map( + (key, value) => MapEntry( + _encryptHelper.decrypt(key), + _encryptHelper.decrypt(value.value, value.iv), + ), + ); + } + + @override + Future> getDomainKeys({ + String domain = defaultDomain, + }) async { + final keys = await _storage.getDomainKeys( + domain: domain, + ); + + return keys.map((key) => _encryptHelper.decrypt(key)).toList(); + } + + @override + Future set( + String key, + String value, { + String domain = defaultDomain, + bool overwrite = true, + }) { + final iv = CipherStorage.ivFromSecureRandom().base64; + + return _storage.set( + _encryptHelper.encrypt(key), + StorageValue(_encryptHelper.encrypt(value, iv), iv), + domain: domain, + overwrite: overwrite, + ); + } + + @override + Future setDomain( + Map pairs, { + String domain = defaultDomain, + bool overwrite = true, + }) => + _storage.setDomain( + pairs.map((key, value) { + final iv = CipherStorage.ivFromSecureRandom().base64; + + return MapEntry( + _encryptHelper.encrypt(key), + StorageValue(_encryptHelper.encrypt(value, iv), iv), + ); + }), + domain: domain, + overwrite: overwrite, + ); +} diff --git a/packages/encrypted_storage/lib/src/storage.dart b/packages/encrypted_storage/lib/src/storage.dart new file mode 100644 index 0000000..6d1512c --- /dev/null +++ b/packages/encrypted_storage/lib/src/storage.dart @@ -0,0 +1,291 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:encrypted_storage/src/abstract_storage.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +const _sqLiteSliceSize = 512; + +/// {@template storage} +/// Storage (db backend) +/// {@endtemplate} +class Storage { + /// {@macro storage} + Storage(); + + late final Database _database; + final _log = Logger('EncryptedStorage: Storage'); + + static const _storageFileName = 'storage.db'; + + /// Init storage + Future init([String dbName = _storageFileName]) async { + WidgetsFlutterBinding.ensureInitialized(); + + _database = await openDatabase( + join( + await getDatabasesPath(), + dbName, + ), + onCreate: _onCreate, + onUpgrade: _onUpgrade, + onDowngrade: _onDowngrade, + version: 1, + ); + + _log.finest('initialized'); + } + + /// Reset storage + Future reset([String dbName = _storageFileName]) async { + WidgetsFlutterBinding.ensureInitialized(); + + await deleteDatabase( + join( + await getDatabasesPath(), + dbName, + ), + ); + + _log.finest('reset'); + } + + Future _onCreate(Database db, int _) async { + await db.execute( + ''' + CREATE TABLE storage ( + domain TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + iv TEXT NOT NULL, + PRIMARY KEY (domain, key) + ); + ''', + ); + await db.execute( + ''' + CREATE INDEX storage_domain_index ON storage(domain); + ''', + ); + + _log.finest('database created'); + } + + FutureOr _onUpgrade(Database _, int oldVersion, int newVersion) { + _log + ..finest('database upgraded from $oldVersion to $newVersion') + ..warning('no upgrade migrations found'); + } + + FutureOr _onDowngrade(Database _, int oldVersion, int newVersion) { + _log + ..finest('database downgraded from $oldVersion to $newVersion') + ..warning('no downgrade migrations found'); + } + + /// Clear storage: all records + Future clearAll() async { + const query = ''' + DELETE FROM storage; + '''; + + await _database.execute(query); + + _log.finest('storage cleared'); + } + + /// Clear storage: all records in one domain + Future clearDomain([String? domain = defaultDomain]) async { + final query = ''' + DELETE FROM storage WHERE domain = '$domain'; + '''; + + await _database.execute(query); + + _log.finest('domain $domain cleared'); + } + + /// Write the key-value pair. [value] will be written for the [key] in + /// [domain]. + /// If the pair was already existed it will be overwritten if [overwrite] + /// is true (by default) + Future set( + String key, + StorageValue value, { + String domain = defaultDomain, + bool overwrite = true, + }) async { + return setDomain( + { + key: value, + }, + domain: domain, + overwrite: overwrite, + ); + } + + /// Write the key-value pair map. [pairs] will be written in [domain]. + /// If the pair was already existed it will be overwritten if [overwrite] + /// is true (by default). Unspecified in [pairs] in db will not be altered + /// or deleted. + Future setDomain( + Map pairs, { + String domain = defaultDomain, + bool overwrite = true, + }) async { + if (pairs.isEmpty) { + _log.info('setAll called with empty pair map'); + + return; + } + + var isFirst = true; + final values = pairs.entries.fold('', (previousValue, pair) { + final prefix = isFirst ? '' : ', '; + final result = + // ignore: lines_longer_than_80_chars + "$previousValue$prefix('$domain', '${pair.key}', '${pair.value.value}', '${pair.value.iv}' )"; + isFirst = false; + + return result; + }); + + final conflictClause = overwrite ? 'REPLACE' : 'IGNORE'; + final query = ''' + INSERT OR $conflictClause INTO storage (domain, key, value, iv) VALUES $values; + '''; + + await _database.execute(query); + } + + /// Delete by [key] from [domain]. + Future delete( + String key, { + String domain = defaultDomain, + }) async { + return deleteDomain([key], domain: domain); + } + + /// Delete by [keys] from [domain]. + Future deleteDomain( + List keys, { + String domain = defaultDomain, + }) async { + if (keys.isEmpty) { + _log.info('deleteDomain called with empty key list'); + + return; + } + + // SQLite has a limit of 999 variables per query + keys.slices(_sqLiteSliceSize).forEach((keys) async { + var isFirst = true; + final andClause = keys.fold('', (previousValue, key) { + final prefix = isFirst ? '' : ' OR '; + final result = "$previousValue$prefix(key = '$key')"; + isFirst = false; + + return result; + }); + + final query = ''' + DELETE FROM storage WHERE domain = '$domain' AND ($andClause) + '''; + + await _database.execute(query); + }); + } + + /// Get value by [key] and [domain]. If not found will return [defaultValue] + Future get( + String key, { + StorageValue? defaultValue, + String domain = defaultDomain, + }) async { + final list = await _database.rawQuery( + ''' + SELECT value, iv FROM storage WHERE domain = '$domain' and key = '$key' LIMIT 1; + ''', + ); + + return list.isNotEmpty + ? StorageValue( + list.first['value']! as String, + list.first['iv']! as String, + ) + : defaultValue; + } + + /// Get key-value pair map from [domain]. + Future> getDomain({ + String domain = defaultDomain, + }) async { + final list = await _database.rawQuery( + ''' + SELECT key, value, iv FROM storage WHERE domain = '$domain'; + ''', + ); + + return { + // There is no way to write null in these fields + // ignore: cast_nullable_to_non_nullable + for (final pair in list) + pair['key']! as String: StorageValue( + pair['value']! as String, + pair['iv']! as String, + ), + }; + } + + /// Get keys from [domain] + Future> getDomainKeys({ + String domain = defaultDomain, + }) async { + final list = await _database.rawQuery( + ''' + SELECT key FROM storage WHERE domain = '$domain'; + ''', + ); + + return [ + // There is no way to write null in these fields + // ignore: cast_nullable_to_non_nullable + for (final pair in list) pair['key']! as String, + ]; + } +} + +/// {@template storage_value} +/// Storage value unit +/// {@endtemplate} +@immutable +class StorageValue implements Comparable { + /// {@macro storage_value} + const StorageValue(this.value, this.iv); + + /// Value + final String value; + + /// Initialization vector + final String iv; + + @override + int compareTo(StorageValue other) { + return (value.compareTo(other.value) == 0 && iv.compareTo(other.iv) == 0) + ? 0 + : 1; + } + + @override + bool operator ==(Object other) { + return other is StorageValue && other.value == value && other.iv == iv; + } + + @override + int get hashCode { + return value.hashCode + iv.hashCode; + } +} diff --git a/packages/encrypted_storage/pubspec.yaml b/packages/encrypted_storage/pubspec.yaml new file mode 100644 index 0000000..741aa98 --- /dev/null +++ b/packages/encrypted_storage/pubspec.yaml @@ -0,0 +1,28 @@ +name: encrypted_storage +description: Encrypted storage +version: 0.1.2 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + collection: ^1.17.0 + encrypt: ^5.0.1 + flutter: + sdk: flutter + flutter_secure_storage: ^9.0.0 + logging: ^1.1.1 + path: ^1.8.2 + sqflite: ^2.2.6 + +dev_dependencies: + build_runner: ^2.4.5 + dart_code_metrics: ^5.6.0 + flutter_test: + sdk: flutter + melos: ^3.1.0 + mocktail: ^1.0.0 + path_provider_platform_interface: ^2.0.6 + plugin_platform_interface: ^2.1.4 + sqflite_common_ffi: ^2.2.2 + very_good_analysis: ^5.1.0 diff --git a/packages/encrypted_storage/test/src/cipher_storade_test.dart b/packages/encrypted_storage/test/src/cipher_storade_test.dart new file mode 100644 index 0000000..af54868 --- /dev/null +++ b/packages/encrypted_storage/test/src/cipher_storade_test.dart @@ -0,0 +1,75 @@ +// ignore_for_file: prefer_const_constructors +import 'package:encrypted_storage/src/cipher_storage.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const String cipherStorageKey = 'B0oxgy0Ddb+NxhGIDgmoRjvhKjESZKPrMfHzqlP9xEk='; +const String cipherStorageIv = 'lOpf2c8QsEg7AoexDh7iOQ=='; + +void main() { + group('CipherStorage', () { + test('can be instantiated', () { + expect(CipherStorage(), isNotNull); + }); + + test('first start test', () async { + FlutterSecureStorage.setMockInitialValues({}); + final cipherStorage = CipherStorage(); + await cipherStorage.init(); + expect(cipherStorage.key.bytes, hasLength(32)); + expect(cipherStorage.iv.bytes, hasLength(16)); + }); + + test('stored creds test', () async { + FlutterSecureStorage.setMockInitialValues( + { + 'cipher_storage_key': cipherStorageKey, + 'cipher_storage_iv': cipherStorageIv, + }, + ); + final cipherStorage = CipherStorage(); + await cipherStorage.init(); + expect(cipherStorage.key.base64, cipherStorageKey); + expect(cipherStorage.iv.base64, cipherStorageIv); + }); + + test('storage persistence test', () async { + FlutterSecureStorage.setMockInitialValues({}); + + final cipherStorage0 = CipherStorage(); + await cipherStorage0.init(); + expect(cipherStorage0.key.bytes, hasLength(32)); + expect(cipherStorage0.iv.bytes, hasLength(16)); + + final cipherStorage1 = CipherStorage(); + await cipherStorage1.init(); + expect(cipherStorage1.key.bytes, hasLength(32)); + expect(cipherStorage1.iv.bytes, hasLength(16)); + + expect(cipherStorage1.key.bytes, cipherStorage0.key.bytes); + expect(cipherStorage1.iv.bytes, cipherStorage0.iv.bytes); + }); + + test('storage persistence test, lost key', () async { + FlutterSecureStorage.setMockInitialValues( + { + 'cipher_storage_iv': cipherStorageIv, + }, + ); + + final cipherStorage = CipherStorage(); + await expectLater(cipherStorage.init, throwsStateError); + }); + + test('storage persistence test, lost iv', () async { + FlutterSecureStorage.setMockInitialValues( + { + 'cipher_storage_key': cipherStorageKey, + }, + ); + + final cipherStorage = CipherStorage(); + await expectLater(cipherStorage.init, throwsStateError); + }); + }); +} diff --git a/packages/encrypted_storage/test/src/encrypt_helper_test.dart b/packages/encrypted_storage/test/src/encrypt_helper_test.dart new file mode 100644 index 0000000..b8f6409 --- /dev/null +++ b/packages/encrypted_storage/test/src/encrypt_helper_test.dart @@ -0,0 +1,108 @@ +// ignore_for_file: prefer_const_constructors +import 'package:encrypted_storage/src/cipher_storage.dart'; +import 'package:encrypted_storage/src/encrypt_helper.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'cipher_storade_test.dart'; + +final testString = String.fromCharCodes( + List.generate( + 1024, + (i) => 1024 + i, + ), +); + +void main() { + group('EncryptHelper', () { + test('can be instantiated', () async { + FlutterSecureStorage.setMockInitialValues({}); + final cipherStorage = CipherStorage(); + await cipherStorage.init(); + expect(EncryptHelper(cipherStorage), isNotNull); + }); + + test('encrypt and decrypt test, same instance', () async { + FlutterSecureStorage.setMockInitialValues({}); + final cipherStorage = CipherStorage(); + await cipherStorage.init(); + final encryptHelper = EncryptHelper(cipherStorage); + + expect( + encryptHelper.decrypt(encryptHelper.encrypt(testString)), + testString, + ); + }); + + test('encrypt and decrypt test, different instances', () async { + FlutterSecureStorage.setMockInitialValues({}); + final cipherStorage0 = CipherStorage(); + await cipherStorage0.init(); + final encryptHelper0 = EncryptHelper(cipherStorage0); + + final encrypted = encryptHelper0.encrypt(testString); + + final cipherStorage1 = CipherStorage(); + await cipherStorage1.init(); + final encryptHelper1 = EncryptHelper(cipherStorage1); + + expect( + encryptHelper1.decrypt(encrypted), + testString, + ); + }); + + test('encrypt and decrypt test, different instances, lost key and iv', + () async { + FlutterSecureStorage.setMockInitialValues({}); + final cipherStorage0 = CipherStorage(); + await cipherStorage0.init(); + final encryptHelper0 = EncryptHelper(cipherStorage0); + + final encrypted = encryptHelper0.encrypt(testString); + + FlutterSecureStorage.setMockInitialValues({}); + + final cipherStorage1 = CipherStorage(); + await cipherStorage1.init(); + final encryptHelper1 = EncryptHelper(cipherStorage1); + + expect(() => encryptHelper1.decrypt(encrypted), throwsArgumentError); + }); + + test('encrypt and decrypt test, different instances, lost key', () async { + FlutterSecureStorage.setMockInitialValues( + { + 'cipher_storage_iv': cipherStorageIv, + }, + ); + + final cipherStorage1 = CipherStorage(); + await expectLater(cipherStorage1.init, throwsStateError); + }); + + test('encrypt and decrypt test, same instance, nullable', () async { + FlutterSecureStorage.setMockInitialValues({}); + final cipherStorage = CipherStorage(); + await cipherStorage.init(); + final encryptHelper = EncryptHelper(cipherStorage); + + expect( + encryptHelper.decryptNullable( + encryptHelper.encryptNullable( + testString, + ), + ), + testString, + ); + expect( + encryptHelper.decryptNullable( + encryptHelper.encryptNullable( + null, + ), + ), + null, + ); + }); + }); +} diff --git a/packages/encrypted_storage/test/src/encrypted_storage_test.dart b/packages/encrypted_storage/test/src/encrypted_storage_test.dart new file mode 100644 index 0000000..c3c7f59 --- /dev/null +++ b/packages/encrypted_storage/test/src/encrypted_storage_test.dart @@ -0,0 +1,312 @@ +// ignore_for_file: prefer_const_constructors, unnecessary_import + +import 'package:encrypted_storage/encrypted_storage.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +const String testDomainName0 = 'test domain name 0'; +final Map testKeyValuePairs0 = { + for (var id in List.generate(256, (index) => index)) + 'key 0: $id': 'value: 0: $id', +}; + +final Map testKeyValuePairs0Update = { + for (var id in List.generate(256, (index) => index)) + 'key 0: $id': 'value: 0: $id update', +}; + +final Map testKeyValuePairs1 = { + for (var id in List.generate(128, (index) => index)) + 'key 1: $id': 'value: 0: $id', +}; + +void main() { + // Initialize ffi implementation + sqfliteFfiInit(); + // Set global factory, do not use isolate here + databaseFactory = databaseFactoryFfiNoIsolate; + + group('EncryptedStorage can be instantiated', () { + setUpAll(() { + FlutterSecureStorage.setMockInitialValues({}); + }); + test('can be instantiated', () { + expect(EncryptedStorage(), isNotNull); + }); + }); + + group('EncryptedStorage db tests', () { + setUpAll(() { + FlutterSecureStorage.setMockInitialValues({}); + }); + setUp(() async { + await EncryptedStorage().reset(); + }); + + test('init and check empty', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + }); + + test('signle pair set and check in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey', 'testValue'); + expect(await storage.get('testKey'), 'testValue'); + }); + + test('signle pair set, update and check in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey', 'testValue'); + await storage.set('testKey', 'testValue updated'); + expect(await storage.get('testKey'), 'testValue updated'); + expect(await storage.getDomain(), hasLength(1)); + }); + + test('signle pair set and delete in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey0', 'testValue0'); + await storage.set('testKey1', 'testValue1'); + await storage.set('testKey2', 'testValue2'); + expect(await storage.getDomain(), hasLength(3)); + + await storage.delete('testKey1'); + expect(await storage.getDomain(), hasLength(2)); + expect(await storage.get('testKey0'), 'testValue0'); + expect(await storage.get('testKey1'), isNull); + expect(await storage.get('testKey2'), 'testValue2'); + }); + + test('signle pair set, NOT update and check in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey', 'testValue'); + await storage.set('testKey', 'testValue updated', overwrite: false); + expect(await storage.get('testKey'), 'testValue'); + expect(await storage.getDomain(), hasLength(1)); + }); + + test('signle check default value in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + expect(await storage.get('testKey'), isNull); + expect( + await storage.get('testKey', defaultValue: 'default value'), + 'default value', + ); + }); + + test('signle pair set and check in custom domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + + await storage.set('testKey', 'testValue', domain: testDomainName0); + expect(await storage.get('testKey'), isNull); + expect( + await storage.get('testKey', domain: testDomainName0), + 'testValue', + ); + }); + + test('signle check default value in custom domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + + expect(await storage.get('testKey', domain: testDomainName0), isNull); + expect( + await storage.get( + 'testKey', + domain: testDomainName0, + defaultValue: 'default value', + ), + 'default value', + ); + }); + + test('separated domain cleaning', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + + await storage.set('testKey', 'testValue'); + await storage.set('testKey', 'testValue', domain: testDomainName0); + + expect(await storage.getDomain(), hasLength(1)); + expect(await storage.getDomain(domain: testDomainName0), hasLength(1)); + + await storage.clearDomain(testDomainName0); + expect(await storage.getDomain(), hasLength(1)); + expect(await storage.getDomain(domain: testDomainName0), hasLength(0)); + + await storage.clearDomain(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + }); + + test('multiple pairs set and check in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomainKeys(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + expect( + await storage.getDomain(), + hasLength(testKeyValuePairs0.length), + ); + expect( + await storage.getDomainKeys(), + hasLength(testKeyValuePairs0.length), + ); + expect(await storage.getDomain(), testKeyValuePairs0); + expect( + (await storage.getDomainKeys())..sort(), + testKeyValuePairs0.keys.toList()..sort(), + ); + for (final pair in testKeyValuePairs0.entries) { + expect(await storage.get(pair.key), pair.value); + } + }); + + test('multiple pairs set, update and check in default domain', () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs0Update); + expect(await storage.getDomain(), hasLength(testKeyValuePairs0.length)); + expect(await storage.getDomain(), testKeyValuePairs0Update); + }); + + test('multiple pairs set, NOT update and check in default domain', + () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs0Update, overwrite: false); + expect(await storage.getDomain(), hasLength(testKeyValuePairs0.length)); + expect(await storage.getDomain(), testKeyValuePairs0); + }); + + test('multiple pairs set, update, append and check in default domain', + () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs0Update); + await storage.setDomain(testKeyValuePairs1); + expect( + await storage.getDomain(), + hasLength( + testKeyValuePairs0.length + testKeyValuePairs1.length, + ), + ); + expect( + await storage.getDomain(), + { + ...testKeyValuePairs0Update, + ...testKeyValuePairs1, + }, + ); + }); + + test('multiple pairs set, append and partially delete in default domain', + () async { + final storage = EncryptedStorage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs1); + expect( + await storage.getDomain(), + hasLength( + testKeyValuePairs0.length + testKeyValuePairs1.length, + ), + ); + await storage.deleteDomain(List.from(testKeyValuePairs0.keys)); + expect( + await storage.getDomain(), + hasLength( + testKeyValuePairs1.length, + ), + ); + expect( + await storage.getDomain(), + { + ...testKeyValuePairs1, + }, + ); + }); + }); + + group('EncryptedStorage multiple init/reset', () { + setUpAll(() { + FlutterSecureStorage.setMockInitialValues({}); + }); + setUp(() async { + await EncryptedStorage().reset(); + }); + + test( + 'run #0', + () async { + final storage = EncryptedStorage(); + await storage.init(); + + await storage.set('testKey', 'testValue'); + expect(await storage.get('testKey'), 'testValue'); + }, + ); + + test( + 'run #1', + () async { + final storage = EncryptedStorage(); + await storage.init(); + + expect(await storage.getDomain(), isEmpty); + }, + ); + }); +} diff --git a/packages/encrypted_storage/test/src/storage_test.dart b/packages/encrypted_storage/test/src/storage_test.dart new file mode 100644 index 0000000..e05cb29 --- /dev/null +++ b/packages/encrypted_storage/test/src/storage_test.dart @@ -0,0 +1,328 @@ +// ignore_for_file: prefer_const_constructors, unnecessary_import + +import 'package:encrypted_storage/src/storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +const String testDomainName0 = 'test domain name 0'; +final Map testKeyValuePairs0 = { + for (var id in List.generate(256, (index) => index)) + 'key 0: $id': StorageValue('value: 0: $id', 'iv: 0: $id'), +}; + +final Map testKeyValuePairs0Update = { + for (var id in List.generate(256, (index) => index)) + 'key 0: $id': StorageValue('value: 0: $id update', 'iv: 0: $id update'), +}; + +final Map testKeyValuePairs1 = { + for (var id in List.generate(128, (index) => index)) + 'key 1: $id': StorageValue('value: 1: $id', 'iv: 1: $id'), +}; + +final Map testKeyValuePairs2 = { + for (var id in List.generate(2048, (index) => index)) + 'key 2: $id': StorageValue('value: 2: $id', 'iv: 2: $id'), +}; + +void main() { + // Initialize ffi implementation + sqfliteFfiInit(); + // Set global factory, do not use isolate here + databaseFactory = databaseFactoryFfiNoIsolate; + + group('Storage can be instantiated', () { + test('can be instantiated', () { + expect(Storage(), isNotNull); + }); + }); + + group('Storage db tests', () { + setUp(() { + Storage().reset(); + }); + test('init and check empty', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + }); + + test('signle pair set and check in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey', StorageValue('testValue', 'testIv')); + expect(await storage.get('testKey'), StorageValue('testValue', 'testIv')); + }); + + test('signle pair set, update and check in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set( + 'testKey', + StorageValue('testValue', 'testIv'), + ); + await storage.set( + 'testKey', + StorageValue('testValue updated', 'testIv updated'), + ); + expect( + await storage.get('testKey'), + StorageValue('testValue updated', 'testIv updated'), + ); + expect(await storage.getDomain(), hasLength(1)); + }); + + test('signle pair set and delete in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey0', StorageValue('testValue0', 'testIv0')); + await storage.set('testKey1', StorageValue('testValue1', 'testIv1')); + await storage.set('testKey2', StorageValue('testValue2', 'testIv2')); + expect(await storage.getDomain(), hasLength(3)); + + await storage.delete('testKey1'); + expect(await storage.getDomain(), hasLength(2)); + expect( + await storage.get('testKey0'), + StorageValue('testValue0', 'testIv0'), + ); + expect(await storage.get('testKey1'), isNull); + expect( + await storage.get('testKey2'), + StorageValue('testValue2', 'testIv2'), + ); + }); + + test('signle pair set, NOT update and check in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.set('testKey', StorageValue('testValue', 'testIv')); + await storage.set( + 'testKey', + StorageValue('testValue0 updated', 'testIv0updated'), + overwrite: false, + ); + expect(await storage.get('testKey'), StorageValue('testValue', 'testIv')); + expect(await storage.getDomain(), hasLength(1)); + }); + + test('signle check default value in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + expect(await storage.get('testKey'), isNull); + expect( + await storage.get( + 'testKey', + defaultValue: StorageValue('default value', 'default iv'), + ), + StorageValue('default value', 'default iv'), + ); + }); + + test('signle pair set and check in custom domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + + await storage.set( + 'testKey', + StorageValue('testValue', 'testIv'), + domain: testDomainName0, + ); + expect(await storage.get('testKey'), isNull); + expect( + await storage.get('testKey', domain: testDomainName0), + StorageValue('testValue', 'testIv'), + ); + }); + + test('signle check default value in custom domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + + expect(await storage.get('testKey', domain: testDomainName0), isNull); + expect( + await storage.get( + 'testKey', + domain: testDomainName0, + defaultValue: StorageValue('default value', 'default iv'), + ), + StorageValue('default value', 'default iv'), + ); + }); + + test('separated domain cleaning', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + + await storage.set( + 'testKey', + StorageValue('testValue', 'testIv'), + ); + await storage.set( + 'testKey', + StorageValue('testValue', 'testIv'), + domain: testDomainName0, + ); + + expect(await storage.getDomain(), hasLength(1)); + expect(await storage.getDomain(domain: testDomainName0), hasLength(1)); + + await storage.clearDomain(testDomainName0); + expect(await storage.getDomain(), hasLength(1)); + expect(await storage.getDomain(domain: testDomainName0), hasLength(0)); + + await storage.clearDomain(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomain(domain: testDomainName0), isEmpty); + }); + + test('multiple pairs set and check in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + expect(await storage.getDomainKeys(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + expect( + await storage.getDomain(), + hasLength(testKeyValuePairs0.length), + ); + expect( + await storage.getDomainKeys(), + hasLength(testKeyValuePairs0.length), + ); + expect(await storage.getDomain(), testKeyValuePairs0); + expect( + (await storage.getDomainKeys())..sort(), + testKeyValuePairs0.keys.toList()..sort(), + ); + for (final pair in testKeyValuePairs0.entries) { + expect(await storage.get(pair.key), pair.value); + } + }); + + test('multiple pairs set, update and check in default domain', () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs0Update); + expect(await storage.getDomain(), hasLength(testKeyValuePairs0.length)); + expect(await storage.getDomain(), testKeyValuePairs0Update); + }); + + test('multiple pairs set, NOT update and check in default domain', + () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs0Update, overwrite: false); + expect(await storage.getDomain(), hasLength(testKeyValuePairs0.length)); + expect(await storage.getDomain(), testKeyValuePairs0); + }); + + test('multiple pairs set, update, append and check in default domain', + () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs0Update); + await storage.setDomain(testKeyValuePairs1); + expect( + await storage.getDomain(), + hasLength( + testKeyValuePairs0.length + testKeyValuePairs1.length, + ), + ); + expect( + await storage.getDomain(), + { + ...testKeyValuePairs0Update, + ...testKeyValuePairs1, + }, + ); + }); + + test('multiple pairs set, append and partially delete in default domain', + () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs0); + await storage.setDomain(testKeyValuePairs1); + expect( + await storage.getDomain(), + hasLength( + testKeyValuePairs0.length + testKeyValuePairs1.length, + ), + ); + await storage.deleteDomain(List.from(testKeyValuePairs0.keys)); + expect( + await storage.getDomain(), + hasLength( + testKeyValuePairs1.length, + ), + ); + expect( + await storage.getDomain(), + { + ...testKeyValuePairs1, + }, + ); + }); + + test('huge pair list delete test (query splitting) in default domain', + () async { + final storage = Storage(); + await storage.init(); + await storage.clearAll(); + expect(await storage.getDomain(), isEmpty); + + await storage.setDomain(testKeyValuePairs2); + expect( + await storage.getDomain(), + hasLength(testKeyValuePairs2.length), + ); + await storage.deleteDomain(List.from(testKeyValuePairs2.keys)); + expect(await storage.getDomain(), isEmpty); + }); + }); +} diff --git a/packages/encrypted_storage/test/src/storage_value_test.dart b/packages/encrypted_storage/test/src/storage_value_test.dart new file mode 100644 index 0000000..dc850cd --- /dev/null +++ b/packages/encrypted_storage/test/src/storage_value_test.dart @@ -0,0 +1,18 @@ +// ignore_for_file: prefer_const_constructors +import 'package:encrypted_storage/src/storage.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('StorageValue', () { + test('can be instantiated', () async { + expect(StorageValue('', ''), isNotNull); + }); + + test('comparable test', () async { + expect(StorageValue('', ''), equals(StorageValue('', ''))); + expect(StorageValue('a', 'b'), equals(StorageValue('a', 'b'))); + expect(StorageValue('a', 'b'), isNot(StorageValue('a', 'c'))); + expect(StorageValue('a', 'b'), isNot(StorageValue('c', 'b'))); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index a0549c3..f896ff6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + url: "https://pub.dev" + source: hosted + version: "1.5.0" async: dependency: "direct main" description: @@ -265,6 +273,21 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + encrypted_storage: + dependency: "direct main" + description: + path: "packages/encrypted_storage" + relative: true + source: path + version: "0.1.2" epubx: dependency: "direct main" description: @@ -348,11 +371,64 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + flutter_secure_storage: + dependency: transitive + description: + name: flutter_secure_storage + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" freezed: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7be6307..28af547 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: async: ^2.11.0 convert: ^3.1.1 crypto: ^3.0.3 + encrypted_storage: + path: packages/encrypted_storage epubx: git: url: https://github.com/nesquikm/epubx.dart.git diff --git a/pubspec_overrides.yaml b/pubspec_overrides.yaml index 4e00a01..5306a74 100644 --- a/pubspec_overrides.yaml +++ b/pubspec_overrides.yaml @@ -1,4 +1,6 @@ -# melos_managed_dependency_overrides: fancy_logger +# melos_managed_dependency_overrides: encrypted_storage,fancy_logger dependency_overrides: + encrypted_storage: + path: packages/encrypted_storage fancy_logger: path: packages/fancy_logger diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..0c50753 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..4fc759c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST