diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 217d860..a344e2f 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -11,7 +11,7 @@ on: - master jobs: - build-test: + build-test-core: strategy: fail-fast: false matrix: @@ -36,7 +36,7 @@ jobs: conan_preset: msvc-20-release os: windows-latest - name: ${{ matrix.build_profile }} + name: core-${{ matrix.build_profile }} runs-on: ${{matrix.os}} @@ -90,14 +90,79 @@ jobs: - name: Build run: | cmake --build --preset conan-${{ matrix.conan_preset }} - cmake -E make_directory ${{runner.workspace}}/install/SEDManager - cmake --install ${{github.workspace}}/build/${{ matrix.conan_preset }} --prefix '${{runner.workspace}}/install/SEDManager' + cmake -E make_directory ${{github.workspace}}/install/SEDManager + cmake --install ${{github.workspace}}/build/${{ matrix.conan_preset }} --prefix '${{github.workspace}}/install/SEDManager' - name: Test run: ${{github.workspace}}/build/${{ matrix.conan_preset }}/bin/Test - name: Upload artifact binary - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: SEDManager-${{ matrix.build_profile }} - path: '${{runner.workspace}}/install/SEDManager' + path: '${{github.workspace}}/install/SEDManager' + if-no-files-found: error + + build-test-flutter: + strategy: + fail-fast: false + matrix: + flutter_target: [windows, linux] + include: + - flutter_target: linux + os: ubuntu-latest + binary_path: build/linux/x64/release/bundle + core_profile: clang20d + - flutter_target: windows + os: windows-latest + binary_path: build/windows/x64/runner/Release + core_profile: msvc20d + + name: flutter-${{ matrix.flutter_target }} + needs: [build-test-core] + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.16.9' + channel: 'stable' + + - name: Install native compilers + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo add-apt-repository ppa:ubuntu-toolchain-r/test + sudo apt update + + sudo apt install ninja-build + sudo apt install gcc-13 g++-13 + sudo apt install libgtk-3-dev + sudo update-alternatives --remove-all gcc || true + sudo update-alternatives --remove-all g++ || true + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 10 --slave /usr/bin/g++ g++ /usr/bin/g++-13 + + - name: "Download core" + uses: actions/download-artifact@v4 + with: + name: SEDManager-${{ matrix.core_profile }} + path: ${{github.workspace}}/core + + - name: "Install GUI Dart dependencies" + working-directory: ${{github.workspace}}/src/SEDManagerGUI + run: flutter pub get + + - name: "Test GUI" + working-directory: ${{github.workspace}}/src/SEDManagerGUI + run: flutter test --dart-define=CAPI_LIBRARY_PATH=${{github.workspace}}/core + + - name: "Build GUI" + working-directory: ${{github.workspace}}/src/SEDManagerGUI + run: flutter build ${{matrix.flutter_target}} --release + + - name: "Upload artifact binary" + uses: actions/upload-artifact@v4 + with: + name: SEDManagerGUI-${{matrix.flutter_target}} + path: "${{github.workspace}}/src/SEDManagerGUI/${{matrix.binary_path}}" + if-no-files-found: error diff --git a/.github/workflows/clang_format.yml b/.github/workflows/clang_format.yml index 17776e7..6a927a6 100644 --- a/.github/workflows/clang_format.yml +++ b/.github/workflows/clang_format.yml @@ -18,5 +18,5 @@ jobs: directory: ${{ runner.temp }}/llvm - name: Verify formatting run: | - python ./support/run-clang-format.py -r src + python ./support/run-clang-format.py -r src --exclude "**/SEDManagerGUI/**" python ./support/run-clang-format.py -r test \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3bbdd5..c866acd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,27 +20,50 @@ jobs: with: ref: ${{ github.event.workflow_run.head_branch }} - - name: Download artifacts + - name: Download Windows core + uses: actions/download-artifact@v4 + with: + name: SEDManager-msvc20r + path: ${{github.workspace}}/SEDManager_Windows_x86_64 + github-token: ${{github.token}} + run-id: ${{ github.event.workflow_run.id }} + + - name: Download Windows flutter + uses: actions/download-artifact@v4 + with: + name: SEDManagerGUI-windows + path: ${{github.workspace}}/SEDManager_Windows_x86_64 + github-token: ${{github.token}} + run-id: ${{ github.event.workflow_run.id }} + + - name: Download Linux core + uses: actions/download-artifact@v4 + with: + name: SEDManager-clang20r + path: ${{github.workspace}}/SEDManager_Linux_x86_64 + github-token: ${{github.token}} + run-id: ${{ github.event.workflow_run.id }} + + - name: Download Linux flutter + uses: actions/download-artifact@v4 + with: + name: SEDManagerGUI-linux + path: ${{github.workspace}}/SEDManager_Linux_x86_64 + github-token: ${{github.token}} + run-id: ${{ github.event.workflow_run.id }} + + - name: Zip artifacts shell: bash + working-directory: ${{github.workspace}} run: | - mkdir -p ./Binaries/Windows_x86_64 - mkdir -p ./Binaries/Linux_x86_64 - gh run download ${{ github.event.workflow_run.id }} --dir ./Binaries/Windows_x86_64 -p *windows*cl*Release*20 - gh run download ${{ github.event.workflow_run.id }} --dir ./Binaries/Linux_x86_64 -p *ubuntu*clang*Release*20 - mv ./Binaries/Windows_x86_64/SEDManager* ./Binaries/Windows_x86_64/SEDManager - mv ./Binaries/Linux_x86_64/SEDManager* ./Binaries/Linux_x86_64/SEDManager - cd ./Binaries/Windows_x86_64 - zip -r SEDManager_Windows_x86_64.zip SEDManager - cd ../.. - cd ./Binaries/Linux_x86_64 - zip -r SEDManager_Linux_x86_64.zip SEDManager - cd ../.. + zip -r SEDManager_Windows_x86_64.zip SEDManager_Windows_x86_64 + zip -r SEDManager_Linux_x86_64.zip SEDManager_Linux_x86_64 - name: Create release shell: bash run: | gh release create ${{ github.event.workflow_run.head_branch }} - gh release upload ${{ github.event.workflow_run.head_branch }} ./Binaries/Windows_x86_64/SEDManager_Windows_x86_64.zip#SEDManager_Windows_x86_64 - gh release upload ${{ github.event.workflow_run.head_branch }} ./Binaries/Linux_x86_64/SEDManager_Linux_x86_64.zip#SEDManager_Linux_x86_64 + gh release upload ${{ github.event.workflow_run.head_branch }} ${{github.workspace}}/SEDManager_Windows_x86_64.zip#SEDManager_Windows_x86_64 + gh release upload ${{ github.event.workflow_run.head_branch }} ${{github.workspace}}/SEDManager_Linux_x86_64.zip#SEDManager_Linux_x86_64 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 269dcca..79b966b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,9 +33,4 @@ add_subdirectory(test) install( TARGETS SEDManagerCLI SEDManagerCAPI DESTINATION "." - RUNTIME_DEPENDENCIES - PRE_EXCLUDE_REGEXES "ext-ms-.*" "api-ms-.*" "hvsifiletrust.dll" "pdmutilities.dll" - DIRECTORIES ${EXTRA_RUNTIME_DEPENDENCY_DIRS} - POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" - DESTINATION "." ) \ No newline at end of file diff --git a/src/SEDManagerGUI/.gitignore b/src/SEDManagerGUI/.gitignore new file mode 100644 index 0000000..d43aade --- /dev/null +++ b/src/SEDManagerGUI/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ +*.lock + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/src/SEDManagerGUI/analysis_options.yaml b/src/SEDManagerGUI/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/src/SEDManagerGUI/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/src/SEDManagerGUI/fonts/CascadiaMono.ttf b/src/SEDManagerGUI/fonts/CascadiaMono.ttf new file mode 100644 index 0000000..d15637e Binary files /dev/null and b/src/SEDManagerGUI/fonts/CascadiaMono.ttf differ diff --git a/src/SEDManagerGUI/fonts/CascadiaMonoItalic.ttf b/src/SEDManagerGUI/fonts/CascadiaMonoItalic.ttf new file mode 100644 index 0000000..2fba921 Binary files /dev/null and b/src/SEDManagerGUI/fonts/CascadiaMonoItalic.ttf differ diff --git a/src/SEDManagerGUI/lib/bindings/encrypted_device.dart b/src/SEDManagerGUI/lib/bindings/encrypted_device.dart new file mode 100644 index 0000000..3810033 --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/encrypted_device.dart @@ -0,0 +1,176 @@ +import 'dart:core'; +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; + +import 'future.dart'; +import 'storage_device.dart'; +import 'string.dart'; +import 'value.dart'; +import 'type.dart'; +import 'sedmanager_capi.dart'; + +typedef UID = int; + +class Session implements Finalizable { + Session(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + final Pointer _handle; + static final _finalizer = NativeFinalizer(_capi.sessionDestroyAddress.cast()); + + UID get securityProvider { + return _capi.sessionGetSecurityProvider(_handle); + } + + Future end() { + final futurePtr = _capi.sessionEnd(_handle); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } + + Stream getTableRows(UID tableUid) { + final streamPtr = _capi.sessionGetTableRows(_handle, tableUid); + final streamWrapper = StreamWrapperUID(streamPtr); + return streamWrapper.toDartStream(); + } + + int getColumnCount(UID table) { + return _capi.sessionGetColumnCount(_handle, table); + } + + String getColumnName(UID table, int column) { + return StringWrapper(_capi.sessionGetColumnName(_handle, table, column)).toDartString(); + } + + Type getColumnType(UID table, int column) { + return Type(_capi.sessionGetColumnType(_handle, table, column)); + } + + Future getValue(UID objectUid, int column) { + final futurePtr = _capi.sessionGetValue(_handle, objectUid, column); + final futureWrapper = FutureWrapperValue(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future setValue(UID objectUid, int column, Value value) { + final futurePtr = _capi.sessionSetValue(_handle, objectUid, column, value.handle()); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future authenticate(UID authority, String? password) { + final bytes = password?.toNativeUtf8(); + final futurePtr = _capi.sessionAuthenticate( + _handle, + authority, + bytes?.cast() ?? nullptr, + bytes?.length ?? 0, + ); + if (bytes != null) { + malloc.free(bytes); + } + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future authenticateBytes(UID authority, ByteData? password) { + if (password != null) { + final length = password.lengthInBytes; + final ptr = malloc.allocate(length); + final bytes = ptr.asTypedList(length); + for (int i = 0; i < length; ++i) { + bytes[i] = password.buffer.asUint8List()[i]; + } + final futurePtr = _capi.sessionAuthenticate(_handle, authority, ptr, length); + malloc.free(ptr); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } else { + final futurePtr = _capi.sessionAuthenticate(_handle, authority, nullptr, 0); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } + } + + Future genMEK(UID lockingRange) { + final futurePtr = _capi.sessionGenMEK(_handle, lockingRange); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future activate(UID securityProvider) { + final futurePtr = _capi.sessionActivate(_handle, securityProvider); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future revert(UID securityProvider) { + final futurePtr = _capi.sessionRevert(_handle, securityProvider); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } +} + +class EncryptedDevice implements Finalizable { + EncryptedDevice(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static Future create(StorageDevice storageDevice) { + final futurePtr = _capi.encryptedDeviceCreate(storageDevice.handle()); + final futureWrapper = FutureWrapperEncryptedDevice(futurePtr); + return futureWrapper.toDartFuture(); + } + + static final _capi = SEDManagerCAPI(); + final Pointer _handle; + static final _finalizer = NativeFinalizer(_capi.encryptedDeviceDestroyAddress.cast()); + + Future login(UID securityProvider) { + final futurePtr = _capi.encryptedDeviceLogin(_handle, securityProvider); + final futureWrapper = FutureWrapperSession(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future findName(UID uid, {UID? securityProvider}) { + final futurePtr = _capi.encryptedDeviceFindName(_handle, uid, securityProvider ?? 0); + final futureWrapper = FutureWrapperString(futurePtr); + return futureWrapper.toDartFuture(); + } + + Future findUid(String name, {UID? securityProvider}) { + final nameWrapper = StringWrapper.fromString(name); + final futurePtr = _capi.encryptedDeviceFindUID(_handle, nameWrapper.handle(), securityProvider ?? 0); + final futureWrapper = FutureWrapperUID(futurePtr); + return futureWrapper.toDartFuture(); + } + + String renderValue(Value value, Type type, UID securityProvider) { + final wrapper = StringWrapper(_capi.encryptedDeviceRenderValue( + _handle, + value.handle(), + type.handle(), + securityProvider, + )); + return wrapper.toDartString(); + } + + Value parseValue(String str, Type type, UID securityProvider) { + final wrapper = StringWrapper.fromString(str); + return Value(_capi.encryptedDeviceParseValue( + _handle, + wrapper.handle(), + type.handle(), + securityProvider, + )); + } + + Future stackReset() { + final futurePtr = _capi.encryptedDeviceStackReset(_handle); + final futureWrapper = FutureWrapperVoid(futurePtr); + return futureWrapper.toDartFuture(); + } +} diff --git a/src/SEDManagerGUI/lib/bindings/errors.dart b/src/SEDManagerGUI/lib/bindings/errors.dart new file mode 100644 index 0000000..37347ff --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/errors.dart @@ -0,0 +1,20 @@ +import 'dart:core'; +import 'string.dart'; +import 'sedmanager_capi.dart'; + +String getLastErrorMessage() { + final capi = SEDManagerCAPI(); + final message = StringWrapper(capi.getLastExceptionMessage()); + return message.toDartString(); +} + +class SEDException implements Exception { + SEDException(this.message); + + final String message; + + @override + String toString() { + return message; + } +} diff --git a/src/SEDManagerGUI/lib/bindings/future.dart b/src/SEDManagerGUI/lib/bindings/future.dart new file mode 100644 index 0000000..7005fdc --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/future.dart @@ -0,0 +1,310 @@ +import "dart:async"; +import "encrypted_device.dart"; +import "string.dart"; +import "errors.dart"; +import "value.dart"; +import "sedmanager_capi.dart"; +import "dart:ffi"; + +class FutureWrapperVoid implements Finalizable { + FutureWrapperVoid(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "Void"; + static final _destroyAddress = _capi.lookupFutureDestroy(_suffix); + static final _startFunc = _capi.lookupFutureStart>(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Future toDartFuture() { + assert(_handle != nullptr); + final completer = Completer(); + + late final NativeCallable)> callable; + void callback(bool success, Pointer result) { + if (!success) { + completer.completeError(SEDException(getLastErrorMessage())); + } else { + completer.complete(); + } + callable.close(); + } + + callable = NativeCallable)>.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + _handle = nullptr; + return completer.future; + } +} + +class FutureWrapperString implements Finalizable { + FutureWrapperString(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "String"; + static final _destroyAddress = _capi.lookupFutureDestroy(_suffix); + static final _startFunc = _capi.lookupFutureStart>(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Future toDartFuture() { + assert(_handle != nullptr); + final completer = Completer(); + + late final NativeCallable)> callable; + void callback(bool success, Pointer result) { + if (!success) { + completer.completeError(SEDException(getLastErrorMessage())); + } else { + completer.complete(StringWrapper(result).toDartString()); + } + callable.close(); + } + + callable = NativeCallable)>.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + _handle = nullptr; + return completer.future; + } +} + +class FutureWrapperEncryptedDevice implements Finalizable { + FutureWrapperEncryptedDevice(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "EncryptedDevice"; + static final _destroyAddress = _capi.lookupFutureDestroy(_suffix); + static final _startFunc = _capi.lookupFutureStart>(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Future toDartFuture() { + assert(_handle != nullptr); + final completer = Completer(); + + late final NativeCallable)> callable; + void callback(bool success, Pointer result) { + if (!success) { + completer.completeError(SEDException(getLastErrorMessage())); + } else { + completer.complete(EncryptedDevice(result)); + } + callable.close(); + } + + callable = NativeCallable)>.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + _handle = nullptr; + return completer.future; + } +} + +class FutureWrapperSession implements Finalizable { + FutureWrapperSession(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "Session"; + static final _destroyAddress = _capi.lookupFutureDestroy(_suffix); + static final _startFunc = _capi.lookupFutureStart>(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Future toDartFuture() { + assert(_handle != nullptr); + final completer = Completer(); + + late final NativeCallable)> callable; + void callback(bool success, Pointer result) { + if (!success) { + completer.completeError(SEDException(getLastErrorMessage())); + } else { + completer.complete(Session(result)); + } + callable.close(); + } + + callable = NativeCallable)>.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + _handle = nullptr; + return completer.future; + } +} + +class FutureWrapperUID implements Finalizable { + FutureWrapperUID(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "UID"; + static final _destroyAddress = _capi.lookupFutureDestroy(_suffix); + static final _startFunc = _capi.lookupFutureStart(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Future toDartFuture() { + assert(_handle != nullptr); + final completer = Completer(); + + late final NativeCallable callable; + void callback(bool success, UID result) { + if (!success) { + completer.completeError(SEDException(getLastErrorMessage())); + } else { + completer.complete(result); + } + callable.close(); + } + + callable = NativeCallable.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + _handle = nullptr; + return completer.future; + } +} + +class FutureWrapperValue implements Finalizable { + FutureWrapperValue(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "Value"; + static final _destroyAddress = _capi.lookupFutureDestroy(_suffix); + static final _startFunc = _capi.lookupFutureStart>(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Future toDartFuture() { + assert(_handle != nullptr); + final completer = Completer(); + + late final NativeCallable)> callable; + void callback(bool success, Pointer result) { + if (!success) { + completer.completeError(SEDException(getLastErrorMessage())); + } else { + completer.complete(Value(result)); + } + callable.close(); + } + + callable = NativeCallable)>.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + _handle = nullptr; + return completer.future; + } +} + +class StreamWrapperUID implements Finalizable { + StreamWrapperUID(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "UID"; + static final _destroyAddress = _capi.lookupStreamDestroy(_suffix); + static final _startFunc = _capi.lookupStreamAdvance(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Stream toDartStream() { + assert(_handle != nullptr); + + return () async* { + UID? value; + while (true) { + final completer = Completer(); + + late final NativeCallable callable; + void callback(bool valid, bool success, UID result) { + if (valid) { + if (success) { + completer.complete(result); + } else { + final message = getLastErrorMessage(); + completer.completeError(SEDException(message)); + } + } else { + completer.complete(null); + callable.close(); + } + } + + callable = NativeCallable.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + value = await completer.future; + + if (value != null) { + yield value; + } else { + break; + } + } + }(); + } +} + +class StreamWrapperString implements Finalizable { + StreamWrapperString(this._handle) { + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + static const _suffix = "String"; + static final _destroyAddress = _capi.lookupStreamDestroy>(_suffix); + static final _startFunc = _capi.lookupStreamAdvance>(_suffix); + static final _finalizer = NativeFinalizer(_destroyAddress.cast()); + Pointer> _handle; + + Stream toDartStream() { + assert(_handle != nullptr); + + return () async* { + String? value; + while (true) { + final completer = Completer(); + + late final NativeCallable)> callable; + void callback(bool valid, bool success, Pointer result) { + if (valid) { + if (success) { + completer.complete(StringWrapper(result).toDartString()); + } else { + completer.completeError(SEDException(getLastErrorMessage())); + } + } else { + completer.complete(null); + callable.close(); + } + } + + callable = NativeCallable)>.listener(callback); + + _startFunc(_handle, callable.nativeFunction); + value = await completer.future; + + if (value != null) { + yield value; + } else { + break; + } + } + }(); + } +} diff --git a/src/SEDManagerGUI/lib/bindings/sedmanager_capi.dart b/src/SEDManagerGUI/lib/bindings/sedmanager_capi.dart new file mode 100644 index 0000000..36158ce --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/sedmanager_capi.dart @@ -0,0 +1,430 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'dart:io' show Platform; +import 'package:path/path.dart' as path; + +final libraryName = Platform.isWindows ? "SEDManagerCAPI.dll" : "libSEDManagerCAPI.so"; +const libraryDirEnv = String.fromEnvironment("CAPI_LIBRARY_PATH", defaultValue: ""); +final libraryDirLocal = path.dirname(Platform.resolvedExecutable); +final libraryPath = path.join(libraryDirEnv.isEmpty ? libraryDirLocal : libraryDirEnv, libraryName); + +typedef CUID = Uint64; + +final class CString extends Opaque {} + +final class CValue extends Opaque {} + +final class CType extends Opaque {} + +final class CStorageDevice extends Opaque {} + +final class CEncryptedDevice extends Opaque {} + +final class CSession extends Opaque {} + +final class CFuture extends Opaque {} + +final class CStream extends Opaque {} + +typedef CFutureVoid = CFuture; + +typedef CFutureEncryptedDevice = CFuture; +typedef CFutureSession = CFuture; +typedef CFutureValue = CFuture; +typedef CFutureString = CFuture; +typedef CFutureUID = CFuture; +typedef CStreamUid = CStream; +typedef CStreamString = CStream; +typedef CFutureCallback = NativeFunction; +typedef CStreamCallback = NativeFunction; + +class SEDManagerCAPI { + SEDManagerCAPI._privateConstructor(); + + static final SEDManagerCAPI _instance = _create(); + static final dylib = DynamicLibrary.open(libraryPath); + + factory SEDManagerCAPI() { + return _instance; + } + + static SEDManagerCAPI _create() { + return SEDManagerCAPI._privateConstructor(); + } + + //---------------------------------------------------------------------------- + // Error handling + //---------------------------------------------------------------------------- + + final getLastExceptionMessage = dylib.lookupFunction Function(), Pointer Function()>( + "CGetLastException_Message", + isLeaf: true, + ); + + //---------------------------------------------------------------------------- + // CString + //---------------------------------------------------------------------------- + + final stringCreate = dylib.lookupFunction Function(), Pointer Function()>( + "CString_Create", + isLeaf: true, + ); + + final stringDestroyAddress = dylib.lookup)>>("CString_Destroy"); + late final stringDestroy = stringDestroyAddress.asFunction)>(isLeaf: true); + + final stringSet = dylib + .lookupFunction, Pointer), void Function(Pointer, Pointer)>( + "CString_Set", + isLeaf: true, + ); + + final stringGet = + dylib.lookupFunction Function(Pointer), Pointer Function(Pointer)>( + "CString_Get", + isLeaf: true, + ); + + //---------------------------------------------------------------------------- + // CValue + //---------------------------------------------------------------------------- + + final valueCreate = dylib.lookupFunction Function(), Pointer Function()>( + "CValue_Create", + isLeaf: true, + ); + + final valueDestroyAddress = dylib.lookup)>>("CValue_Destroy"); + late final valueDestroy = valueDestroyAddress.asFunction)>(isLeaf: true); + + final valueHasValue = dylib.lookupFunction), bool Function(Pointer)>( + "CValue_HasValue", + isLeaf: true, + ); + + final valueIsBytes = dylib.lookupFunction), bool Function(Pointer)>( + "CValue_IsBytes", + isLeaf: true, + ); + + final valueIsCommand = dylib.lookupFunction), bool Function(Pointer)>( + "CValue_IsCommand", + isLeaf: true, + ); + + final valueIsInteger = dylib.lookupFunction), bool Function(Pointer)>( + "CValue_IsInteger", + isLeaf: true, + ); + + final valueIsList = dylib.lookupFunction), bool Function(Pointer)>( + "CValue_IsList", + isLeaf: true, + ); + + final valueIsNamed = dylib.lookupFunction), bool Function(Pointer)>( + "CValue_IsNamed", + isLeaf: true, + ); + + final valueGetBytes = + dylib.lookupFunction Function(Pointer), Pointer Function(Pointer)>( + "CValue_GetBytes", + isLeaf: true, + ); + + final valueGetCommand = dylib.lookupFunction), int Function(Pointer)>( + "CValue_GetCommand", + isLeaf: true, + ); + + final valueGetInteger = dylib.lookupFunction), int Function(Pointer)>( + "CValue_GetInteger", + isLeaf: true, + ); + + final valueGetListElement = dylib + .lookupFunction Function(Pointer, Size), Pointer Function(Pointer, int)>( + "CValue_GetList_Element", + isLeaf: true, + ); + + final valueGetNamedName = + dylib.lookupFunction Function(Pointer), Pointer Function(Pointer)>( + "CValue_GetNamed_Name", + isLeaf: true, + ); + + final valueGetNamedValue = + dylib.lookupFunction Function(Pointer), Pointer Function(Pointer)>( + "CValue_GetNamed_Value", + isLeaf: true, + ); + + final valueGetLength = dylib.lookupFunction), int Function(Pointer)>( + "CValue_GetLength", + isLeaf: true, + ); + + final valueSetBytes = dylib.lookupFunction, Pointer, Size), + void Function(Pointer, Pointer, int)>( + "CValue_SetBytes", + isLeaf: true, + ); + + final valueSetCommand = + dylib.lookupFunction, Uint8), void Function(Pointer, int)>( + "CValue_SetCommand", + isLeaf: true, + ); + + final valueSetInteger = dylib.lookupFunction, Int64, Uint8, Bool), + void Function(Pointer, int, int, bool)>( + "CValue_SetInteger", + isLeaf: true, + ); + + final valueSetList = dylib.lookupFunction, Pointer>, Size), + void Function(Pointer, Pointer>, int)>( + "CValue_SetList", + isLeaf: true, + ); + + final valueSetNamed = dylib.lookupFunction, Pointer, Pointer), + void Function(Pointer, Pointer, Pointer)>( + "CValue_SetNamed", + isLeaf: true, + ); + + //---------------------------------------------------------------------------- + // CValue + //---------------------------------------------------------------------------- + + final typeCreate = dylib.lookupFunction Function(), Pointer Function()>( + "CType_Create", + isLeaf: true, + ); + + final typeDestroyAddress = dylib.lookup)>>("CType_Destroy"); + late final typeDestroy = typeDestroyAddress.asFunction)>(isLeaf: true); + + //---------------------------------------------------------------------------- + // CStorageDevice + //---------------------------------------------------------------------------- + + final enumerateStorageDevices = dylib.lookupFunction Function(), Pointer Function()>( + "CEnumerateStorageDevices", + isLeaf: true, + ); + + final storageDeviceCreate = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CStorageDevice_Create", + isLeaf: true, + ); + + final storageDeviceDestroyAddress = + dylib.lookup)>>>("CStorageDevice_Destroy"); + + final storageDeviceGetName = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CStorageDevice_GetName", + isLeaf: true, + ); + + final storageDeviceGetSerial = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CStorageDevice_GetSerial", + isLeaf: true, + ); + + final storageDeviceGetFirmware = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CStorageDevice_GetFirmware", + isLeaf: true, + ); + + final storageDeviceGetInterface = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CStorageDevice_GetInterface", + isLeaf: true, + ); + + final storageDeviceGetSSCs = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CStorageDevice_GetSSCs", + isLeaf: true, + ); + + //---------------------------------------------------------------------------- + // CEncryptedDevice + //---------------------------------------------------------------------------- + + final encryptedDeviceCreate = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CEncryptedDevice_Create", + isLeaf: true, + ); + + final encryptedDeviceDestroyAddress = + dylib.lookup)>>>("CEncryptedDevice_Destroy"); + + final encryptedDeviceLogin = dylib.lookupFunction Function(Pointer, CUID), + Pointer Function(Pointer, int)>( + "CEncryptedDevice_Login", + isLeaf: true, + ); + + final encryptedDeviceStackReset = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CEncryptedDevice_StackReset", + isLeaf: true, + ); + + final encryptedDeviceReset = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CEncryptedDevice_Reset", + isLeaf: true, + ); + + final encryptedDeviceFindName = dylib.lookupFunction< + Pointer Function(Pointer, CUID, CUID), + Pointer Function(Pointer, int, int)>( + "CEncryptedDevice_FindName", + isLeaf: true, + ); + + final encryptedDeviceFindUID = dylib.lookupFunction< + Pointer Function(Pointer, Pointer, CUID), + Pointer Function(Pointer, Pointer, int)>( + "CEncryptedDevice_FindUID", + isLeaf: true, + ); + + final encryptedDeviceRenderValue = dylib.lookupFunction< + Pointer Function(Pointer, Pointer, Pointer, CUID), + Pointer Function(Pointer, Pointer, Pointer, int)>( + "CEncryptedDevice_RenderValue", + isLeaf: true, + ); + + final encryptedDeviceParseValue = dylib.lookupFunction< + Pointer Function(Pointer, Pointer, Pointer, CUID), + Pointer Function(Pointer, Pointer, Pointer, int)>( + "CEncryptedDevice_ParseValue", + isLeaf: true, + ); + + //---------------------------------------------------------------------------- + // CSession + //---------------------------------------------------------------------------- + + final sessionDestroyAddress = + dylib.lookup)>>>("CSession_Destroy"); + + final sessionAuthenticate = dylib.lookupFunction< + Pointer Function(Pointer, CUID, Pointer, Size), + Pointer Function(Pointer, int, Pointer, int)>( + "CSession_Authenticate", + isLeaf: true, + ); + + final sessionEnd = dylib.lookupFunction Function(Pointer), + Pointer Function(Pointer)>( + "CSession_End", + isLeaf: true, + ); + + final sessionGetSecurityProvider = dylib.lookupFunction), + int Function(Pointer)>( + "CSession_GetSecurityProvider", + isLeaf: true, + ); + + final sessionGetTableRows = dylib.lookupFunction Function(Pointer, CUID), + Pointer Function(Pointer, int)>( + "CSession_GetTableRows", + isLeaf: true, + ); + + final sessionGetColumnCount = + dylib.lookupFunction, CUID), int Function(Pointer, int)>( + "CSession_GetColumnCount", + isLeaf: true, + ); + + final sessionGetColumnName = dylib.lookupFunction Function(Pointer, CUID, Uint32), + Pointer Function(Pointer, int, int)>( + "CSession_GetColumnName", + isLeaf: true, + ); + + final sessionGetColumnType = dylib.lookupFunction Function(Pointer, CUID, Uint32), + Pointer Function(Pointer, int, int)>( + "CSession_GetColumnType", + isLeaf: true, + ); + + final sessionGetValue = dylib.lookupFunction Function(Pointer, CUID, Int32), + Pointer Function(Pointer, int, int)>( + "CSession_GetValue", + isLeaf: true, + ); + + final sessionSetValue = dylib.lookupFunction< + Pointer Function(Pointer, CUID, Int32, Pointer), + Pointer Function(Pointer, int, int, Pointer)>( + "CSession_SetValue", + isLeaf: true, + ); + + final sessionGenMEK = dylib.lookupFunction Function(Pointer, CUID), + Pointer Function(Pointer, int)>( + "CSession_GenMEK", + isLeaf: true, + ); + + final sessionRevert = dylib.lookupFunction Function(Pointer, CUID), + Pointer Function(Pointer, int)>( + "CSession_Revert", + isLeaf: true, + ); + + final sessionActivate = dylib.lookupFunction Function(Pointer, CUID), + Pointer Function(Pointer, int)>( + "CSession_Activate", + isLeaf: true, + ); + + //---------------------------------------------------------------------------- + // Futures + //---------------------------------------------------------------------------- + + Pointer>)>> lookupFutureDestroy(String suffix) { + final address = dylib.lookup>)>>("CFuture${suffix}_Destroy"); + return address; + } + + void Function(Pointer>, Pointer>) + lookupFutureStart(String suffix) { + final address = dylib.lookup>, Pointer)>>( + "CFuture${suffix}_Start"); + final function = address + .asFunction>, Pointer>)>(isLeaf: false); + return function; + } + + Pointer>)>> lookupStreamDestroy(String suffix) { + final address = dylib.lookup>)>>("CStream${suffix}_Destroy"); + return address; + } + + void Function(Pointer>, Pointer>) + lookupStreamAdvance(String suffix) { + final address = dylib.lookup>, Pointer)>>( + "CStream${suffix}_Advance"); + final function = address + .asFunction>, Pointer>)>(isLeaf: false); + return function; + } +} diff --git a/src/SEDManagerGUI/lib/bindings/storage_device.dart b/src/SEDManagerGUI/lib/bindings/storage_device.dart new file mode 100644 index 0000000..95b6c37 --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/storage_device.dart @@ -0,0 +1,70 @@ +import 'dart:core'; +import 'dart:ffi'; +import 'package:sed_manager_gui/bindings/errors.dart'; +import 'package:sed_manager_gui/bindings/string.dart'; +import 'sedmanager_capi.dart'; + +class StorageDevice implements Finalizable { + StorageDevice(String path) { + final api = SEDManagerCAPI(); + final pathWrapper = StringWrapper.fromString(path); + _handle = api.storageDeviceCreate(pathWrapper.handle()); + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + } + + static final _capi = SEDManagerCAPI(); + late final Pointer _handle; + static final _finalizer = NativeFinalizer(_capi.storageDeviceDestroyAddress.cast()); + + String getName() { + final nameWrapper = StringWrapper(_capi.storageDeviceGetName(_handle)); + return nameWrapper.toDartString(); + } + + String getSerial() { + final nameWrapper = StringWrapper(_capi.storageDeviceGetSerial(_handle)); + return nameWrapper.toDartString(); + } + + String getFirmware() { + final nameWrapper = StringWrapper(_capi.storageDeviceGetFirmware(_handle)); + return nameWrapper.toDartString(); + } + + String getInterface() { + final nameWrapper = StringWrapper(_capi.storageDeviceGetInterface(_handle)); + return nameWrapper.toDartString(); + } + + List getSSCs() { + final nameWrapper = StringWrapper(_capi.storageDeviceGetSSCs(_handle)); + var sscs = nameWrapper.toDartString().split(";"); + sscs.removeWhere((element) => element.isEmpty); + return sscs; + } + + Pointer handle() { + return _handle; + } +} + +List enumerateStorageDevices() { + final capi = SEDManagerCAPI(); + final devicePaths = StringWrapper(capi.enumerateStorageDevices()).toDartString(); + + final paths = devicePaths.split(';'); + paths.retainWhere((element) => element.isNotEmpty); + + var devices = []; + for (var path in paths) { + try { + devices.add(StorageDevice(path)); + } catch (ex) { + // Ignore devices that failed to open. + } + } + return devices; +} diff --git a/src/SEDManagerGUI/lib/bindings/string.dart b/src/SEDManagerGUI/lib/bindings/string.dart new file mode 100644 index 0000000..866a5c6 --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/string.dart @@ -0,0 +1,35 @@ +import "dart:ffi"; +import "package:ffi/ffi.dart"; +import "package:sed_manager_gui/bindings/errors.dart"; +import "sedmanager_capi.dart"; + +class StringWrapper implements Finalizable { + StringWrapper(this._handle) { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + } + + StringWrapper.fromString(String str) { + _handle = _capi.stringCreate(); + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + final nativeStr = str.toNativeUtf8(); + _capi.stringSet(_handle, nativeStr); + malloc.free(nativeStr); + } + + static final _capi = SEDManagerCAPI(); + static final _finalizer = NativeFinalizer(_capi.stringDestroyAddress.cast()); + late Pointer _handle; + + String toDartString() { + return _capi.stringGet(_handle).toDartString(); + } + + Pointer handle() { + return _handle; + } +} diff --git a/src/SEDManagerGUI/lib/bindings/type.dart b/src/SEDManagerGUI/lib/bindings/type.dart new file mode 100644 index 0000000..7ff0a46 --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/type.dart @@ -0,0 +1,27 @@ +import "dart:ffi"; +import "errors.dart"; +import "sedmanager_capi.dart"; + +class Type implements Finalizable { + Type(this._handle) { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + } + + Type.empty() { + _handle = _capi.typeCreate(); + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + } + + static final _capi = SEDManagerCAPI(); + static final _finalizer = NativeFinalizer(_capi.typeDestroyAddress.cast()); + late Pointer _handle; + + Pointer handle() { + return _handle; + } +} diff --git a/src/SEDManagerGUI/lib/bindings/value.dart b/src/SEDManagerGUI/lib/bindings/value.dart new file mode 100644 index 0000000..dbd919a --- /dev/null +++ b/src/SEDManagerGUI/lib/bindings/value.dart @@ -0,0 +1,116 @@ +import "dart:ffi"; +import "dart:typed_data"; +import "package:ffi/ffi.dart"; + +import "errors.dart"; +import "sedmanager_capi.dart"; + +class Value implements Finalizable { + Value(this._handle) { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + } + + Value.empty() : _handle = _capi.valueCreate() { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + } + + Value.bytes(ByteData data) : _handle = _capi.valueCreate() { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + final length = data.lengthInBytes; + final ptr = malloc.allocate(length); + final bytes = ptr.asTypedList(length); + for (int i = 0; i < length; ++i) { + bytes[i] = data.buffer.asUint8List()[i]; + } + _capi.valueSetBytes(_handle, ptr, length); + malloc.free(ptr); + } + + Value.bytesFromString(String data) : _handle = _capi.valueCreate() { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + + final ptr = data.toNativeUtf8(); + try { + _capi.valueSetBytes(_handle, ptr.cast(), ptr.length); + } finally { + malloc.free(ptr); + } + } + + Value.integer(int value, int width, bool signed) : _handle = _capi.valueCreate() { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + _capi.valueSetInteger(_handle, value, width, signed); + } + + Value.list(List values) : _handle = _capi.valueCreate() { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + final ptr = malloc.allocate>(sizeOf>() * values.length); + for (int i = 0; i < values.length; ++i) { + ptr[i] = values[i].handle(); + } + _capi.valueSetList(_handle, ptr, values.length); + malloc.free(ptr); + } + + Value.named(Value name, Value value) : _handle = _capi.valueCreate() { + if (_handle == nullptr) { + throw SEDException(getLastErrorMessage()); + } + _finalizer.attach(this, _handle.cast(), detach: this); + _capi.valueSetNamed(_handle, name.handle(), value.handle()); + } + + static final _capi = SEDManagerCAPI(); + static final _finalizer = NativeFinalizer(_capi.valueDestroyAddress.cast()); + final Pointer _handle; + + bool get hasValue { + return _capi.valueHasValue(_handle); + } + + bool get isBytes { + return _capi.valueIsBytes(_handle); + } + + bool get isInteger { + return _capi.valueIsInteger(_handle); + } + + ByteData getBytes() { + if (isBytes) { + final data = _capi.valueGetBytes(_handle); + final length = _capi.valueGetLength(_handle); + return data.asTypedList(length).buffer.asByteData(); + } + throw SEDException("value is not a byte array"); + } + + int getInteger() { + if (isInteger) { + return _capi.valueGetInteger(_handle); + } + throw SEDException("value is not an integer"); + } + + Pointer handle() { + return _handle; + } +} diff --git a/src/SEDManagerGUI/lib/interface/activity_launcher_page.dart b/src/SEDManagerGUI/lib/interface/activity_launcher_page.dart new file mode 100644 index 0000000..0e758be --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/activity_launcher_page.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/storage_device.dart'; +import 'package:sed_manager_gui/interface/locking_wizard.dart'; +import 'factory_reset_wizard.dart'; +import 'stack_reset_page.dart'; +import 'table_editor_page.dart'; + +class ActivityLauncherPage extends StatelessWidget { + const ActivityLauncherPage(this.device, {super.key}); + + final StorageDevice device; + + void _launchActivity( + BuildContext context, + Widget Function(BuildContext) builder, + ) { + Navigator.of(context).push( + MaterialPageRoute( + builder: builder, + ), + ); + } + + void _launchTableEditor(BuildContext context) { + _launchActivity(context, (BuildContext context) => TableEditorPage(device)); + } + + void _launchLockingWizard(BuildContext context) { + _launchActivity(context, (BuildContext context) => LockingWizard(device)); + } + + void _launchStackReset(BuildContext context) { + _launchActivity(context, (BuildContext context) => StackResetPage(device)); + } + + void _launchFactoryReset(BuildContext context) { + _launchActivity(context, (BuildContext context) => FactoryResetWizard(device)); + } + + Widget _buildIcon(BuildContext context, String caption, IconData icon, void Function()? callback) { + var face = Container( + width: 90, + height: 86, + margin: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Column( + children: [ + Icon( + icon, + size: 45, + ), + Text(caption, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center), + ], + ), + ); + final style = ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ); + return TextButton(onPressed: callback, style: style, child: face); + } + + @override + Widget build(BuildContext context) { + late final String title = "${device.getName()} - ${device.getSerial()}"; + + final icons = [ + _buildIcon(context, "Table editor", Icons.table_chart_outlined, () => _launchTableEditor(context)), + _buildIcon(context, "Locking wizard", Icons.lock_outline_rounded, () => _launchLockingWizard(context)), + _buildIcon(context, "Change password", Icons.password, null), + _buildIcon(context, "Stack reset", Icons.restart_alt_rounded, () => _launchStackReset(context)), + _buildIcon(context, "Factory reset", Icons.clear_rounded, () => _launchFactoryReset(context)), + ]; + + return Scaffold( + appBar: AppBar(title: Text(title)), + body: Container( + margin: const EdgeInsets.all(12), + child: Wrap( + runSpacing: 6, + spacing: 6, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + direction: Axis.horizontal, + children: icons, + ), + ), + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/cached_stream.dart b/src/SEDManagerGUI/lib/interface/components/cached_stream.dart new file mode 100644 index 0000000..6ac5556 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/cached_stream.dart @@ -0,0 +1,11 @@ +class CachedStream { + CachedStream(this.source); + + final Stream source; + T? latest; + + late final stream = source.map((event) { + latest = event; + return event; + }); +} \ No newline at end of file diff --git a/src/SEDManagerGUI/lib/interface/components/encrypted_device_builder.dart b/src/SEDManagerGUI/lib/interface/components/encrypted_device_builder.dart new file mode 100644 index 0000000..c8f496f --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/encrypted_device_builder.dart @@ -0,0 +1,27 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import "package:sed_manager_gui/bindings/encrypted_device.dart"; +import "package:sed_manager_gui/bindings/storage_device.dart"; + +class EncryptedDeviceBuilder extends StatelessWidget { + const EncryptedDeviceBuilder( + this.storageDevice, { + required this.builder, + super.key, + }); + + final StorageDevice storageDevice; + final Widget Function(BuildContext context, AsyncSnapshot snapshot) builder; + + Future _getEncryptedDevice() async { + return await EncryptedDevice.create(storageDevice); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getEncryptedDevice(), + builder: builder, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/object_dropdown.dart b/src/SEDManagerGUI/lib/interface/components/object_dropdown.dart new file mode 100644 index 0000000..68bc43b --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/object_dropdown.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; + +class ObjectDropdown extends StatelessWidget { + const ObjectDropdown( + this.objects, { + this.width, + this.onSelected, + this.hintText, + this.enabled = true, + super.key, + }); + + final List<(UID, String)> objects; + final void Function(UID object)? onSelected; + final double? width; + final String? hintText; + final bool enabled; + + @override + Widget build(BuildContext context) { + final items = objects.map((sp) { + return DropdownMenuEntry(value: sp.$1, label: sp.$2); + }).toList(); + + return DropdownMenu( + onSelected: (int? value) { + if (value != null) { + onSelected?.call(value); + } + }, + dropdownMenuEntries: items, + controller: SearchController(), + hintText: objects.isNotEmpty ? hintText : "Empty", + enabled: enabled && objects.isNotEmpty, + width: width, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/session_builder.dart b/src/SEDManagerGUI/lib/interface/components/session_builder.dart new file mode 100644 index 0000000..046c542 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/session_builder.dart @@ -0,0 +1,40 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import "package:sed_manager_gui/bindings/encrypted_device.dart"; + +class SessionBuilder extends StatefulWidget { + SessionBuilder( + this.encryptedDevice, + this.securityProvider, { + required this.builder, + }) : super(key: UniqueKey()); + final EncryptedDevice encryptedDevice; + final UID securityProvider; + final Widget Function(BuildContext context, AsyncSnapshot session) builder; + + @override + State createState() => _SessionBuilderState(); +} + +class _SessionBuilderState extends State { + Session? _session; + + @override + void deactivate() { + _session?.end().ignore(); + super.deactivate(); + } + + Future _getSession() async { + _session = await widget.encryptedDevice.login(widget.securityProvider); + return _session!; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getSession(), + builder: widget.builder, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/snapshot_builder.dart b/src/SEDManagerGUI/lib/interface/components/snapshot_builder.dart new file mode 100644 index 0000000..d8ab374 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/snapshot_builder.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class SnapshotBuilder extends StatelessWidget { + const SnapshotBuilder( + this.snapshot, { + this.none, + this.waiting, + this.success, + this.error, + super.key, + }); + + final AsyncSnapshot snapshot; + final Widget Function(BuildContext context)? none; + final Widget Function(BuildContext context)? waiting; + final Widget Function(BuildContext context, T? data)? success; + final Widget Function(BuildContext context, Object error)? error; + + @override + Widget build(BuildContext context) { + if (snapshot.connectionState == ConnectionState.none) { + return none?.call(context) ?? const Placeholder(); + } else if (snapshot.connectionState == ConnectionState.waiting) { + return waiting?.call(context) ?? const Placeholder(); + } else { + if (snapshot.error != null) { + return error?.call(context, snapshot.error!) ?? const Placeholder(); + } else { + return success?.call(context, snapshot.data) ?? const Placeholder(); + } + } + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/status_indicator.dart b/src/SEDManagerGUI/lib/interface/components/status_indicator.dart new file mode 100644 index 0000000..303a082 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/status_indicator.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +enum Status { + none, + waiting, + success, + error, +} + +class StatusIndicator extends StatelessWidget { + const StatusIndicator( + this._status, { + this.message, + this.messagePosition = AxisDirection.left, + this.iconSize = 16.0, + this.style, + super.key, + }); + + const StatusIndicator.none({ + this.message, + this.messagePosition = AxisDirection.left, + this.iconSize = 16.0, + this.style, + super.key, + }) : _status = Status.none; + + const StatusIndicator.waiting({ + this.message, + this.messagePosition = AxisDirection.left, + this.iconSize = 16.0, + this.style, + super.key, + }) : _status = Status.waiting; + + const StatusIndicator.success({ + this.message, + this.messagePosition = AxisDirection.left, + this.iconSize = 16.0, + this.style, + super.key, + }) : _status = Status.success; + + const StatusIndicator.error({ + this.message, + this.messagePosition = AxisDirection.left, + this.iconSize = 16.0, + this.style, + super.key, + }) : _status = Status.error; + + final Status _status; + final String? message; + final AxisDirection messagePosition; + final double iconSize; + final TextStyle? style; + + Widget _buildIcon(BuildContext context) { + if (_status == Status.error) { + return Icon(Icons.error_outline, size: iconSize, color: Colors.red); + } else if (_status == Status.success) { + return Icon(Icons.check_circle_outline, size: iconSize, color: Colors.green); + } else if (_status == Status.none) { + return Icon(Icons.question_mark_outlined, size: iconSize, color: Colors.transparent); + } else { + return SizedBox(width: iconSize, height: iconSize, child: const CircularProgressIndicator()); + } + } + + Widget _buildMessage() { + return Text( + message ?? "", + style: style, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + @override + Widget build(BuildContext context) { + final iconWidget = _buildIcon(context); + final messageWidget = _buildMessage(); + final tooltipWidget = message != null ? Tooltip(message: message, child: messageWidget) : messageWidget; + + const spacer = SizedBox(width: 6, height: 6); + var children = []; + if (messagePosition == AxisDirection.left || messagePosition == AxisDirection.down) { + children = [iconWidget, spacer, Flexible(child: tooltipWidget)]; + } else { + children = [Flexible(child: tooltipWidget), spacer, iconWidget]; + } + + if (messagePosition == AxisDirection.left || messagePosition == AxisDirection.right) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: children, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: children, + ); + } + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/status_page.dart b/src/SEDManagerGUI/lib/interface/components/status_page.dart new file mode 100644 index 0000000..9e5aade --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/status_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'status_indicator.dart'; + +class StatusPage extends StatelessWidget { + const StatusPage.waiting({ + this.message, + this.onClose, + this.messagePosition = AxisDirection.down, + this.iconSize = 48.0, + this.style, + super.key, + }) : _status = Status.waiting; + + const StatusPage.success({ + this.message, + this.onClose, + this.messagePosition = AxisDirection.down, + this.iconSize = 48.0, + this.style, + super.key, + }) : _status = Status.success; + + const StatusPage.error({ + this.message, + this.onClose, + this.messagePosition = AxisDirection.down, + this.iconSize = 48.0, + this.style, + super.key, + }) : _status = Status.error; + + final Status _status; + final String? message; + final void Function()? onClose; + final AxisDirection messagePosition; + final double iconSize; + final TextStyle? style; + + String _closeButtonText() { + switch (_status) { + case Status.waiting: + return "Cancel"; + case Status.error: + return "Back"; + case Status.success: + return "Done"; + default: + return "Close"; + } + } + + @override + Widget build(BuildContext context) { + final indicator = StatusIndicator( + _status, + message: message, + iconSize: iconSize, + messagePosition: messagePosition, + style: style, + ); + + final closeButton = FilledButton( + onPressed: onClose, + child: Text(_closeButtonText()), + ); + + final elements = onClose == null + ? [indicator] + : [ + Flexible(flex: 1, child: FractionallySizedBox(widthFactor: 0.8, child: Center(child: indicator))), + Align(alignment: Alignment.centerRight, child: closeButton), + ]; + + return Container( + margin: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: elements, + ), + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/components/utility.dart b/src/SEDManagerGUI/lib/interface/components/utility.dart new file mode 100644 index 0000000..5f1f869 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/utility.dart @@ -0,0 +1,16 @@ +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; + +Future getDisplayName(UID object, EncryptedDevice encryptedDevice, {UID? securityProvider}) async { + try { + return await encryptedDevice.findName(object, securityProvider: securityProvider ?? 0); + } catch (ex) { + return object.toRadixString(16).padLeft(16, '0'); + } +} + +Future> enumerateTable(UID table, EncryptedDevice encryptedDevice, Session session) async { + return session + .getTableRows(table) + .asyncMap((object) async => (object, await getDisplayName(object, encryptedDevice))) + .toList(); +} diff --git a/src/SEDManagerGUI/lib/interface/components/wizard.dart b/src/SEDManagerGUI/lib/interface/components/wizard.dart new file mode 100644 index 0000000..63d90e8 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/components/wizard.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/interface/components/snapshot_builder.dart'; +import 'package:sed_manager_gui/interface/components/status_indicator.dart'; + +class _WizardPageRoute extends PageRoute { + _WizardPageRoute({required this.builder}); + + final Widget Function(BuildContext context) builder; + + @override + Color? get barrierColor { + if (navigator != null) { + return Theme.of(navigator!.context).colorScheme.background; + } + return null; + } + + @override + String? get barrierLabel => null; + + @override + Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + return builder(context); + } + + @override + Widget buildTransitions( + BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + final position = Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, + ).animate(animation); + return SlideTransition(position: position, child: child); + } + + @override + bool get maintainState => true; + + @override + Duration get transitionDuration => const Duration(milliseconds: 200); +} + +class _ValidationDialog extends StatelessWidget { + const _ValidationDialog(this._validate, {this.next, super.key}); + + final Future Function() _validate; + final Widget Function(BuildContext context)? next; + + void _stayOnPage(BuildContext context) { + Navigator.of(context).pop(); // Pops this dialog. + } + + void _goNextPage(BuildContext context) { + Navigator.of(context).pop(); // Pops this dialog. + if (next != null) { + Navigator.of(context).push(_WizardPageRoute(builder: (context) { + return next!(context); + })); + } else { + // Pops until the first wizard page. + Navigator.of(context).popUntil((route) => route.runtimeType != _WizardPageRoute); + Navigator.of(context).pop(); // Pops the first wizard page. + } + } + + Future _validatePage(BuildContext context) async { + await _validate(); + if (context.mounted) { + _goNextPage(context); + } + } + + Widget _buildContent(BuildContext context, Widget status, void Function(BuildContext)? action) { + final actionButton = FilledButton( + onPressed: action != null ? () => action(context) : null, + child: const Text("OK"), + ); + + return Container( + margin: const EdgeInsets.all(12), + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + status, + const SizedBox(height: 12), + Align(alignment: Alignment.centerRight, child: actionButton), + ], + ), + ); + } + + Widget _buildError(BuildContext context, Object error) { + return _buildContent( + context, + StatusIndicator.error( + message: "Error: ${error.toString()}", + iconSize: 48, + messagePosition: AxisDirection.down, + ), + _stayOnPage, + ); + } + + Widget _buildWaiting(BuildContext context) { + return _buildContent( + context, + const StatusIndicator.waiting( + iconSize: 48, + messagePosition: AxisDirection.down, + ), + null, + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: FutureBuilder( + future: _validatePage(context), + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: _buildWaiting, + error: _buildError, + ); + }, + ), + ); + } +} + +class WizardPage extends StatelessWidget { + const WizardPage({ + required this.title, + required this.onValidate, + this.onNext, + required this.child, + super.key, + }); + + final String title; + final Future Function() onValidate; + final Widget Function(BuildContext context)? onNext; + final Widget child; + + void _onCancel(BuildContext context) { + Navigator.of(context).popUntil((route) => route.runtimeType != _WizardPageRoute); + Navigator.of(context).pop(); + } + + void _onNext(BuildContext context) { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => _ValidationDialog(onValidate, next: onNext), + ); + } + + void _onFinish(BuildContext context) { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => _ValidationDialog(onValidate), + ); + } + + @override + Widget build(BuildContext context) { + final cancelButton = FilledButton(onPressed: () => _onCancel(context), child: const Text("Cancel")); + final nextButton = FilledButton(onPressed: () => _onNext(context), child: const Text("Next")); + final finishButton = FilledButton(onPressed: () => _onFinish(context), child: const Text("Finish")); + + return Scaffold( + appBar: AppBar(title: Text(title)), + body: Column( + children: [ + Expanded(child: child), + Container( + margin: const EdgeInsets.fromLTRB(20, 6, 20, 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [cancelButton, const SizedBox(width: 6), onNext != null ? nextButton : finishButton], + ), + ), + ], + ), + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/drive_launcher_page.dart b/src/SEDManagerGUI/lib/interface/drive_launcher_page.dart new file mode 100644 index 0000000..d326df3 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/drive_launcher_page.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/interface/activity_launcher_page.dart'; +import 'package:sed_manager_gui/interface/components/status_indicator.dart'; +import '../bindings/storage_device.dart'; + +class StorageDeviceProperties { + StorageDeviceProperties(this.name, this.serial, this.firmware, this.interface, this.supportedSSCs); + final String name; + final String serial; + final String firmware; + String interface; + final List supportedSSCs; +} + +class StorageDeviceCard extends StatelessWidget { + StorageDeviceCard( + this._storageDevice, { + required this.onConfigure, + super.key, + }); + + final StorageDevice _storageDevice; + final void Function() onConfigure; + + Future _getProperties() async { + final name = _storageDevice.getName(); + final serial = _storageDevice.getSerial(); + final firmware = _storageDevice.getFirmware(); + final interface = _storageDevice.getInterface(); + final sscs = _storageDevice.getSSCs(); + return StorageDeviceProperties(name, serial, firmware, interface, sscs); + } + + Widget _buildCard(BuildContext context, Widget child) { + return SizedBox( + width: 280, + height: 180, + child: Card( + child: Container(margin: const EdgeInsets.all(8), child: child), + ), + ); + } + + Decoration? _getDriveDataDecoration(bool? parity) { + if (parity != null) { + return BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + (parity ? Colors.black : Colors.white).withAlpha(12), + (parity ? Colors.black : Colors.white).withAlpha(12), + (parity ? Colors.black : Colors.white).withAlpha(12), + (parity ? Colors.black : Colors.white).withAlpha(12), + Colors.transparent, + ], + ), + ); + } + return null; + } + + Widget _buildDriveDataRow(String record, String value, {bool? parity}) { + return Container( + decoration: _getDriveDataDecoration(parity), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("$record:"), + Text(value, overflow: TextOverflow.ellipsis), + ], + ), + ); + } + + Widget _buildWithData(BuildContext context, StorageDeviceProperties data) { + const titleStyle = TextStyle(fontSize: 18, fontWeight: FontWeight.bold); + + final sscNames = data.supportedSSCs.isNotEmpty ? data.supportedSSCs.join(', ') : "-"; + + final items = [ + Center(child: Text(data.name, style: titleStyle)), + Divider(height: 11, thickness: 1, color: Theme.of(context).colorScheme.onSurface.withAlpha(48)), + _buildDriveDataRow("Serial", data.serial, parity: true), + _buildDriveDataRow("Firmware", data.firmware, parity: false), + _buildDriveDataRow("Encryption", sscNames, parity: true), + _buildDriveDataRow("Interface", data.interface, parity: false), + ]; + + if (data.supportedSSCs.isNotEmpty) { + items.add( + Expanded( + child: Align( + alignment: Alignment.bottomRight, + child: FilledButton( + onPressed: onConfigure, + child: const Text("Configure"), + ), + ), + ), + ); + } + + final content = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ); + return _buildCard(context, content); + } + + Widget _buildWithError(BuildContext context, Object error) { + const titleStyle = TextStyle(fontSize: 18, fontWeight: FontWeight.bold); + + final name = _storageDevice.getName(); + + final content = Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(name, style: titleStyle), + Expanded(child: Center(child: StatusIndicator.error(message: error.toString()))), + ], + ); + return _buildCard(context, content); + } + + Widget _buildWaiting(BuildContext context) { + const titleStyle = TextStyle(fontSize: 18, fontWeight: FontWeight.bold); + + final name = _storageDevice.getName(); + + final content = Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(name, style: titleStyle), + const Expanded(child: Center(child: SizedBox(width: 48, height: 48, child: CircularProgressIndicator()))), + ], + ); + return _buildCard(context, content); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getProperties(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildWithData(context, snapshot.data!); + } else if (snapshot.hasError) { + return _buildWithError(context, snapshot.error!); + } + return _buildWaiting(context); + }, + ); + } +} + +class DriveLauncherPage extends StatefulWidget { + const DriveLauncherPage(this.onFinished, {super.key}); + + final void Function() onFinished; + + @override + State createState() => _DriveLauncherPageState(); +} + +class _DriveLauncherPageState extends State { + List storageDevices = []; + + @override + void initState() { + storageDevices = enumerateStorageDevices(); + super.initState(); + } + + void _onConfigure(StorageDevice device) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => ActivityLauncherPage(device), + ), + ); + } + + @override + Widget build(BuildContext context) { + final cards = storageDevices.map((device) { + return StorageDeviceCard(device, onConfigure: () => _onConfigure(device)); + }); + + final appBar = AppBar( + title: const Text("Storage devices"), + ); + + final body = Container( + margin: const EdgeInsets.all(12), + child: Wrap( + direction: Axis.horizontal, + spacing: 16, + children: cards.toList(), + ), + ); + + return Scaffold( + appBar: appBar, + body: body, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/factory_reset_wizard.dart b/src/SEDManagerGUI/lib/interface/factory_reset_wizard.dart new file mode 100644 index 0000000..7dd0b34 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/factory_reset_wizard.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/bindings/storage_device.dart'; +import 'package:sed_manager_gui/interface/components/encrypted_device_builder.dart'; +import 'package:sed_manager_gui/interface/components/snapshot_builder.dart'; +import 'package:sed_manager_gui/interface/components/status_page.dart'; +import 'package:sed_manager_gui/interface/components/wizard.dart'; + +const UID _adminSpUid = 0x0000020500000001; +const UID _lockingSpUid = 0x0000020500000002; +const UID _sidUid = 0x0000000900000006; +const UID _psidUid = 0x000000090001FF01; + +class _SuccessPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return WizardPage( + title: "Factory reset", + onValidate: () async {}, + child: const Center(child: StatusPage.success(message: "Factory reset successful!")), + ); + } +} + +class _RevertPage extends StatefulWidget { + const _RevertPage(this._encryptedDevice, this._isPsidSupported); + + final EncryptedDevice _encryptedDevice; + final bool _isPsidSupported; + + @override + State createState() => _RevertPageState(); +} + +class _RevertPageState extends State<_RevertPage> { + UID _selectedAuthority = _sidUid; + int _selectedScope = _adminSpUid; + final _passwordController = TextEditingController(); + + Future _performReset() async { + final session = await widget._encryptedDevice.login(_adminSpUid); + try { + await session.authenticate(_selectedAuthority, _passwordController.text); + await session.revert(_selectedScope); + } finally { + await session.end(); + } + } + + Widget _buildForm(BuildContext context) { + const width = 280.0; + + const dropdownSid = DropdownMenuEntry( + label: "Owner (SID)", + value: _sidUid, + ); + final dropdownPsid = DropdownMenuEntry( + label: "Physical owner (PSID)", + value: _psidUid, + enabled: widget._isPsidSupported, + ); + final authoritySelector = DropdownMenu( + dropdownMenuEntries: [dropdownSid, dropdownPsid], + initialSelection: _selectedAuthority, + helperText: "How you authenticate", + width: width, + onSelected: (value) => setState(() { + _selectedAuthority = value ?? _sidUid; + _selectedScope = _adminSpUid; + }), + ); + + const dropdownResetAll = DropdownMenuEntry( + label: "Entire configuration", + value: _adminSpUid, + ); + final dropdownResetLocking = DropdownMenuEntry( + label: "Locking configuration", + value: _lockingSpUid, + enabled: _selectedAuthority != _psidUid, + ); + final resetSelector = DropdownMenu( + dropdownMenuEntries: [dropdownResetAll, dropdownResetLocking], + initialSelection: _selectedScope, + helperText: "What to reset", + width: width, + onSelected: (value) => setState(() { + _selectedScope = value ?? _adminSpUid; + }), + ); + + final passwordHint = _selectedAuthority == _sidUid ? "Owner (SID) password" : "Phys. owner (PSID) password"; + + final passwordEntry = TextField( + decoration: InputDecoration(hintText: passwordHint), + obscureText: true, + controller: _passwordController, + ); + + return SizedBox( + width: width, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + authoritySelector, + const SizedBox(height: 6), + resetSelector, + const SizedBox(height: 6), + passwordEntry, + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return WizardPage( + title: "Factory reset", + onValidate: _performReset, + child: _buildForm(context), + onNext: (context) => _SuccessPage(), + ); + } +} + +class FactoryResetWizard extends StatelessWidget { + const FactoryResetWizard(this._storageDevice, {super.key}); + + final StorageDevice _storageDevice; + + Future _isPsidSupported(EncryptedDevice encryptedDevice) async { + try { + final session = await encryptedDevice.login(_adminSpUid); + try { + await session.getValue(_psidUid, 0); + } finally { + session.end(); + } + return true; + } catch (ex) { + return false; + } + } + + Widget _buildSession(BuildContext context, EncryptedDevice encryptedDevice) { + return FutureBuilder( + future: _isPsidSupported(encryptedDevice), + builder: (context, snapshot) { + return _RevertPage(encryptedDevice, snapshot.data ?? false); + }, + ); + } + + @override + Widget build(BuildContext context) { + return EncryptedDeviceBuilder( + _storageDevice, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + error: (context, error) => Material( + child: StatusPage.error( + message: "Failed to open device: $error", + onClose: () => Navigator.of(context).pop(), + ), + ), + waiting: (context) => Material( + child: StatusPage.waiting( + message: "Opening device...", + onClose: () => Navigator.of(context).pop(), + ), + ), + success: (context, data) => _buildSession(context, data!), + ); + }, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/locking_wizard.dart b/src/SEDManagerGUI/lib/interface/locking_wizard.dart new file mode 100644 index 0000000..39047c2 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/locking_wizard.dart @@ -0,0 +1,461 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/bindings/storage_device.dart'; +import 'package:sed_manager_gui/bindings/value.dart'; +import 'package:sed_manager_gui/interface/components/encrypted_device_builder.dart'; +import 'package:sed_manager_gui/interface/components/snapshot_builder.dart'; +import 'package:sed_manager_gui/interface/components/wizard.dart'; + +import 'components/status_page.dart'; + +const UID _adminSpUid = 0x0000020500000001; +const UID _lockingSpUid = 0x0000020500000002; +const UID _sidUid = 0x0000000900000006; +const UID _psidUid = 0x000000090001FF01; + +class _SuccessPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return WizardPage( + title: "Factory reset", + onValidate: () async {}, + child: const Center( + child: StatusPage.success( + message: "Configuration successful! Your device is now passsword protected.", + ), + ), + ); + } +} + +class _UserSetupPage extends StatelessWidget { + _UserSetupPage(this._encryptedDevice, this._sidPassword); + + final EncryptedDevice _encryptedDevice; + final String _sidPassword; + final _userNameController = TextEditingController(); + final _userPasswordController = TextEditingController(); + final _userRepeatController = TextEditingController(); + + Widget _buildContent(BuildContext context) { + final nameField = Tooltip( + waitDuration: Durations.long2, + message: "You can use this to login during pre-boot authentication.", + child: TextField( + controller: _userNameController, + decoration: const InputDecoration(hintText: "Username"), + ), + ); + + final passwordField = Tooltip( + waitDuration: Durations.long2, + message: "This password will be used for pre-boot authentication. It can be the same as your Owner password.", + child: TextField( + controller: _userPasswordController, + decoration: const InputDecoration(hintText: "New password"), + obscureText: true, + ), + ); + + final repeatField = TextField( + controller: _userRepeatController, + decoration: const InputDecoration(hintText: "Repeat password"), + obscureText: true, + ); + + return Center( + child: SizedBox( + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Set your credentials for pre-boot authentication:", textAlign: TextAlign.center), + nameField, + passwordField, + repeatField, + ], + ), + ), + ); + } + + Future _setupUser() async { + if (_userNameController.text.isEmpty) { + throw Exception("username cannot be empty"); + } + if (_userPasswordController.text != _userRepeatController.text) { + throw Exception("the two passwords don't match"); + } + final session = await _encryptedDevice.login(_lockingSpUid); + try { + final admin1Uid = await _encryptedDevice.findUid("Authority::Admin1", securityProvider: _lockingSpUid); + final user1Uid = await _encryptedDevice.findUid("Authority::User1", securityProvider: _lockingSpUid); + final cPinUser1 = await _encryptedDevice.findUid("C_PIN::User1", securityProvider: _lockingSpUid); + final setRdLocked = await _encryptedDevice.findUid( + "ACE::Locking_GlobalRange_Set_RdLocked", + securityProvider: _lockingSpUid, + ); + final setWrLocked = await _encryptedDevice.findUid( + "ACE::Locking_GlobalRange_Set_WrLocked", + securityProvider: _lockingSpUid, + ); + final mbrControl = await _encryptedDevice.findUid( + "ACE::MBRControl_Set_DoneToDOR", + securityProvider: _lockingSpUid, + ); + final authorityObjectRefUid = await _encryptedDevice.findUid("Type::Authority_object_ref"); + final booleanAceUid = await _encryptedDevice.findUid("Type::boolean_ACE"); + final globalRangeUid = await _encryptedDevice.findUid("Locking::GlobalRange", securityProvider: _lockingSpUid); + + // Authenticate as Admin1. + await session.authenticate(admin1Uid, _sidPassword); + + // Set User1's common name. + await session.setValue(user1Uid, 2, Value.bytesFromString(_userNameController.text)); + + // Set User1's password. + await session.setValue(cPinUser1, 3, Value.bytesFromString(_userPasswordController.text)); + + // Set global range and MBR locking permissions. + final halfUid = ByteData(4); + + halfUid.setUint32(0, authorityObjectRefUid & 0xFFFFFFFF); + final authorityObjectRefValue = Value.bytes(halfUid); + halfUid.setUint32(0, booleanAceUid & 0xFFFFFFFF); + final booleanAceValue = Value.bytes(halfUid); + + final fullUid = ByteData(8); + fullUid.setUint64(0, admin1Uid); + final admin1Value = Value.bytes(fullUid); + fullUid.setUint64(0, user1Uid); + final user1Value = Value.bytes(fullUid); + + final admin1OrUser1Rights = Value.list([ + Value.named(authorityObjectRefValue, admin1Value), + Value.named(authorityObjectRefValue, user1Value), + Value.named(booleanAceValue, Value.integer(1, 1, false)), + ]); + + await session.setValue(setRdLocked, 3, admin1OrUser1Rights); + await session.setValue(setWrLocked, 3, admin1OrUser1Rights); + await session.setValue(mbrControl, 3, admin1OrUser1Rights); + + // Enable locking of global range. + await session.setValue(globalRangeUid, 5, Value.integer(1, 1, false)); // RD + await session.setValue(globalRangeUid, 6, Value.integer(1, 1, false)); // WR + } finally { + await session.end(); + } + } + + @override + Widget build(BuildContext context) { + return WizardPage( + title: "Configure locking", + onValidate: _setupUser, + onNext: (context) => _SuccessPage(), + child: _buildContent(context), + ); + } +} + +class _ActivateLockingPage extends StatelessWidget { + const _ActivateLockingPage(this._encryptedDevice, this._sidPassword); + + final EncryptedDevice _encryptedDevice; + final String _sidPassword; + + Widget _buildEnableNow(BuildContext context) { + return const Center( + child: SizedBox( + width: 280, + child: Text("Locking is ready to be enabled.\nPress next to continue.", textAlign: TextAlign.center), + ), + ); + } + + Widget _buildAlreadyEnabled(BuildContext context) { + return const Center( + child: SizedBox( + width: 280, + child: Text("Locking is already enabled.\nPress next to continue.", textAlign: TextAlign.center), + ), + ); + } + + Future _enableLocking() async { + final session = await _encryptedDevice.login(_adminSpUid); + try { + await session.authenticate(_sidUid, _sidPassword); + await session.activate(_lockingSpUid); + } finally { + await session.end(); + } + } + + Future _enableMBR() async { + final session = await _encryptedDevice.login(_lockingSpUid); + try { + final admin1Uid = await _encryptedDevice.findUid("Authority::Admin1", securityProvider: _lockingSpUid); + final mbrControl = await _encryptedDevice.findUid("MBRControl::MBRControl", securityProvider: _lockingSpUid); + await session.authenticate(admin1Uid, _sidPassword); + await session.setValue(mbrControl, 1, Value.integer(1, 1, false)); + } finally { + await session.end(); + } + } + + Future _enableLockingIfDisabled() async { + if (!await _isLockingEnabled() || !await _isMBREnabled()) { + await _enableLocking(); + await _enableMBR(); + } + } + + Future _isLockingEnabled() async { + final session = await _encryptedDevice.login(_adminSpUid); + try { + const manufacturedInactive = 8; + final lifeCycleState = (await session.getValue(_lockingSpUid, 6)).getInteger(); + return lifeCycleState != manufacturedInactive; + } finally { + await session.end(); + } + } + + Future _isMBREnabled() async { + try { + final session = await _encryptedDevice.login(_lockingSpUid); + try { + final mbrControl = await _encryptedDevice.findUid("MBRControl::MBRControl", securityProvider: _lockingSpUid); + final mbrEnabled = (await session.getValue(mbrControl, 1)).getInteger(); + return mbrEnabled != 0; + } finally { + await session.end(); + } + } catch (ex) { + return false; + } + } + + @override + Widget build(BuildContext context) { + return WizardPage( + title: "Enable locking", + onValidate: _enableLockingIfDisabled, + onNext: (context) => _UserSetupPage(_encryptedDevice, _sidPassword), + child: FutureBuilder( + future: (() async => await _isLockingEnabled() && await _isMBREnabled())(), + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => const StatusPage.waiting(message: "Checking locking state..."), + error: (context, error) => StatusPage.error(message: "Error: $error"), + success: (context, data) => data! ? _buildAlreadyEnabled(context) : _buildEnableNow(context), + ); + }, + ), + ); + } +} + +class _OwnershipPage extends StatelessWidget { + _OwnershipPage(this._encryptedDevice); + + final EncryptedDevice _encryptedDevice; + final _passwordController = TextEditingController(); + final _repeatController = TextEditingController(); + + Widget _buildNewOwner(BuildContext context) { + const instruction = Tooltip( + waitDuration: Durations.long1, + message: "You'll later need this to do further configuration in the table editor or to perform a device reset." + " Losing this password means you likely lose access to all your data and have to perform a PSID factory" + " reset.", + child: Text("Choose your owner's (SID) password:"), + ); + + final password = TextField( + obscureText: true, + decoration: const InputDecoration(hintText: "New password"), + controller: _passwordController, + ); + + final repeat = TextField( + obscureText: true, + decoration: const InputDecoration(hintText: "Repeat new password"), + controller: _repeatController, + ); + + return Center( + child: SizedBox( + width: 280, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + instruction, + const SizedBox(height: 6), + password, + const SizedBox(height: 6), + repeat, + ], + ), + ), + ); + } + + Widget _buildAlreadyOwned(BuildContext context) { + const instruction = Tooltip( + waitDuration: Durations.long1, + message: "Looks like you've already configured this device. You can still go through the configuration and" + " change settings.", + child: Text("Enter your owner's (SID) password:"), + ); + + final forgot = Tooltip( + triggerMode: TooltipTriggerMode.tap, + waitDuration: Durations.short4, + message: "You have to perform a factory reset using your physical owner's (PSID) password. " + " This password is usually printed on the device's label under 'PSID'. You can find the factory reset on the" + " device's landing page.", + child: Text( + "Forgot your password?", + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 10.0, + ), + ), + ); + + final password = TextField( + obscureText: true, + decoration: const InputDecoration(hintText: "Owner (SID) password"), + controller: _passwordController, + ); + + return Center( + child: SizedBox( + width: 280, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + instruction, + const SizedBox(height: 6), + password, + const SizedBox(height: 4), + forgot, + ], + ), + ), + ); + } + + Future _takeOwnership() async { + if (_passwordController.text != _repeatController.text) { + throw Exception("the two passwords don't match"); + } + // Start a session on the Admin SP + final session = await _encryptedDevice.login(_adminSpUid); + try { + // Authenticate as SID using the MSID password. + final cPinMsid = await _encryptedDevice.findUid("C_PIN::MSID", securityProvider: _adminSpUid); + final cPinSid = await _encryptedDevice.findUid("C_PIN::SID", securityProvider: _adminSpUid); + final msidPassword = await session.getValue(cPinMsid, 3); + await session.authenticateBytes(_sidUid, msidPassword.getBytes()); + // Change SID password to the user-provided one. + final password = Value.bytesFromString(_passwordController.text); + await session.setValue(cPinSid, 3, password); + } finally { + await session.end(); + } + } + + Future _checkOwnerCredentials() async { + final session = await _encryptedDevice.login(_adminSpUid); + try { + // Authenticate as SID using the user-provided password. + await session.authenticate(_sidUid, _passwordController.text); + } finally { + await session.end(); + } + } + + Future _takeOrCheckOwnership() async { + if (await _isAlreadyOwned()) { + await _checkOwnerCredentials(); + } else { + await _takeOwnership(); + } + } + + Future _isAlreadyOwned() async { + final session = await _encryptedDevice.login(_adminSpUid); + try { + final cPinMsid = await _encryptedDevice.findUid("C_PIN::MSID", securityProvider: _adminSpUid); + final msidPassword = await session.getValue(cPinMsid, 3); + await session.authenticateBytes(_sidUid, msidPassword.getBytes()); + return false; + } catch (ex) { + return true; + } finally { + await session.end(); + } + } + + Widget _next(BuildContext context) { + return _ActivateLockingPage(_encryptedDevice, _passwordController.text); + } + + @override + Widget build(BuildContext context) { + return WizardPage( + title: "Take ownership", + onValidate: _takeOrCheckOwnership, + onNext: _next, + child: FutureBuilder( + future: _isAlreadyOwned(), + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => const StatusPage.waiting(message: "Checking device ownership..."), + error: (context, error) => StatusPage.error(message: "Error: $error"), + success: (context, data) => data! ? _buildAlreadyOwned(context) : _buildNewOwner(context), + ); + }, + ), + ); + } +} + +class LockingWizard extends StatelessWidget { + const LockingWizard(this._storageDevice, {super.key}); + + final StorageDevice _storageDevice; + + @override + Widget build(BuildContext context) { + return EncryptedDeviceBuilder( + _storageDevice, + builder: (context, encryptedDevice) { + return SnapshotBuilder( + encryptedDevice, + error: (context, error) => Material( + child: StatusPage.error( + message: "Failed to open device: $error", + onClose: () => Navigator.of(context).pop(), + ), + ), + waiting: (context) => Material( + child: StatusPage.waiting( + message: "Opening device...", + onClose: () => Navigator.of(context).pop(), + ), + ), + success: (context, data) => _OwnershipPage(data!), + ); + }, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/stack_reset_page.dart b/src/SEDManagerGUI/lib/interface/stack_reset_page.dart new file mode 100644 index 0000000..a5f531b --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/stack_reset_page.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/bindings/storage_device.dart'; +import 'package:sed_manager_gui/interface/components/encrypted_device_builder.dart'; +import 'components/snapshot_builder.dart'; +import 'components/status_page.dart'; + +class StackResetPage extends StatelessWidget { + const StackResetPage(this.storageDevice, {super.key}); + final StorageDevice storageDevice; + + static Future _stackReset(EncryptedDevice encryptedDevice) async { + await encryptedDevice.stackReset(); + } + + void _close(BuildContext context) { + Navigator.of(context).pop(); + } + + Widget _buildBody(EncryptedDevice encryptedDevice) { + return FutureBuilder( + future: _stackReset(encryptedDevice), + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => StatusPage.waiting( + message: "Resetting stack...", + onClose: () => _close(context), + ), + success: (context, data) => StatusPage.success( + message: "Stack successfully reset!", + onClose: () => _close(context), + ), + error: (context, error) => StatusPage.error( + message: "Error: $error", + onClose: () => _close(context), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Stack reset")), + body: EncryptedDeviceBuilder( + storageDevice, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + error: (context, error) => StatusPage.error( + message: "Failed to open device: $error", + onClose: () => Navigator.of(context).pop(), + ), + waiting: (context) => StatusPage.waiting( + message: "Opening device...", + onClose: () => Navigator.of(context).pop(), + ), + success: (context, data) => _buildBody(data!), + ); + }, + ), + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/table_cell_view.dart b/src/SEDManagerGUI/lib/interface/table_cell_view.dart new file mode 100644 index 0000000..7931b2b --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/table_cell_view.dart @@ -0,0 +1,470 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/interface/components/snapshot_builder.dart'; +import 'package:table_sticky_headers/table_sticky_headers.dart'; +import 'package:sed_manager_gui/bindings/type.dart'; +import 'components/status_indicator.dart'; + +class TableCell extends StatelessWidget { + const TableCell({ + this.fill = false, + required this.child, + super.key, + }); + + final bool fill; + final Widget child; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + final border = Border.all(color: colorScheme.outlineVariant); + + return Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: border, + color: fill ? colorScheme.secondary : colorScheme.surface, + ), + child: Container( + margin: const EdgeInsets.all(2), + child: child, + ), + ); + } +} + +class TableHeaderCell extends StatelessWidget { + const TableHeaderCell(this.name, {super.key}); + + final String name; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return TableCell( + fill: true, + child: Text( + name, + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + color: colorScheme.onPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ); + } +} + +class TableUIDCell extends StatelessWidget { + const TableUIDCell( + this.encryptedDevice, + this.object, + this.securityProvider, { + super.key, + }); + + final EncryptedDevice encryptedDevice; + final UID object; + final UID securityProvider; + + Future _getFriendlyName() async { + try { + return await encryptedDevice.findName(object, securityProvider: securityProvider); + } catch (ex) { + return null; + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getFriendlyName(), + builder: (context, snapshot) { + final text = snapshot.data ?? object.toRadixString(16).padLeft(16, '0'); + return Tooltip( + waitDuration: Durations.long4, + message: text, + child: TableCell( + child: Text(text, overflow: TextOverflow.ellipsis), + ), + ); + }, + ); + } +} + +class TableCellEditDialog extends StatefulWidget { + const TableCellEditDialog( + this.encryptedDevice, + this.session, + this.object, + this.column, + this.type, + this.initialValue, { + this.onFinished, + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + final UID object; + final int column; + final Type type; + final String initialValue; + final void Function()? onFinished; + + @override + State createState() => _TableCellEditDialogState(); +} + +class _TableCellEditDialogState extends State { + var _snapshot = const AsyncSnapshot.waiting(); + late final _controller = TextEditingController(text: widget.initialValue); + + Future _setValue(String value) async { + try { + final parsed = widget.encryptedDevice.parseValue(value, widget.type, widget.session.securityProvider); + await widget.session.setValue(widget.object, widget.column, parsed); + setState(() { + _snapshot = const AsyncSnapshot.withData(ConnectionState.done, true); + }); + } catch (ex) { + setState(() { + _snapshot = AsyncSnapshot.withError(ConnectionState.done, ex); + }); + } + } + + void _set() { + _setValue(_controller.text); + } + + void _close() { + Navigator.of(context).pop(); + widget.onFinished?.call(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + const title = Text("Edit cell value", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)); + + final textEdit = TextField( + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.top, + style: const TextStyle(fontSize: 13, fontFamily: "CascadiaCode"), + expands: true, + maxLines: null, + controller: _controller, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(6)), + borderSide: BorderSide(color: colorScheme.outlineVariant), + ), + contentPadding: const EdgeInsets.all(2), + ), + ); + + final errorStrip = _snapshot.hasData + ? const StatusIndicator.success() + : _snapshot.hasError + ? StatusIndicator.error(message: _snapshot.error!.toString()) + : const StatusIndicator.none(); + + final setButton = OutlinedButton(onPressed: _set, child: const Text("Set")); + + final closeButton = OutlinedButton( + onPressed: _close, + child: const Text("Close"), + ); + + return Dialog( + child: FractionallySizedBox( + widthFactor: 0.66, + heightFactor: 0.60, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + child: Column( + children: [ + const Center(child: title), + const SizedBox(height: 6), + Expanded(child: textEdit), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded(child: errorStrip), + const SizedBox(width: 6), + setButton, + const SizedBox(width: 6), + closeButton, + ], + ), + ], + ), + ), + ), + ); + } +} + +class TableValueCell extends StatelessWidget { + TableValueCell( + this.encryptedDevice, + this.session, + this.object, + this.column, + this.type, { + super.key, + }) { + _getValue().ignore(); + } + + final EncryptedDevice encryptedDevice; + final Session session; + final UID object; + final int column; + final Type type; + final _controller = TextEditingController(); + final _currentValue = StreamController(); + + Future _showFailureDialog(BuildContext context, String message) { + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Could not set value"), + content: Text(message), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + Future _getValue() async { + try { + final value = await session.getValue(object, column); + final rendered = value.hasValue ? encryptedDevice.renderValue(value, type, session.securityProvider) : ""; + _currentValue.add(rendered); + } catch (ex) { + _currentValue.addError(ex); + } + } + + Future _setValue(BuildContext context, String value) async { + try { + final parsed = encryptedDevice.parseValue(value, type, session.securityProvider); + await session.setValue(object, column, parsed); + _getValue().ignore(); + } catch (ex) { + if (context.mounted) { + _showFailureDialog(context, ex.toString()); + } + } + } + + Widget _buildExpandable(BuildContext context, {required String initialValue, required Widget child}) { + return GestureDetector( + child: child, + onDoubleTap: () { + showDialog( + context: context, + builder: (context) { + return TableCellEditDialog( + encryptedDevice, + session, + object, + column, + type, + initialValue, + onFinished: () => _getValue().ignore(), + ); + }, + ); + }, + ); + } + + Widget _buildContentWithData(BuildContext context, String data) { + _controller.text = data; + + return TextField( + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.center, + autocorrect: false, + readOnly: false, + maxLines: 1, + controller: _controller, + decoration: const InputDecoration( + border: OutlineInputBorder(borderRadius: BorderRadius.zero, borderSide: BorderSide.none), + contentPadding: EdgeInsets.all(0), + ), + onSubmitted: (value) => _setValue(context, value).ignore(), + ); + } + + Widget _buildContentWithError(Object error) { + return Tooltip( + message: error.toString(), + child: const SizedBox(width: 14, height: 14, child: Icon(Icons.error_rounded)), + ); + } + + Widget _buildContentWaiting() { + return const Center( + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _currentValue.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => TableCell(child: _buildContentWaiting()), + success: (context, data) { + final child = _buildContentWithData(context, data!); + return TableCell(child: _buildExpandable(context, initialValue: data, child: child)); + }, + error: (context, error) { + final child = _buildContentWithError(error); + return TableCell(child: _buildExpandable(context, initialValue: "", child: child)); + }, + ); + }, + ); + } +} + +class ColumnDesc { + ColumnDesc(this.name, this.type); + final String name; + final Type type; +} + +class TableCellView extends StatelessWidget { + const TableCellView( + this.encryptedDevice, + this.session, + this.table, { + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + final UID table; + + Future<(List, List)> _getLayout() async { + final rows = await session.getTableRows(table).toList(); + final columns = []; + for (int column = 0; column < session.getColumnCount(table); ++column) { + columns.add(ColumnDesc( + session.getColumnName(table, column), + session.getColumnType(table, column), + )); + } + return (rows, columns); + } + + Widget _buildWithData( + BuildContext context, + List rows, + List columns, + ) { + Widget headerBuilder(columnIdx) { + return TableHeaderCell(columns[columnIdx + 1].name); + } + + Widget rowBuilder(rowIdx) { + return TableUIDCell(encryptedDevice, rows[rowIdx], session.securityProvider); + } + + Widget valueBuilder(columnIdx, rowIdx) { + return TableValueCell( + encryptedDevice, + session, + rows[rowIdx], + columnIdx + 1, + columns[columnIdx + 1].type, + ); + } + + final cellDimensions = CellDimensions.variableColumnWidth( + columnWidths: List.filled(columns.length - 1, 120.0), + contentCellHeight: 26, + stickyLegendWidth: 256, + stickyLegendHeight: 26, + ); + + return StickyHeadersTable( + columnsLength: columns.length - 1, + rowsLength: rows.length, + columnsTitleBuilder: headerBuilder, + rowsTitleBuilder: rowBuilder, + contentCellBuilder: valueBuilder, + legendCell: headerBuilder(-1), + showHorizontalScrollbar: true, + showVerticalScrollbar: true, + cellDimensions: cellDimensions, + ); + } + + Widget _buildWithError(Object error) { + return Text(error.toString()); + } + + Widget _buildWaiting() { + return const Column(children: [ + SizedBox( + width: 48, + height: 48, + child: CircularProgressIndicator(), + ), + Text("Loading layout..."), + ]); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getLayout(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final rows = snapshot.data!.$1; + final columns = snapshot.data!.$2; + return _buildWithData(context, rows, columns); + } else if (snapshot.hasError) { + return _buildWithError(snapshot.error!); + } + return _buildWaiting(); + }, + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/table_editor_page.dart b/src/SEDManagerGUI/lib/interface/table_editor_page.dart new file mode 100644 index 0000000..4914ff7 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/table_editor_page.dart @@ -0,0 +1,420 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:marquee_widget/marquee_widget.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/bindings/storage_device.dart'; +import 'package:sed_manager_gui/interface/components/status_indicator.dart'; +import 'package:sed_manager_gui/interface/components/utility.dart'; +import 'package:sed_manager_gui/interface/table_cell_view.dart'; +import 'components/encrypted_device_builder.dart'; +import 'components/session_builder.dart'; +import 'components/cached_stream.dart'; +import 'components/snapshot_builder.dart'; +import 'components/status_page.dart'; +import 'table_editor_tools_view.dart'; +import 'components/object_dropdown.dart'; + +class SecurityProviderDropdown extends StatelessWidget { + SecurityProviderDropdown({ + this.onSelected, + super.key, + }); + + SecurityProviderDropdown.fetch( + EncryptedDevice encryptedDevice, { + this.onSelected, + super.key, + }) { + () async { + final admin = await encryptedDevice.findUid("SP::Admin"); + final session = await encryptedDevice.login(admin); + try { + update(await getSecurityProviders(encryptedDevice, session)); + } finally { + await session.end(); + } + }() + .ignore(); + } + + final void Function(UID)? onSelected; + final _securityProviderStream = StreamController>(); + + void update(List<(UID, String)> securityProviders) { + _securityProviderStream.add(securityProviders); + } + + static Future _isSecurityProviderActive(UID subjectSp, Session session) async { + const issued = 0; + const disabled = 1; + const manufactured = 9; + const manufacturedDisabled = 10; + try { + final lifeCycleState = (await session.getValue(subjectSp, 6)).getInteger(); + final canOpenSession = {issued, disabled, manufactured, manufacturedDisabled}.contains(lifeCycleState); + return canOpenSession; + } catch (ex) { + return true; + } + } + + static Future> getSecurityProviders(EncryptedDevice encryptedDevice, Session session) async { + final table = await encryptedDevice.findUid("SP"); + final securityProviders = <(UID, String)>[]; + await for (final sp in session.getTableRows(table)) { + if (await _isSecurityProviderActive(sp, session)) { + securityProviders.add((sp, await getDisplayName(sp, encryptedDevice))); + } + } + return securityProviders; + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _securityProviderStream.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => const ObjectDropdown( + [], + hintText: "Loading...", + enabled: false, + width: 240, + ), + success: (context, sps) => ObjectDropdown( + sps!, + hintText: "Select security provider", + width: 240, + onSelected: onSelected, + ), + error: (context, error) => Tooltip( + message: error.toString(), + child: const ObjectDropdown( + [], + enabled: false, + hintText: "Error", + width: 240, + ), + ), + ); + }, + ); + } +} + +class TableRowListView extends StatelessWidget { + const TableRowListView( + this.encryptedDevice, + this.session, { + this.onSelected, + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + final void Function(UID)? onSelected; + + Future> _getTables() async { + final tableTable = await encryptedDevice.findUid("Table", securityProvider: session.securityProvider); + + List<(UID, String)> tables = []; + try { + await for (final tableDesc in session.getTableRows(tableTable)) { + final table = tableDesc << 32; + try { + final name = await encryptedDevice.findName(table); + tables.add((table, name)); + } catch (ex) { + tables.add((table, table.toRadixString(16).padLeft(16, '0'))); + } + } + return tables; + } catch (ex) { + rethrow; + } + } + + Widget _buildBody(BuildContext context, List<(UID, String)> tables) { + final entries = tables.map((table) { + return NavigationDrawerDestination( + icon: Icon(IconData(table.$2.codeUnits[0])), + label: Text(table.$2), + ); + }).toList(); + + final header = Container( + margin: const EdgeInsets.all(6), + child: Center( + child: Text( + "Tables", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + + final selected = StreamController(); + return StreamBuilder( + stream: selected.stream, + builder: (context, snapshot) { + return NavigationDrawer( + tilePadding: const EdgeInsets.symmetric(horizontal: 8), + onDestinationSelected: (value) { + onSelected?.call(tables[value].$1); + selected.add(value); + }, + selectedIndex: snapshot.data, + children: [header, ...entries], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _getTables(), + builder: (context, snapshot) { + return SizedBox( + width: 200, + child: SnapshotBuilder( + snapshot, + waiting: (context) => const StatusIndicator.waiting( + message: "Loading tables...", + iconSize: 48, + messagePosition: AxisDirection.down, + ), + error: (context, error) => StatusIndicator.waiting( + message: "Error: $error", + iconSize: 48, + messagePosition: AxisDirection.down, + ), + success: (context, data) => _buildBody(context, data!), + ), + ); + }, + ); + } +} + +class TableEditorPage extends StatelessWidget { + TableEditorPage(this.storageDevice, {super.key}); + final StorageDevice storageDevice; + final securityProviderStream = StreamController(); + + void _onSecurityProvider(UID securityProvider) { + securityProviderStream.add(securityProvider); + } + + static Stream> _accumulateAuthorities(Stream authorities) async* { + Set collection = {}; + await for (final authority in authorities) { + collection.add(authority); + yield collection; + } + } + + static Stream> _stringifyAuthoritySets( + EncryptedDevice encryptedDevice, + Stream> authoritySets, + UID securityProvider, + ) async* { + await for (final authoritySet in authoritySets) { + final nameSet = []; + for (final authority in authoritySet) { + try { + nameSet.add(await encryptedDevice.findName(authority, securityProvider: securityProvider)); + } catch (ex) { + nameSet.add(authority.toRadixString(16).padLeft(16, '0')); + } + } + nameSet.sort(); + yield nameSet; + } + } + + static Widget _buildSessionContent( + BuildContext context, + EncryptedDevice encryptedDevice, + Session session, { + void Function(UID authority)? onAuthenticated, + void Function(UID securityProvider)? onActivated, + }) { + final tableStream = StreamController(); + final cachedTableStream = CachedStream(tableStream.stream); + final authorityStream = StreamController(); + final accumulatedAuthorityStream = _stringifyAuthoritySets( + encryptedDevice, + _accumulateAuthorities(authorityStream.stream), + session.securityProvider, + ); + + final authoritiesView = StreamBuilder( + stream: accumulatedAuthorityStream, + builder: (context, snapshot) { + final text = (snapshot.data ?? ["Anybody"]).join(" "); + return Marquee(child: Text(text, style: const TextStyle(color: Colors.green))); + }, + ); + + final tableListView = TableRowListView( + encryptedDevice, + session, + onSelected: (table) { + tableStream.add(table); + }, + ); + + final tableView = StreamBuilder( + stream: cachedTableStream.stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + final table = snapshot.data!; + return TableCellView( + encryptedDevice, + session, + table, + key: ObjectKey((session, table)), + ); + } + return const Center(child: Text("Select a table.")); + }, + ); + + final toolsView = TableEditorToolsView( + encryptedDevice, + session, + onAuthenticated: (authority) { + authorityStream.add(authority); + if (cachedTableStream.latest != null) { + tableStream.add(cachedTableStream.latest!); + } + onAuthenticated?.call(authority); + }, + onActivated: onActivated, + ); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + tableListView, + const SizedBox(width: 12), + Expanded( + child: + Column(children: [Expanded(flex: 1, child: tableView), SizedBox(height: 32, child: authoritiesView)])), + const SizedBox(width: 12), + Center(child: toolsView), + ], + ); + } + + static Widget _buildSession( + BuildContext context, + EncryptedDevice encryptedDevice, + UID securityProvider, { + void Function(UID authority)? onAuthenticated, + void Function(UID securityProvider)? onActivated, + }) { + return SessionBuilder( + encryptedDevice, + securityProvider, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => const StatusPage.waiting(message: "Starting session..."), + error: (context, error) => StatusPage.error(message: "Session failed: $error"), + success: (context, data) => _buildSessionContent( + context, + encryptedDevice, + snapshot.data!, + onAuthenticated: onAuthenticated, + onActivated: onActivated, + ), + ); + }, + ); + } + + Widget _buildBody(EncryptedDevice encryptedDevice) { + final securityProviderDropdown = SecurityProviderDropdown.fetch( + encryptedDevice, + onSelected: _onSecurityProvider, + ); + + const selectionPrompt = Center(child: Text("Select a security provider.")); + + final sessionPanel = StreamBuilder( + stream: securityProviderStream.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + none: (context) => selectionPrompt, + waiting: (context) => selectionPrompt, + error: (context, error) => const StatusIndicator.error(message: "no security providers available."), + success: (context, securityProvider) { + return SessionBuilder( + encryptedDevice, + securityProvider!, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => const StatusPage.waiting(message: "Starting session..."), + error: (context, error) => StatusPage.error(message: "Session failed: $error"), + success: (context, session) => _buildSessionContent( + context, + encryptedDevice, + session!, + onAuthenticated: null, + onActivated: (securityProvider) async => securityProviderDropdown + .update(await SecurityProviderDropdown.getSecurityProviders(encryptedDevice, session)), + ), + ); + }, + ); + }, + ); + }, + ); + + return Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 7), + securityProviderDropdown, + const Divider(height: 13, thickness: 1, indent: 8, endIndent: 8), + Expanded(child: sessionPanel), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Table editor")), + body: EncryptedDeviceBuilder( + storageDevice, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + error: (context, error) => StatusPage.error( + message: "Failed to open device: $error", + onClose: () => Navigator.of(context).pop(), + ), + waiting: (context) => StatusPage.waiting( + message: "Opening device...", + onClose: () => Navigator.of(context).pop(), + ), + success: (context, data) => _buildBody(data!), + ); + }, + ), + ); + } +} diff --git a/src/SEDManagerGUI/lib/interface/table_editor_tools_view.dart b/src/SEDManagerGUI/lib/interface/table_editor_tools_view.dart new file mode 100644 index 0000000..f0b6bc8 --- /dev/null +++ b/src/SEDManagerGUI/lib/interface/table_editor_tools_view.dart @@ -0,0 +1,643 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/bindings/value.dart'; +import 'package:sed_manager_gui/interface/components/snapshot_builder.dart'; +import 'package:sed_manager_gui/interface/components/status_indicator.dart'; +import 'package:sed_manager_gui/interface/components/object_dropdown.dart'; +import 'package:sed_manager_gui/interface/components/utility.dart'; + +Future> _getObjects( + EncryptedDevice encryptedDevice, + Session session, + String tableName, + FutureOr Function(UID object, Session session)? filter, +) async { + final table = await encryptedDevice.findUid(tableName, securityProvider: session.securityProvider); + final objects = <(UID, String)>[]; + + await for (final object in session.getTableRows(table)) { + final include = await filter?.call(object, session) ?? true; + if (include) { + objects.add((object, await getDisplayName(object, encryptedDevice, securityProvider: session.securityProvider))); + } + } + return objects; +} + +Widget _buildObjectDropdown( + Future> objects, + void Function(UID object) onSelected, +) { + return FutureBuilder( + future: objects, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + waiting: (context) => const ObjectDropdown( + [], + width: 280, + hintText: "Loading...", + enabled: false, + ), + success: (context, data) => ObjectDropdown( + data!, + width: 280, + enabled: true, + onSelected: onSelected, + ), + error: (context, error) => Tooltip( + message: error.toString(), + child: const ObjectDropdown( + [], + width: 280, + hintText: "Error", + enabled: false, + )), + ); + }); +} + +class TableEditorToolDialog extends StatelessWidget { + const TableEditorToolDialog( + this.title, { + required this.children, + super.key, + }); + + final String title; + final List children; + + @override + Widget build(BuildContext context) { + final header = Text(title, style: TextStyle(fontSize: 18, color: Theme.of(context).colorScheme.primary)); + + const separator = SizedBox(height: 6); + final separatedChildren = [header]; + for (final child in children) { + separatedChildren.add(separator); + separatedChildren.add(child); + } + + return Dialog( + child: Container( + margin: const EdgeInsets.all(8), + width: 280, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: separatedChildren, + ), + ), + ); + } +} + +class AuthneticateDialog extends StatefulWidget { + const AuthneticateDialog( + this.encryptedDevice, + this.session, { + this.onAuthenticated, + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + final void Function(UID authority)? onAuthenticated; + + @override + State createState() => _AuthenticateDialogState(); +} + +class _AuthenticateDialogState extends State { + final _passwordController = TextEditingController(); + int? _selectedAuthority; + final _result = StreamController(); + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + void _onAuthenticate() async { + if (_selectedAuthority != null) { + try { + await widget.session.authenticate(_selectedAuthority!, _passwordController.text); + _result.add(null); + widget.onAuthenticated?.call(_selectedAuthority!); + } catch (ex) { + _result.addError(ex); + } + } else { + _result.addError(Exception("select an authority!")); + } + } + + void _onBack(BuildContext context) { + Navigator.of(context).pop(); + } + + static Future _filter(UID authority, Session session) async { + try { + final credentialValue = await session.getValue(authority, 10); + if (!credentialValue.hasValue) { + return true; + } + final credential = credentialValue.getBytes().getUint64(0); + return credential != 0; + } catch (ex) { + return true; + } + } + + @override + Widget build(BuildContext context) { + final authorities = _getObjects(widget.encryptedDevice, widget.session, "Authority", _filter); + final authoritySelector = _buildObjectDropdown( + authorities, + (object) => setState(() { + _selectedAuthority = object; + }), + ); + + final passwordField = TextField( + obscureText: true, + controller: _passwordController, + decoration: const InputDecoration(hintText: "Password"), + ); + + final buttonStrip = Row( + children: [ + Expanded( + flex: 1, + child: FilledButton( + onPressed: _onAuthenticate, + child: const Text("Authenticate"), + ), + ), + const SizedBox(width: 6), + Expanded( + flex: 1, + child: FilledButton( + onPressed: () { + _onBack(context); + }, + child: const Text("Back"), + ), + ), + ], + ); + + final errorStrip = StreamBuilder( + stream: _result.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + none: (context) => const StatusIndicator.none(), + waiting: (context) => const StatusIndicator.none(), + success: (context, data) => const StatusIndicator.success(message: "Authentication successful!"), + error: (context, error) => StatusIndicator.error(message: "Error: $error"), + ); + }, + ); + + return TableEditorToolDialog("Authenticate", children: [ + authoritySelector, + passwordField, + buttonStrip, + Align(alignment: Alignment.centerLeft, child: errorStrip), + ]); + } +} + +class PasswordDialog extends StatefulWidget { + const PasswordDialog( + this.encryptedDevice, + this.session, { + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + + @override + State createState() => _PasswordDialogState(); +} + +class _PasswordDialogState extends State { + final _passwordController = TextEditingController(); + final _repeatController = TextEditingController(); + int? _selectedAuthority; + final _result = StreamController(); + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + void _onChangePassword() async { + if (_selectedAuthority != null) { + if (_passwordController.text == _repeatController.text) { + try { + final credential = await widget.session.getValue(_selectedAuthority!, 10); + final credentialUid = credential.getBytes().getUint64(0); + final password = Value.bytesFromString(_passwordController.text); + await widget.session.setValue(credentialUid, 3, password); + _result.add(null); + } catch (ex) { + _result.addError(ex); + } + } else { + _result.addError(Exception("password do not match!")); + } + } else { + _result.addError(Exception("select an authority!")); + } + } + + void _onBack(BuildContext context) { + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final authorities = _getObjects(widget.encryptedDevice, widget.session, "Authority", null); + final authoritySelector = _buildObjectDropdown( + authorities, + (object) => setState(() { + _selectedAuthority = object; + }), + ); + + final passwordField = TextField( + obscureText: true, + controller: _passwordController, + decoration: const InputDecoration(hintText: "Password"), + ); + + final repeatField = TextField( + obscureText: true, + controller: _repeatController, + decoration: const InputDecoration(hintText: "Repeat password"), + ); + + final buttonStrip = Row( + children: [ + Expanded( + flex: 1, + child: FilledButton( + onPressed: _onChangePassword, + child: const Text("Change"), + ), + ), + const SizedBox(width: 6), + Expanded( + flex: 1, + child: FilledButton( + onPressed: () { + _onBack(context); + }, + child: const Text("Back"), + ), + ), + ], + ); + + final errorStrip = StreamBuilder( + stream: _result.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + none: (context) => const StatusIndicator.none(), + waiting: (context) => const StatusIndicator.none(), + success: (context, data) => const StatusIndicator.success(message: "Password changed!"), + error: (context, error) => StatusIndicator.error(message: "Error: $error"), + ); + }, + ); + + return TableEditorToolDialog("Change password", children: [ + authoritySelector, + passwordField, + repeatField, + buttonStrip, + Align(alignment: Alignment.centerLeft, child: errorStrip), + ]); + } +} + +class GenerateMEKDialog extends StatefulWidget { + const GenerateMEKDialog( + this.encryptedDevice, + this.session, { + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + + @override + State createState() => _GenerateMEKDialogState(); +} + +class _GenerateMEKDialogState extends State { + UID? _selectedLockingRange; + final _result = StreamController(); + + Future _genMEK() async { + if (_selectedLockingRange != null) { + try { + final activeKey = await widget.session.getValue(_selectedLockingRange!, 10); + final activeKeyUid = activeKey.getBytes().getUint64(0); + await widget.session.genMEK(activeKeyUid); + _result.add(null); + } catch (ex) { + _result.addError(ex); + } + } else { + _result.addError(Exception("Select a locking range!")); + } + } + + void _onBack(BuildContext context) { + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final lockingRanges = _getObjects(widget.encryptedDevice, widget.session, "Locking", null); + final lockingRangeSelector = _buildObjectDropdown( + lockingRanges, + (object) => setState(() { + _selectedLockingRange = object; + }), + ); + + const warningText = Row( + children: [ + Icon(Icons.warning_outlined, color: Colors.amber), + SizedBox(width: 6), + Expanded(child: Text("This will erase all data in the selected locking range!")) + ], + ); + + final buttonStrip = Row( + children: [ + Expanded( + flex: 1, + child: FilledButton( + onPressed: () => _genMEK().ignore, + child: const Text("Generate"), + ), + ), + const SizedBox(width: 6), + Expanded( + flex: 1, + child: FilledButton( + onPressed: () { + _onBack(context); + }, + child: const Text("Back"), + ), + ), + ], + ); + + final errorStrip = StreamBuilder( + stream: _result.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + none: (context) => const StatusIndicator.none(), + waiting: (context) => const StatusIndicator.none(), + success: (context, data) => const StatusIndicator.success(message: "Encryption key generated!"), + error: (context, error) => StatusIndicator.error(message: "Error: $error"), + ); + }, + ); + + return TableEditorToolDialog( + "Generate media encryption key", + children: [ + warningText, + lockingRangeSelector, + buttonStrip, + Align(alignment: Alignment.centerLeft, child: errorStrip), + ], + ); + } +} + +class ActivateDialog extends StatefulWidget { + const ActivateDialog( + this.encryptedDevice, + this.session, { + this.onActivated, + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + final void Function(UID securityProvider)? onActivated; + + @override + State createState() => _ActivateDialogState(); +} + +class _ActivateDialogState extends State { + int? _selectedSecurityProvider; + final _result = StreamController(); + + void _onActivate() async { + if (_selectedSecurityProvider != null) { + try { + await widget.session.activate(_selectedSecurityProvider!); + widget.onActivated?.call(_selectedSecurityProvider!); + _result.add(null); + } catch (ex) { + _result.addError(ex); + } + } else { + _result.addError(Exception("select a security provider!")); + } + } + + void _onBack(BuildContext context) { + Navigator.of(context).pop(); + } + + Future _filter(UID subjectSp, Session session) async { + try { + const manufacturedInactive = 8; + final lifeCycleState = (await session.getValue(subjectSp, 6)).getInteger(); + return lifeCycleState == manufacturedInactive; + } catch (ex) { + return false; + } + } + + @override + Widget build(BuildContext context) { + final securityProviders = _getObjects(widget.encryptedDevice, widget.session, "SP", _filter); + final spSelector = _buildObjectDropdown( + securityProviders, + (object) => setState(() { + _selectedSecurityProvider = object; + }), + ); + + final buttonStrip = Row( + children: [ + Expanded( + flex: 1, + child: FilledButton( + onPressed: _onActivate, + child: const Text("Activate"), + ), + ), + const SizedBox(width: 6), + Expanded( + flex: 1, + child: FilledButton( + onPressed: () { + _onBack(context); + }, + child: const Text("Back"), + ), + ), + ], + ); + + final errorStrip = StreamBuilder( + stream: _result.stream, + builder: (context, snapshot) { + return SnapshotBuilder( + snapshot, + none: (context) => const StatusIndicator.none(), + waiting: (context) => const StatusIndicator.none(), + success: (context, data) => const StatusIndicator.success(message: "Security provider activated!"), + error: (context, error) => StatusIndicator.error(message: "Error: $error"), + ); + }, + ); + + return TableEditorToolDialog("Activate security provider", children: [ + spSelector, + buttonStrip, + Align(alignment: Alignment.centerLeft, child: errorStrip), + ]); + } +} + +class TableEditorToolsView extends StatelessWidget { + const TableEditorToolsView( + this.encryptedDevice, + this.session, { + this.onAuthenticated, + this.onActivated, + super.key, + }); + + final EncryptedDevice encryptedDevice; + final Session session; + final void Function(UID authority)? onAuthenticated; + final void Function(UID securityProvider)? onActivated; + + Widget _buildButton(IconData icon, String title, void Function()? onPressed) { + final style = ButtonStyle( + padding: const MaterialStatePropertyAll(EdgeInsets.all(6)), + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + + return Container( + margin: const EdgeInsets.fromLTRB(0, 3, 0, 3), + child: ElevatedButton( + onPressed: onPressed, + style: style, + child: Tooltip( + waitDuration: Durations.medium1, + message: title, + child: Icon(icon, size: 40), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + const UID adminSpUid = 0x0000020500000001; + const UID lockingSpUid = 0x0000020500000002; + + final authenticateButton = _buildButton( + Icons.person, + "Authenticate", + () { + showDialog( + context: context, + builder: (context) => AuthneticateDialog( + encryptedDevice, + session, + onAuthenticated: onAuthenticated, + ), + ); + }, + ); + + final changePassButton = _buildButton(Icons.password, "Change password", () { + showDialog( + context: context, + builder: (context) => PasswordDialog(encryptedDevice, session), + ); + }); + + final genMekButton = _buildButton( + Icons.key, + "Generate media encryption key", + session.securityProvider != lockingSpUid + ? null + : () { + showDialog( + context: context, + builder: (context) => GenerateMEKDialog(encryptedDevice, session), + ); + }, + ); + + final activateButton = _buildButton( + Icons.rocket_launch, + "Activate security provider", + session.securityProvider != adminSpUid + ? null + : () { + showDialog( + context: context, + builder: (context) => ActivateDialog( + encryptedDevice, + session, + onActivated: onActivated, + ), + ); + }, + ); + + return SizedBox( + width: 64, + child: ListView( + shrinkWrap: true, + itemExtent: 70, + children: [ + authenticateButton, + changePassButton, + genMekButton, + activateButton, + ], + ), + ); + } +} diff --git a/src/SEDManagerGUI/lib/main.dart b/src/SEDManagerGUI/lib/main.dart new file mode 100644 index 0000000..683d239 --- /dev/null +++ b/src/SEDManagerGUI/lib/main.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'interface/drive_launcher_page.dart'; + +void main() { + runApp(const SEDManagerApp()); +} + +class SEDManagerApp extends StatelessWidget { + const SEDManagerApp({super.key}); + + ThemeData _getTheme() { + //const accent = Color.fromARGB(255, 25, 145, 45); + const accent = Color.fromARGB(255, 0, 156, 204); + const background = Color.fromARGB(255, 24, 24, 24); + const foreground = Color.fromARGB(255, 220, 220, 220); + const errorTint = Color.fromARGB(255, 242, 16, 16); + const inverse = Color.fromARGB(255, 36, 138, 255); + + final colorScheme = ColorScheme( + background: background, + brightness: Brightness.dark, + error: Color.lerp(foreground, errorTint, 0.75)!, + errorContainer: Color.lerp(background, errorTint, 0.25)!, + inversePrimary: foreground, + inverseSurface: inverse, + onBackground: foreground, + onError: foreground, + onErrorContainer: foreground, + onInverseSurface: foreground, + onPrimary: Color.lerp(foreground, Colors.white, 0.5)!, + onPrimaryContainer: Color.lerp(foreground, Colors.white, 0.5)!, + onSecondary: Color.lerp(foreground, Colors.white, 0.5)!, + onSecondaryContainer: Color.lerp(foreground, Colors.white, 0.5)!, + onSurface: foreground, + onSurfaceVariant: foreground, + onTertiary: foreground, + onTertiaryContainer: foreground, + outline: Color.lerp(accent, background, 0.2)!, + outlineVariant: Color.lerp(accent, background, 0.5)!, + primary: accent, + primaryContainer: Color.lerp(accent, background, 0.15)!, + scrim: Color.lerp(accent, background, 0.15)!, + secondary: Color.lerp(accent, background, 0.60)!, + secondaryContainer: Color.lerp(accent, background, 0.68)!, + shadow: Colors.black, + surface: Color.lerp(foreground, background, 0.85)!, + surfaceTint: accent, + surfaceVariant: Color.lerp(foreground, background, 0.90)!, + tertiary: Color.lerp(accent, background, 0.82)!, + tertiaryContainer: Color.lerp(accent, background, 0.88)!, + ); + + return ThemeData( + colorScheme: colorScheme, + appBarTheme: AppBarTheme( + backgroundColor: colorScheme.tertiary, + foregroundColor: colorScheme.onSecondary, + ), + useMaterial3: true, + ); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'SEDManager', + theme: _getTheme(), + home: DriveLauncherPage(() {}), + ); + } +} diff --git a/src/SEDManagerGUI/linux/.gitignore b/src/SEDManagerGUI/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/src/SEDManagerGUI/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/src/SEDManagerGUI/linux/CMakeLists.txt b/src/SEDManagerGUI/linux/CMakeLists.txt new file mode 100644 index 0000000..7cbb40d --- /dev/null +++ b/src/SEDManagerGUI/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "SEDManagerGUI") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "petiaccja.sed_manager_gui") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/src/SEDManagerGUI/linux/flutter/CMakeLists.txt b/src/SEDManagerGUI/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/src/SEDManagerGUI/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/src/SEDManagerGUI/linux/flutter/generated_plugin_registrant.cc b/src/SEDManagerGUI/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/src/SEDManagerGUI/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/src/SEDManagerGUI/linux/flutter/generated_plugin_registrant.h b/src/SEDManagerGUI/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/src/SEDManagerGUI/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/src/SEDManagerGUI/linux/flutter/generated_plugins.cmake b/src/SEDManagerGUI/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/src/SEDManagerGUI/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/src/SEDManagerGUI/linux/main.cc b/src/SEDManagerGUI/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/src/SEDManagerGUI/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/src/SEDManagerGUI/linux/my_application.cc b/src/SEDManagerGUI/linux/my_application.cc new file mode 100644 index 0000000..6dd6a04 --- /dev/null +++ b/src/SEDManagerGUI/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "SEDManager"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "SEDManager"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/src/SEDManagerGUI/linux/my_application.h b/src/SEDManagerGUI/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/src/SEDManagerGUI/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/src/SEDManagerGUI/pubspec.yaml b/src/SEDManagerGUI/pubspec.yaml new file mode 100644 index 0000000..f1aefa9 --- /dev/null +++ b/src/SEDManagerGUI/pubspec.yaml @@ -0,0 +1,89 @@ +name: sed_manager_gui +description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.1.5 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + ffi: ^2.1.0 + table_sticky_headers: ^2.0.5 + marquee_widget: ^1.2.0 + path: ^1.8.3 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: CascadiaCode + fonts: + - asset: fonts/CascadiaMono.ttf + - asset: fonts/CascadiaMonoItalic.ttf + style: italic + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/src/SEDManagerGUI/test/test_components/test_encrypted_device_builder.dart b/src/SEDManagerGUI/test/test_components/test_encrypted_device_builder.dart new file mode 100644 index 0000000..0277166 --- /dev/null +++ b/src/SEDManagerGUI/test/test_components/test_encrypted_device_builder.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sed_manager_gui/interface/components/encrypted_device_builder.dart'; +import '../utility.dart'; + +void testEncryptedDeviceBuilder() { + group("EncryptedDeviceBuilder", () { + testWidgets('with mock SD', (WidgetTester tester) async { + await tester.pumpWidget(standalone(EncryptedDeviceBuilder( + mockSD(), + builder: (context, snapshot) => const Text("BUILT"), + ))); + expect(find.text('BUILT'), findsOneWidget); + }); + }); +} diff --git a/src/SEDManagerGUI/test/test_components/test_session_builder.dart b/src/SEDManagerGUI/test/test_components/test_session_builder.dart new file mode 100644 index 0000000..98caf26 --- /dev/null +++ b/src/SEDManagerGUI/test/test_components/test_session_builder.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sed_manager_gui/bindings/encrypted_device.dart'; +import 'package:sed_manager_gui/interface/components/session_builder.dart'; +import '../utility.dart'; + +void testSessionBuilder() { + group("SessionBuilder", () { + testWidgets('with mock SD', (WidgetTester tester) async { + final sd = mockSD(); + final maybeEncryptedDevice = await tester.runAsync(() async => await EncryptedDevice.create(sd)); + final maybeAdminSp = await tester.runAsync(() async => await maybeEncryptedDevice!.findUid("SP::Admin")); + await tester.pumpWidget(standalone(SessionBuilder( + maybeEncryptedDevice!, + maybeAdminSp!, + builder: (context, snapshot) => const Text("BUILT"), + ))); + expect(find.text('BUILT'), findsOneWidget); + }); + }); +} diff --git a/src/SEDManagerGUI/test/test_components/test_snapshot_builder.dart b/src/SEDManagerGUI/test/test_components/test_snapshot_builder.dart new file mode 100644 index 0000000..3bfda66 --- /dev/null +++ b/src/SEDManagerGUI/test/test_components/test_snapshot_builder.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sed_manager_gui/interface/components/snapshot_builder.dart'; +import '../utility.dart'; + +Widget snapshotBuilder(AsyncSnapshot snapshot) { + return SnapshotBuilder( + snapshot, + none: (context) => const Text("NONE"), + waiting: (context) => const Text("WAITING"), + success: (context, value) => const Text("SUCCESS"), + error: (context, object) => const Text("ERROR"), + ); +} + +void testSnapshotBuilder() { + group("SnapshotBuilder", () { + testWidgets('none', (WidgetTester tester) async { + const snapshot = AsyncSnapshot.nothing(); + await tester.pumpWidget(standalone(snapshotBuilder(snapshot))); + expect(find.text('NONE'), findsOneWidget); + }); + + testWidgets('waiting', (WidgetTester tester) async { + const snapshot = AsyncSnapshot.waiting(); + await tester.pumpWidget(standalone(snapshotBuilder(snapshot))); + expect(find.text('WAITING'), findsOneWidget); + }); + + testWidgets('success', (WidgetTester tester) async { + const snapshot = AsyncSnapshot.withData(ConnectionState.done, null); + await tester.pumpWidget(standalone(snapshotBuilder(snapshot))); + expect(find.text('SUCCESS'), findsOneWidget); + }); + + testWidgets('error', (WidgetTester tester) async { + const snapshot = AsyncSnapshot.withError(ConnectionState.done, ""); + await tester.pumpWidget(standalone(snapshotBuilder(snapshot))); + expect(find.text('ERROR'), findsOneWidget); + }); + }); +} diff --git a/src/SEDManagerGUI/test/test_components/test_status_indicator.dart b/src/SEDManagerGUI/test/test_components/test_status_indicator.dart new file mode 100644 index 0000000..927d598 --- /dev/null +++ b/src/SEDManagerGUI/test/test_components/test_status_indicator.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sed_manager_gui/interface/components/status_indicator.dart'; +import '../utility.dart'; + +void testStatusIndicator() { + group("StatusIndicator", () { + testWidgets('message / none', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.none(message: "STATUS"))); + expect(find.text('STATUS'), findsOneWidget); + }); + + testWidgets('message / waiting', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.waiting(message: "STATUS"))); + expect(find.text('STATUS'), findsOneWidget); + }); + + testWidgets('message / success', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.success(message: "STATUS"))); + expect(find.text('STATUS'), findsOneWidget); + }); + + testWidgets('message / error', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.error(message: "STATUS"))); + expect(find.text('STATUS'), findsOneWidget); + }); + + testWidgets('icon / none', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.none(iconSize: 64))); + final iconFinder = find.byType(Icon); + expect(iconFinder, findsOneWidget); + expect(iconFinder.found.first.size!.height, 64); + }); + + testWidgets('icon / waiting', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.waiting(iconSize: 64))); + final iconFinder = find.byType(CircularProgressIndicator); + expect(iconFinder, findsOneWidget); + expect(iconFinder.found.first.size!.height, 64); + }); + + testWidgets('icon / success', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.success(iconSize: 64))); + final iconFinder = find.byType(Icon); + expect(iconFinder, findsOneWidget); + expect(iconFinder.found.first.size!.height, 64); + }); + + testWidgets('icon / error', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.error(iconSize: 64))); + final iconFinder = find.byType(Icon); + expect(iconFinder, findsOneWidget); + expect(iconFinder.found.first.size!.height, 64); + }); + + testWidgets('text position down', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.success( + iconSize: 64, + message: "STATUS", + messagePosition: AxisDirection.down, + ))); + final iconFinder = find.byType(Icon); + final textFinder = find.byType(Text); + expect(iconFinder, findsOneWidget); + expect(textFinder, findsOneWidget); + expect(tester.getCenter(iconFinder).dy, lessThan(tester.getCenter(textFinder).dy)); + }); + + testWidgets('text position up', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.success( + iconSize: 64, + message: "STATUS", + messagePosition: AxisDirection.up, + ))); + final iconFinder = find.byType(Icon); + final textFinder = find.byType(Text); + expect(iconFinder, findsOneWidget); + expect(textFinder, findsOneWidget); + expect(tester.getCenter(iconFinder).dy, greaterThan(tester.getCenter(textFinder).dy)); + }); + + testWidgets('text position left', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.success( + iconSize: 64, + message: "STATUS", + messagePosition: AxisDirection.left, + ))); + final iconFinder = find.byType(Icon); + final textFinder = find.byType(Text); + expect(iconFinder, findsOneWidget); + expect(textFinder, findsOneWidget); + expect(tester.getCenter(iconFinder).dx, lessThan(tester.getCenter(textFinder).dx)); + }); + + testWidgets('text position right', (WidgetTester tester) async { + await tester.pumpWidget(standalone(const StatusIndicator.success( + iconSize: 64, + message: "STATUS", + messagePosition: AxisDirection.right, + ))); + final iconFinder = find.byType(Icon); + final textFinder = find.byType(Text); + expect(iconFinder, findsOneWidget); + expect(textFinder, findsOneWidget); + expect(tester.getCenter(iconFinder).dx, greaterThan(tester.getCenter(textFinder).dx)); + }); + }); +} diff --git a/src/SEDManagerGUI/test/utility.dart b/src/SEDManagerGUI/test/utility.dart new file mode 100644 index 0000000..30ce581 --- /dev/null +++ b/src/SEDManagerGUI/test/utility.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:sed_manager_gui/bindings/storage_device.dart'; + +Widget standalone(Widget testee) { + return MaterialApp(home: testee); +} + +StorageDevice mockSD() { + return StorageDevice("/dev/mock_device"); +} \ No newline at end of file diff --git a/src/SEDManagerGUI/test/widget_test.dart b/src/SEDManagerGUI/test/widget_test.dart new file mode 100644 index 0000000..4ce387b --- /dev/null +++ b/src/SEDManagerGUI/test/widget_test.dart @@ -0,0 +1,11 @@ +import 'test_components/test_status_indicator.dart'; +import 'test_components/test_snapshot_builder.dart'; +import 'test_components/test_encrypted_device_builder.dart'; +import 'test_components/test_session_builder.dart'; + +void main() { + testStatusIndicator(); + testSnapshotBuilder(); + testSessionBuilder(); + testEncryptedDeviceBuilder(); +} diff --git a/src/SEDManagerGUI/windows/.gitignore b/src/SEDManagerGUI/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/src/SEDManagerGUI/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/src/SEDManagerGUI/windows/CMakeLists.txt b/src/SEDManagerGUI/windows/CMakeLists.txt new file mode 100644 index 0000000..aae9ab9 --- /dev/null +++ b/src/SEDManagerGUI/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(sed_manager_gui LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "SEDManagerGUI") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/src/SEDManagerGUI/windows/flutter/CMakeLists.txt b/src/SEDManagerGUI/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/src/SEDManagerGUI/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/src/SEDManagerGUI/windows/flutter/generated_plugin_registrant.cc b/src/SEDManagerGUI/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/src/SEDManagerGUI/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/src/SEDManagerGUI/windows/flutter/generated_plugin_registrant.h b/src/SEDManagerGUI/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/src/SEDManagerGUI/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/src/SEDManagerGUI/windows/flutter/generated_plugins.cmake b/src/SEDManagerGUI/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b93c4c3 --- /dev/null +++ b/src/SEDManagerGUI/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/src/SEDManagerGUI/windows/runner/CMakeLists.txt b/src/SEDManagerGUI/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/src/SEDManagerGUI/windows/runner/Runner.rc b/src/SEDManagerGUI/windows/runner/Runner.rc new file mode 100644 index 0000000..765c6d3 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "sed_manager_gui" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "sed_manager_gui" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "sed_manager_gui.exe" "\0" + VALUE "ProductName", "sed_manager_gui" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/SEDManagerGUI/windows/runner/flutter_window.cpp b/src/SEDManagerGUI/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/src/SEDManagerGUI/windows/runner/flutter_window.h b/src/SEDManagerGUI/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/src/SEDManagerGUI/windows/runner/main.cpp b/src/SEDManagerGUI/windows/runner/main.cpp new file mode 100644 index 0000000..cebb577 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"SEDManager", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/src/SEDManagerGUI/windows/runner/resource.h b/src/SEDManagerGUI/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/SEDManagerGUI/windows/runner/resources/app_icon.ico b/src/SEDManagerGUI/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/src/SEDManagerGUI/windows/runner/resources/app_icon.ico differ diff --git a/src/SEDManagerGUI/windows/runner/runner.exe.manifest b/src/SEDManagerGUI/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/src/SEDManagerGUI/windows/runner/utils.cpp b/src/SEDManagerGUI/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/src/SEDManagerGUI/windows/runner/utils.h b/src/SEDManagerGUI/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/src/SEDManagerGUI/windows/runner/win32_window.cpp b/src/SEDManagerGUI/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/src/SEDManagerGUI/windows/runner/win32_window.h b/src/SEDManagerGUI/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/src/SEDManagerGUI/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_