Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/onyx/build.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
targets:
$default:
builders:
source_gen|combining_builder:
generate_for:
- lib/**.dart
# only to resolve build_runner conflicts
dart_mappable_builder:
options:
build_extensions:
'lib/{{path}}/{{file}}.dart': 'lib/{{path}}/generated/{{file}}.g.dart'
'lib/{{path}}/{{file}}.dart':
- 'lib/{{path}}/generated/{{file}}.mapper.dart'
- 'lib/{{path}}/generated/{{file}}.init.dart'

123 changes: 93 additions & 30 deletions apps/onyx/lib/core/cache_service.dart
Original file line number Diff line number Diff line change
@@ -1,62 +1,118 @@
import 'dart:convert';

import 'package:biometric_storage/biometric_storage.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:izlyclient/izlyclient.dart';
import 'package:lyon1casclient/lyon1casclient.dart';
import 'package:onyx/core/res.dart';
import 'package:onyx/screens/settings/domain/model/theme_settings_model.dart';
import 'package:sembast/sembast_io.dart';

import 'encrypt_codec.dart';

class CacheService {
static List<int>? secureKey;
static BiometricStorageFile? _storageFile;
static bool? _isBiometricEnabled;
static Database? db;

static void resetDb() {
db = null;
}

static Future<Database> _getDb({List<int>? secureKey}) async {
if (db == null) {
throw Exception(
"CacheService not initialized. Call CacheService.init() first.",
);
}
if (secureKey != null) {
// Utilisation du codec d'encryption Sembast (Salsa20)
final codec = getEncryptSembastCodec(
password: base64Url.encode(secureKey),
);
return await databaseFactoryIo.openDatabase(db!.path, codec: codec);
}
return db!;
}

static StoreRef<String, dynamic> _getStore<E>() {
return StoreRef<String, dynamic>('cached_${E.toString()}');
}

static Future<E?> get<E>({int index = 0, List<int>? secureKey}) async {
try {
Box<E> box = await Hive.openBox<E>(
"cached_$E",
encryptionCipher: (secureKey != null) ? HiveAesCipher(secureKey) : null,
crashRecovery: false,
);
return box.get("cache$index");
final db = await _getDb(secureKey: secureKey);
final store = _getStore<E>();
final value = await store.record('cache$index').get(db);

// Try to decode using dart_mappable if possible
if (value != null) {
try {
return MapperContainer.globals.fromValue<E>(value);
} catch (e) {
Res.logger.e("error while decoding cache for $E: $e");
return value as E;
}
}
return null;
} catch (e) {
Res.logger.e("error while getting cache for $E: $e");
await reset<E>();
await reset<E>(secureKey: secureKey);
return null;
}
}

static Future<void> set<E>(E data,
{int index = 0, List<int>? secureKey}) async {
static Future<void> set<E>(
E data, {
int index = 0,
List<int>? secureKey,
}) async {
dynamic toStore = data;
try {
Box box = await Hive.openBox<E>(
"cached_$E",
encryptionCipher: (secureKey != null) ? HiveAesCipher(secureKey) : null,
crashRecovery: false,
);
await box.put("cache$index", data);
if (secureKey != null) await box.close();
final db = await _getDb(secureKey: secureKey);
final store = _getStore<E>();

var a = RestaurantListModel(restaurantList: []);
var b = ThemeSettingsModel();
b.toMap();
a.toMap();
// Try to encode using dart_mappable if possible
try {
toStore = MapperContainer.globals.toValue(data);
} catch (parseError) {
Res.logger.e("error while encoding cache for $E: $parseError");
}

await store.record('cache$index').put(db, toStore);
} catch (e) {
Res.logger.e("error while setting cache for $E: $e");
await reset<E>();
//Res.logger.e("data was: $toStore");
await reset<E>(secureKey: secureKey);
}
}

static Future<bool> exist<E>({int index = 0, List<int>? secureKey}) async {
try {
Box box = await Hive.openBox<E>(
"cached_$E",
encryptionCipher: (secureKey != null) ? HiveAesCipher(secureKey) : null,
crashRecovery: false,
);
return box.containsKey("cache$index");
final db = await _getDb(secureKey: secureKey);
final store = _getStore<E>();
return await store.record('cache$index').exists(db);
} catch (e) {
Res.logger.e("error while checking existence of cache for $E: $e");
await reset<E>();
await reset<E>(secureKey: secureKey);
return false;
}
}

static Future<void> reset<E>() async {
await Hive.deleteBoxFromDisk("cached_$E");
static Future<void> reset<E>({List<int>? secureKey}) async {
try {
final db = await _getDb(secureKey: secureKey);
final store = _getStore<E>();
// Supprime tous les enregistrements du store
await store.drop(db);
} catch (e) {
Res.logger.e("error while resetting cache for $E: $e");
}
}

static Future<bool> toggleBiometricAuth(bool biometricAuth) async {
Expand All @@ -81,8 +137,10 @@ class CacheService {
return true;
}

static Future<List<int>> getEncryptionKey(bool biometricAuth,
{bool autoRetry = false}) async {
static Future<List<int>> getEncryptionKey(
bool biometricAuth, {
bool autoRetry = false,
}) async {
if (_isBiometricEnabled != null && _isBiometricEnabled != biometricAuth) {
await toggleBiometricAuth(biometricAuth);
}
Expand All @@ -107,7 +165,12 @@ class CacheService {
try {
String? data = await _storageFile!.read();
if (data == null) {
data = base64Encode(Hive.generateSecureKey());
// Générer une clé aléatoire (32 bytes)
final key = List<int>.generate(
32,
(i) => i * DateTime.now().millisecondsSinceEpoch % 256,
);
data = base64Encode(key);
await _storageFile!.write(data);
}
secureKey = base64Url.decode(data);
Expand Down
183 changes: 183 additions & 0 deletions apps/onyx/lib/core/encrypt_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';

import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';

// ignore: implementation_imports
import 'package:sembast/src/api/v2/sembast.dart';

final _random = () {
try {
// Try secure
return Random.secure();
} catch (_) {
return Random();
}
}();

/// Random bytes generator
Uint8List _randBytes(int length) {
return Uint8List.fromList(
List<int>.generate(length, (i) => _random.nextInt(256)),
);
}

/// FOR DEMONSTRATION PURPOSES ONLY -- do not use in production as-is!
///
/// This is a demonstration on how to bring encryption to sembast, but it is an
/// insecure implementation. The encryption is unauthenticated,
/// the password conversion to bytes is underpowered (password hashes like
/// bcyrpt, scrypt, argon2id, and pbkdf2 are some examples of correct algorithms),
/// and the random bytes generator doesn't use a cryptographically secure source
/// of randomness.
///
/// See https://github.com/tekartik/sembast.dart/pull/339 for more information
///
/// Generate an encryption password based on a user input password
///
/// It uses MD5 which generates a 16 bytes blob, size needed for Salsa20
Uint8List _generateEncryptPassword(String password) {
var blob = Uint8List.fromList(md5.convert(utf8.encode(password)).bytes);
assert(blob.length == 16);
return blob;
}

/// Salsa20 based encoder
class _EncryptEncoder extends Converter<Object?, String> {
final Salsa20 salsa20;

_EncryptEncoder(this.salsa20);

@override
String convert(dynamic input) {
// Generate random initial value
final iv = _randBytes(8);
final ivEncoded = base64.encode(iv);
assert(ivEncoded.length == 12);

// Encode the input value
final encoded = Encrypter(
salsa20,
).encrypt(json.encode(input), iv: IV(iv)).base64;

// Prepend the initial value
return '$ivEncoded$encoded';
}
}

/// Salsa20 based decoder
class _EncryptDecoder extends Converter<String, Object?> {
final Salsa20 salsa20;

_EncryptDecoder(this.salsa20);

@override
dynamic convert(String input) {
// Read the initial value that was prepended
assert(input.length >= 12);
final iv = base64.decode(input.substring(0, 12));

// Extract the real input
input = input.substring(12);

// Decode the input
var decoded = json.decode(Encrypter(salsa20).decrypt64(input, iv: IV(iv)));
if (decoded is Map) {
return decoded.cast<String, Object?>();
}
return decoded;
}
}

/// Salsa20 based Codec
class _EncryptCodec extends Codec<Object?, String> {
late _EncryptEncoder _encoder;
late _EncryptDecoder _decoder;

_EncryptCodec(Uint8List passwordBytes) {
var salsa20 = Salsa20(Key(passwordBytes));
_encoder = _EncryptEncoder(salsa20);
_decoder = _EncryptDecoder(salsa20);
}

@override
Converter<String, Object?> get decoder => _decoder;

@override
Converter<Object?, String> get encoder => _encoder;
}

/// Our plain text signature
const _encryptCodecSignature = 'encrypt';

/// Create a codec to use to open a database with encrypted stored data.
///
/// Hash (md5) of the password is used (but never stored) as a key to encrypt
/// the data using the Salsa20 algorithm with a random (8 bytes) initial value
///
/// This is just used as a demonstration and should not be considered as a
/// reference since its implementation (and storage format) might change.
///
/// No performance metrics has been made to check whether this is a viable
/// solution for big databases.
///
/// The usage is then
///
/// ```dart
/// // Initialize the encryption codec with a user password
/// var codec = getEncryptSembastCodec(password: '[your_user_password]');
/// // Open the database with the codec
/// Database db = await factory.openDatabase(dbPath, codec: codec);
///
/// // ...your database is ready to use
/// ```
SembastCodec getEncryptSembastCodec({required String password}) => SembastCodec(
signature: _encryptCodecSignature,
codec: _EncryptCodec(_generateEncryptPassword(password)),
);

/// Wrap a factory to always use the codec
class EncryptedDatabaseFactory implements DatabaseFactory {
final DatabaseFactory databaseFactory;
late final SembastCodec codec;

EncryptedDatabaseFactory({
required this.databaseFactory,
required String password,
}) {
codec = getEncryptSembastCodec(password: password);
}

@override
Future<void> deleteDatabase(String path) =>
databaseFactory.deleteDatabase(path);

@override
bool get hasStorage => databaseFactory.hasStorage;

/// To use with codec, null
@override
Future<Database> openDatabase(
String path, {
int? version,
OnVersionChangedFunction? onVersionChanged,
DatabaseMode? mode,
SembastCodec? codec,
}) {
assert(codec == null);
return databaseFactory.openDatabase(
path,
version: version,
onVersionChanged: onVersionChanged,
mode: mode,
codec: this.codec,
);
}

@override
Future<bool> databaseExists(String path) {
return databaseFactory.databaseExists(path);
}
}
Loading
Loading