diff --git a/hive/CHANGELOG.md b/hive/CHANGELOG.md index eedc3a4d..43c975b4 100644 --- a/hive/CHANGELOG.md +++ b/hive/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.18.0 +- Fixes an issue with IsolatedHive encryption - Automatically disables the unsafe isolate warning when running in a test ## 2.17.0 diff --git a/hive/lib/src/backend/js/native/backend_manager.dart b/hive/lib/src/backend/js/native/backend_manager.dart index 5d8f4c4c..2442f935 100644 --- a/hive/lib/src/backend/js/native/backend_manager.dart +++ b/hive/lib/src/backend/js/native/backend_manager.dart @@ -18,6 +18,7 @@ class BackendManager implements BackendManagerInterface { String? path, bool crashRecovery, HiveCipher? cipher, + int? keyCrc, String? collection, ) async { // compatibility for old store format diff --git a/hive/lib/src/backend/storage_backend.dart b/hive/lib/src/backend/storage_backend.dart index 79bb4330..211ec832 100644 --- a/hive/lib/src/backend/storage_backend.dart +++ b/hive/lib/src/backend/storage_backend.dart @@ -54,6 +54,7 @@ abstract class BackendManagerInterface { String? path, bool crashRecovery, HiveCipher? cipher, + int? keyCrc, String? collection, ); diff --git a/hive/lib/src/backend/storage_backend_memory.dart b/hive/lib/src/backend/storage_backend_memory.dart index 02dd667e..81256888 100644 --- a/hive/lib/src/backend/storage_backend_memory.dart +++ b/hive/lib/src/backend/storage_backend_memory.dart @@ -9,13 +9,14 @@ import 'package:hive_ce/src/box/keystore.dart'; /// In-memory Storage backend class StorageBackendMemory extends StorageBackend { final HiveCipher? _cipher; + final int? _keyCrc; final FrameHelper _frameHelper; Uint8List? _bytes; /// Not part of public API - StorageBackendMemory(Uint8List? bytes, this._cipher) + StorageBackendMemory(Uint8List? bytes, this._cipher, this._keyCrc) : _bytes = bytes, _frameHelper = FrameHelper(); @@ -37,6 +38,7 @@ class StorageBackendMemory extends StorageBackend { keystore, registry, _cipher, + _keyCrc, ); if (recoveryOffset != -1) { diff --git a/hive/lib/src/backend/stub/backend_manager.dart b/hive/lib/src/backend/stub/backend_manager.dart index dad11cc0..df385b75 100644 --- a/hive/lib/src/backend/stub/backend_manager.dart +++ b/hive/lib/src/backend/stub/backend_manager.dart @@ -17,6 +17,7 @@ class BackendManager implements BackendManagerInterface { String? path, bool crashRecovery, HiveCipher? cipher, + int? keyCrc, String? collection, ) { throw UnimplementedError(); diff --git a/hive/lib/src/backend/vm/backend_manager.dart b/hive/lib/src/backend/vm/backend_manager.dart index e376e59f..aeb4f28e 100644 --- a/hive/lib/src/backend/vm/backend_manager.dart +++ b/hive/lib/src/backend/vm/backend_manager.dart @@ -23,6 +23,7 @@ class BackendManager implements BackendManagerInterface { String? path, bool crashRecovery, HiveCipher? cipher, + int? keyCrc, String? collection, ) async { if (path == null) { @@ -45,7 +46,8 @@ class BackendManager implements BackendManagerInterface { final file = await findHiveFileAndCleanUp(name, path); final lockFile = File('$path$_delimiter$name.lock'); - final backend = StorageBackendVm(file, lockFile, crashRecovery, cipher); + final backend = + StorageBackendVm(file, lockFile, crashRecovery, cipher, keyCrc); await backend.open(); return backend; } diff --git a/hive/lib/src/backend/vm/storage_backend_vm.dart b/hive/lib/src/backend/vm/storage_backend_vm.dart index 8f1e8902..583bf150 100644 --- a/hive/lib/src/backend/vm/storage_backend_vm.dart +++ b/hive/lib/src/backend/vm/storage_backend_vm.dart @@ -18,25 +18,11 @@ import 'package:meta/meta.dart'; /// Storage backend for the Dart VM class StorageBackendVm extends StorageBackend { - /// Warning for existing lock of unmatched isolation - @visibleForTesting - static const unmatchedIsolationWarning = ''' -⚠️ WARNING: HIVE MULTI-ISOLATE RISK DETECTED ⚠️ - -You are opening this box with Hive, but this box was previously opened with -IsolatedHive. This can lead to DATA CORRUPTION as Hive boxes are not designed -for concurrent access across isolates. Each isolate would maintain its own box -cache, potentially causing data inconsistency and corruption. - -RECOMMENDED ACTIONS: -- ALWAYS use IsolatedHive to perform box operations when working with multiple - isolates -'''; - final File _file; final File _lockFile; final bool _crashRecovery; final HiveCipher? _cipher; + final int? _keyCrc; final FrameIoHelper _frameHelper; final ReadWriteSync _sync; @@ -73,6 +59,7 @@ RECOMMENDED ACTIONS: this._lockFile, this._crashRecovery, this._cipher, + this._keyCrc, ) : _frameHelper = FrameIoHelper(), _sync = ReadWriteSync(); @@ -82,6 +69,7 @@ RECOMMENDED ACTIONS: this._lockFile, this._crashRecovery, this._cipher, + this._keyCrc, this._frameHelper, this._sync, ); @@ -116,7 +104,7 @@ RECOMMENDED ACTIONS: props = LockProps(); } if (Logger.unmatchedIsolationWarning && props.isolated && !isolated) { - Logger.w(unmatchedIsolationWarning); + Logger.w(HiveWarning.unmatchedIsolation); } } @@ -132,10 +120,12 @@ RECOMMENDED ACTIONS: keystore, registry, _cipher, + _keyCrc, verbatim: isolated, ); } else { - recoveryOffset = await _frameHelper.keysFromFile(path, keystore, _cipher); + recoveryOffset = + await _frameHelper.keysFromFile(path, keystore, _cipher, _keyCrc); } if (recoveryOffset != -1) { @@ -158,8 +148,12 @@ RECOMMENDED ACTIONS: final bytes = await readRaf.read(frame.length!); final reader = BinaryReaderImpl(bytes, registry); - final readFrame = - reader.readFrame(cipher: _cipher, lazy: false, verbatim: verbatim); + final readFrame = reader.readFrame( + cipher: _cipher, + keyCrc: _keyCrc, + lazy: false, + verbatim: verbatim, + ); if (readFrame == null) { throw HiveError( @@ -177,8 +171,12 @@ RECOMMENDED ACTIONS: final writer = BinaryWriterImpl(registry); for (final frame in frames) { - frame.length = - writer.writeFrame(frame, cipher: _cipher, verbatim: verbatim); + frame.length = writer.writeFrame( + frame, + cipher: _cipher, + keyCrc: _keyCrc, + verbatim: verbatim, + ); } try { diff --git a/hive/lib/src/binary/binary_reader_impl.dart b/hive/lib/src/binary/binary_reader_impl.dart index ea15bd3a..19140a69 100644 --- a/hive/lib/src/binary/binary_reader_impl.dart +++ b/hive/lib/src/binary/binary_reader_impl.dart @@ -7,6 +7,7 @@ import 'package:hive_ce/src/crypto/crc32.dart'; import 'package:hive_ce/src/object/hive_list_impl.dart'; import 'package:hive_ce/src/registry/type_registry_impl.dart'; import 'package:hive_ce/src/util/extensions.dart'; +import 'package:hive_ce/src/util/logger.dart'; import 'package:meta/meta.dart'; /// Not part of public API @@ -19,6 +20,8 @@ class BinaryReaderImpl extends BinaryReader { int _bufferLimit; var _offset = 0; + var _crcRecomputeWarningPrinted = false; + /// Not part of public API BinaryReaderImpl(this._buffer, TypeRegistry typeRegistry, [int? bufferLength]) : _byteData = ByteData.view(_buffer.buffer, _buffer.offsetInBytes), @@ -259,6 +262,7 @@ class BinaryReaderImpl extends BinaryReader { /// Not part of public API Frame? readFrame({ HiveCipher? cipher, + int? keyCrc, bool lazy = false, int frameOffset = 0, bool verbatim = false, @@ -274,15 +278,35 @@ class BinaryReaderImpl extends BinaryReader { if (availableBytes < frameLength - 4) return null; final crc = _buffer.readUint32(_offset + frameLength - 8); + final crcOffset = _offset - 4; + final crcLength = frameLength - 4; final computedCrc = Crc32.compute( _buffer, - offset: _offset - 4, - length: frameLength - 4, - crc: cipher?.calculateKeyCrc() ?? 0, + offset: crcOffset, + length: crcLength, + crc: keyCrc ?? cipher?.calculateKeyCrc() ?? 0, ); // frame is corrupted or provided chiper is different - if (computedCrc != crc) return null; + if (computedCrc != crc) { + if (keyCrc != null) { + // Attempt to compute the crc without the key crc + // This maintains compatibility with data written by IsolatedHive before keyCrc was introduced + final computedCrc2 = Crc32.compute( + _buffer, + offset: crcOffset, + length: crcLength, + ); + if (computedCrc2 != crc) return null; + + if (Logger.crcRecomputeWarning && !_crcRecomputeWarningPrinted) { + Logger.w(HiveWarning.crcRecomputeNeeded); + _crcRecomputeWarningPrinted = true; + } + } else { + return null; + } + } _limitAvailableBytes(frameLength - 8); Frame frame; diff --git a/hive/lib/src/binary/binary_writer_impl.dart b/hive/lib/src/binary/binary_writer_impl.dart index cdebc275..c151e627 100644 --- a/hive/lib/src/binary/binary_writer_impl.dart +++ b/hive/lib/src/binary/binary_writer_impl.dart @@ -6,7 +6,6 @@ import 'package:hive_ce/src/binary/frame.dart'; import 'package:hive_ce/src/crypto/crc32.dart'; import 'package:hive_ce/src/object/hive_list_impl.dart'; import 'package:hive_ce/src/registry/type_registry_impl.dart'; -import 'package:hive_ce/src/util/debug_utils.dart'; import 'package:hive_ce/src/util/extensions.dart'; import 'package:hive_ce/src/util/logger.dart'; import 'package:meta/meta.dart'; @@ -18,12 +17,6 @@ class BinaryWriterImpl extends BinaryWriter { /// The maximum integer that can be stored in a 64 bit float (2^53) static const maxInt = 9007199254740992; - /// Warning message printed when attempting to store an integer that is too large - static const intWarning = - 'WARNING: Writing integer values greater than 2^53 will result in precision loss. ' - 'This is due to Hive storing all numbers as 64 bit floats. ' - 'Consider using a BigInt.'; - /// The type registry to use for writing values final TypeRegistryImpl typeRegistry; var _buffer = Uint8List(_initBufferSize); @@ -107,7 +100,9 @@ class BinaryWriterImpl extends BinaryWriter { @override void writeInt(int value) { // Web truncates values greater than 2^53 to 2^53 - if (kDebugMode && value >= maxInt) Logger.w(intWarning); + if (Logger.bigIntWarning && value >= maxInt) { + Logger.w(HiveWarning.bigInt); + } writeDouble(value.toDouble()); } @@ -265,7 +260,12 @@ class BinaryWriterImpl extends BinaryWriter { } /// Not part of public API - int writeFrame(Frame frame, {HiveCipher? cipher, bool verbatim = false}) { + int writeFrame( + Frame frame, { + HiveCipher? cipher, + int? keyCrc, + bool verbatim = false, + }) { final startOffset = _offset; _reserveBytes(4); _offset += 4; // reserve bytes for length @@ -289,7 +289,7 @@ class BinaryWriterImpl extends BinaryWriter { _buffer, offset: startOffset, length: frameLength - 4, - crc: cipher?.calculateKeyCrc() ?? 0, + crc: keyCrc ?? cipher?.calculateKeyCrc() ?? 0, ); writeUint32(crc); diff --git a/hive/lib/src/binary/frame_helper.dart b/hive/lib/src/binary/frame_helper.dart index 525333ae..2af7795c 100644 --- a/hive/lib/src/binary/frame_helper.dart +++ b/hive/lib/src/binary/frame_helper.dart @@ -11,7 +11,8 @@ class FrameHelper { Uint8List bytes, Keystore? keystore, TypeRegistry registry, - HiveCipher? cipher, { + HiveCipher? cipher, + int? keyCrc, { bool verbatim = false, }) { final reader = BinaryReaderImpl(bytes, registry); @@ -21,6 +22,7 @@ class FrameHelper { final frame = reader.readFrame( cipher: cipher, + keyCrc: keyCrc, lazy: false, frameOffset: frameOffset, verbatim: verbatim, diff --git a/hive/lib/src/hive_impl.dart b/hive/lib/src/hive_impl.dart index 5194396d..12b17586 100644 --- a/hive/lib/src/hive_impl.dart +++ b/hive/lib/src/hive_impl.dart @@ -23,21 +23,6 @@ import 'package:hive_ce/src/backend/storage_backend.dart'; /// Not part of public API class HiveImpl extends TypeRegistryImpl implements HiveInterface { - /// Warning message printed when accessing Hive from an unsafe isolate - @visibleForTesting - static final unsafeIsolateWarning = ''' -⚠️ WARNING: HIVE MULTI-ISOLATE RISK DETECTED ⚠️ - -Accessing Hive from an unsafe isolate (current isolate: "$isolateDebugName") -This can lead to DATA CORRUPTION as Hive boxes are not designed for concurrent -access across isolates. Each isolate would maintain its own box cache, -potentially causing data inconsistency and corruption. - -RECOMMENDED ACTIONS: -- Use IsolatedHive instead - -'''; - static final BackendManagerInterface _defaultBackendManager = BackendManager.select(); @@ -71,7 +56,7 @@ RECOMMENDED ACTIONS: !{'main', hiveIsolateName}.contains(isolateDebugName) && // Do not print this warning if this code is running in a test !isolateDebugName.startsWith('test_suite')) { - Logger.w(unsafeIsolateWarning); + Logger.w(HiveWarning.unsafeIsolate); } homePath = path; _managerOverride = BackendManager.select(backendPreference); @@ -81,6 +66,7 @@ RECOMMENDED ACTIONS: String name, bool lazy, HiveCipher? cipher, + int? keyCrc, KeyComparator comparator, CompactionStrategy compaction, bool recovery, @@ -119,13 +105,14 @@ RECOMMENDED ACTIONS: try { StorageBackend backend; if (bytes != null) { - backend = StorageBackendMemory(bytes, cipher); + backend = StorageBackendMemory(bytes, cipher, keyCrc); } else { backend = await _manager.open( name, path ?? homePath, recovery, cipher, + keyCrc, collection, ); } @@ -172,6 +159,7 @@ RECOMMENDED ACTIONS: Future> openBox( String name, { HiveCipher? encryptionCipher, + int? keyCrc, KeyComparator keyComparator = defaultKeyComparator, CompactionStrategy compactionStrategy = defaultCompactionStrategy, bool crashRecovery = true, @@ -187,6 +175,7 @@ RECOMMENDED ACTIONS: name, false, encryptionCipher, + keyCrc, keyComparator, compactionStrategy, crashRecovery, @@ -200,6 +189,7 @@ RECOMMENDED ACTIONS: Future> openLazyBox( String name, { HiveCipher? encryptionCipher, + int? keyCrc, KeyComparator keyComparator = defaultKeyComparator, CompactionStrategy compactionStrategy = defaultCompactionStrategy, bool crashRecovery = true, @@ -214,6 +204,7 @@ RECOMMENDED ACTIONS: name, true, encryptionCipher, + keyCrc, keyComparator, compactionStrategy, crashRecovery, diff --git a/hive/lib/src/io/frame_io_helper.dart b/hive/lib/src/io/frame_io_helper.dart index 7cb86215..7bda68af 100644 --- a/hive/lib/src/io/frame_io_helper.dart +++ b/hive/lib/src/io/frame_io_helper.dart @@ -28,11 +28,12 @@ class FrameIoHelper extends FrameHelper { String path, Keystore keystore, HiveCipher? cipher, + int? keyCrc, ) async { final raf = await openFile(path); final fileReader = BufferedFileReader(raf); try { - return await _KeyReader(fileReader).readKeys(keystore, cipher); + return await _KeyReader(fileReader).readKeys(keystore, cipher, keyCrc); } finally { await raf.close(); } @@ -43,7 +44,8 @@ class FrameIoHelper extends FrameHelper { String path, Keystore keystore, TypeRegistry registry, - HiveCipher? cipher, { + HiveCipher? cipher, + int? keyCrc, { bool verbatim = false, }) async { final bytes = await readFile(path); @@ -52,6 +54,7 @@ class FrameIoHelper extends FrameHelper { keystore, registry, cipher, + keyCrc, verbatim: verbatim, ); } @@ -64,7 +67,11 @@ class _KeyReader { _KeyReader(this.fileReader); - Future readKeys(Keystore keystore, HiveCipher? cipher) async { + Future readKeys( + Keystore keystore, + HiveCipher? cipher, + int? keyCrc, + ) async { await _load(4); while (true) { final frameOffset = fileReader.offset; @@ -86,6 +93,7 @@ class _KeyReader { final frame = _reader.readFrame( cipher: cipher, + keyCrc: keyCrc, lazy: true, frameOffset: frameOffset, ); diff --git a/hive/lib/src/isolate/handler/isolated_hive_handler.dart b/hive/lib/src/isolate/handler/isolated_hive_handler.dart index f1aa7b08..fc23d8dc 100644 --- a/hive/lib/src/isolate/handler/isolated_hive_handler.dart +++ b/hive/lib/src/isolate/handler/isolated_hive_handler.dart @@ -20,40 +20,52 @@ Future handleHiveMethodCall( Logger.level = LoggerLevel.values.byName(loggerLevel); case 'openBox': final name = call.arguments['name']; + final lazy = call.arguments['lazy']; + if (boxHandlers.containsKey(name)) { // Ensure this is a valid `openBox` call - Hive.box(name); + if (lazy) { + Hive.lazyBox(name); + } else { + Hive.box(name); + } return; } - final box = await Hive.openBox( - name, - keyComparator: call.arguments['keyComparator'] ?? defaultKeyComparator, - compactionStrategy: - call.arguments['compactionStrategy'] ?? defaultCompactionStrategy, - crashRecovery: call.arguments['crashRecovery'], - path: call.arguments['path'], - bytes: call.arguments['bytes'], - collection: call.arguments['collection'], - ); - boxHandlers[name] = IsolatedBoxHandler(box, connection); - case 'openLazyBox': - final name = call.arguments['name']; - if (boxHandlers.containsKey(name)) { - // Ensure this is a valid `openLazyBox` call - Hive.lazyBox(name); - return; + final keyCrc = call.arguments['keyCrc']; + final keyComparator = + call.arguments['keyComparator'] ?? defaultKeyComparator; + final compactionStrategy = + call.arguments['compactionStrategy'] ?? defaultCompactionStrategy; + final crashRecovery = call.arguments['crashRecovery']; + final path = call.arguments['path']; + final bytes = call.arguments['bytes']; + final collection = call.arguments['collection']; + + final BoxBase box; + if (lazy) { + box = await (Hive as HiveImpl).openLazyBox( + name, + keyCrc: keyCrc, + keyComparator: keyComparator, + compactionStrategy: compactionStrategy, + crashRecovery: crashRecovery, + path: path, + collection: collection, + ); + } else { + box = await (Hive as HiveImpl).openBox( + name, + keyCrc: keyCrc, + keyComparator: keyComparator, + compactionStrategy: compactionStrategy, + crashRecovery: crashRecovery, + path: path, + bytes: bytes, + collection: collection, + ); } - final box = await Hive.openLazyBox( - name, - keyComparator: call.arguments['keyComparator'] ?? defaultKeyComparator, - compactionStrategy: - call.arguments['compactionStrategy'] ?? defaultCompactionStrategy, - crashRecovery: call.arguments['crashRecovery'], - path: call.arguments['path'], - collection: call.arguments['collection'], - ); boxHandlers[name] = IsolatedBoxHandler(box, connection); case 'deleteBoxFromDisk': await Hive.deleteBoxFromDisk( diff --git a/hive/lib/src/isolate/isolated_hive_impl/hive_isolate.dart b/hive/lib/src/isolate/isolated_hive_impl/hive_isolate.dart index 21f70821..1e62b49c 100644 --- a/hive/lib/src/isolate/isolated_hive_impl/hive_isolate.dart +++ b/hive/lib/src/isolate/isolated_hive_impl/hive_isolate.dart @@ -7,21 +7,6 @@ import 'package:meta/meta.dart'; /// /// Used for testing abstract class HiveIsolate { - /// Warning message printed when using [IsolatedHive] without an [IsolateNameServer] - static final noIsolateNameServerWarning = ''' -⚠️ WARNING: HIVE MULTI-ISOLATE RISK DETECTED ⚠️ - -Using IsolatedHive without an IsolateNameServer is unsafe. This can lead to -DATA CORRUPTION as Hive boxes are not designed for concurrent access across -isolates. Using an IsolateNameServer allows IsolatedHive to maintain a single -isolate for all Hive operations. - -RECOMMENDED ACTIONS: -- Initialize IsolatedHive with IsolatedHive.initFlutter from hive_ce_flutter -- Provide your own IsolateNameServer - -'''; - /// Access to the isolate connection for testing @visibleForTesting IsolateConnection get connection; diff --git a/hive/lib/src/isolate/isolated_hive_impl/impl/isolated_hive_impl_vm.dart b/hive/lib/src/isolate/isolated_hive_impl/impl/isolated_hive_impl_vm.dart index adfab3f8..acb72c78 100644 --- a/hive/lib/src/isolate/isolated_hive_impl/impl/isolated_hive_impl_vm.dart +++ b/hive/lib/src/isolate/isolated_hive_impl/impl/isolated_hive_impl_vm.dart @@ -57,7 +57,7 @@ class IsolatedHiveImpl extends TypeRegistryImpl _isolateNameServer = isolateNameServer; if (Logger.noIsolateNameServerWarning && _isolateNameServer == null) { - Logger.w(HiveIsolate.noIsolateNameServerWarning); + Logger.w(HiveWarning.noIsolateNameServer); } final send = @@ -122,6 +122,8 @@ class IsolatedHiveImpl extends TypeRegistryImpl try { final params = { 'name': name, + 'lazy': lazy, + 'keyCrc': cipher?.calculateKeyCrc(), 'keyComparator': comparator, 'compactionStrategy': compaction, 'crashRecovery': recovery, @@ -130,26 +132,23 @@ class IsolatedHiveImpl extends TypeRegistryImpl 'collection': collection, }; - final IsolatedBoxBaseImpl newBox; - if (lazy) { - await _hiveChannel.invokeMethod('openLazyBox', params); - newBox = IsolatedLazyBoxImpl( - this, - name, - cipher, - connection, - _boxChannel, - ); - } else { - await _hiveChannel.invokeMethod('openBox', params); - newBox = IsolatedBoxImpl( - this, - name, - cipher, - connection, - _boxChannel, - ); - } + await _hiveChannel.invokeMethod('openBox', params); + + final newBox = lazy + ? IsolatedLazyBoxImpl( + this, + name, + cipher, + connection, + _boxChannel, + ) + : IsolatedBoxImpl( + this, + name, + cipher, + connection, + _boxChannel, + ); _boxes[name] = newBox; diff --git a/hive/lib/src/util/logger.dart b/hive/lib/src/util/logger.dart index fbe7bb98..c18419d1 100644 --- a/hive/lib/src/util/logger.dart +++ b/hive/lib/src/util/logger.dart @@ -1,3 +1,4 @@ +import 'package:hive_ce/src/isolate/isolate_debug_name/isolate_debug_name.dart'; import 'package:hive_ce/src/util/debug_utils.dart'; /// Configures the logging behavior of Hive @@ -5,6 +6,9 @@ abstract class Logger { /// The overall logging level static var level = kDebugMode ? LoggerLevel.debug : LoggerLevel.info; + /// If the big int warning is enabled + static var bigIntWarning = true; + /// If the unsafe isolate warning is enabled static var unsafeIsolateWarning = true; @@ -14,6 +18,9 @@ abstract class Logger { /// If the no isolate name server warning is enabled static var noIsolateNameServerWarning = true; + /// If the crc recompute warning is enabled + static var crcRecomputeWarning = true; + /// Log a verbose message static void v(Object? message) { if (level.index > LoggerLevel.verbose.index) return; @@ -65,3 +72,69 @@ enum LoggerLevel { /// Errors error; } + +/// Warning messages from Hive +abstract class HiveWarning { + const HiveWarning._(); + + /// Warning message printed when attempting to store an integer that is too large + static const bigInt = + 'WARNING: Writing integer values greater than 2^53 will result in precision loss.' + ' This is due to Hive storing all numbers as 64 bit floats.' + ' Consider using a BigInt.'; + + /// Warning message printed when accessing Hive from an unsafe isolate + static final unsafeIsolate = ''' +⚠️ WARNING: HIVE MULTI-ISOLATE RISK DETECTED ⚠️ + +Accessing Hive from an unsafe isolate (current isolate: "$isolateDebugName") +This can lead to DATA CORRUPTION as Hive boxes are not designed for concurrent +access across isolates. Each isolate would maintain its own box cache, +potentially causing data inconsistency and corruption. + +RECOMMENDED ACTIONS: +- Use IsolatedHive instead + +'''; + + /// Warning for existing lock of unmatched isolation + static const unmatchedIsolation = ''' +⚠️ WARNING: HIVE MULTI-ISOLATE RISK DETECTED ⚠️ + +You are opening this box with Hive, but this box was previously opened with +IsolatedHive. This can lead to DATA CORRUPTION as Hive boxes are not designed +for concurrent access across isolates. Each isolate would maintain its own box +cache, potentially causing data inconsistency and corruption. + +RECOMMENDED ACTIONS: +- ALWAYS use IsolatedHive to perform box operations when working with multiple + isolates +'''; + + /// Warning message printed when using [IsolatedHive] without an [IsolateNameServer] + static final noIsolateNameServer = ''' +⚠️ WARNING: HIVE MULTI-ISOLATE RISK DETECTED ⚠️ + +Using IsolatedHive without an IsolateNameServer is unsafe. This can lead to +DATA CORRUPTION as Hive boxes are not designed for concurrent access across +isolates. Using an IsolateNameServer allows IsolatedHive to maintain a single +isolate for all Hive operations. + +RECOMMENDED ACTIONS: +- Initialize IsolatedHive with IsolatedHive.initFlutter from hive_ce_flutter +- Provide your own IsolateNameServer + +'''; + + /// Warning message printed when CRC recompute is needed + static const crcRecomputeNeeded = 'WARNING: CRC recompute needed for frame.' + ' This happens when IsolatedHive was used with encryption before it was properly handled.' + ' IsolatedHive will continue to work, but read performance may be degraded for old entries.' + ' To restore performance, rewrite all box entries.' + ' This only needs to be done once.' + '\n\nEXAMPLE:\n' + ''' +for (final key in await box.keys) { + await box.put(key, await box.get(key)); +}'''; +} diff --git a/hive/pubspec.yaml b/hive/pubspec.yaml index 08d4c5b4..c50e6fd5 100644 --- a/hive/pubspec.yaml +++ b/hive/pubspec.yaml @@ -1,6 +1,6 @@ name: hive_ce description: Hive Community Edition - A spiritual continuation of Hive v2 -version: 2.17.0 +version: 2.18.0 homepage: https://github.com/IO-Design-Team/hive_ce/tree/main/hive topics: diff --git a/hive/test/integration/isolate_test.dart b/hive/test/integration/isolate_test.dart index 68f8d09d..2531d81c 100644 --- a/hive/test/integration/isolate_test.dart +++ b/hive/test/integration/isolate_test.dart @@ -5,11 +5,11 @@ import 'dart:async'; import 'dart:isolate'; import 'package:hive_ce/hive_ce.dart'; -import 'package:hive_ce/src/backend/vm/storage_backend_vm.dart'; import 'package:hive_ce/src/hive_impl.dart'; import 'package:hive_ce/src/isolate/handler/isolate_entry_point.dart'; import 'package:hive_ce/src/isolate/isolated_hive_impl/hive_isolate.dart'; import 'package:hive_ce/src/isolate/isolated_hive_impl/isolated_hive_impl.dart'; +import 'package:hive_ce/src/util/logger.dart'; import 'package:isolate_channel/isolate_channel.dart'; import 'package:test/test.dart'; @@ -122,7 +122,7 @@ void main() { group('warnings', () { test('unsafe isolate', () async { final patchedWarning = - HiveImpl.unsafeIsolateWarning.replaceFirst(isolateNameRegex, ''); + HiveWarning.unsafeIsolate.replaceFirst(isolateNameRegex, ''); final safeOutput = await Isolate.run( debugName: 'main', @@ -172,7 +172,7 @@ void main() { await captureOutput(() => IsolatedHiveImpl().init(null)).toList(); expect( unsafeOutput, - contains(HiveIsolate.noIsolateNameServerWarning), + contains(HiveWarning.noIsolateNameServer), ); final safeOutput = await captureOutput( @@ -200,7 +200,7 @@ void main() { expect( output, - contains(StorageBackendVm.unmatchedIsolationWarning), + contains(HiveWarning.unmatchedIsolation), ); await IsolatedHive.openBox('box2'); @@ -211,12 +211,12 @@ void main() { expect( ignoredOutput, - isNot(contains(StorageBackendVm.unmatchedIsolationWarning)), + isNot(contains(HiveWarning.unmatchedIsolation)), ); }); }); - test('IsolatedHive data compatable with Hive', () async { + test('IsolatedHive data compatible with Hive', () async { final dir = await getTempDir(); final isolatedHive = IsolatedHiveImpl(); @@ -234,9 +234,149 @@ void main() { final box = await hive.openBox('test'); expect(await box.get('key'), 'value'); }); + + test('Hive data compatible with IsolatedHive', () async { + final dir = await getTempDir(); + + final hive = HiveImpl(); + addTearDown(hive.close); + hive.init(dir.path); + + final box = await hive.openBox('test'); + await box.put('key', 'value'); + await box.close(); + + final isolatedHive = IsolatedHiveImpl(); + addTearDown(isolatedHive.close); + await isolatedHive.init(dir.path, isolateNameServer: StubIns()); + + final isolatedBox = await isolatedHive.openBox('test'); + expect(await isolatedBox.get('key'), 'value'); + }); + + test('Encrypted IsolatedHive data compatible with Hive', () async { + final dir = await getTempDir(); + final cipher = HiveAesCipher(Hive.generateSecureKey()); + + final isolatedHive = IsolatedHiveImpl(); + addTearDown(isolatedHive.close); + await isolatedHive.init(dir.path, isolateNameServer: StubIns()); + + final isolatedBox = + await isolatedHive.openBox('test', encryptionCipher: cipher); + await isolatedBox.put('key', 'value'); + await isolatedBox.close(); + + final hive = HiveImpl(); + addTearDown(hive.close); + hive.init(dir.path); + + final box = await hive.openBox('test', encryptionCipher: cipher); + expect(await box.get('key'), 'value'); + }); + + test('Encrypted Hive data compatible with IsolatedHive', () async { + final dir = await getTempDir(); + final cipher = HiveAesCipher(Hive.generateSecureKey()); + + final hive = HiveImpl(); + addTearDown(hive.close); + hive.init(dir.path); + + final box = await hive.openBox('test', encryptionCipher: cipher); + await box.put('key', 'value'); + await box.close(); + + final isolatedHive = IsolatedHiveImpl(); + addTearDown(isolatedHive.close); + await isolatedHive.init(dir.path, isolateNameServer: StubIns()); + + final isolatedBox = + await isolatedHive.openBox('test', encryptionCipher: cipher); + expect(await isolatedBox.get('key'), 'value'); + }); + + test( + 'IsolatedHive data encrypted with no keyCrc is readable', + () async { + final dir = await getTempDir(); + final key = Hive.generateSecureKey(); + + final isolatedHive = IsolatedHiveImpl(); + addTearDown(isolatedHive.close); + await isolatedHive.init(dir.path, isolateNameServer: StubIns()); + + final box = await isolatedHive.openBox( + 'test', + encryptionCipher: ZeroKeyCrcCipher(key), + ); + await box.put('key', 'value'); + await box.close(); + + final box2 = await isolatedHive.openBox( + 'test', + encryptionCipher: HiveAesCipher(key), + ); + expect(await box2.get('key'), 'value'); + }, + ); + + test('Encrypted IsolatedBox data compatible with IsolatedLazyBox', + () async { + final dir = await getTempDir(); + final key = Hive.generateSecureKey(); + + final isolatedHive = IsolatedHiveImpl(); + addTearDown(isolatedHive.close); + await isolatedHive.init(dir.path, isolateNameServer: StubIns()); + + final box = await isolatedHive.openBox( + 'test', + encryptionCipher: HiveAesCipher(key), + ); + await box.put('key', 'value'); + await box.close(); + + final lazyBox = await isolatedHive.openLazyBox( + 'test', + encryptionCipher: HiveAesCipher(key), + ); + expect(await lazyBox.get('key'), 'value'); + }); + + test('Encrypted IsolatedLazyBox data compatible with IsolatedBox', + () async { + final dir = await getTempDir(); + final key = Hive.generateSecureKey(); + + final isolatedHive = IsolatedHiveImpl(); + addTearDown(isolatedHive.close); + await isolatedHive.init(dir.path, isolateNameServer: StubIns()); + + final lazyBox = await isolatedHive.openLazyBox( + 'test', + encryptionCipher: HiveAesCipher(key), + ); + await lazyBox.put('key', 'value'); + await lazyBox.close(); + + final box = await isolatedHive.openBox( + 'test', + encryptionCipher: HiveAesCipher(key), + ); + expect(await box.get('key'), 'value'); + }); }, onPlatform: { 'chrome': Skip('Isolates are not supported on web'), }, ); } + +/// Test cipher that always returns a zero key CRC +class ZeroKeyCrcCipher extends HiveAesCipher { + ZeroKeyCrcCipher(super.key); + + @override + int calculateKeyCrc() => 0; +} diff --git a/hive/test/tests/backend/storage_backend_memory_test.dart b/hive/test/tests/backend/storage_backend_memory_test.dart index 57f3943b..83641587 100644 --- a/hive/test/tests/backend/storage_backend_memory_test.dart +++ b/hive/test/tests/backend/storage_backend_memory_test.dart @@ -10,19 +10,19 @@ import '../common.dart'; void main() { group('StorageBackendMemory', () { test('.path is null', () { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); expect(backend.path, null); }); test('.supportsCompaction is false', () { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); expect(backend.supportsCompaction, false); }); group('.initialize()', () { test('throws if frames cannot be decoded', () { final bytes = Uint8List.fromList([1, 2, 3, 4]); - final backend = StorageBackendMemory(bytes, null); + final backend = StorageBackendMemory(bytes, null, null); expect( () => backend.initialize(TypeRegistryImpl.nullImpl, null, false), throwsHiveError(['Wrong checksum']), @@ -31,7 +31,7 @@ void main() { }); test('.readValue() throws UnsupportedError', () { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); expect( () => backend.readValue(Frame('key', 'val')), throwsUnsupportedError, @@ -39,27 +39,27 @@ void main() { }); test('.writeFrames() does nothing', () async { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); await backend.writeFrames([Frame('key', 'val')]); }); test('.compact() throws UnsupportedError', () { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); expect(() => backend.compact([]), throwsUnsupportedError); }); test('.clear() does nothing', () async { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); await backend.clear(); }); test('.close() does nothing', () async { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); await backend.close(); }); test('.deleteFromDisk() throws UnsupportedError', () { - final backend = StorageBackendMemory(null, null); + final backend = StorageBackendMemory(null, null, null); expect(backend.deleteFromDisk, throwsUnsupportedError); }); }); diff --git a/hive/test/tests/backend/vm/storage_backend_vm_test.dart b/hive/test/tests/backend/vm/storage_backend_vm_test.dart index bd0786b0..66cf7b1b 100644 --- a/hive/test/tests/backend/vm/storage_backend_vm_test.dart +++ b/hive/test/tests/backend/vm/storage_backend_vm_test.dart @@ -40,6 +40,7 @@ StorageBackendVm _getBackend({ File? lockFile, bool crashRecovery = false, HiveCipher? cipher, + int? keyCrc, FrameIoHelper? ioHelper, ReadWriteSync? sync, RandomAccessFile? readRaf, @@ -50,6 +51,7 @@ StorageBackendVm _getBackend({ lockFile ?? MockFile(), crashRecovery, cipher, + keyCrc, ioHelper ?? MockFrameIoHelper(), sync ?? ReadWriteSync(), ); @@ -129,6 +131,7 @@ void main() { any(), any(), any(), + any(), ), ).thenAnswer((i) => Future.value(recoveryOffset)); when( @@ -136,6 +139,7 @@ void main() { any(), any(), any(), + any(), ), ).thenAnswer((i) => Future.value(recoveryOffset)); return helper; diff --git a/hive/test/tests/binary/binary_writer_test.dart b/hive/test/tests/binary/binary_writer_test.dart index 33ed6ade..25cdacb8 100644 --- a/hive/test/tests/binary/binary_writer_test.dart +++ b/hive/test/tests/binary/binary_writer_test.dart @@ -6,6 +6,7 @@ import 'package:hive_ce/src/binary/binary_writer_impl.dart'; import 'package:hive_ce/src/binary/frame.dart'; import 'package:hive_ce/src/object/hive_object.dart'; import 'package:hive_ce/src/registry/type_registry_impl.dart'; +import 'package:hive_ce/src/util/logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -160,7 +161,7 @@ void main() { final output2 = await captureOutput(() => bw.writeInt(BinaryWriterImpl.maxInt)) .toList(); - expect(output2, contains(BinaryWriterImpl.intWarning)); + expect(output2, contains(HiveWarning.bigInt)); bw = getWriter(); bw.writeInt(BinaryWriterImpl.maxInt + 1); diff --git a/hive/test/tests/io/frame_io_helper_test.dart b/hive/test/tests/io/frame_io_helper_test.dart index 60b644e3..16e74f4a 100644 --- a/hive/test/tests/io/frame_io_helper_test.dart +++ b/hive/test/tests/io/frame_io_helper_test.dart @@ -44,7 +44,7 @@ void main() { final keystore = Keystore.debug(); final ioHelper = _FrameIoHelperTest(_getBytes(frameBytes)); final recoveryOffset = - await ioHelper.keysFromFile('null', keystore, null); + await ioHelper.keysFromFile('null', keystore, null, null); expect(recoveryOffset, -1); final testKeystore = Keystore.debug( @@ -58,7 +58,7 @@ void main() { final keystore = Keystore.debug(); final ioHelper = _FrameIoHelperTest(_getBytes(frameBytesEncrypted)); final recoveryOffset = - await ioHelper.keysFromFile('null', keystore, testCipher); + await ioHelper.keysFromFile('null', keystore, testCipher, null); expect(recoveryOffset, -1); final testKeystore = Keystore.debug( @@ -77,8 +77,13 @@ void main() { test('frame', () async { final keystore = Keystore.debug(); final ioHelper = _FrameIoHelperTest(_getBytes(frameBytes)); - final recoveryOffset = - await ioHelper.framesFromFile('null', keystore, testRegistry, null); + final recoveryOffset = await ioHelper.framesFromFile( + 'null', + keystore, + testRegistry, + null, + null, + ); expect(recoveryOffset, -1); final testKeystore = Keystore.debug( @@ -96,6 +101,7 @@ void main() { keystore, testRegistry, testCipher, + null, ); expect(recoveryOffset, -1);