From 347c4a28191a501085d63e694c7724ad414d92c2 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 11 Aug 2026 20:19:46 +1000 Subject: [PATCH 1/5] Fix the issue that the security key can be retrieved from web localStorage --- lib/src/solid/constants/common.dart | 8 + lib/src/solid/utils/key_storage.dart | 120 +++++++++++- .../utils/web_secure_key_cache_stub.dart | 49 +++++ .../solid/utils/web_secure_key_cache_web.dart | 182 ++++++++++++++++++ pubspec.yaml | 3 +- 5 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 lib/src/solid/utils/web_secure_key_cache_stub.dart create mode 100644 lib/src/solid/utils/web_secure_key_cache_web.dart diff --git a/lib/src/solid/constants/common.dart b/lib/src/solid/constants/common.dart index e0bd7ae0..d33a9655 100644 --- a/lib/src/solid/constants/common.dart +++ b/lib/src/solid/constants/common.dart @@ -196,6 +196,14 @@ const String demoWebID = /// same-origin script (e.g. via XSS) can recover both key and ciphertext. /// Web therefore provides encryption-at-rest but not the full trust-no-one /// guarantee unless a `wrapKey` is supplied. +/// +/// Because of this, the *security key* — which derives the master key that +/// protects every encrypted resource — is NEVER persisted here on web. It is +/// kept in memory for the session only ([KeyStorage] handles this) and must +/// be re-entered after a page reload. The DPoP private key and OIDC tokens +/// ([AuthDataManager]) are still cached in web localStorage as a usability +/// trade-off; they grant session access but not data decryption, and expire. +/// Supplying a `WebOptions.wrapKey` would harden those too. FlutterSecureStorage secureStorage = const FlutterSecureStorage( iOptions: IOSOptions( diff --git a/lib/src/solid/utils/key_storage.dart b/lib/src/solid/utils/key_storage.dart index c063c146..1332dfab 100644 --- a/lib/src/solid/utils/key_storage.dart +++ b/lib/src/solid/utils/key_storage.dart @@ -28,10 +28,12 @@ library; -import 'package:flutter/foundation.dart' show debugPrint; +import 'package:flutter/foundation.dart' show debugPrint, kIsWeb; import 'package:solidpod/src/solid/constants/common.dart'; import 'package:solidpod/src/solid/utils/misc.dart'; +import 'package:solidpod/src/solid/utils/web_secure_key_cache_stub.dart' + if (dart.library.js_interop) 'package:solidpod/src/solid/utils/web_secure_key_cache_web.dart'; /// Manages secure storage operations for the security key. @@ -40,9 +42,61 @@ class KeyStorage { static const String _securityKeySecureStorageKey = '_solid_security_key'; - /// Check if the security key exists in secure storage. + // Session copy of the security key on web (avoids an IndexedDB round-trip on + // every read within a session). + // + // Why the web path is special: plain `flutter_secure_storage` keeps its + // AES-GCM key *unwrapped* in the same `localStorage` as the ciphertext, so a + // same-origin script or a copy of `localStorage` recovers both — and the + // security key derives the master key protecting every encrypted resource. + // + // On web we therefore cache the security key via [WebSecureKeyCache]: it is + // encrypted under a **non-extractable** AES-GCM key held as an opaque + // `CryptoKey` in IndexedDB. A storage dump then yields only ciphertext and a + // key handle whose bytes can never be exported, so the key cannot be + // recovered offline. (A live same-origin XSS can still *use* — but not + // exfiltrate — the key; that is an inherent browser limitation, mitigated by + // CSP/XSS prevention, not storage.) The cache survives reloads, so the user + // is not forced to re-enter the key. + // + // Native platforms are unaffected: they use the OS-backed secure store + // (Keychain / Keystore / DPAPI / libsecret) and persist as before. + + static String? _webSecurityKey; + + // Best-effort removal of any security key a previous build may have written + // to web `localStorage`, so upgrading users do not leave the exposed value + // behind. Never re-reads the value. + + static Future _purgeLegacyWebEntry() async { + try { + if (await secureStorage.containsKey(key: _securityKeySecureStorageKey)) { + await secureStorage.delete(key: _securityKeySecureStorageKey); + debugPrint('KeyStorage => purged legacy web security key'); + } + } on Object catch (e) { + debugPrint('KeyStorage => _purgeLegacyWebEntry() error: ${e.runtimeType}'); + } + } + + /// Check if the security key exists. + /// + /// On web this checks the in-memory session value and the encrypted + /// IndexedDB cache; on native platforms it queries secure storage. static Future hasStoredSecurityKey() async { + if (kIsWeb) { + if (_webSecurityKey != null) { + return true; + } + try { + return await WebSecureKeyCache.has(); + } on Object catch (e) { + debugPrint('KeyStorage => hasStoredSecurityKey(web) ' + 'error: ${e.runtimeType}'); + return false; + } + } try { final key = await secureStorage.read(key: _securityKeySecureStorageKey); return key != null; @@ -56,9 +110,27 @@ class KeyStorage { } } - /// Read the security key from secure storage. + /// Read the security key. + /// + /// On web this returns the in-memory session value, falling back to the + /// encrypted IndexedDB cache (never a plaintext `localStorage` value); on + /// native platforms it reads from secure storage. static Future readSecurityKey() async { + if (kIsWeb) { + if (_webSecurityKey != null) { + return _webSecurityKey; + } + try { + _webSecurityKey = await WebSecureKeyCache.read(); + } on Object catch (e) { + // Any decode/crypto error => behave as if not cached (re-prompt). + + debugPrint('KeyStorage => readSecurityKey(web) error: ${e.runtimeType}'); + _webSecurityKey = null; + } + return _webSecurityKey; + } try { return await secureStorage.read(key: _securityKeySecureStorageKey); } catch (e) { @@ -69,22 +141,58 @@ class KeyStorage { } } - /// Write the security key to secure storage. + /// Write the security key. + /// + /// On web the key is kept in memory and cached in encrypted form via + /// [WebSecureKeyCache] (non-extractable IndexedDB key); any legacy plaintext + /// `localStorage` copy is purged. On native platforms it is written to secure + /// storage. static Future writeSecurityKey(String securityKey) async { + if (kIsWeb) { + _webSecurityKey = securityKey; + try { + // Encrypted, dump-resistant persistence (survives reload). + + await WebSecureKeyCache.write(securityKey); + } on Object catch (e) { + // Best-effort: on failure we keep the key in memory for this session + // only (the user re-enters it after a reload). + + debugPrint('KeyStorage => writeSecurityKey(web) ' + 'cache failed: ${e.runtimeType}'); + } + // Remove any plaintext value persisted by an earlier build. + + await _purgeLegacyWebEntry(); + return; + } await writeToSecureStorage( _securityKeySecureStorageKey, securityKey, ); } - /// Remove the security key from secure storage. + /// Remove the security key. /// /// This function is platform-safe: - /// - Uses FlutterSecureStorage which is safe on all platforms including web. + /// - On web it clears the in-memory value, deletes the encrypted IndexedDB + /// cache, and purges any legacy plaintext `localStorage` copy. + /// - On native platforms it deletes from FlutterSecureStorage. /// - Errors during deletion are logged but don't prevent function from completing. static Future deleteSecurityKey() async { + if (kIsWeb) { + _webSecurityKey = null; + try { + await WebSecureKeyCache.delete(); + } on Object catch (e) { + debugPrint('KeyStorage => deleteSecurityKey(web) ' + 'error: ${e.runtimeType}'); + } + await _purgeLegacyWebEntry(); + return; + } try { if (await secureStorage.containsKey(key: _securityKeySecureStorageKey)) { try { diff --git a/lib/src/solid/utils/web_secure_key_cache_stub.dart b/lib/src/solid/utils/web_secure_key_cache_stub.dart new file mode 100644 index 00000000..e03e9a7d --- /dev/null +++ b/lib/src/solid/utils/web_secure_key_cache_stub.dart @@ -0,0 +1,49 @@ +/// Native stub for [WebSecureKeyCache]. +/// +/// 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 + +library; + +/// No-op stand-in for the web-only secure key cache (see the web variant). + +class WebSecureKeyCache { + /// Persist the security key (web only). No-op on native. + + static Future write(String securityKey) async {} + + /// Read the cached security key (web only). Always null on native. + + static Future read() async => null; + + /// Remove the cached security key (web only). No-op on native. + + static Future delete() async {} + + /// Whether a security key is cached (web only). Always false on native. + + static Future has() async => false; +} diff --git a/lib/src/solid/utils/web_secure_key_cache_web.dart b/lib/src/solid/utils/web_secure_key_cache_web.dart new file mode 100644 index 00000000..32f034f6 --- /dev/null +++ b/lib/src/solid/utils/web_secure_key_cache_web.dart @@ -0,0 +1,182 @@ +/// Web-only secure cache for the security key. +/// +/// 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 + +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:web/web.dart' as web; + +// JS object literals for the Web Crypto algorithm parameters. `js_interop` +// turns an extension-type factory with named parameters into a plain JS object. + +extension type _AesKeyGenParams._(JSObject _) implements JSObject { + external factory _AesKeyGenParams({required String name, required int length}); +} + +extension type _AesGcmParams._(JSObject _) implements JSObject { + external factory _AesGcmParams({required String name, required JSAny iv}); +} + +const String _dbName = 'solidpod_secure'; +const String _storeName = 'kv'; +const String _wrapKeyId = 'security_key_wrap_key'; +const String _ivId = 'security_key_iv'; +const String _ctId = 'security_key_ct'; + +/// Web implementation of the security-key cache (see the library doc comment). + +class WebSecureKeyCache { + static web.SubtleCrypto get _subtle => web.window.crypto.subtle; + + // IndexedDB helpers. + + static Future _openDb() { + final completer = Completer(); + final req = web.window.indexedDB.open(_dbName, 1); + req.onupgradeneeded = ((web.Event _) { + final db = req.result as web.IDBDatabase; + if (!db.objectStoreNames.contains(_storeName)) { + db.createObjectStore(_storeName); + } + }).toJS; + req.onsuccess = ((web.Event _) { + completer.complete(req.result as web.IDBDatabase); + }).toJS; + req.onerror = ((web.Event _) { + completer.completeError(StateError('IndexedDB open failed')); + }).toJS; + return completer.future; + } + + static Future _await(web.IDBRequest req) { + final completer = Completer(); + req.onsuccess = ((web.Event _) { + completer.complete(req.result); + }).toJS; + req.onerror = ((web.Event _) { + completer.completeError(StateError('IndexedDB request failed')); + }).toJS; + return completer.future; + } + + static Future _put(web.IDBDatabase db, String id, JSAny value) async { + final tx = db.transaction(_storeName.toJS, 'readwrite'); + await _await(tx.objectStore(_storeName).put(value, id.toJS)); + } + + static Future _get(web.IDBDatabase db, String id) async { + final tx = db.transaction(_storeName.toJS, 'readonly'); + return _await(tx.objectStore(_storeName).get(id.toJS)); + } + + // Wrapping key. + + static Future _getOrCreateWrapKey(web.IDBDatabase db) async { + final existing = await _get(db, _wrapKeyId); + if (existing != null) { + // Only CryptoKey objects are ever stored under this id. + + return existing as web.CryptoKey; + } + final key = (await _subtle + .generateKey( + _AesKeyGenParams(name: 'AES-GCM', length: 256), + false, // extractable: false — bytes can never be exported. + ['encrypt'.toJS, 'decrypt'.toJS].toJS, + ) + .toDart) as web.CryptoKey; + await _put(db, _wrapKeyId, key); + return key; + } + + // Public API. + + /// Encrypt and persist [securityKey] under the non-extractable wrapping key. + + static Future write(String securityKey) async { + final db = await _openDb(); + final wrapKey = await _getOrCreateWrapKey(db); + + final iv = + (web.window.crypto.getRandomValues(Uint8List(12).toJS) as JSUint8Array) + .toDart; + final ctBuf = (await _subtle + .encrypt( + _AesGcmParams(name: 'AES-GCM', iv: iv.toJS), + wrapKey, + Uint8List.fromList(utf8.encode(securityKey)).toJS, + ) + .toDart) as JSArrayBuffer; + + await _put(db, _ivId, iv.toJS); + await _put(db, _ctId, ctBuf.toDart.asUint8List().toJS); + } + + /// Decrypt and return the cached security key, or null if none/undecryptable. + + static Future read() async { + final db = await _openDb(); + final keyAny = await _get(db, _wrapKeyId); + final ivAny = await _get(db, _ivId); + final ctAny = await _get(db, _ctId); + if (keyAny == null || ivAny == null || ctAny == null) { + return null; + } + final ptBuf = (await _subtle + .decrypt( + _AesGcmParams(name: 'AES-GCM', iv: ivAny), + keyAny as web.CryptoKey, + ctAny as JSUint8Array, + ) + .toDart) as JSArrayBuffer; + return utf8.decode(ptBuf.toDart.asUint8List()); + } + + /// Remove all cached security-key material (key handle, IV, ciphertext). + + static Future delete() async { + final db = await _openDb(); + for (final id in const [_wrapKeyId, _ivId, _ctId]) { + final tx = db.transaction(_storeName.toJS, 'readwrite'); + await _await(tx.objectStore(_storeName).delete(id.toJS)); + } + } + + /// Whether a wrapped security key is currently cached. + + static Future has() async { + final db = await _openDb(); + final key = await _get(db, _wrapKeyId); + final ct = await _get(db, _ctId); + return key != null && ct != null; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 55864787..f3c89a1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 1.0.15 homepage: https://github.com/anusii/solidpod environment: - sdk: '>=3.2.3 <4.0.0' + sdk: '>=3.3.0 <4.0.0' flutter: '>=1.17.0' # To automatically upgrade package dependencies to the latest versions: @@ -36,6 +36,7 @@ dependencies: rdflib: ^0.2.12 solid_auth: ^1.0.2 universal_io: ^2.3.1 + web: ^1.1.0 dev_dependencies: build_runner: ^2.10.5 From b233d225defd49c39cd928b32097b4c6513b8b88 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 11 Aug 2026 20:21:40 +1000 Subject: [PATCH 2/5] Lint --- lib/src/solid/utils/key_storage.dart | 6 ++- .../solid/utils/web_secure_key_cache_web.dart | 39 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/src/solid/utils/key_storage.dart b/lib/src/solid/utils/key_storage.dart index 1332dfab..f8357c82 100644 --- a/lib/src/solid/utils/key_storage.dart +++ b/lib/src/solid/utils/key_storage.dart @@ -75,7 +75,8 @@ class KeyStorage { debugPrint('KeyStorage => purged legacy web security key'); } } on Object catch (e) { - debugPrint('KeyStorage => _purgeLegacyWebEntry() error: ${e.runtimeType}'); + debugPrint( + 'KeyStorage => _purgeLegacyWebEntry() error: ${e.runtimeType}'); } } @@ -126,7 +127,8 @@ class KeyStorage { } on Object catch (e) { // Any decode/crypto error => behave as if not cached (re-prompt). - debugPrint('KeyStorage => readSecurityKey(web) error: ${e.runtimeType}'); + debugPrint( + 'KeyStorage => readSecurityKey(web) error: ${e.runtimeType}'); _webSecurityKey = null; } return _webSecurityKey; diff --git a/lib/src/solid/utils/web_secure_key_cache_web.dart b/lib/src/solid/utils/web_secure_key_cache_web.dart index 32f034f6..b21aa637 100644 --- a/lib/src/solid/utils/web_secure_key_cache_web.dart +++ b/lib/src/solid/utils/web_secure_key_cache_web.dart @@ -39,7 +39,8 @@ import 'package:web/web.dart' as web; // turns an extension-type factory with named parameters into a plain JS object. extension type _AesKeyGenParams._(JSObject _) implements JSObject { - external factory _AesKeyGenParams({required String name, required int length}); + external factory _AesKeyGenParams( + {required String name, required int length}); } extension type _AesGcmParams._(JSObject _) implements JSObject { @@ -108,12 +109,12 @@ class WebSecureKeyCache { return existing as web.CryptoKey; } final key = (await _subtle - .generateKey( - _AesKeyGenParams(name: 'AES-GCM', length: 256), - false, // extractable: false — bytes can never be exported. - ['encrypt'.toJS, 'decrypt'.toJS].toJS, - ) - .toDart) as web.CryptoKey; + .generateKey( + _AesKeyGenParams(name: 'AES-GCM', length: 256), + false, // extractable: false — bytes can never be exported. + ['encrypt'.toJS, 'decrypt'.toJS].toJS, + ) + .toDart) as web.CryptoKey; await _put(db, _wrapKeyId, key); return key; } @@ -130,12 +131,12 @@ class WebSecureKeyCache { (web.window.crypto.getRandomValues(Uint8List(12).toJS) as JSUint8Array) .toDart; final ctBuf = (await _subtle - .encrypt( - _AesGcmParams(name: 'AES-GCM', iv: iv.toJS), - wrapKey, - Uint8List.fromList(utf8.encode(securityKey)).toJS, - ) - .toDart) as JSArrayBuffer; + .encrypt( + _AesGcmParams(name: 'AES-GCM', iv: iv.toJS), + wrapKey, + Uint8List.fromList(utf8.encode(securityKey)).toJS, + ) + .toDart) as JSArrayBuffer; await _put(db, _ivId, iv.toJS); await _put(db, _ctId, ctBuf.toDart.asUint8List().toJS); @@ -152,12 +153,12 @@ class WebSecureKeyCache { return null; } final ptBuf = (await _subtle - .decrypt( - _AesGcmParams(name: 'AES-GCM', iv: ivAny), - keyAny as web.CryptoKey, - ctAny as JSUint8Array, - ) - .toDart) as JSArrayBuffer; + .decrypt( + _AesGcmParams(name: 'AES-GCM', iv: ivAny), + keyAny as web.CryptoKey, + ctAny as JSUint8Array, + ) + .toDart) as JSArrayBuffer; return utf8.decode(ptBuf.toDart.asUint8List()); } From a325e9594c719e38146576cf77274e7631f00839 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 11 Aug 2026 20:30:09 +1000 Subject: [PATCH 3/5] Lint --- lib/src/solid/utils/key_storage.dart | 6 ++++-- lib/src/solid/utils/web_secure_key_cache_web.dart | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/solid/utils/key_storage.dart b/lib/src/solid/utils/key_storage.dart index f8357c82..5d417180 100644 --- a/lib/src/solid/utils/key_storage.dart +++ b/lib/src/solid/utils/key_storage.dart @@ -76,7 +76,8 @@ class KeyStorage { } } on Object catch (e) { debugPrint( - 'KeyStorage => _purgeLegacyWebEntry() error: ${e.runtimeType}'); + 'KeyStorage => _purgeLegacyWebEntry() error: ${e.runtimeType}', + ); } } @@ -128,7 +129,8 @@ class KeyStorage { // Any decode/crypto error => behave as if not cached (re-prompt). debugPrint( - 'KeyStorage => readSecurityKey(web) error: ${e.runtimeType}'); + 'KeyStorage => readSecurityKey(web) error: ${e.runtimeType}', + ); _webSecurityKey = null; } return _webSecurityKey; diff --git a/lib/src/solid/utils/web_secure_key_cache_web.dart b/lib/src/solid/utils/web_secure_key_cache_web.dart index b21aa637..95e7283f 100644 --- a/lib/src/solid/utils/web_secure_key_cache_web.dart +++ b/lib/src/solid/utils/web_secure_key_cache_web.dart @@ -39,8 +39,10 @@ import 'package:web/web.dart' as web; // turns an extension-type factory with named parameters into a plain JS object. extension type _AesKeyGenParams._(JSObject _) implements JSObject { - external factory _AesKeyGenParams( - {required String name, required int length}); + external factory _AesKeyGenParams({ + required String name, + required int length, + }); } extension type _AesGcmParams._(JSObject _) implements JSObject { From 838a93fa9aa79816a624eab5135f441f979f2479 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 11 Aug 2026 20:31:11 +1000 Subject: [PATCH 4/5] Lint --- .lycheeignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.lycheeignore b/.lycheeignore index 49be24e1..52cd7105 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -85,6 +85,8 @@ https://server/POD_NAME/APP_NAME/data/FILE_PATH https://server/alice/ https://server/alice/myapp/ https://anusii.github.io/solidpodeg/client-profile.jsonld +https://anushkavidanage.github.io/solidpod/example/redirect.html +https://anushkavidanage.github.io/solidpod/example/client-profile.jsonld # 20260605 gjw Failing solid servers From da7cf3eeca27b7bf9ebf4f1024aa21a85a621bf3 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 11 Aug 2026 20:39:22 +1000 Subject: [PATCH 5/5] Lint --- .../solid/utils/web_secure_key_cache_web.dart | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/src/solid/utils/web_secure_key_cache_web.dart b/lib/src/solid/utils/web_secure_key_cache_web.dart index 95e7283f..e7f927e5 100644 --- a/lib/src/solid/utils/web_secure_key_cache_web.dart +++ b/lib/src/solid/utils/web_secure_key_cache_web.dart @@ -31,24 +31,11 @@ library; import 'dart:async'; import 'dart:convert'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'dart:typed_data'; import 'package:web/web.dart' as web; -// JS object literals for the Web Crypto algorithm parameters. `js_interop` -// turns an extension-type factory with named parameters into a plain JS object. - -extension type _AesKeyGenParams._(JSObject _) implements JSObject { - external factory _AesKeyGenParams({ - required String name, - required int length, - }); -} - -extension type _AesGcmParams._(JSObject _) implements JSObject { - external factory _AesGcmParams({required String name, required JSAny iv}); -} - const String _dbName = 'solidpod_secure'; const String _storeName = 'kv'; const String _wrapKeyId = 'security_key_wrap_key'; @@ -60,6 +47,19 @@ const String _ctId = 'security_key_ct'; class WebSecureKeyCache { static web.SubtleCrypto get _subtle => web.window.crypto.subtle; + // JS object literals for the Web Crypto algorithm parameters. Built with + // setProperty rather than an extension-type factory so the sunset + // dart_code_metrics unused-code check (which cannot parse extension types) + // does not report false positives. + + static JSObject _aesKeyGenParams() => JSObject() + ..setProperty('name'.toJS, 'AES-GCM'.toJS) + ..setProperty('length'.toJS, 256.toJS); + + static JSObject _aesGcmParams(JSAny iv) => JSObject() + ..setProperty('name'.toJS, 'AES-GCM'.toJS) + ..setProperty('iv'.toJS, iv); + // IndexedDB helpers. static Future _openDb() { @@ -112,7 +112,7 @@ class WebSecureKeyCache { } final key = (await _subtle .generateKey( - _AesKeyGenParams(name: 'AES-GCM', length: 256), + _aesKeyGenParams(), false, // extractable: false — bytes can never be exported. ['encrypt'.toJS, 'decrypt'.toJS].toJS, ) @@ -134,7 +134,7 @@ class WebSecureKeyCache { .toDart; final ctBuf = (await _subtle .encrypt( - _AesGcmParams(name: 'AES-GCM', iv: iv.toJS), + _aesGcmParams(iv.toJS), wrapKey, Uint8List.fromList(utf8.encode(securityKey)).toJS, ) @@ -156,7 +156,7 @@ class WebSecureKeyCache { } final ptBuf = (await _subtle .decrypt( - _AesGcmParams(name: 'AES-GCM', iv: ivAny), + _aesGcmParams(ivAny), keyAny as web.CryptoKey, ctAny as JSUint8Array, )