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/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 3918892a..009693fe 100644 --- a/packages/celest_core/pubspec.yaml +++ b/packages/celest_core/pubspec.yaml @@ -13,6 +13,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