From 56946f415cea91c918591632911f60fbcaf47513 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sun, 16 Aug 2026 20:57:01 +1000 Subject: [PATCH 1/3] Use sessionStorage for web security key storage --- example/pubspec.yaml | 2 +- lib/src/widgets/solid_backup_dialog.dart | 2 +- lib/src/widgets/solid_status_bar_models.dart | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bf4b9049..30377511 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: intl: ^0.20.2 markdown_tooltip: ^0.0.10 rdflib: ^0.2.12 - solidpod: ^1.0.13 + solidpod: ^1.0.15 solidui: path: .. universal_io: ^2.3.1 diff --git a/lib/src/widgets/solid_backup_dialog.dart b/lib/src/widgets/solid_backup_dialog.dart index f234e4eb..f294135c 100644 --- a/lib/src/widgets/solid_backup_dialog.dart +++ b/lib/src/widgets/solid_backup_dialog.dart @@ -378,7 +378,7 @@ class _SolidBackupDialogState extends State { icon: const Icon(kIsWeb ? Icons.refresh : Icons.exit_to_app), label: const Text(kIsWeb ? 'OK' : 'Close App'), onPressed: () { - // Close the dialogs first. If the platform declines to close + // Close the dialogs first. If the platform declines to cse // the app, the user is at least left with it usable and the // banner still explaining that a restart is needed. diff --git a/lib/src/widgets/solid_status_bar_models.dart b/lib/src/widgets/solid_status_bar_models.dart index 7e78f346..2684c8e3 100644 --- a/lib/src/widgets/solid_status_bar_models.dart +++ b/lib/src/widgets/solid_status_bar_models.dart @@ -30,6 +30,7 @@ library; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:solidui/src/utils/web_id_parser.dart'; @@ -313,10 +314,18 @@ class SolidSecurityKeyStatus { }); /// Get the display text based on key status. + /// + /// On the web the security key is cached in `sessionStorage` (per-tab, cleared + /// when the tab/window is closed), so the "saved" wording reflects that it is + /// session-scoped rather than persisted on the device. Native platforms keep + /// the "Cached Locally" wording. String get displayText { if (isKeySaved == true) { - return keySavedText ?? 'Security Key Cached Locally'; + if (keySavedText != null) return keySavedText!; + return kIsWeb + ? 'Security Key Cached for Session' + : 'Security Key Cached Locally'; } else { return keyNotSavedText ?? 'Security Key Not Cached'; } From 5748bab53e860a18d9ed1c558c9aa1c8554c5d3a Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sun, 16 Aug 2026 21:05:14 +1000 Subject: [PATCH 2/3] Lint --- analysis_options.yaml | 7 + example/analysis_options.yaml | 9 + lib/src/services/solid_backup_models.dart | 232 +++++++++++++++++++++ lib/src/services/solid_backup_service.dart | 205 +----------------- lib/src/widgets/solid_popup_login.dart | 4 +- 5 files changed, 252 insertions(+), 205 deletions(-) create mode 100644 lib/src/services/solid_backup_models.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 42239ef8..6f3026d4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -28,3 +28,10 @@ analyzer: exclude: - ignore/** - ignore/ + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 0d290213..bf8d4218 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -7,6 +7,15 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml linter: diff --git a/lib/src/services/solid_backup_models.dart b/lib/src/services/solid_backup_models.dart new file mode 100644 index 00000000..d8ea5d01 --- /dev/null +++ b/lib/src/services/solid_backup_models.dart @@ -0,0 +1,232 @@ +/// Data models for the backup service (see `solid_backup_service.dart`). +/// +/// Copyright (C) 2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// 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. +/// +/// Authors: Tony Chen + +part of 'solid_backup_service.dart'; + +// Public models. + +/// The metadata read from a backup file's header before any decryption. +/// +/// This is what [SolidBackupService.inspect] returns so the UI can decide +/// whether the file even belongs to this application, and later verify a +/// user-supplied security key against [keyFingerprint] — all without touching +/// the (still-encrypted) payload. + +class SolidBackupHeader { + /// Constructor. + + const SolidBackupHeader({ + required this.formatVersion, + required this.appId, + required this.appUrl, + required this.createdAt, + required this.fileCount, + required this.saltBase64, + required this.ivBase64, + required this.keyFingerprint, + required this.payloadBase64, + }); + + /// The backup container format version (see [kBackupFormatVersion]). + + final int formatVersion; + + /// The canonical application identifier (the app's POD folder name), used to + /// verify that a backup was created by the same application even when it + /// originated on a different POD (which changes [appUrl]). + + final String appId; + + /// The full URL of the source app's data folder at export time. Kept for + /// display and provenance; cross-POD verification relies on [appId], which is + /// derived from this URL and is POD-independent. + + final String appUrl; + + /// When the backup was created (ISO-8601, UTC). + + final String createdAt; + + /// The number of data files stored in the backup. + + final int fileCount; + + /// The base64 salt used both to derive the AES key from the original + /// security key and to compute [keyFingerprint]. + + final String saltBase64; + + /// The base64 initialisation vector for the AES-CBC payload. + + final String ivBase64; + + /// A one-way fingerprint of the security key used at export time. It cannot + /// reconstruct the key but uniquely confirms whether a supplied key matches. + + final String keyFingerprint; + + /// The base64 AES-CBC ciphertext of the (JSON) file bundle. + + final String payloadBase64; +} + +/// The outcome of an export operation. + +class SolidBackupExport { + /// Constructor. + + const SolidBackupExport({ + required this.bytes, + required this.suggestedFileName, + required this.fileCount, + }); + + /// The complete, self-contained backup file content (gzip-compressed). + + final Uint8List bytes; + + /// A sensible default file name, e.g. `myapp_backup_20260715_1032.solidbak`. + + final String suggestedFileName; + + /// The number of data files captured. + + final int fileCount; +} + +/// The outcome of an import operation. + +class SolidBackupImport { + /// Constructor. + + const SolidBackupImport({ + required this.restoredCount, + required this.removedCount, + required this.skipped, + }); + + /// The number of files written (re-encrypted) to the destination POD. + + final int restoredCount; + + /// The number of pre-existing data files removed to mirror the backup. + + final int removedCount; + + /// Files that could not be restored, mapped to the reason. Empty on a fully + /// successful import. + + final Map skipped; +} + +/// Thrown when an imported backup was created by a different application. + +class BackupAppMismatchException implements Exception { + /// Constructor. + + BackupAppMismatchException(this.backupAppId, this.currentAppId); + + /// The application identifier recorded in the backup. + + final String backupAppId; + + /// The identifier of the application attempting the import. + + final String currentAppId; + + @override + String toString() => 'BackupAppMismatchException: backup was created by ' + '"$backupAppId" but this application is "$currentAppId"'; +} + +/// Thrown when a file's content is not a well-formed solidui backup. + +class InvalidBackupFileException implements Exception { + /// Constructor. + + InvalidBackupFileException(this.message); + + /// The error message. + + final String message; + + @override + String toString() => 'InvalidBackupFileException: $message'; +} + +// A single backed-up file: its path relative to the app's data folder, the +// decrypted content, and whether it was encrypted at rest on the source POD +// (so it can be written back with the same protection). +// +// A "large" entry is one of solidpod's chunked large files (see _PodFile). +// Its bytes are arbitrary binary, so the content is base64 rather than the +// file's text. + +class _BackupEntry { + _BackupEntry({ + required this.path, + required this.content, + required this.encrypted, + this.large = false, + }); + + factory _BackupEntry.fromJson(Map json) => _BackupEntry( + path: json['path'] as String, + content: json['content'] as String, + encrypted: json['encrypted'] as bool? ?? true, + large: json['large'] as bool? ?? false, + ); + + final String path; + final String content; + final bool encrypted; + final bool large; + + Map toJson() => { + 'path': path, + 'content': content, + 'encrypted': encrypted, + if (large) 'large': true, + }; +} + +// A file discovered in the data folder. +// +// solidpod stores a large file (written with writeLargeFile()) as a hidden +// ".chunks" directory of encrypted binary chunks plus a ".ttl" +// metadata file. Those parts cannot be read or restored individually — the +// chunks are not text, and their key is bound to the source POD — so the walk +// reports the large file itself and the backup goes through solidpod's +// large-file API instead. + +class _PodFile { + _PodFile(this.path, {required this.large}); + + final String path; + final bool large; +} diff --git a/lib/src/services/solid_backup_service.dart b/lib/src/services/solid_backup_service.dart index ce59991f..e8c6ac84 100644 --- a/lib/src/services/solid_backup_service.dart +++ b/lib/src/services/solid_backup_service.dart @@ -58,6 +58,8 @@ import 'package:solidpod/solidpod.dart' writeLargeFile, writePod; +part 'solid_backup_models.dart'; + // Format constants. /// The magic string that identifies a solidui backup file. Stored (in the @@ -96,209 +98,6 @@ const int _ivLength = 16; const String _kdfInfoAesKey = 'solidui-backup/v1/aes-key'; const String _kdfInfoFingerprint = 'solidui-backup/v1/fingerprint'; -// Public models. - -/// The metadata read from a backup file's header before any decryption. -/// -/// This is what [SolidBackupService.inspect] returns so the UI can decide -/// whether the file even belongs to this application, and later verify a -/// user-supplied security key against [keyFingerprint] — all without touching -/// the (still-encrypted) payload. - -class SolidBackupHeader { - /// Constructor. - - const SolidBackupHeader({ - required this.formatVersion, - required this.appId, - required this.appUrl, - required this.createdAt, - required this.fileCount, - required this.saltBase64, - required this.ivBase64, - required this.keyFingerprint, - required this.payloadBase64, - }); - - /// The backup container format version (see [kBackupFormatVersion]). - - final int formatVersion; - - /// The canonical application identifier (the app's POD folder name), used to - /// verify that a backup was created by the same application even when it - /// originated on a different POD (which changes [appUrl]). - - final String appId; - - /// The full URL of the source app's data folder at export time. Kept for - /// display and provenance; cross-POD verification relies on [appId], which is - /// derived from this URL and is POD-independent. - - final String appUrl; - - /// When the backup was created (ISO-8601, UTC). - - final String createdAt; - - /// The number of data files stored in the backup. - - final int fileCount; - - /// The base64 salt used both to derive the AES key from the original - /// security key and to compute [keyFingerprint]. - - final String saltBase64; - - /// The base64 initialisation vector for the AES-CBC payload. - - final String ivBase64; - - /// A one-way fingerprint of the security key used at export time. It cannot - /// reconstruct the key but uniquely confirms whether a supplied key matches. - - final String keyFingerprint; - - /// The base64 AES-CBC ciphertext of the (JSON) file bundle. - - final String payloadBase64; -} - -/// The outcome of an export operation. - -class SolidBackupExport { - /// Constructor. - - const SolidBackupExport({ - required this.bytes, - required this.suggestedFileName, - required this.fileCount, - }); - - /// The complete, self-contained backup file content (gzip-compressed). - - final Uint8List bytes; - - /// A sensible default file name, e.g. `myapp_backup_20260715_1032.solidbak`. - - final String suggestedFileName; - - /// The number of data files captured. - - final int fileCount; -} - -/// The outcome of an import operation. - -class SolidBackupImport { - /// Constructor. - - const SolidBackupImport({ - required this.restoredCount, - required this.removedCount, - required this.skipped, - }); - - /// The number of files written (re-encrypted) to the destination POD. - - final int restoredCount; - - /// The number of pre-existing data files removed to mirror the backup. - - final int removedCount; - - /// Files that could not be restored, mapped to the reason. Empty on a fully - /// successful import. - - final Map skipped; -} - -/// Thrown when an imported backup was created by a different application. - -class BackupAppMismatchException implements Exception { - /// Constructor. - - BackupAppMismatchException(this.backupAppId, this.currentAppId); - - /// The application identifier recorded in the backup. - - final String backupAppId; - - /// The identifier of the application attempting the import. - - final String currentAppId; - - @override - String toString() => 'BackupAppMismatchException: backup was created by ' - '"$backupAppId" but this application is "$currentAppId"'; -} - -/// Thrown when a file's content is not a well-formed solidui backup. - -class InvalidBackupFileException implements Exception { - /// Constructor. - - InvalidBackupFileException(this.message); - - /// The error message. - - final String message; - - @override - String toString() => 'InvalidBackupFileException: $message'; -} - -// A single backed-up file: its path relative to the app's data folder, the -// decrypted content, and whether it was encrypted at rest on the source POD -// (so it can be written back with the same protection). -// -// A "large" entry is one of solidpod's chunked large files (see _PodFile). -// Its bytes are arbitrary binary, so the content is base64 rather than the -// file's text. - -class _BackupEntry { - _BackupEntry({ - required this.path, - required this.content, - required this.encrypted, - this.large = false, - }); - - factory _BackupEntry.fromJson(Map json) => _BackupEntry( - path: json['path'] as String, - content: json['content'] as String, - encrypted: json['encrypted'] as bool? ?? true, - large: json['large'] as bool? ?? false, - ); - - final String path; - final String content; - final bool encrypted; - final bool large; - - Map toJson() => { - 'path': path, - 'content': content, - 'encrypted': encrypted, - if (large) 'large': true, - }; -} - -// A file discovered in the data folder. -// -// solidpod stores a large file (written with writeLargeFile()) as a hidden -// ".chunks" directory of encrypted binary chunks plus a ".ttl" -// metadata file. Those parts cannot be read or restored individually — the -// chunks are not text, and their key is bound to the source POD — so the walk -// reports the large file itself and the backup goes through solidpod's -// large-file API instead. - -class _PodFile { - _PodFile(this.path, {required this.large}); - - final String path; - final bool large; -} - // Service. /// Exports and imports the current application's POD data folder as a single diff --git a/lib/src/widgets/solid_popup_login.dart b/lib/src/widgets/solid_popup_login.dart index 36adb24d..f5456c1c 100644 --- a/lib/src/widgets/solid_popup_login.dart +++ b/lib/src/widgets/solid_popup_login.dart @@ -1,4 +1,4 @@ -/// pop up login button +/// Pop up login button /// /// Copyright (C) 2025, Software Innovation Institute, ANU. /// @@ -185,7 +185,7 @@ class _SolidPopupLoginState extends State { if (!context.mounted) return false; - return _checkAndSetupPod(context); + return await _checkAndSetupPod(context); } on Object catch (e) { debugPrint('solidAuthenticate() failed: $e'); From 234dda9275056c430f136c738f1a0fb789fcbf77 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Sun, 16 Aug 2026 21:09:12 +1000 Subject: [PATCH 3/3] Lint --- .lycheeignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.lycheeignore b/.lycheeignore index 3b2e8120..51029500 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -115,3 +115,7 @@ https://papertrail.anu.edu.au/ https://www.reddit.com/r/SAI_Rattle/ https://rdrr.io/rforge/tm/man/stopwords.html + +# 20260816 tonypioneer solidpod failures +https://anushkavidanage.github.io/solidpod/example/redirect.html +https://anushkavidanage.github.io/solidpod/example/client-profile.jsonld