diff --git a/.github/workflows/celest_core.yaml b/.github/workflows/celest_core.yaml index 589ce335..1d748201 100644 --- a/.github/workflows/celest_core.yaml +++ b/.github/workflows/celest_core.yaml @@ -78,7 +78,7 @@ jobs: uses: subosito/flutter-action@62f096cacda5168a3bd7b95793373be14fa4fbaf # 2.13.0 with: cache: true - - name: Get Packages + - name: Get Packages (Example) working-directory: packages/celest_core/example run: flutter pub get - name: Enable KVM @@ -94,3 +94,60 @@ jobs: api-level: 31 arch: x86_64 script: cd packages/celest_core/example && flutter test -d emulator integration_test/secure_storage_test.dart + test_linux: + needs: analyze_and_format + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Git Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1 + - name: Setup Flutter + uses: subosito/flutter-action@62f096cacda5168a3bd7b95793373be14fa4fbaf # 2.13.0 + with: + cache: true + - name: Install Build Dependencies + run: sudo apt-get update && sudo apt-get install -y clang cmake git ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev + - name: Setup Test Environment + working-directory: packages/celest_core + run: tool/setup-ci.sh + - name: Get Packages + working-directory: packages/celest_core + run: dart pub get + - name: Test + working-directory: packages/celest_core + run: dart test + - name: Get Packages (Example) + working-directory: packages/celest_core/example + run: flutter pub get + - name: Test (Linux) + working-directory: packages/celest_core/example + run: | + # Headless tests require virtual display for the linux tests to run. + export DISPLAY=:99 + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + flutter test -d linux integration_test/secure_storage_test.dart + # TODO: Re-enable + # Need to fix this: Git error. Command: `git clone --mirror https://github.com/dart-lang/native /c/Users/runneradmin/.pub-cache\git\cache\native-647c69ed8027da6d6def6bc40efa87cf1a2f76aa` + # test_windows: + # needs: analyze_and_format + # runs-on: windows-latest + # timeout-minutes: 15 + # steps: + # - name: Git Checkout + # uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1 + # - name: Setup Flutter + # uses: subosito/flutter-action@62f096cacda5168a3bd7b95793373be14fa4fbaf # 2.13.0 + # with: + # cache: true + # - name: Get Packages + # working-directory: packages/celest_core + # run: dart pub get --no-example + # - name: Test + # working-directory: packages/celest_core + # run: dart test + # - name: Get Packages (Example) + # working-directory: packages/celest_core/example + # run: flutter pub get + # - name: Test (Windows) + # working-directory: packages/celest_core/example + # run: flutter test -d windows integration_test/secure_storage_test.dart diff --git a/LICENSE b/LICENSE index f01f71fe..27ecec44 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,19 @@ -MIT License - -Copyright (c) 2024 Celest - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2024 Teo, Inc. (Celest) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by: + +(a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or + +(b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution. + +Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise. + +DISCLAIMER + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/celest/LICENSE b/packages/celest/LICENSE index f01f71fe..27ecec44 100644 --- a/packages/celest/LICENSE +++ b/packages/celest/LICENSE @@ -1,21 +1,19 @@ -MIT License - -Copyright (c) 2024 Celest - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2024 Teo, Inc. (Celest) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by: + +(a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or + +(b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution. + +Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise. + +DISCLAIMER + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/celest_core/LICENSE b/packages/celest_core/LICENSE index f01f71fe..27ecec44 100644 --- a/packages/celest_core/LICENSE +++ b/packages/celest_core/LICENSE @@ -1,21 +1,19 @@ -MIT License - -Copyright (c) 2024 Celest - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2024 Teo, Inc. (Celest) + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by: + +(a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or + +(b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution. + +Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise. + +DISCLAIMER + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/celest_core/example/integration_test/secure_storage_shared.dart b/packages/celest_core/example/integration_test/secure_storage_shared.dart index cdf4f1b1..4561692c 100644 --- a/packages/celest_core/example/integration_test/secure_storage_shared.dart +++ b/packages/celest_core/example/integration_test/secure_storage_shared.dart @@ -97,7 +97,7 @@ final _random = Random(); Iterable<(int, String)> get _largeKeyValuePairs sync* { for (final length in const [100, 1000, 10000]) { final string = String.fromCharCodes( - List.generate(length, (_) => _random.nextInt(255) + 1), + List.generate(length, (_) => _random.nextInt(94) + 33), ); yield (length, string); } diff --git a/packages/celest_core/ffigen.glib.yaml b/packages/celest_core/ffigen.glib.yaml new file mode 100644 index 00000000..0af4a67b --- /dev/null +++ b/packages/celest_core/ffigen.glib.yaml @@ -0,0 +1,45 @@ +name: Glib +description: | + Bindings for glib on Linux. + + Regenerate bindings with `dart run ffigen --config=ffigen.glib.yaml`. +language: c +output: lib/src/native/linux/glib.ffi.dart +headers: + entry-points: + - /usr/include/glib-2.0/glib.h + - /usr/include/glib-2.0/glib-object.h + - /usr/include/glib-2.0/gio/gio.h +preamble: | + // ignore_for_file: type=lint + // ignore_for_file: return_of_invalid_type + // ignore_for_file: unnecessary_non_null_assertion +comments: + style: any + length: full + +exclude-all-by-default: true +typedefs: + include: + - gboolean + - gint + - gpointer +functions: + include: + - g_hash_table_new + - g_hash_table_insert + - g_hash_table_destroy + - g_application_get_default + - g_application_get_application_id + - g_error_free +structs: + include: + - _GError + - _GHashTable + - _GCancellable + - _GObject + rename: + "_GError": GError + "_GHashTable": GHashTable + "_GCancellable": GCancellable + "_GObject": GObject diff --git a/packages/celest_core/ffigen.libsecret.yaml b/packages/celest_core/ffigen.libsecret.yaml new file mode 100644 index 00000000..d3ebea78 --- /dev/null +++ b/packages/celest_core/ffigen.libsecret.yaml @@ -0,0 +1,69 @@ +name: Libsecret +description: | + Bindings for Libsecret on Linux. + + Regenerate bindings with `dart run ffigen --config=ffigen.libsecret.yaml`. +language: c +output: lib/src/native/linux/libsecret.ffi.dart +headers: + entry-points: + - /usr/include/libsecret-1/libsecret/secret.h +preamble: | + // ignore_for_file: type=lint + // ignore_for_file: return_of_invalid_type + // ignore_for_file: unnecessary_non_null_assertion +library-imports: + glib: package:celest_core/src/native/linux/glib.ffi.dart +comments: + style: any + length: full + +exclude-all-by-default: true +functions: + include: + - secret_password_storev_sync + - secret_password_lookupv_sync + - secret_password_clearv_sync + - secret_password_free +structs: + include: + - SecretSchema + - SecretSchemaAttribute +enums: + include: + - SecretSchemaAttributeType + - SecretSchemaFlags +macros: + include: + - SECRET_COLLECTION_DEFAULT +type-map: + typedefs: + GHashTable: + lib: glib + c-type: GHashTable + dart-type: GHashTable + GError: + lib: glib + c-type: GError + dart-type: GError + GCancellable: + lib: glib + c-type: GCancellable + dart-type: GCancellable + gpointer: + lib: glib + c-type: gpointer + dart-type: gpointer + gboolean: + lib: glib + c-type: gboolean + dart-type: int + gchar: + lib: pkg_ffi + c-type: Utf8 + dart-type: Char + gint: + lib: glib + c-type: gint + dart-type: int + diff --git a/packages/celest_core/lib/src/native/linux/glib.ffi.dart b/packages/celest_core/lib/src/native/linux/glib.ffi.dart new file mode 100644 index 00000000..b516a959 --- /dev/null +++ b/packages/celest_core/lib/src/native/linux/glib.ffi.dart @@ -0,0 +1,218 @@ +// ignore_for_file: type=lint +// ignore_for_file: return_of_invalid_type +// ignore_for_file: unnecessary_non_null_assertion + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// Bindings for glib on Linux. +/// +/// Regenerate bindings with `dart run ffigen --config=ffigen.glib.yaml`. +/// +class Glib { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + Glib(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + Glib.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + void g_error_free( + ffi.Pointer error, + ) { + return _g_error_free( + error, + ); + } + + late final _g_error_freePtr = + _lookup)>>( + 'g_error_free'); + late final _g_error_free = + _g_error_freePtr.asFunction)>(); + + ffi.Pointer g_hash_table_new( + ffi.Pointer< + ffi.NativeFunction)>> + hash_func, + ffi.Pointer< + ffi.NativeFunction< + gboolean Function( + ffi.Pointer, ffi.Pointer)>> + key_equal_func, + ) { + return _g_hash_table_new( + hash_func, + key_equal_func, + ); + } + + late final _g_hash_table_newPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer< + ffi.NativeFunction< + ffi.UnsignedInt Function(ffi.Pointer)>>, + ffi.Pointer< + ffi.NativeFunction< + gboolean Function(ffi.Pointer, + ffi.Pointer)>>)>>('g_hash_table_new'); + late final _g_hash_table_new = _g_hash_table_newPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer< + ffi + .NativeFunction)>>, + ffi.Pointer< + ffi.NativeFunction< + gboolean Function( + ffi.Pointer, ffi.Pointer)>>)>(); + + void g_hash_table_destroy( + ffi.Pointer hash_table, + ) { + return _g_hash_table_destroy( + hash_table, + ); + } + + late final _g_hash_table_destroyPtr = + _lookup)>>( + 'g_hash_table_destroy'); + late final _g_hash_table_destroy = _g_hash_table_destroyPtr + .asFunction)>(); + + int g_hash_table_insert( + ffi.Pointer hash_table, + gpointer key, + gpointer value, + ) { + return _g_hash_table_insert( + hash_table, + key, + value, + ); + } + + late final _g_hash_table_insertPtr = _lookup< + ffi.NativeFunction< + gboolean Function(ffi.Pointer, gpointer, + gpointer)>>('g_hash_table_insert'); + late final _g_hash_table_insert = _g_hash_table_insertPtr + .asFunction, gpointer, gpointer)>(); + + ffi.Pointer g_application_get_application_id( + ffi.Pointer<_GApplication> application, + ) { + return _g_application_get_application_id( + application, + ); + } + + late final _g_application_get_application_idPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer<_GApplication>)>>('g_application_get_application_id'); + late final _g_application_get_application_id = + _g_application_get_application_idPtr.asFunction< + ffi.Pointer Function(ffi.Pointer<_GApplication>)>(); + + ffi.Pointer<_GApplication> g_application_get_default() { + return _g_application_get_default(); + } + + late final _g_application_get_defaultPtr = + _lookup Function()>>( + 'g_application_get_default'); + late final _g_application_get_default = _g_application_get_defaultPtr + .asFunction Function()>(); +} + +final class GError extends ffi.Struct { + @ffi.UnsignedInt() + external int domain; + + @gint() + external int code; + + external ffi.Pointer message; +} + +typedef gint = ffi.Int; +typedef Dartgint = int; + +final class GHashTable extends ffi.Opaque {} + +typedef gboolean = gint; +typedef gpointer = ffi.Pointer; + +/// GObject: +/// +/// The base object type. +/// +/// All the fields in the `GObject` structure are private to the implementation +/// and should never be accessed directly. +/// +/// Since GLib 2.72, all #GObjects are guaranteed to be aligned to at least the +/// alignment of the largest basic GLib type (typically this is #guint64 or +/// #gdouble). If you need larger alignment for an element in a #GObject, you +/// should allocate it on the heap (aligned), or arrange for your #GObject to be +/// appropriately padded. This guarantee applies to the #GObject (or derived) +/// struct, the #GObjectClass (or derived) struct, and any private data allocated +/// by G_ADD_PRIVATE(). +final class GObject extends ffi.Struct { + external _GTypeInstance g_type_instance; + + /// (atomic) + @ffi.UnsignedInt() + external int ref_count; + + external ffi.Pointer<_GData> qdata; +} + +/// GTypeInstance: +/// +/// An opaque structure used as the base of all type instances. +final class _GTypeInstance extends ffi.Struct { + /// < private > + external ffi.Pointer<_GTypeClass> g_class; +} + +/// Basic Type Structures +/// / +/// /** +/// GTypeClass: +/// +/// An opaque structure used as the base of all classes. +final class _GTypeClass extends ffi.Struct { + /// < private > + @ffi.UnsignedLong() + external int g_type; +} + +final class _GData extends ffi.Opaque {} + +final class GCancellable extends ffi.Struct { + external GObject parent_instance; + + /// < private > + external ffi.Pointer<_GCancellablePrivate> priv; +} + +final class _GCancellablePrivate extends ffi.Opaque {} + +final class _GApplication extends ffi.Struct { + /// < private > + external GObject parent_instance; + + external ffi.Pointer<_GApplicationPrivate> priv; +} + +final class _GApplicationPrivate extends ffi.Opaque {} diff --git a/packages/celest_core/lib/src/native/linux/libsecret.ffi.dart b/packages/celest_core/lib/src/native/linux/libsecret.ffi.dart new file mode 100644 index 00000000..a585cb67 --- /dev/null +++ b/packages/celest_core/lib/src/native/linux/libsecret.ffi.dart @@ -0,0 +1,194 @@ +// ignore_for_file: type=lint +// ignore_for_file: return_of_invalid_type +// ignore_for_file: unnecessary_non_null_assertion + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; +import 'package:celest_core/src/native/linux/glib.ffi.dart' as glib; +import 'package:ffi/ffi.dart' as pkg_ffi; + +/// Bindings for Libsecret on Linux. +/// +/// Regenerate bindings with `dart run ffigen --config=ffigen.libsecret.yaml`. +/// +class Libsecret { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + Libsecret(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + Libsecret.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + int secret_password_storev_sync( + ffi.Pointer schema, + ffi.Pointer attributes, + ffi.Pointer collection, + ffi.Pointer label, + ffi.Pointer password, + ffi.Pointer cancellable, + ffi.Pointer> error, + ) { + return _secret_password_storev_sync( + schema, + attributes, + collection, + label, + password, + cancellable, + error, + ); + } + + late final _secret_password_storev_syncPtr = _lookup< + ffi.NativeFunction< + glib.gboolean Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>( + 'secret_password_storev_sync'); + late final _secret_password_storev_sync = + _secret_password_storev_syncPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>(); + + ffi.Pointer secret_password_lookupv_sync( + ffi.Pointer schema, + ffi.Pointer attributes, + ffi.Pointer cancellable, + ffi.Pointer> error, + ) { + return _secret_password_lookupv_sync( + schema, + attributes, + cancellable, + error, + ); + } + + late final _secret_password_lookupv_syncPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>( + 'secret_password_lookupv_sync'); + late final _secret_password_lookupv_sync = + _secret_password_lookupv_syncPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>(); + + int secret_password_clearv_sync( + ffi.Pointer schema, + ffi.Pointer attributes, + ffi.Pointer cancellable, + ffi.Pointer> error, + ) { + return _secret_password_clearv_sync( + schema, + attributes, + cancellable, + error, + ); + } + + late final _secret_password_clearv_syncPtr = _lookup< + ffi.NativeFunction< + glib.gboolean Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>>( + 'secret_password_clearv_sync'); + late final _secret_password_clearv_sync = + _secret_password_clearv_syncPtr.asFunction< + int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>)>(); + + void secret_password_free( + ffi.Pointer password, + ) { + return _secret_password_free( + password, + ); + } + + late final _secret_password_freePtr = + _lookup)>>( + 'secret_password_free'); + late final _secret_password_free = _secret_password_freePtr + .asFunction)>(); +} + +abstract class SecretSchemaAttributeType { + static const int SECRET_SCHEMA_ATTRIBUTE_STRING = 0; + static const int SECRET_SCHEMA_ATTRIBUTE_INTEGER = 1; + static const int SECRET_SCHEMA_ATTRIBUTE_BOOLEAN = 2; +} + +final class SecretSchemaAttribute extends ffi.Struct { + external ffi.Pointer name; + + @ffi.Int32() + external int type; +} + +abstract class SecretSchemaFlags { + static const int SECRET_SCHEMA_NONE = 0; + static const int SECRET_SCHEMA_DONT_MATCH_NAME = 2; +} + +final class SecretSchema extends ffi.Struct { + external ffi.Pointer name; + + @ffi.Int32() + external int flags; + + @ffi.Array.multi([32]) + external ffi.Array attributes; + + /// + @glib.gint() + external int reserved; + + external glib.gpointer reserved1; + + external glib.gpointer reserved2; + + external glib.gpointer reserved3; + + external glib.gpointer reserved4; + + external glib.gpointer reserved5; + + external glib.gpointer reserved6; + + external glib.gpointer reserved7; +} + +const String SECRET_COLLECTION_DEFAULT = 'default'; diff --git a/packages/celest_core/lib/src/native/windows/windows_folders.dart b/packages/celest_core/lib/src/native/windows/windows_folders.dart new file mode 100644 index 00000000..44acbc83 --- /dev/null +++ b/packages/celest_core/lib/src/native/windows/windows_folders.dart @@ -0,0 +1,246 @@ +// Copied from: +// https://github.com/flutter/packages/blob/36a7b99381f85e86914e82c75fc7d9038ed96cca/packages/path_provider/path_provider_windows/lib/src/folders.dart + +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of 'windows_paths.dart'; + +// ignore_for_file: non_constant_identifier_names + +// ignore: avoid_classes_with_only_static_members +/// A class containing the GUID references for each of the documented Windows +/// known folders. A property of this class may be passed to the `getPath` +/// method in the [PathProviderWindows] class to retrieve a known folder from +/// Windows. +class WindowsKnownFolder { + /// The file system directory that is used to store administrative tools for + /// an individual user. The MMC will save customized consoles to this + /// directory, and it will roam with the user. + static String get AdminTools => FOLDERID_AdminTools; + + /// The file system directory that acts as a staging area for files waiting to + /// be written to a CD. A typical path is C:\Documents and + /// Settings\username\Local Settings\Application Data\Microsoft\CD Burning. + static String get CDBurning => FOLDERID_CDBurning; + + /// The file system directory that contains administrative tools for all users + /// of the computer. + static String get CommonAdminTools => FOLDERID_CommonAdminTools; + + /// The file system directory that contains the directories for the common + /// program groups that appear on the Start menu for all users. A typical path + /// is C:\Documents and Settings\All Users\Start Menu\Programs. + static String get CommonPrograms => FOLDERID_CommonPrograms; + + /// The file system directory that contains the programs and folders that + /// appear on the Start menu for all users. A typical path is C:\Documents and + /// Settings\All Users\Start Menu. + static String get CommonStartMenu => FOLDERID_CommonStartMenu; + + /// The file system directory that contains the programs that appear in the + /// Startup folder for all users. A typical path is C:\Documents and + /// Settings\All Users\Start Menu\Programs\Startup. + static String get CommonStartup => FOLDERID_CommonStartup; + + /// The file system directory that contains the templates that are available + /// to all users. A typical path is C:\Documents and Settings\All + /// Users\Templates. + static String get CommonTemplates => FOLDERID_CommonTemplates; + + /// The virtual folder that represents My Computer, containing everything on + /// the local computer: storage devices, printers, and Control Panel. The + /// folder can also contain mapped network drives. + static String get ComputerFolder => FOLDERID_ComputerFolder; + + /// The virtual folder that represents Network Connections, that contains + /// network and dial-up connections. + static String get ConnectionsFolder => FOLDERID_ConnectionsFolder; + + /// The virtual folder that contains icons for the Control Panel applications. + static String get ControlPanelFolder => FOLDERID_ControlPanelFolder; + + /// The file system directory that serves as a common repository for Internet + /// cookies. A typical path is C:\Documents and Settings\username\Cookies. + static String get Cookies => FOLDERID_Cookies; + + /// The virtual folder that represents the Windows desktop, the root of the + /// namespace. + static String get Desktop => FOLDERID_Desktop; + + /// The virtual folder that represents the My Documents desktop item. + static String get Documents => FOLDERID_Documents; + + /// The file system directory that serves as a repository for Internet + /// downloads. + static String get Downloads => FOLDERID_Downloads; + + /// The file system directory that serves as a common repository for the + /// user's favorite items. A typical path is C:\Documents and + /// Settings\username\Favorites. + static String get Favorites => FOLDERID_Favorites; + + /// A virtual folder that contains fonts. A typical path is C:\Windows\Fonts. + static String get Fonts => FOLDERID_Fonts; + + /// The file system directory that serves as a common repository for Internet + /// history items. + static String get History => FOLDERID_History; + + /// The file system directory that serves as a common repository for temporary + /// Internet files. A typical path is C:\Documents and Settings\username\Local + /// Settings\Temporary Internet Files. + static String get InternetCache => FOLDERID_InternetCache; + + /// A virtual folder for Internet Explorer. + static String get InternetFolder => FOLDERID_InternetFolder; + + /// The file system directory that serves as a data repository for local + /// (nonroaming) applications. A typical path is C:\Documents and + /// Settings\username\Local Settings\Application Data. + static String get LocalAppData => FOLDERID_LocalAppData; + + /// The file system directory that serves as a common repository for music + /// files. A typical path is C:\Documents and Settings\User\My Documents\My + /// Music. + static String get Music => FOLDERID_Music; + + /// A file system directory that contains the link objects that may exist in + /// the My Network Places virtual folder. A typical path is C:\Documents and + /// Settings\username\NetHood. + static String get NetHood => FOLDERID_NetHood; + + /// The folder that represents other computers in your workgroup. + static String get NetworkFolder => FOLDERID_NetworkFolder; + + /// The file system directory that serves as a common repository for image + /// files. A typical path is C:\Documents and Settings\username\My + /// Documents\My Pictures. + static String get Pictures => FOLDERID_Pictures; + + /// The file system directory that contains the link objects that can exist in + /// the Printers virtual folder. A typical path is C:\Documents and + /// Settings\username\PrintHood. + static String get PrintHood => FOLDERID_PrintHood; + + /// The virtual folder that contains installed printers. + static String get PrintersFolder => FOLDERID_PrintersFolder; + + /// The user's profile folder. A typical path is C:\Users\username. + /// Applications should not create files or folders at this level. + static String get Profile => FOLDERID_Profile; + + /// The file system directory that contains application data for all users. A + /// typical path is C:\Documents and Settings\All Users\Application Data. This + /// folder is used for application data that is not user specific. For + /// example, an application can store a spell-check dictionary, a database of + /// clip art, or a log file in the CSIDL_COMMON_APPDATA folder. This + /// information will not roam and is available to anyone using the computer. + static String get ProgramData => FOLDERID_ProgramData; + + /// The Program Files folder. A typical path is C:\Program Files. + static String get ProgramFiles => FOLDERID_ProgramFiles; + + /// The common Program Files folder. A typical path is C:\Program + /// Files\Common. + static String get ProgramFilesCommon => FOLDERID_ProgramFilesCommon; + + /// On 64-bit systems, a link to the common Program Files folder. A typical path is + /// C:\Program Files\Common Files. + static String get ProgramFilesCommonX64 => FOLDERID_ProgramFilesCommonX64; + + /// On 64-bit systems, a link to the 32-bit common Program Files folder. A + /// typical path is C:\Program Files (x86)\Common Files. On 32-bit systems, a + /// link to the Common Program Files folder. + static String get ProgramFilesCommonX86 => FOLDERID_ProgramFilesCommonX86; + + /// On 64-bit systems, a link to the Program Files folder. A typical path is + /// C:\Program Files. + static String get ProgramFilesX64 => FOLDERID_ProgramFilesX64; + + /// On 64-bit systems, a link to the 32-bit Program Files folder. A typical + /// path is C:\Program Files (x86). On 32-bit systems, a link to the Common + /// Program Files folder. + static String get ProgramFilesX86 => FOLDERID_ProgramFilesX86; + + /// The file system directory that contains the user's program groups (which + /// are themselves file system directories). + static String get Programs => FOLDERID_Programs; + + /// The file system directory that contains files and folders that appear on + /// the desktop for all users. A typical path is C:\Documents and Settings\All + /// Users\Desktop. + static String get PublicDesktop => FOLDERID_PublicDesktop; + + /// The file system directory that contains documents that are common to all + /// users. A typical path is C:\Documents and Settings\All Users\Documents. + static String get PublicDocuments => FOLDERID_PublicDocuments; + + /// The file system directory that serves as a repository for music files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Music. + static String get PublicMusic => FOLDERID_PublicMusic; + + /// The file system directory that serves as a repository for image files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Pictures. + static String get PublicPictures => FOLDERID_PublicPictures; + + /// The file system directory that serves as a repository for video files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Videos. + static String get PublicVideos => FOLDERID_PublicVideos; + + /// The file system directory that contains shortcuts to the user's most + /// recently used documents. A typical path is C:\Documents and + /// Settings\username\My Recent Documents. + static String get Recent => FOLDERID_Recent; + + /// The virtual folder that contains the objects in the user's Recycle Bin. + static String get RecycleBinFolder => FOLDERID_RecycleBinFolder; + + /// The file system directory that contains resource data. A typical path is + /// C:\Windows\Resources. + static String get ResourceDir => FOLDERID_ResourceDir; + + /// The file system directory that serves as a common repository for + /// application-specific data. A typical path is C:\Documents and + /// Settings\username\Application Data. + static String get RoamingAppData => FOLDERID_RoamingAppData; + + /// The file system directory that contains Send To menu items. A typical path + /// is C:\Documents and Settings\username\SendTo. + static String get SendTo => FOLDERID_SendTo; + + /// The file system directory that contains Start menu items. A typical path + /// is C:\Documents and Settings\username\Start Menu. + static String get StartMenu => FOLDERID_StartMenu; + + /// The file system directory that corresponds to the user's Startup program + /// group. The system starts these programs whenever the associated user logs + /// on. A typical path is C:\Documents and Settings\username\Start + /// Menu\Programs\Startup. + static String get Startup => FOLDERID_Startup; + + /// The Windows System folder. A typical path is C:\Windows\System32. + static String get System => FOLDERID_System; + + /// The 32-bit Windows System folder. On 32-bit systems, this is typically + /// C:\Windows\system32. On 64-bit systems, this is typically + /// C:\Windows\syswow64. + static String get SystemX86 => FOLDERID_SystemX86; + + /// The file system directory that serves as a common repository for document + /// templates. A typical path is C:\Documents and Settings\username\Templates. + static String get Templates => FOLDERID_Templates; + + /// The file system directory that serves as a common repository for video + /// files. A typical path is C:\Documents and Settings\username\My + /// Documents\My Videos. + static String get Videos => FOLDERID_Videos; + + /// The Windows directory or SYSROOT. This corresponds to the %windir% or + /// %SYSTEMROOT% environment variables. A typical path is C:\Windows. + static String get Windows => FOLDERID_Windows; +} diff --git a/packages/celest_core/lib/src/native/windows/windows_paths.dart b/packages/celest_core/lib/src/native/windows/windows_paths.dart new file mode 100644 index 00000000..7ccc48fc --- /dev/null +++ b/packages/celest_core/lib/src/native/windows/windows_paths.dart @@ -0,0 +1,268 @@ +// Copied from: +// https://github.com/flutter/packages/blob/36a7b99381f85e86914e82c75fc7d9038ed96cca/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart + +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: prefer_asserts_with_message + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:win32/win32.dart'; + +part 'windows_folders.dart'; + +/// Constant for en-US language used in VersionInfo keys. +@visibleForTesting +const String languageEn = '0409'; + +/// Constant for CP1252 encoding used in VersionInfo keys +@visibleForTesting +const String encodingCP1252 = '04e4'; + +/// Constant for Unicode encoding used in VersionInfo keys +@visibleForTesting +const String encodingUnicode = '04b0'; + +/// Wraps the Win32 VerQueryValue API call. +/// +/// This class exists to allow injecting alternate metadata in tests without +/// building multiple custom test binaries. +@visibleForTesting +class VersionInfoQuerier { + /// Returns the value for [key] in [versionInfo]s in section with given + /// language and encoding, or null if there is no such entry, + /// or if versionInfo is null. + /// + /// See https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource + /// for list of possible language and encoding values. + String? getStringValue( + Pointer? versionInfo, + String key, { + required String language, + required String encoding, + }) { + assert(language.isNotEmpty); + assert(encoding.isNotEmpty); + if (versionInfo == null) { + return null; + } + final keyPath = TEXT('\\StringFileInfo\\$language$encoding\\$key'); + final length = calloc(); + final valueAddress = calloc>(); + try { + if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) { + return null; + } + return valueAddress.value.toDartString(); + } finally { + calloc + ..free(keyPath) + ..free(length) + ..free(valueAddress); + } + } +} + +/// The Windows implementation of path provider. +/// +/// This class implements the `package:path_provider` functionality for Windows. +class PathProviderWindows { + /// The object to use for performing VerQueryValue calls. + @visibleForTesting + static VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier(); + + /// This is typically the same as the TMP environment variable. + static String? getTemporaryPath() { + final buffer = calloc(MAX_PATH + 1).cast(); + String path; + + try { + final length = GetTempPath(MAX_PATH, buffer); + + if (length == 0) { + final error = GetLastError(); + throw WindowsException(error); + } else { + path = buffer.toDartString(); + + // GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does + // not. Strip off trailing backslash for consistency with other methods + // here. + if (path.endsWith(r'\')) { + path = path.substring(0, path.length - 1); + } + } + + // Ensure that the directory exists, since GetTempPath doesn't. + final directory = Directory(path); + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + + return path; + } finally { + calloc.free(buffer); + } + } + + static String? getApplicationSupportPath() => + _createApplicationSubdirectory(WindowsKnownFolder.RoamingAppData); + + static String? getApplicationDocumentsPath() => + getPath(WindowsKnownFolder.Documents); + + static String? getApplicationCachePath() => + _createApplicationSubdirectory(WindowsKnownFolder.LocalAppData); + + static String? getDownloadsPath() => getPath(WindowsKnownFolder.Downloads); + + /// Retrieve any known folder from Windows. + /// + /// folderID is a GUID that represents a specific known folder ID, drawn from + /// [WindowsKnownFolder]. + static String? getPath(String folderID) { + final pathPtrPtr = calloc>(); + final knownFolderID = calloc()..ref.setGUID(folderID); + + try { + final hr = SHGetKnownFolderPath( + knownFolderID, + KF_FLAG_DEFAULT, + NULL, + pathPtrPtr, + ); + + if (FAILED(hr)) { + if (hr == E_INVALIDARG || hr == E_FAIL) { + throw WindowsException(hr); + } + return null; + } + + final path = pathPtrPtr.value.toDartString(); + return path; + } finally { + calloc + ..free(pathPtrPtr) + ..free(knownFolderID); + } + } + + static String? _getStringValue(Pointer? infoBuffer, String key) => + versionInfoQuerier.getStringValue( + infoBuffer, + key, + language: languageEn, + encoding: encodingCP1252, + ) ?? + versionInfoQuerier.getStringValue( + infoBuffer, + key, + language: languageEn, + encoding: encodingUnicode, + ); + + /// Returns the relative path string to append to the root directory returned + /// by Win32 APIs for application storage (such as RoamingAppDir) to get a + /// directory that is unique to the application. + /// + /// The convention is to use company-name\product-name\. This will use that if + /// possible, using the data in the VERSIONINFO resource, with the following + /// fallbacks: + /// - If the company name isn't there, that component will be dropped. + /// - If the product name isn't there, it will use the exe's filename (without + /// extension). + static String _getApplicationSpecificSubdirectory() { + String? companyName; + String? productName; + + final moduleNameBuffer = wsalloc(MAX_PATH + 1); + final unused = calloc(); + Pointer? infoBuffer; + try { + // Get the module name. + final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH); + if (moduleNameLength == 0) { + final error = GetLastError(); + throw WindowsException(error); + } + + // From that, load the VERSIONINFO resource + final infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); + if (infoSize != 0) { + infoBuffer = calloc(infoSize); + if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) == + 0) { + calloc.free(infoBuffer); + infoBuffer = null; + } + } + companyName = + _sanitizedDirectoryName(_getStringValue(infoBuffer, 'CompanyName')); + productName = + _sanitizedDirectoryName(_getStringValue(infoBuffer, 'ProductName')); + + // If there was no product name, use the executable name. + productName ??= + path.basenameWithoutExtension(moduleNameBuffer.toDartString()); + + return companyName != null + ? path.join(companyName, productName) + : productName; + } finally { + calloc + ..free(moduleNameBuffer) + ..free(unused); + if (infoBuffer != null) { + calloc.free(infoBuffer); + } + } + } + + /// Makes [rawString] safe as a directory component. See + /// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + /// + /// If after sanitizing the string is empty, returns null. + static String? _sanitizedDirectoryName(String? rawString) { + if (rawString == null) { + return null; + } + var sanitized = rawString + // Replace banned characters. + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + // Remove trailing whitespace. + .trimRight() + // Ensure that it does not end with a '.'. + .replaceAll(RegExp(r'[.]+$'), ''); + const kMaxComponentLength = 255; + if (sanitized.length > kMaxComponentLength) { + sanitized = sanitized.substring(0, kMaxComponentLength); + } + return sanitized.isEmpty ? null : sanitized; + } + + static String? _createApplicationSubdirectory(String folderId) { + final baseDir = getPath(folderId); + if (baseDir == null) { + return null; + } + final directory = + Directory(path.join(baseDir, _getApplicationSpecificSubdirectory())); + // Ensure that the directory exists if possible, since it will on other + // platforms. If the name is longer than MAXPATH, creating will fail, so + // skip that step; it's up to the client to decide what to do with the path + // in that case (e.g., using a short path). + if (directory.path.length <= MAX_PATH) { + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + } + return directory.path; + } +} diff --git a/packages/celest_core/lib/src/secure_storage/secure_storage.linux.dart b/packages/celest_core/lib/src/secure_storage/secure_storage.linux.dart new file mode 100644 index 00000000..c50cc425 --- /dev/null +++ b/packages/celest_core/lib/src/secure_storage/secure_storage.linux.dart @@ -0,0 +1,153 @@ +import 'dart:ffi'; +import 'dart:io'; + +import 'package:celest_core/src/native/linux/glib.ffi.dart'; +import 'package:celest_core/src/native/linux/libsecret.ffi.dart'; +import 'package:celest_core/src/secure_storage/secure_storage_exception.dart'; +import 'package:celest_core/src/secure_storage/secure_storage_platform.vm.dart'; +import 'package:ffi/ffi.dart'; + +final class SecureStoragePlatformLinux extends SecureStoragePlatform { + SecureStoragePlatformLinux({ + required this.scope, + }) : super.base(); + + final String scope; + + final _glibDylib = DynamicLibrary.open('libglib-2.0.so.0'); + late final _glib = Glib(_glibDylib); + + final _gioDylib = DynamicLibrary.open('libgio-2.0.so'); + late final _gio = Glib(_gioDylib); + + final _libSecretDylib = DynamicLibrary.open('libsecret-1.so.0'); + late final _libSecret = Libsecret(_libSecretDylib); + + late final _gStrHashPointer = + _glibDylib.lookup)>>( + 'g_str_hash'); + + late final String _appName = () { + final application = _gio.g_application_get_default(); + if (application == nullptr) { + return File('/proc/self/exe').resolveSymbolicLinksSync(); + } + return _gio + .g_application_get_application_id(application) + .cast() + .toDartString(); + }(); + + String _labelFor(String key) => '$scope/$key'; + Pointer _schemaFor(Arena arena) => arena() + ..ref.name = _appName.toNativeUtf8(allocator: arena) + ..ref.flags = SecretSchemaFlags.SECRET_SCHEMA_NONE + ..ref.attributes[0].name = 'key'.toNativeUtf8(allocator: arena) + ..ref.attributes[0].type = + SecretSchemaAttributeType.SECRET_SCHEMA_ATTRIBUTE_STRING; + + Pointer _attributes({ + String? key, + required Arena arena, + }) { + final hashTable = _glib.g_hash_table_new(_gStrHashPointer, nullptr); + if (key != null) { + _glib.g_hash_table_insert( + hashTable, + 'key'.toNativeUtf8(allocator: arena).cast(), + key.toNativeUtf8(allocator: arena).cast(), + ); + } + arena.onReleaseAll(() => _glib.g_hash_table_destroy(hashTable)); + return hashTable; + } + + @override + void clear() => using((arena) { + final schema = _schemaFor(arena); + final attributes = _attributes(arena: arena); + _check( + (err) => _libSecret.secret_password_clearv_sync( + schema, + attributes, + nullptr, + err, + ), + arena: arena, + ); + }); + + @override + String? delete(String key) => using((arena) { + final secret = read(key); + final schema = _schemaFor(arena); + final attributes = _attributes(key: key, arena: arena); + _check( + (err) => _libSecret.secret_password_clearv_sync( + schema, + attributes, + nullptr, + err, + ), + arena: arena, + ); + return secret; + }); + + @override + String? read(String key) => using((arena) { + final attributes = _attributes(key: key, arena: arena); + final schema = _schemaFor(arena); + final result = _check( + (err) => _libSecret.secret_password_lookupv_sync( + schema, + attributes, + nullptr, + err, + ), + arena: arena, + ); + if (result == nullptr) { + return null; + } + arena.onReleaseAll(() => _libSecret.secret_password_free(result)); + return result.toDartString(); + }); + + @override + String write(String key, String value) { + using((arena) { + final label = _labelFor(key).toNativeUtf8(allocator: arena); + final secret = value.toNativeUtf8(allocator: arena); + final attributes = _attributes(key: key, arena: arena); + _check( + (err) => _libSecret.secret_password_storev_sync( + _schemaFor(arena), + attributes, + nullptr, + label, + secret, + nullptr, + err, + ), + arena: arena, + ); + }); + return value; + } + + R _check( + R Function(Pointer> err) action, { + required Arena arena, + }) { + final err = arena>(); + final result = action(err); + final error = err.value; + if (error != nullptr) { + arena.onReleaseAll(() => _glib.g_error_free(error)); + final message = error.ref.message.cast().toDartString(); + throw SecureStorageUnknownException(message); + } + return result; + } +} diff --git a/packages/celest_core/lib/src/secure_storage/secure_storage.windows.dart b/packages/celest_core/lib/src/secure_storage/secure_storage.windows.dart new file mode 100644 index 00000000..b2171d0b --- /dev/null +++ b/packages/celest_core/lib/src/secure_storage/secure_storage.windows.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:celest_core/src/native/windows/windows_paths.dart'; +import 'package:celest_core/src/secure_storage/secure_storage_exception.dart'; +import 'package:celest_core/src/secure_storage/secure_storage_platform.vm.dart'; +import 'package:path/path.dart' as p; +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +final class SecureStoragePlatformWindows extends SecureStoragePlatform { + SecureStoragePlatformWindows({ + required this.scope, + }) : super.base(); + + final String scope; + late final File _storage = File( + p.join(PathProviderWindows.getApplicationSupportPath()!, '$scope.json'), + ); + + Map _readData() { + if (!_storage.existsSync()) { + return {}; + } + return (jsonDecode(_storage.readAsStringSync()) as Map).cast(); + } + + void _writeData(Map data) { + if (!_storage.existsSync()) { + _storage.createSync(recursive: true); + } + _storage.writeAsStringSync(jsonEncode(data)); + } + + @override + void clear() { + if (_storage.existsSync()) { + _storage.deleteSync(); + } + } + + @override + String? delete(String key) { + final data = _readData(); + final value = data.remove(key); + _writeData(data); + return value; + } + + @override + String? read(String key) { + final value = _readData()[key]; + if (value == null) { + return null; + } + return _decrypt(value); + } + + @override + String write(String key, String value) { + final encrypted = _encrypt(value); + final data = _readData()..[key] = encrypted; + _writeData(data); + return value; + } + + WindowsException get _lastException => + WindowsException(HRESULT_FROM_WIN32(GetLastError())); + + /// A wrapper around [CryptProtectData] for encrypting [Uint8List]. + String _encrypt(String value) { + return using((Arena arena) { + final bytes = utf8.encode(value); + final blob = bytes.allocatePointerInArena(arena); + final dataPtr = arena() + ..ref.cbData = bytes.length + ..ref.pbData = blob; + final encryptedPtr = arena(); + CryptProtectData( + dataPtr, + nullptr, // no label + nullptr, // no added entropy + nullptr, // reserved + nullptr, // no prompt + 0, // default flag + encryptedPtr, + ); + final err = GetLastError(); + if (err != ERROR_SUCCESS) { + throw SecureStorageUnknownException(_lastException.toString()); + } + final encryptedBlob = encryptedPtr.ref; + final encryptedBytes = + encryptedBlob.pbData.asTypedList(encryptedBlob.cbData); + return base64Encode(encryptedBytes); + }); + } + + /// A wrapper around [CryptUnprotectData] for decrypting a blob. + String _decrypt(String value) { + return using((Arena arena) { + final data = base64Decode(value); + final blob = data.allocatePointerInArena(arena); + final dataPtr = arena() + ..ref.cbData = data.length + ..ref.pbData = blob; + final unencryptedPtr = arena(); + CryptUnprotectData( + dataPtr, + nullptr, // no label + nullptr, // no added entropy + nullptr, // reserved + nullptr, // no prompt + 0, // default flag + unencryptedPtr, + ); + final err = GetLastError(); + if (err != ERROR_SUCCESS) { + throw SecureStorageUnknownException(_lastException.toString()); + } + final unencryptedDataBlob = unencryptedPtr.ref; + final unencryptedBlob = unencryptedDataBlob.pbData.asTypedList( + unencryptedDataBlob.cbData, + ); + return utf8.decode(unencryptedBlob); + }); + } +} + +extension on Uint8List { + /// Alternative to [allocatePointer] from win32, which uses an [Arena]. + Pointer allocatePointerInArena(Arena arena) { + final ptr = arena(length); + ptr.asTypedList(length).setAll(0, this); + return ptr; + } +} diff --git a/packages/celest_core/lib/src/secure_storage/secure_storage_platform.vm.dart b/packages/celest_core/lib/src/secure_storage/secure_storage_platform.vm.dart index 5b5d63db..a4ddb92a 100644 --- a/packages/celest_core/lib/src/secure_storage/secure_storage_platform.vm.dart +++ b/packages/celest_core/lib/src/secure_storage/secure_storage_platform.vm.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:celest_core/src/secure_storage/secure_storage.android.dart'; import 'package:celest_core/src/secure_storage/secure_storage.dart'; import 'package:celest_core/src/secure_storage/secure_storage.darwin.dart'; +import 'package:celest_core/src/secure_storage/secure_storage.linux.dart'; +import 'package:celest_core/src/secure_storage/secure_storage.windows.dart'; import 'package:meta/meta.dart'; abstract base class SecureStoragePlatform implements SecureStorage { @@ -15,6 +17,12 @@ abstract base class SecureStoragePlatform implements SecureStorage { if (Platform.isAndroid) { return SecureStoragePlatformAndroid(scope: scope ?? _defaultScope); } + if (Platform.isLinux) { + return SecureStoragePlatformLinux(scope: scope ?? _defaultScope); + } + if (Platform.isWindows) { + return SecureStoragePlatformWindows(scope: scope ?? _defaultScope); + } throw UnsupportedError('This platform is not yet supported.'); } diff --git a/packages/celest_core/pubspec.yaml b/packages/celest_core/pubspec.yaml index 798454f3..770e72dd 100644 --- a/packages/celest_core/pubspec.yaml +++ b/packages/celest_core/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: jni: ^0.7.2 meta: ^1.10.0 os_detect: ^2.0.1 + path: ^1.9.0 + win32: ^5.2.0 dev_dependencies: # TODO(dnys1): Use ^12.0.0 when released diff --git a/packages/celest_core/tool/ffigen.sh b/packages/celest_core/tool/ffigen.sh index 10438207..dee1ce1a 100755 --- a/packages/celest_core/tool/ffigen.sh +++ b/packages/celest_core/tool/ffigen.sh @@ -11,6 +11,15 @@ popd echo "Generating FFI bindings..." dart run ffigen --config=ffigen.core_foundation.yaml dart run ffigen --config=ffigen.security.yaml +if !command -v pkg-config >&/dev/null; then + echo "Skipping Linux bindings." >&2 +else + GLIB_OPTS=$(pkg-config --cflags-only-I glib-2.0) + dart run ffigen --config=ffigen.glib.yaml --compiler-opts="$GLIB_OPTS" + + LIBSECRET_OPTS=$(pkg-config --cflags-only-I libsecret-1) + dart run ffigen --config=ffigen.libsecret.yaml --compiler-opts="$LIBSECRET_OPTS" +fi echo "Generating JNI bindings..." dart run jnigen --config=jnigen.yaml diff --git a/packages/celest_core/tool/setup-ci.sh b/packages/celest_core/tool/setup-ci.sh new file mode 100755 index 00000000..f986e842 --- /dev/null +++ b/packages/celest_core/tool/setup-ci.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + + sudo apt-get update && sudo apt-get install -y libsecret-1-dev gnome-keyring + + # If running in headless mode, re-run script in dbus session. + if [ -z $DBUS_SESSION_BUS_ADDRESS && -n $1 ]; then + exec dbus-run-session -- $@ + fi + + # Set up keyring in CI env + if [ -n $CI ]; then + echo 'password' | gnome-keyring-daemon --start --replace --daemonize --unlock + fi +fi