Repository: AvaloniaUI/Avalonia

Issue:
Mac App Store SandBox File Access with Bookmarks feature
**Is your feature request related to a problem? Please describe.**
OSX native lib can have support for managing file access bookmarks in sandbox.

**What is sandbox bookmarks?**

By default your app in sandbox have no access to any file in the system. You can request access to Downloads/Desktop/Home folders in your entitlements - but this is a bad practice. Best practice - is to get access to the files only over system OpenFileDialog and SaveFileDialog. This is tricky:

1. you should get`NSURL` from dialog and put it into bookmarks
2. result of creation a bookmark - is byte array with bookmark identifier. You should save it anywhere so you can create a `NSURL` object from it later, after app restart for instance.
3. Before and after access to the file you should call startAccess and StopAccess methods of `NSURL`.

[Sandbox Bookmarks Apple Documentation](https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16)

**Describe the solution you'd like**
1. analyze code in https://github.com/leighmcculloch/AppSandboxFileAccess . It's an objectiveC library with all logic we need realized.
2. enrich `IAvnSystemDialogs` to support sandbox, create an interface to work with NSURL objects (get bookmark data, create url from bookmark data, delete bookmark, open bookmark, close bookmark, etc.).
3. create it in osx native lib and register it correctly to fulfill COM requirements (is there any guide for this?)
4. realize interface and test it in real sandboxed app


**Direct API changes(my thoughts):**
1. Add MacOSSandbox flag into `MacOSPlatformOptions`.
2. Check this flag in `IAvnSystemDialogs` realization and if it's true - add all selected `NSURL` to bookmarks and return bookmark data `byte[][]` for every `NSURL`.
3. Add `IAvnSandBoxBookmarks` interface with:
    * bool DeleteBookmark(byte[])
    * IDisposable OpenBookmark(byte[]) - dispose will close bookmark


Hints:
I've found wtf is `.idl` and how to edit COM API between OSX native lib and Avalonia.Native.
So I've started to implementing this feature. This PR #6540 already contains COM API changes.

Patch:
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index 734f126a1b2..9a67ee0161e 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -34,7 +34,7 @@
 		1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
 		37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
 		37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
-		37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
+		37C09D8821580FE4006A6758 /* StorageProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* StorageProvider.mm */; };
 		37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; };
 		37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
 		520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
@@ -95,7 +95,7 @@
 		379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
 		37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
 		37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = "<group>"; };
-		37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = "<group>"; };
+		37C09D8721580FE4006A6758 /* StorageProvider.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StorageProvider.mm; sourceTree = "<group>"; };
 		37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = "<group>"; };
 		37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = "<group>"; };
 		37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = "<group>"; };
@@ -202,7 +202,7 @@
 				523484CB26EA68AA00EA0C2C /* trayicon.h */,
 				1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
 				37A517B22159597E00FBA241 /* Screens.mm */,
-				37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
+				37C09D8721580FE4006A6758 /* StorageProvider.mm */,
 				EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */,
 				AB7A61F02147C815003C5833 /* Products */,
 				AB661C1C2148230E00291242 /* Frameworks */,
@@ -339,7 +339,7 @@
 				1AFD334123E03C4F0042899B /* controlhost.mm in Sources */,
 				1A465D10246AB61600C5858B /* dnd.mm in Sources */,
 				AB00E4F72147CA920032A60A /* main.mm in Sources */,
-				37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
+				37C09D8821580FE4006A6758 /* StorageProvider.mm in Sources */,
 				1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */,
 				F10084862BFF1FB40024303E /* TopLevelImpl.mm in Sources */,
 				1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */,
diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm
similarity index 85%
rename from native/Avalonia.Native/src/OSX/SystemDialogs.mm
rename to native/Avalonia.Native/src/OSX/StorageProvider.mm
index 97c8108edcb..0fd77c6789e 100644
--- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm
+++ b/native/Avalonia.Native/src/OSX/StorageProvider.mm
@@ -64,12 +64,91 @@ - (void)popupAction:(id)sender {
 
 @end
 
-class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
+class StorageProvider : public ComSingleObject<IAvnStorageProvider, &IID_IAvnStorageProvider>
 {
     ExtensionDropdownHandler* __strong _extension_dropdown_handler;
     
 public:
     FORWARD_IUNKNOWN()
+
+    virtual HRESULT SaveBookmarkToBytes (
+         IAvnString* fileUriStr,
+         void** err,
+         IAvnString** ppv
+    ) override
+    {
+        @autoreleasepool
+        {
+            if(ppv == nullptr)
+                return E_POINTER;
+
+            NSError* error;
+            auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)];
+            auto bookmarkData = [fileUri bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
+            if (bookmarkData)
+            {
+                *ppv = CreateByteArray((void*)bookmarkData.bytes, (int)bookmarkData.length);
+            }
+            if (error != nil)
+            {
+                *err = CreateAvnString([error localizedDescription]);
+            }
+            return S_OK;
+        }
+    }
+
+    virtual HRESULT ReadBookmarkFromBytes (
+       void* ptr,
+       int len,
+       IAvnString** ppv
+    ) override {
+        @autoreleasepool
+        {
+            if(ppv == nullptr)
+                return E_POINTER;
+
+            auto bookmarkData = [[NSData alloc] initWithBytes:ptr length:len];
+            auto fileUri = [NSURL URLByResolvingBookmarkData: bookmarkData
+                                                     options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI
+                                               relativeToURL:nil
+                                         bookmarkDataIsStale:nil
+                                                       error:nil];
+            
+            if (fileUri)
+            {
+                *ppv = CreateAvnString([fileUri absoluteString]);
+            }
+            return S_OK;
+        }
+    }
+
+    virtual void ReleaseBookmark (
+        IAvnString* fileUriStr
+    ) override {
+        // no-op
+    }
+
+    virtual bool OpenSecurityScope (
+        IAvnString* fileUriStr
+    ) override {
+        @autoreleasepool
+        {
+            auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)];
+            auto success = [fileUri startAccessingSecurityScopedResource];
+            return success;
+        }
+    }
+
+    virtual void CloseSecurityScope (
+        IAvnString* fileUriStr
+    ) override {
+        @autoreleasepool
+        {
+            auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)];
+            [fileUri stopAccessingSecurityScopedResource];
+        }
+    }
+
     virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
                                      IAvnSystemDialogEvents* events,
                                      bool allowMultiple,
@@ -105,19 +184,9 @@ virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
                     
                     if(urls.count > 0)
                     {
-                        void* strings[urls.count];
-                        
-                        for(int i = 0; i < urls.count; i++)
-                        {
-                            auto url = [urls objectAtIndex:i];
-                            
-                            auto string = [url path];
-                            
-                            strings[i] = (void*)[string UTF8String];
-                        }
-                        
-                        events->OnCompleted((int)urls.count, &strings[0]);
-                        
+                        auto uriStrings = CreateAvnStringArray(urls);
+                        events->OnCompleted(uriStrings);
+    
                         [panel orderOut:panel];
                         
                         if(parentWindowHandle != nullptr)
@@ -130,7 +199,7 @@ virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
                     }
                 }
                 
-                events->OnCompleted(0, nullptr);
+                events->OnCompleted(nullptr);
                 
             };
             
@@ -188,19 +257,9 @@ virtual void OpenFileDialog (IAvnWindow* parentWindowHandle,
                     
                     if(urls.count > 0)
                     {
-                        void* strings[urls.count];
-                        
-                        for(int i = 0; i < urls.count; i++)
-                        {
-                            auto url = [urls objectAtIndex:i];
-                            
-                            auto string = [url path];
-                            
-                            strings[i] = (void*)[string UTF8String];
-                        }
-                        
-                        events->OnCompleted((int)urls.count, &strings[0]);
-                        
+                        auto uriStrings = CreateAvnStringArray(urls);
+                        events->OnCompleted(uriStrings);
+
                         [panel orderOut:panel];
                         
                         if(parentWindowHandle != nullptr)
@@ -213,7 +272,7 @@ virtual void OpenFileDialog (IAvnWindow* parentWindowHandle,
                     }
                 }
                 
-                events->OnCompleted(0, nullptr);
+                events->OnCompleted(nullptr);
                 
             };
             
@@ -264,15 +323,11 @@ virtual void SaveFileDialog (IAvnWindow* parentWindowHandle,
             auto handler = ^(NSModalResponse result) {
                 if(result == NSFileHandlingPanelOKButton)
                 {
-                    void* strings[1];
-                    
                     auto url = [panel URL];
-                    
-                    auto string = [url path];
-                    strings[0] = (void*)[string UTF8String];
-                    
-                    events->OnCompleted(1, &strings[0]);
-                    
+                    auto urls = [NSArray<NSURL*> arrayWithObject:url];
+                    auto uriStrings = CreateAvnStringArray(urls);
+                    events->OnCompleted(uriStrings);
+
                     [panel orderOut:panel];
                     
                     if(parentWindowHandle != nullptr)
@@ -284,7 +339,7 @@ virtual void SaveFileDialog (IAvnWindow* parentWindowHandle,
                     return;
                 }
                 
-                events->OnCompleted(0, nullptr);
+                events->OnCompleted(nullptr);
                 
             };
             
@@ -519,7 +574,7 @@ void SetAccessoryView(NSSavePanel* panel,
     };
 };
 
-extern IAvnSystemDialogs* CreateSystemDialogs()
+extern IAvnStorageProvider* CreateStorageProvider()
 {
-    return new SystemDialogs();
+    return new StorageProvider();
 }
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index fab11d6e4f7..36c157704de 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -14,7 +14,7 @@ extern void PostDispatcherCallback(IAvnActionCallback* cb);
 extern IAvnTopLevel* CreateAvnTopLevel(IAvnTopLevelEvents* events);
 extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events);
 extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events);
-extern IAvnSystemDialogs* CreateSystemDialogs();
+extern IAvnStorageProvider* CreateStorageProvider();
 extern IAvnScreens* CreateScreens(IAvnScreenEvents* cb);
 extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*);
 extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*);
diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm
index 47a5ba73c5f..d1dbe9d1862 100644
--- a/native/Avalonia.Native/src/OSX/main.mm
+++ b/native/Avalonia.Native/src/OSX/main.mm
@@ -276,13 +276,13 @@ virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface*
         }
     }
     
-    virtual HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv) override
+    virtual HRESULT CreateStorageProvider(IAvnStorageProvider** ppv) override
     {
         START_COM_CALL;
         
         @autoreleasepool
         {
-            *ppv = ::CreateSystemDialogs();
+            *ppv = ::CreateStorageProvider();
             return  S_OK;
         }
     }
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index fc66f533c6b..5b7814fb5d1 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -254,17 +254,24 @@ List<FileDialogFilter> GetFilters()
 
                 if (file is not null)
                 {
-                    // Sync disposal of StreamWriter is not supported on WASM
+                    try
+                    {
+                        // Sync disposal of StreamWriter is not supported on WASM
 #if NET6_0_OR_GREATER
-                    await using var stream = await file.OpenWriteAsync();
-                    await using var writer = new System.IO.StreamWriter(stream);
+                        await using var stream = await file.OpenWriteAsync();
+                        await using var writer = new System.IO.StreamWriter(stream);
 #else
-                    using var stream = await file.OpenWriteAsync();
-                    using var writer = new System.IO.StreamWriter(stream);
+                        using var stream = await file.OpenWriteAsync();
+                        using var writer = new System.IO.StreamWriter(stream);
 #endif
-                    await writer.WriteLineAsync(openedFileContent.Text);
+                        await writer.WriteLineAsync(openedFileContent.Text);
 
-                    SetFolder(await file.GetParentAsync());
+                        SetFolder(await file.GetParentAsync());
+                    }
+                    catch (Exception ex)
+                    {
+                        openedFileContent.Text = ex.ToString();
+                    }
                 }
 
                 await SetPickerResult(file is null ? null : new[] { file });
@@ -280,8 +287,6 @@ List<FileDialogFilter> GetFilters()
                 });
 
                 await SetPickerResult(folders);
-
-                SetFolder(folders.FirstOrDefault());
             };
             this.Get<Button>("OpenFileFromBookmark").Click += async delegate
             {
@@ -298,7 +303,6 @@ List<FileDialogFilter> GetFilters()
                     : null;
 
                 await SetPickerResult(folder is null ? null : new[] { folder });
-                SetFolder(folder);
             };
             
             this.Get<Button>("LaunchUri").Click += async delegate
@@ -360,16 +364,30 @@ async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
             Content:
             ";
 
-                        resultText += await ReadTextFromFile(file, 500);
+                        try
+                        {
+                            resultText += await ReadTextFromFile(file, 500);
+                        }
+                        catch (Exception ex)
+                        {
+                            resultText += ex.ToString();
+                        }
                     }
 
                     openedFileContent.Text = resultText;
 
-                    var parent = await item.GetParentAsync();
-                    SetFolder(parent);
-                    if (parent is not null)
+                    if (item is IStorageFolder storageFolder)
                     {
-                        mappedResults.Add(FullPathOrName(parent));
+                        SetFolder(storageFolder);
+                    }
+                    else
+                    {
+                        var parent = await item.GetParentAsync();
+                        SetFolder(parent);
+                        if (parent is not null)
+                        {
+                            mappedResults.Add(FullPathOrName(parent));
+                        }
                     }
 
                     foreach (var selectedItem in items)
@@ -391,7 +409,7 @@ async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
             }
         }
 
-        public static async Task<string> ReadTextFromFile(IStorageFile file, int length)
+        internal static async Task<string> ReadTextFromFile(IStorageFile file, int length)
         {
 #if NET6_0_OR_GREATER
             await using var stream = await file.OpenReadAsync();
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
index 952717729ff..bb27379a702 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
@@ -10,6 +10,7 @@
 using Android.Webkit;
 using Avalonia.Logging;
 using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
 using Java.Lang;
 using AndroidUri = Android.Net.Uri;
 using Exception = System.Exception;
@@ -53,7 +54,8 @@ protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExtern
         }
 
         Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
-        return Uri.ToString();
+
+        return StorageBookmarkHelper.EncodeBookmark(AndroidStorageProvider.AndroidKey, Uri.ToString()!);
     }
 
     public async Task ReleaseBookmarkAsync()
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
index 64c9c3a3cb1..54d6afad43f 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
@@ -1,12 +1,14 @@
 ﻿using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Text;
 using System.Threading.Tasks;
 using Android;
 using Android.App;
 using Android.Content;
 using Android.Provider;
 using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
 using AndroidUri = Android.Net.Uri;
 using Exception = System.Exception;
 using JavaFile = Java.IO.File;
@@ -15,6 +17,7 @@ namespace Avalonia.Android.Platform.Storage;
 
 internal class AndroidStorageProvider : IStorageProvider
 {
+    public static ReadOnlySpan<byte> AndroidKey => "android"u8;
     private readonly Activity _activity;
 
     public AndroidStorageProvider(Activity activity)
@@ -30,8 +33,8 @@ public AndroidStorageProvider(Activity activity)
 
     public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
     {
-        var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
-        return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri, false));
+        var uri = DecodeUriFromBookmark(bookmark);
+        return Task.FromResult<IStorageBookmarkFolder?>(uri is null ? null : new AndroidStorageFolder(_activity, uri, false));
     }
 
     public async Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
@@ -129,8 +132,19 @@ public AndroidStorageProvider(Activity activity)
 
     public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
     {
-        var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
-        return Task.FromResult<IStorageBookmarkFile?>(new AndroidStorageFile(_activity, uri));
+        var uri = DecodeUriFromBookmark(bookmark);
+        return Task.FromResult<IStorageBookmarkFile?>(uri is null ? null : new AndroidStorageFile(_activity, uri));
+    }
+
+    private static AndroidUri? DecodeUriFromBookmark(string bookmark)
+    {
+        return StorageBookmarkHelper.TryDecodeBookmark(AndroidKey, bookmark, out var bytes) switch
+        {
+            StorageBookmarkHelper.DecodeResult.Success => AndroidUri.Parse(Encoding.UTF8.GetString(bytes!)),
+            // Attempt to decode 11.0 android bookmarks
+            StorageBookmarkHelper.DecodeResult.InvalidFormat => AndroidUri.Parse(bookmark),
+            _ => null
+        };
     }
 
     public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
index 715fc182d64..c465dddb847 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
@@ -1,115 +1,10 @@
-﻿using System;
-using System.IO;
-using System.Security;
+﻿using System.IO;
 using System.Threading.Tasks;
 
 namespace Avalonia.Platform.Storage.FileIO;
 
-internal class BclStorageFile : IStorageBookmarkFile
+internal sealed class BclStorageFile(FileInfo fileInfo) : BclStorageItem(fileInfo), IStorageBookmarkFile
 {
-    public BclStorageFile(FileInfo fileInfo)
-    {
-        FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
-    }
-
-    public FileInfo FileInfo { get; }
-    
-    public string Name => FileInfo.Name;
-
-    public virtual bool CanBookmark => true;
-
-    public Uri Path
-    {
-        get
-        {
-            try
-            {
-                if (FileInfo.Directory is not null)
-                {
-                    return StorageProviderHelpers.FilePathToUri(FileInfo.FullName);
-                } 
-            }
-            catch (SecurityException)
-            {
-            }
-            return new Uri(FileInfo.Name, UriKind.Relative);
-        }
-    }
-    
-    public Task<StorageItemProperties> GetBasicPropertiesAsync()
-    {
-        if (FileInfo.Exists)
-        {
-            return Task.FromResult(new StorageItemProperties(
-                (ulong)FileInfo.Length,
-                FileInfo.CreationTimeUtc,
-                FileInfo.LastAccessTimeUtc));
-        }
-        return Task.FromResult(new StorageItemProperties());
-    }
-
-    public Task<IStorageFolder?> GetParentAsync()
-    {
-        if (FileInfo.Directory is { } directory)
-        {
-            return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
-        }
-        return Task.FromResult<IStorageFolder?>(null);
-    }
-
-    public Task<Stream> OpenReadAsync()
-    {
-        return Task.FromResult<Stream>(FileInfo.OpenRead());
-    }
-
-    public Task<Stream> OpenWriteAsync()
-    {
-        var stream = new FileStream(FileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Write);
-        return Task.FromResult<Stream>(stream);
-    }
-
-    public virtual Task<string?> SaveBookmarkAsync()
-    {
-        return Task.FromResult<string?>(FileInfo.FullName);
-    }
-
-    public Task ReleaseBookmarkAsync()
-    {
-        // No-op
-        return Task.CompletedTask;
-    }
-
-    protected virtual void Dispose(bool disposing)
-    {
-    }
-
-    ~BclStorageFile()
-    {
-        Dispose(disposing: false);
-    }
-
-    public void Dispose()
-    {
-        Dispose(disposing: true);
-        GC.SuppressFinalize(this);
-    }
-
-    public Task DeleteAsync()
-    {
-        FileInfo.Delete();
-        return Task.CompletedTask;
-    }
-
-    public Task<IStorageItem?> MoveAsync(IStorageFolder destination)
-    {
-        if (destination is BclStorageFolder storageFolder)
-        {
-            var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, FileInfo.Name);
-            FileInfo.MoveTo(newPath);
-
-            return Task.FromResult<IStorageItem?>(new BclStorageFile(new FileInfo(newPath)));
-        }
-
-        return Task.FromResult<IStorageItem?>(null);
-    }
+    public Task<Stream> OpenReadAsync() => Task.FromResult<Stream>(OpenReadCore(fileInfo));
+    public Task<Stream> OpenWriteAsync() => Task.FromResult<Stream>(OpenWriteCore(fileInfo));
 }
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
index a5727dc3cda..05572d60586 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
@@ -1,128 +1,22 @@
-﻿using System;
-using System.Collections.Generic;
+﻿using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Security;
 using System.Threading.Tasks;
 using Avalonia.Utilities;
 
 namespace Avalonia.Platform.Storage.FileIO;
 
-internal class BclStorageFolder : IStorageBookmarkFolder
+internal sealed class BclStorageFolder(DirectoryInfo directoryInfo)
+    : BclStorageItem(directoryInfo), IStorageBookmarkFolder
 {
-    public BclStorageFolder(DirectoryInfo directoryInfo)
-    {
-        DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
-        if (!DirectoryInfo.Exists)
-        {
-            throw new ArgumentException("Directory must exist", nameof(directoryInfo));
-        }
-    }
+    public IAsyncEnumerable<IStorageItem> GetItemsAsync() => GetItemsCore(directoryInfo)
+        .Select(WrapFileSystemInfo)
+        .Where(f => f is not null)
+        .AsAsyncEnumerable()!;
 
-    public string Name => DirectoryInfo.Name;
+    public Task<IStorageFile?> CreateFileAsync(string name) => Task.FromResult(
+        (IStorageFile?)WrapFileSystemInfo(CreateFileCore(directoryInfo, name)));
 
-    public DirectoryInfo DirectoryInfo { get; }
-
-    public bool CanBookmark => true;
-
-    public Uri Path
-    {
-        get
-        {
-            try
-            {
-                return StorageProviderHelpers.FilePathToUri(DirectoryInfo.FullName);
-            }
-            catch (SecurityException)
-            {
-                return new Uri(DirectoryInfo.Name, UriKind.Relative);
-            }
-        }
-    }
-    
-    public Task<StorageItemProperties> GetBasicPropertiesAsync()
-    {
-        var props = new StorageItemProperties(
-            null,
-            DirectoryInfo.CreationTimeUtc,
-            DirectoryInfo.LastAccessTimeUtc);
-        return Task.FromResult(props);
-    }
-
-    public Task<IStorageFolder?> GetParentAsync()
-    {
-        if (DirectoryInfo.Parent is { } directory)
-        {
-            return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
-        }
-        return Task.FromResult<IStorageFolder?>(null);
-    }
-
-    public IAsyncEnumerable<IStorageItem> GetItemsAsync()
-        => DirectoryInfo.EnumerateDirectories()
-            .Select(d => (IStorageItem)new BclStorageFolder(d))
-            .Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f)))
-            .AsAsyncEnumerable();
-
-    public virtual Task<string?> SaveBookmarkAsync()
-    {
-        return Task.FromResult<string?>(DirectoryInfo.FullName);
-    }
-    
-    public Task ReleaseBookmarkAsync()
-    {
-        // No-op
-        return Task.CompletedTask;
-    }
-
-    protected virtual void Dispose(bool disposing)
-    {
-    }
-
-    ~BclStorageFolder()
-    {
-        Dispose(disposing: false);
-    }
-
-    public void Dispose()
-    {
-        Dispose(disposing: true);
-        GC.SuppressFinalize(this);
-    }
-
-    public Task DeleteAsync()
-    {
-        DirectoryInfo.Delete(true);
-        return Task.CompletedTask;
-    }
-
-    public Task<IStorageItem?> MoveAsync(IStorageFolder destination)
-    {
-        if (destination is BclStorageFolder storageFolder)
-        {
-            var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, DirectoryInfo.Name);
-            DirectoryInfo.MoveTo(newPath);
-
-            return Task.FromResult<IStorageItem?>(new BclStorageFolder(new DirectoryInfo(newPath)));
-        }
-
-        return Task.FromResult<IStorageItem?>(null);
-    }
-
-    public Task<IStorageFile?> CreateFileAsync(string name)
-    {
-        var fileName = System.IO.Path.Combine(DirectoryInfo.FullName, name);
-        var newFile = new FileInfo(fileName);
-        
-        using var stream = newFile.Create();
-
-        return Task.FromResult<IStorageFile?>(new BclStorageFile(newFile));
-    }
-
-    public Task<IStorageFolder?> CreateFolderAsync(string name)
-    {
-        var newFolder = DirectoryInfo.CreateSubdirectory(name);
-
-        return Task.FromResult<IStorageFolder?>(new BclStorageFolder(newFolder));
-    }
+    public Task<IStorageFolder?> CreateFolderAsync(string name) => Task.FromResult(
+        (IStorageFolder?)WrapFileSystemInfo(CreateFolderCore(directoryInfo, name)));
 }
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs
new file mode 100644
index 00000000000..123d0e9283d
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Threading.Tasks;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+internal abstract class BclStorageItem(FileSystemInfo fileSystemInfo) : IStorageBookmarkItem, IStorageItemWithFileSystemInfo
+{
+    public FileSystemInfo FileSystemInfo { get; } = fileSystemInfo switch
+    {
+        null => throw new ArgumentNullException(nameof(fileSystemInfo)),
+        DirectoryInfo { Exists: false } => throw new ArgumentException("Directory must exist", nameof(fileSystemInfo)),
+        _ => fileSystemInfo
+    };
+
+    public string Name => FileSystemInfo.Name;
+
+    public bool CanBookmark => true;
+
+    public Uri Path => GetPathCore(FileSystemInfo);
+
+    public Task<StorageItemProperties> GetBasicPropertiesAsync()
+    {
+        return Task.FromResult(GetBasicPropertiesAsyncCore(FileSystemInfo));
+    }
+
+    public Task<IStorageFolder?> GetParentAsync() => Task.FromResult(
+        (IStorageFolder?)WrapFileSystemInfo(GetParentCore(FileSystemInfo)));
+
+    public Task DeleteAsync()
+    {
+        DeleteCore(FileSystemInfo);
+        return Task.CompletedTask;
+    }
+
+    public Task<IStorageItem?> MoveAsync(IStorageFolder destination) => Task.FromResult(
+        WrapFileSystemInfo(MoveCore(FileSystemInfo, destination)));
+
+    public Task<string?> SaveBookmarkAsync()
+    {
+        var path = FileSystemInfo.FullName;
+        return Task.FromResult<string?>(StorageBookmarkHelper.EncodeBclBookmark(path));
+    }
+
+    public Task ReleaseBookmarkAsync() => Task.CompletedTask;
+
+    public void Dispose() { }
+
+    [return: NotNullIfNotNull(nameof(fileSystemInfo))]
+    protected IStorageItem? WrapFileSystemInfo(FileSystemInfo? fileSystemInfo) => fileSystemInfo switch
+    {
+        DirectoryInfo directoryInfo => new BclStorageFolder(directoryInfo),
+        FileInfo fileInfo => new BclStorageFile(fileInfo),
+        _ => null
+    };
+
+    internal static void DeleteCore(FileSystemInfo fileSystemInfo) => fileSystemInfo.Delete();
+
+    internal static Uri GetPathCore(FileSystemInfo fileSystemInfo)
+    {
+        try
+        {
+            if (fileSystemInfo is DirectoryInfo { Parent: not null } or FileInfo { Directory: not null })
+            {
+                return StorageProviderHelpers.UriFromFilePath(fileSystemInfo.FullName, fileSystemInfo is DirectoryInfo);
+            }
+        }
+        catch (SecurityException)
+        {
+        }
+
+        return new Uri(fileSystemInfo.Name, UriKind.Relative);
+    }
+
+    internal static StorageItemProperties GetBasicPropertiesAsyncCore(FileSystemInfo fileSystemInfo)
+    {
+        if (fileSystemInfo.Exists)
+        {
+            return new StorageItemProperties(
+                fileSystemInfo is FileInfo fileInfo ? (ulong)fileInfo.Length : 0,
+                fileSystemInfo.CreationTimeUtc,
+                fileSystemInfo.LastAccessTimeUtc);
+        }
+
+        return new StorageItemProperties();
+    }
+
+    internal static DirectoryInfo? GetParentCore(FileSystemInfo fileSystemInfo) => fileSystemInfo switch
+    {
+        FileInfo { Directory: { } directory } => directory,
+        DirectoryInfo { Parent: { } parent } => parent,
+        _ => null
+    };
+
+    internal static FileSystemInfo? MoveCore(FileSystemInfo fileSystemInfo, IStorageFolder destination)
+    {
+        if (destination?.TryGetLocalPath() is { } destinationPath)
+        {
+            var newPath = System.IO.Path.Combine(destinationPath, fileSystemInfo.Name);
+            if (fileSystemInfo is DirectoryInfo directoryInfo)
+            {
+                directoryInfo.MoveTo(newPath);
+                return new DirectoryInfo(newPath);
+            }
+
+            if (fileSystemInfo is FileInfo fileInfo)
+            {
+                fileInfo.MoveTo(newPath);
+                return new FileInfo(newPath);
+            }
+        }
+
+        return null;
+    }
+
+    internal static FileStream OpenReadCore(FileInfo fileInfo) => fileInfo.OpenRead();
+
+    internal static FileStream OpenWriteCore(FileInfo fileInfo) =>
+        new(fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Write);
+
+    internal static IEnumerable<FileSystemInfo> GetItemsCore(DirectoryInfo directoryInfo) => directoryInfo
+        .EnumerateDirectories()
+        .OfType<FileSystemInfo>()
+        .Concat(directoryInfo.EnumerateFiles());
+
+    internal static FileInfo CreateFileCore(DirectoryInfo directoryInfo, string name)
+    {
+        var fileName = System.IO.Path.Combine(directoryInfo.FullName, name);
+        var newFile = new FileInfo(fileName);
+
+        using var stream = newFile.Create();
+        return newFile;
+    }
+
+    internal static DirectoryInfo CreateFolderCore(DirectoryInfo directoryInfo, string name) =>
+        directoryInfo.CreateSubdirectory(name);
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
index 34409f5fdae..fd2f499d06f 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
@@ -4,6 +4,7 @@
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Avalonia.Compatibility;
+using Avalonia.Logging;
 
 namespace Avalonia.Platform.Storage.FileIO;
 
@@ -20,18 +21,12 @@ internal abstract class BclStorageProvider : IStorageProvider
 
     public virtual Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
     {
-        var file = new FileInfo(bookmark);
-        return file.Exists
-            ? Task.FromResult<IStorageBookmarkFile?>(new BclStorageFile(file))
-            : Task.FromResult<IStorageBookmarkFile?>(null);
+        return Task.FromResult(OpenBookmark(bookmark) as IStorageBookmarkFile);
     }
 
     public virtual Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
     {
-        var folder = new DirectoryInfo(bookmark);
-        return folder.Exists
-            ? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder))
-            : Task.FromResult<IStorageBookmarkFolder?>(null);
+        return Task.FromResult(OpenBookmark(bookmark) as IStorageBookmarkFolder);
     }
 
     public virtual Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
@@ -63,6 +58,16 @@ internal abstract class BclStorageProvider : IStorageProvider
     }
 
     public virtual Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
+    {
+        if (TryGetWellKnownFolderCore(wellKnownFolder) is { } directoryInfo)
+        {
+            return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directoryInfo));
+        }
+
+        return Task.FromResult<IStorageFolder?>(null);
+    }
+
+    internal static DirectoryInfo? TryGetWellKnownFolderCore(WellKnownFolder wellKnownFolder)
     {
         // Note, this BCL API returns different values depending on the .NET version.
         // We should also document it. 
@@ -82,16 +87,16 @@ internal abstract class BclStorageProvider : IStorageProvider
 
         if (folderPath is null)
         {
-            return Task.FromResult<IStorageFolder?>(null);
+            return null;
         }
 
         var directory = new DirectoryInfo(folderPath);
         if (!directory.Exists)
         {
-            return Task.FromResult<IStorageFolder?>(null);
+            return null;
         }
-        
-        return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
+
+        return directory;
 
         string GetFromSpecialFolder(Environment.SpecialFolder folder) =>
             Environment.GetFolderPath(folder, Environment.SpecialFolderOption.Create);
@@ -104,7 +109,7 @@ string GetFromSpecialFolder(Environment.SpecialFolder folder) =>
         if (OperatingSystemEx.IsWindows())
         {
             return Environment.OSVersion.Version.Major < 6 ? null :
-                SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero);
+                Marshal.PtrToStringUni(SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero));
         }
 
         if (OperatingSystemEx.IsLinux())
@@ -123,8 +128,27 @@ string GetFromSpecialFolder(Environment.SpecialFolder folder) =>
 
         return null;
     }
-    
+
+    private IStorageBookmarkItem? OpenBookmark(string bookmark)
+    {
+        try
+        {
+            if (StorageBookmarkHelper.TryDecodeBclBookmark(bookmark, out var localPath))
+            {
+                return StorageProviderHelpers.TryCreateBclStorageItem(localPath);   
+            }
+
+            return null;
+        }
+        catch (Exception ex)
+        {
+            Logger.TryGet(LogEventLevel.Information, LogArea.Platform)?
+                .Log(this, "Unable to read file bookmark: {Exception}", ex);
+            return null;
+        }
+    }
+
     private static readonly Guid s_folderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
-    [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
-    private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token);
+    [DllImport("shell32.dll")]
+    private static extern IntPtr SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token);
 }
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs
new file mode 100644
index 00000000000..0e0ffa3b1b3
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs
@@ -0,0 +1,116 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+/// <summary>
+/// Stream wrapper currently used by Apple platforms,
+/// where in sandboxed scenario it's advised to call [NSUri startAccessingSecurityScopedResource].
+/// </summary>
+internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _securityScope) : Stream
+{
+    public override bool CanRead => _stream.CanRead;
+
+    public override bool CanSeek => _stream.CanSeek;
+
+    public override bool CanWrite => _stream.CanWrite;
+
+    public override long Length => _stream.Length;
+
+    public override long Position
+    {
+        get => _stream.Position;
+        set => _stream.Position = value;
+    }
+
+    public override void Flush() =>
+        _stream.Flush();
+
+    public override Task FlushAsync(CancellationToken cancellationToken) =>
+        _stream.FlushAsync(cancellationToken);
+
+    public override int ReadByte() =>
+        _stream.ReadByte();
+
+    public override int Read(byte[] buffer, int offset, int count) =>
+        _stream.Read(buffer, offset, count);
+
+    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
+        _stream.ReadAsync(buffer, offset, count, cancellationToken);
+
+#if NET6_0_OR_GREATER
+    public override int Read(Span<byte> buffer) => _stream.Read(buffer);
+
+    public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) =>
+        _stream.ReadAsync(buffer, cancellationToken);
+#endif
+
+    public override void Write(byte[] buffer, int offset, int count) =>
+        _stream.Write(buffer, offset, count);
+
+    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
+        _stream.WriteAsync(buffer, offset, count, cancellationToken);
+
+#if NET6_0_OR_GREATER
+    public override void Write(ReadOnlySpan<byte> buffer) => _stream.Write(buffer);
+
+    public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
+        _stream.WriteAsync(buffer, cancellationToken);
+#endif
+
+    public override void WriteByte(byte value) => _stream.WriteByte(value);
+
+    public override long Seek(long offset, SeekOrigin origin) =>
+        _stream.Seek(offset, origin);
+
+    public override void SetLength(long value) =>
+        _stream.SetLength(value);
+
+#if NET6_0_OR_GREATER
+    public override void CopyTo(Stream destination, int bufferSize) => _stream.CopyTo(destination, bufferSize);
+#endif
+
+    public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) =>
+        _stream.CopyToAsync(destination, bufferSize, cancellationToken);
+
+    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
+        _stream.BeginRead(buffer, offset, count, callback, state);
+
+    public override int EndRead(IAsyncResult asyncResult) => _stream.EndRead(asyncResult);
+
+    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
+        _stream.BeginWrite(buffer, offset, count, callback, state);
+
+    public override void EndWrite(IAsyncResult asyncResult) => _stream.EndWrite(asyncResult);
+
+    protected override void Dispose(bool disposing)
+    {
+        try
+        {
+            if (disposing)
+            {
+                _stream.Dispose();
+            }
+        }
+        finally
+        {
+            _securityScope.Dispose();
+        }
+    }
+
+#if NET6_0_OR_GREATER
+    public override async ValueTask DisposeAsync()
+    {
+        try
+        {
+            await _stream.DisposeAsync();
+        }
+        finally
+        {
+            _securityScope.Dispose();
+        }
+    }
+#endif
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs
new file mode 100644
index 00000000000..a71dd6f3e60
--- /dev/null
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Text;
+
+namespace Avalonia.Platform.Storage.FileIO;
+
+/// <summary>
+/// In order to have unique bookmarks across platforms, we prepend a platform specific suffix before native bookmark.
+/// And always encoding them in base64 before returning to the user.
+/// </summary>
+/// <remarks>
+/// Bookmarks are encoded as:
+/// 0-6 - avalonia prefix with version number
+/// 7-15  - platform key
+/// 16+ - native bookmark value
+/// Which is then encoded in Base64.
+/// </remarks>
+internal static class StorageBookmarkHelper
+{
+    private const int HeaderLength = 16;
+    private static ReadOnlySpan<byte> AvaHeaderPrefix => "ava.v1."u8;
+    private static ReadOnlySpan<byte> FakeBclBookmarkPlatform => "bcl"u8;
+
+    [return: NotNullIfNotNull(nameof(nativeBookmark))]
+    public static string? EncodeBookmark(ReadOnlySpan<byte> platform, string? nativeBookmark) =>
+        nativeBookmark is null ? null : EncodeBookmark(platform, Encoding.UTF8.GetBytes(nativeBookmark));
+
+    public static string? EncodeBookmark(ReadOnlySpan<byte> platform, ReadOnlySpan<byte> nativeBookmarkBytes)
+    {
+        if (nativeBookmarkBytes.Length == 0)
+        {
+            return null;
+        }
+
+        if (platform.Length > HeaderLength)
+        {
+            throw new ArgumentException($"Platform name should not be longer than {HeaderLength} bytes", nameof(platform));
+        }
+
+        var arrayLength = HeaderLength + nativeBookmarkBytes.Length;
+        var arrayPool = ArrayPool<byte>.Shared.Rent(arrayLength);
+        try
+        {
+            // Write platform into first 16 bytes.
+            var arraySpan = arrayPool.AsSpan(0, arrayLength);
+            AvaHeaderPrefix.CopyTo(arraySpan);
+            platform.CopyTo(arraySpan.Slice(AvaHeaderPrefix.Length));
+
+            // Write bookmark bytes.
+            nativeBookmarkBytes.CopyTo(arraySpan.Slice(HeaderLength));
+
+            // We must use span overload because ArrayPool might return way too big array. 
+#if NET6_0_OR_GREATER
+            return Convert.ToBase64String(arraySpan);
+#else
+            return Convert.ToBase64String(arraySpan.ToArray(), Base64FormattingOptions.None);
+#endif
+        }
+        finally
+        {
+            ArrayPool<byte>.Shared.Return(arrayPool);
+        }
+    }
+
+    public enum DecodeResult
+    {
+        Success = 0,
+        InvalidFormat,
+        InvalidPlatform
+    }
+
+    public static DecodeResult TryDecodeBookmark(ReadOnlySpan<byte> platform, string? base64bookmark, out byte[]? nativeBookmark)
+    {
+        if (platform.Length > HeaderLength
+            || platform.Length == 0
+            || base64bookmark is null
+            || base64bookmark.Length % 4 != 0)
+        {
+            nativeBookmark = null;
+            return DecodeResult.InvalidFormat;
+        }
+
+        Span<byte> decodedBookmark;
+#if NET6_0_OR_GREATER
+        // Each base64 character represents 6 bits, but to be safe, 
+        var arrayPool = ArrayPool<byte>.Shared.Rent(HeaderLength + base64bookmark.Length * 6);
+        if (Convert.TryFromBase64Chars(base64bookmark, arrayPool, out int bytesWritten))
+        {
+            decodedBookmark = arrayPool.AsSpan().Slice(0, bytesWritten);
+        }
+        else
+        {
+            nativeBookmark = null;
+            return DecodeResult.InvalidFormat;
+        }
+#else
+        decodedBookmark = Convert.FromBase64String(base64bookmark).AsSpan();
+#endif
+        try
+        {
+            if (decodedBookmark.Length < HeaderLength
+                // Check if decoded string starts with the correct prefix, checking v1 at the same time.
+                && !AvaHeaderPrefix.SequenceEqual(decodedBookmark.Slice(0, AvaHeaderPrefix.Length)))
+            {
+                nativeBookmark = null;
+                return DecodeResult.InvalidFormat;
+            }
+
+            var actualPlatform = decodedBookmark.Slice(AvaHeaderPrefix.Length, platform.Length);
+            if (!actualPlatform.SequenceEqual(platform))
+            {
+                nativeBookmark = null;
+                return DecodeResult.InvalidPlatform;
+            }
+
+            nativeBookmark = decodedBookmark.Slice(HeaderLength).ToArray();
+            return DecodeResult.Success;
+        }
+        finally
+        {
+#if NET6_0_OR_GREATER
+            ArrayPool<byte>.Shared.Return(arrayPool);
+#endif
+        }
+    }
+
+    public static string EncodeBclBookmark(string localPath) => EncodeBookmark(FakeBclBookmarkPlatform, localPath);
+
+    public static bool TryDecodeBclBookmark(string nativeBookmark, [NotNullWhen(true)] out string? localPath)
+    {
+        var decodeResult = TryDecodeBookmark(FakeBclBookmarkPlatform, nativeBookmark, out var bytes);
+        if (decodeResult == DecodeResult.Success)
+        {
+            localPath = Encoding.UTF8.GetString(bytes!);
+            return true;
+        }
+        if (decodeResult == DecodeResult.InvalidFormat
+            && nativeBookmark.IndexOfAny(Path.GetInvalidPathChars()) < 0
+            && !string.IsNullOrEmpty(Path.GetDirectoryName(nativeBookmark)))
+        {
+            // Attempt to restore old BCL bookmarks.
+            // Don't check for File.Exists here, as it will be done at later point in TryGetStorageItem.
+            // Just validate if it looks like a valid file path.
+            localPath = nativeBookmark;
+            return true;
+        }
+
+        localPath = null;
+        return false;
+    }
+}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
index 76323eb900e..48cc79672b8 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Platform.Storage.FileIO;
 
 internal static class StorageProviderHelpers
 {
-    public static IStorageItem? TryCreateBclStorageItem(string path)
+    public static BclStorageItem? TryCreateBclStorageItem(string path)
     {
         if (!string.IsNullOrWhiteSpace(path))
         {
@@ -28,28 +28,36 @@ internal static class StorageProviderHelpers
         return null;
     }
 
-    public static Uri FilePathToUri(string path)
+    public static string? TryGetPathFromFileUri(Uri? uri)
+    {
+        // android "content:", browser and ios relative links are ignored.
+        return uri is { IsAbsoluteUri: true, Scheme: "file" } ? uri.LocalPath : null;
+    }
+
+    public static Uri UriFromFilePath(string path, bool isDirectory)
     {
         var uriPath = new StringBuilder(path)
             .Replace("%", $"%{(int)'%':X2}")
             .Replace("[", $"%{(int)'[':X2}")
-            .Replace("]", $"%{(int)']':X2}")
-            .ToString();
+            .Replace("]", $"%{(int)']':X2}");
+
+        if (!path.EndsWith('/') && isDirectory)
+        {
+            uriPath.Append('/');
+        }
 
-        return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
+        return new UriBuilder("file", string.Empty) { Path = uriPath.ToString() }.Uri;
     }
-    
-    public static bool TryFilePathToUri(string path, [NotNullWhen(true)] out Uri? uri)
+
+    public static Uri? TryGetUriFromFilePath(string path, bool isDirectory)
     {
         try
         {
-            uri = FilePathToUri(path);
-            return true;
+            return UriFromFilePath(path, isDirectory);
         }
         catch
         {
-            uri = null;
-            return false;
+            return null;
         }
     }
     
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
index 40f2720ee8c..70c4f4bb0b3 100644
--- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
+++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
@@ -1,8 +1,14 @@
-﻿using System.Threading.Tasks;
+﻿using System.IO;
+using System.Threading.Tasks;
 using Avalonia.Metadata;
 
 namespace Avalonia.Platform.Storage;
 
+internal interface IStorageItemWithFileSystemInfo : IStorageItem
+{
+    FileSystemInfo FileSystemInfo { get; }
+}
+
 [NotClientImplementable]
 public interface IStorageBookmarkItem : IStorageItem
 {
diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
index 3933d23e4fb..8f0c890ed1f 100644
--- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
+++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
@@ -17,7 +17,7 @@ public static class StorageProviderExtensions
             return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
         }
 
-        if (StorageProviderHelpers.TryFilePathToUri(filePath, out var uri))
+        if (StorageProviderHelpers.TryGetUriFromFilePath(filePath, false) is { } uri)
         {
             return provider.TryGetFileFromPathAsync(uri);
         }
@@ -34,7 +34,7 @@ public static class StorageProviderExtensions
             return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
         }
 
-        if (StorageProviderHelpers.TryFilePathToUri(folderPath, out var uri))
+        if (StorageProviderHelpers.TryGetUriFromFilePath(folderPath, true) is { } uri)
         {
             return provider.TryGetFolderFromPathAsync(uri);
         }
@@ -56,21 +56,11 @@ public static class StorageProviderExtensions
     {
         // We can avoid double escaping of the path by checking for BclStorageFolder.
         // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users.
-        if (item is BclStorageFolder storageFolder)
+        if (item is IStorageItemWithFileSystemInfo storageItem)
         {
-            return storageFolder.DirectoryInfo.FullName;
-        }
-        if (item is BclStorageFile storageFile)
-        {
-            return storageFile.FileInfo.FullName;
-        }
-
-        if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
-        {
-            return absolutePath.LocalPath;
+            return storageItem.FileSystemInfo.FullName;
         }
 
-        // android "content:", browser and ios relative links go here. 
-        return null;
+        return StorageProviderHelpers.TryGetPathFromFileUri(item.Path);
     }
 }
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Conventions.cs b/src/Avalonia.Diagnostics/Diagnostics/Conventions.cs
index 540f2b549da..691bf664ed1 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Conventions.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Conventions.cs
@@ -1,21 +1,7 @@
-﻿using System;
-using System.IO;
-
-namespace Avalonia.Diagnostics
+﻿namespace Avalonia.Diagnostics
 {
     internal static class Conventions
     {
-        public static string DefaultScreenshotsRoot
-        {
-            get
-            {
-                var dir = System.IO.Path.Combine(
-                    Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Screenshots");
-                Directory.CreateDirectory(dir);
-                return dir;
-            }
-        }
-
         public static IScreenshotHandler DefaultScreenshotHandler { get; } =
             new Screenshots.FilePickerHandler();
     }
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
index 6ea46b6d54b..65998091e7f 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.Screenshots
     public sealed class FilePickerHandler : BaseRenderToStreamHandler
     {
         private readonly string _title;
-        private readonly string _screenshotRoot;
+        private readonly string? _screenshotRoot;
 
         /// <summary>
         /// Instance FilePickerHandler
@@ -35,7 +35,7 @@ public FilePickerHandler(
             string? screenshotRoot = default)
         {
             _title = title ?? "Save Screenshot to ...";
-            _screenshotRoot = screenshotRoot ?? Conventions.DefaultScreenshotsRoot;
+            _screenshotRoot = screenshotRoot;
         }
 
         private static TopLevel GetTopLevel(Control control)
@@ -54,8 +54,15 @@ private static TopLevel GetTopLevel(Control control)
         protected override async Task<Stream?> GetStream(Control control)
         {
             var storageProvider = GetTopLevel(control).StorageProvider;
-            var defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot)
-                                ?? await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures);
+            IStorageFolder? defaultFolder = null;
+            if (_screenshotRoot is not null)
+            {
+                defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot);
+            }
+            if (defaultFolder is null)
+            {
+                defaultFolder = await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures);
+            }
 
             var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
             {
diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
index 0a735f8f9ee..109a43c1ae5 100644
--- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Platform;
 using Avalonia.Native.Interop;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
@@ -17,13 +18,15 @@ void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
         {
             ((IApplicationPlatformEvents)Application.Current)?.RaiseUrlsOpened(urls.ToStringArray());
 
-            if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
+            if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime
+                && AvaloniaLocator.Current.GetService<IStorageProviderFactory>() is StorageProviderApi storageApi)
             {
                 var filePaths = urls.ToStringArray();
                 var files = new List<IStorageItem>(filePaths.Length);
                 foreach (var filePath in filePaths)
                 {
-                    if (StorageProviderHelpers.TryCreateBclStorageItem(filePath) is { } file)
+                    if (StorageProviderHelpers.TryGetUriFromFilePath(filePath, false) is { } fileUri
+                        && storageApi.TryGetStorageItem(fileUri) is { } file)
                     {
                         files.Add(file);
                     }
@@ -41,7 +44,8 @@ void IAvnApplicationEvents.UrlsOpened(IAvnStringArray urls)
             // Raise the urls opened event to be compatible with legacy behavior.
             ((IApplicationPlatformEvents)Application.Current)?.RaiseUrlsOpened(urls.ToStringArray());
 
-            if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime)
+            if (AvaloniaLocator.Current.GetService<IActivatableLifetime>() is ActivatableLifetimeBase lifetime
+                && AvaloniaLocator.Current.GetService<IStorageProviderFactory>() is StorageProviderApi storageApi)
             {
                 var files = new List<IStorageItem>();
                 var uris = new List<Uri>();
@@ -51,7 +55,7 @@ void IAvnApplicationEvents.UrlsOpened(IAvnStringArray urls)
                     {
                         if (uri.Scheme == Uri.UriSchemeFile)
                         {
-                            if (StorageProviderHelpers.TryCreateBclStorageItem(uri.LocalPath) is { } file)
+                            if (storageApi.TryGetStorageItem(uri) is { } file)
                             {
                                 files.Add(file);
                             }
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index f8696d3a1c1..00b57350cde 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -107,8 +107,7 @@ void DoInitialize(AvaloniaNativePlatformOptions options)
             }
 
             AvaloniaLocator.CurrentMutable
-                .Bind<IDispatcherImpl>()
-                .ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()))
+                .Bind<IDispatcherImpl>().ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()))
                 .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
                 .Bind<IScreenImpl>().ToConstant(new ScreenImpl(_factory.CreateScreens))
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
@@ -121,7 +120,8 @@ void DoInitialize(AvaloniaNativePlatformOptions options)
                 .Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
                 .Bind<IPlatformLifetimeEventsImpl>().ToConstant(applicationPlatform)
                 .Bind<INativeApplicationCommands>().ToConstant(new MacOSNativeMenuCommands(_factory.CreateApplicationCommands()))
-                .Bind<IActivatableLifetime>().ToSingleton<MacOSActivatableLifetime>();
+                .Bind<IActivatableLifetime>().ToSingleton<MacOSActivatableLifetime>()
+                .Bind<IStorageProviderFactory>().ToConstant(new StorageProviderApi(_factory.CreateStorageProvider(), options.AppSandboxEnabled));
 
             var hotkeys = new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt);
             hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers));
diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
index 67da919a01c..f8974865a7d 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
@@ -80,6 +80,13 @@ public class AvaloniaNativePlatformOptions
         /// and make your Avalonia app run with it. The default value is null.
         /// </summary>
         public string AvaloniaNativeLibraryPath { get; set; }
+
+        /// <summary>
+        /// If you distribute your app in App Store - it should be with sandbox enabled.
+        /// This parameter enables <see cref="Avalonia.Platform.Storage.IStorageItem.SaveBookmarkAsync"/> and related APIs,
+        /// as well as wrapping all storage related calls in secure context. The default value is true.
+        /// </summary>
+        public bool AppSandboxEnabled { get; set; } = true;
     }
 
     // ReSharper disable once InconsistentNaming
diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs
index 5b9c3acaaf5..0fc4d762242 100644
--- a/src/Avalonia.Native/ClipboardImpl.cs
+++ b/src/Avalonia.Native/ClipboardImpl.cs
@@ -2,18 +2,21 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Logging;
 using Avalonia.Native.Interop;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage.FileIO;
+using MicroCom.Runtime;
 
 namespace Avalonia.Native
 {
     class ClipboardImpl : IClipboard, IDisposable
     {
         private IAvnClipboard _native;
+
         // TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
         private const string NSPasteboardTypeString = "public.utf8-plain-text";
         private const string NSFilenamesPboardType = "NSFilenamesPboardType";
@@ -86,7 +89,13 @@ public IEnumerable<string> GetFileNames()
 
         public IEnumerable<IStorageItem> GetFiles()
         {
-            return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
+            var storageApi = (StorageProviderApi)AvaloniaLocator.Current.GetRequiredService<IStorageProviderFactory>();
+    
+            // TODO: use non-deprecated AppKit API to get NSUri instead of file names.
+            return GetFileNames()?
+                .Select(f => StorageProviderHelpers.TryGetUriFromFilePath(f, false) is { } uri
+                    ? storageApi.TryGetStorageItem(uri)
+                    : null)
                 .Where(f => f is not null);
         }
 
diff --git a/src/Avalonia.Native/StorageItem.cs b/src/Avalonia.Native/StorageItem.cs
new file mode 100644
index 00000000000..12e4cc0a5e4
--- /dev/null
+++ b/src/Avalonia.Native/StorageItem.cs
@@ -0,0 +1,152 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
+using Avalonia.Utilities;
+
+namespace Avalonia.Native;
+
+internal class StorageItem : IStorageBookmarkItem, IStorageItemWithFileSystemInfo
+{
+    private readonly StorageProviderApi _storageProviderApi;
+    private readonly FileSystemInfo _fileSystemInfo;
+
+    protected StorageItem(StorageProviderApi storageProviderApi, FileSystemInfo fileSystemInfo, Uri uri, Uri scopeOwnerUri)
+    {
+        _storageProviderApi = storageProviderApi;
+        Path = uri;
+        _fileSystemInfo = fileSystemInfo;
+        ScopeOwnerUri = scopeOwnerUri;
+    }
+
+    public string Name => _fileSystemInfo.Name;
+    public Uri Path { get; }
+    public Uri ScopeOwnerUri { get; }
+
+    public Task<StorageItemProperties> GetBasicPropertiesAsync()
+    {
+        using var scope = OpenScope();
+        return Task.FromResult(
+            BclStorageItem.GetBasicPropertiesAsyncCore(_fileSystemInfo));
+    }
+
+    public bool CanBookmark => true;
+    public FileSystemInfo FileSystemInfo => _fileSystemInfo;
+
+    protected IDisposable? OpenScope()
+    {
+        return _storageProviderApi.OpenSecurityScope(ScopeOwnerUri.AbsoluteUri);
+    }
+
+    public Task<string?> SaveBookmarkAsync()
+    {
+        using var scope = OpenScope();
+        return Task.FromResult(_storageProviderApi.SaveBookmark(Path));
+    }
+
+    public Task ReleaseBookmarkAsync()
+    {
+        _storageProviderApi.ReleaseBookmark(Path);
+        return Task.CompletedTask;
+    }
+
+    public Task<IStorageFolder?> GetParentAsync()
+    {
+        using var scope = OpenScope();
+        var parent = BclStorageItem.GetParentCore(_fileSystemInfo);
+        return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(parent, null));
+    }
+
+    public Task DeleteAsync()
+    {
+        using var scope = OpenScope();
+        BclStorageItem.DeleteCore(_fileSystemInfo);
+        return Task.CompletedTask;
+    }
+
+    public Task<IStorageItem?> MoveAsync(IStorageFolder destination)
+    {
+        using var destinationScope = (destination as StorageItem)?.OpenScope();
+        using var scope = OpenScope();
+        var item = WrapFileSystemInfo(BclStorageItem.MoveCore(_fileSystemInfo, destination), null);
+        return Task.FromResult(item);
+    }
+
+    [return: NotNullIfNotNull(nameof(fileSystemInfo))]
+    protected IStorageItem? WrapFileSystemInfo(FileSystemInfo? fileSystemInfo, Uri? scopedOwner)
+    {
+        if (fileSystemInfo is null) return null;
+
+        // It might not be always correct to assume NSUri from the file path, but that's the best we have here without using native API directly.
+        var fileUri = BclStorageItem.GetPathCore(fileSystemInfo);
+        return fileSystemInfo switch
+        {
+            DirectoryInfo directoryInfo => new StorageFolder(
+                _storageProviderApi, directoryInfo, fileUri, scopedOwner ?? fileUri),
+            FileInfo fileInfo => new StorageFile(
+                _storageProviderApi, fileInfo, fileUri, scopedOwner ?? fileUri),
+            _ => throw new ArgumentOutOfRangeException(nameof(fileSystemInfo), fileSystemInfo, null)
+        };
+    }
+
+    public void Dispose()
+    {
+    }
+}
+
+internal class StorageFile(
+    StorageProviderApi storageProviderApi, FileInfo fileInfo, Uri uri, Uri scopeOwnerUri)
+    : StorageItem(storageProviderApi, fileInfo, uri, scopeOwnerUri), IStorageBookmarkFile
+{
+    public Task<Stream> OpenReadAsync()
+    {
+        var scope = OpenScope();
+        var innerStream = BclStorageItem.OpenReadCore(fileInfo);
+        return Task.FromResult<Stream>(scope is not null ? new SecurityScopedStream(innerStream, scope) : innerStream);
+    }
+
+    public Task<Stream> OpenWriteAsync()
+    {
+        var scope = OpenScope();
+        var innerStream = BclStorageItem.OpenWriteCore(fileInfo);
+        return Task.FromResult<Stream>(scope is not null ? new SecurityScopedStream(innerStream, scope) : innerStream);
+    }
+}
+
+internal class StorageFolder(
+    StorageProviderApi storageProviderApi, DirectoryInfo directoryInfo, Uri uri, Uri scopeOwnerUri)
+    : StorageItem(storageProviderApi, directoryInfo, uri, scopeOwnerUri), IStorageBookmarkFolder
+{
+    public IAsyncEnumerable<IStorageItem> GetItemsAsync()
+    {
+        return GetItems().AsAsyncEnumerable();
+
+        IEnumerable<IStorageItem> GetItems()
+        {
+            using var scope = OpenScope();
+            foreach (var item in BclStorageItem.GetItemsCore(directoryInfo))
+            {
+                yield return WrapFileSystemInfo(item, ScopeOwnerUri);
+            }
+        }
+    }
+
+    public Task<IStorageFile?> CreateFileAsync(string name)
+    {
+        using var scope = OpenScope();
+        var file = BclStorageItem.CreateFileCore(directoryInfo, name);
+        return Task.FromResult((IStorageFile?)WrapFileSystemInfo(file, ScopeOwnerUri));
+    }
+
+    public Task<IStorageFolder?> CreateFolderAsync(string name)
+    {
+        using var scope = OpenScope();
+        var folder = BclStorageItem.CreateFolderCore(directoryInfo, name);
+        return Task.FromResult((IStorageFolder?)WrapFileSystemInfo(folder, ScopeOwnerUri));
+    }
+}
diff --git a/src/Avalonia.Native/StorageProviderApi.cs b/src/Avalonia.Native/StorageProviderApi.cs
new file mode 100644
index 00000000000..4d326ee8c34
--- /dev/null
+++ b/src/Avalonia.Native/StorageProviderApi.cs
@@ -0,0 +1,289 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Logging;
+using Avalonia.Native.Interop;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
+using Avalonia.Reactive;
+using MicroCom.Runtime;
+
+namespace Avalonia.Native;
+
+internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnabled) : IStorageProviderFactory, IDisposable
+{
+    private readonly Dictionary<string, int> _openScopes = new();
+    private readonly IAvnStorageProvider _native = native;
+
+    public IStorageProvider CreateProvider(TopLevel topLevel)
+    {
+        return new StorageProviderImpl((TopLevelImpl)topLevel.PlatformImpl!, this);
+    }
+
+    public IStorageItem? TryGetStorageItem(Uri? itemUri, bool create = false)
+    {
+        if (itemUri is not null && StorageProviderHelpers.TryGetPathFromFileUri(itemUri) is { } itemPath)
+        {
+            if (new FileInfo(itemPath) is { } fileInfo
+                && (create || fileInfo.Exists))
+            {
+                return sandboxEnabled
+                    ? new StorageFile(this, fileInfo, itemUri, itemUri)
+                    : new BclStorageFile(fileInfo);
+            }
+            if (new DirectoryInfo(itemPath) is { } directoryInfo
+                && (create || directoryInfo.Exists))
+            {
+                return sandboxEnabled
+                    ? new StorageFolder(this, directoryInfo, itemUri, itemUri)
+                    : new BclStorageFolder(directoryInfo);
+            }
+        }
+
+        return null;
+    }
+
+    public IDisposable? OpenSecurityScope(string uriString)
+    {
+        // Multiple entries are possible.
+        // For example, user might open OpenRead stream, and read file properties before closing the file.
+        // If we don't check for nested scopes, inner closing scope will break access of the outer scope.
+        if (AddUse(this, uriString) == 1)
+        {
+            using var nsUriString = new AvnString(uriString);
+            var scopeOpened = _native.OpenSecurityScope(nsUriString).FromComBool();
+            if (!scopeOpened)
+            {
+                RemoveUse(this, uriString);
+                Logger.TryGet(LogEventLevel.Information, LogArea.macOSPlatform)?
+                     .Log(this, "OpenSecurityScope returned false for the {Uri}", uriString);
+                return null;
+            }
+        }
+
+        return Disposable.Create((api: this, uriString), static state =>
+        {
+            if (RemoveUse(state.api, state.uriString) == 0)
+            {
+                using var nsUriString = new AvnString(state.uriString);
+                state.api._native.CloseSecurityScope(nsUriString);
+            }
+        });
+
+        static int AddUse(StorageProviderApi api, string uriString)
+        {
+            lock (api)
+            {
+                api._openScopes.TryGetValue(uriString, out var useValue);
+                api._openScopes[uriString] = ++useValue;
+                return useValue;
+            }
+        }
+        static int RemoveUse(StorageProviderApi api, string uriString)
+        {
+            lock (api)
+            {
+                api._openScopes.TryGetValue(uriString, out var useValue);
+                useValue--;
+                if (useValue == 0)
+                    api._openScopes.Remove(uriString);
+                else
+                    api._openScopes[uriString] = useValue;
+                return useValue;
+            }
+        }
+    }
+
+    // Avalonia.Native technically can be used for more than just macOS,
+    // In which case we should provide different bookmark platform keys, and parse accordingly.
+    private static ReadOnlySpan<byte> MacOSKey => "macOS"u8;
+    public unsafe string? SaveBookmark(Uri uri)
+    {
+        void* error = null;
+        using var uriString = new AvnString(uri.AbsoluteUri);
+        using var bookmarkStr = _native.SaveBookmarkToBytes(uriString, &error);
+
+        if (error != null)
+        {
+            using var errorStr = MicroComRuntime.CreateProxyOrNullFor<IAvnString>(error, true);
+            Logger.TryGet(LogEventLevel.Warning, LogArea.macOSPlatform)?
+                .Log(this, "SaveBookmark for {Uri} failed with an error\r\n{Error}", uri, errorStr.String);
+            return null;
+        }
+
+        return StorageBookmarkHelper.EncodeBookmark(MacOSKey, bookmarkStr?.Bytes);
+    }
+
+    // Support both kinds of bookmarks when reading.
+    // Since "save bookmark" implementation will be different depending on the configuration.
+    public unsafe Uri? ReadBookmark(string bookmark, bool isDirectory)
+    {
+        if (StorageBookmarkHelper.TryDecodeBookmark(MacOSKey, bookmark, out var bytes) == StorageBookmarkHelper.DecodeResult.Success)
+        {
+            fixed (byte* ptr = bytes)
+            {
+                using var uriString = _native.ReadBookmarkFromBytes(ptr, bytes.Length);
+                return uriString is not null && Uri.TryCreate(uriString.String, UriKind.Absolute, out var uri) ?
+                    uri :
+                    null;
+            }
+        }
+        if (StorageBookmarkHelper.TryDecodeBclBookmark(bookmark, out var path))
+        {
+            return StorageProviderHelpers.UriFromFilePath(path, isDirectory);
+        }
+
+        return null;
+    }
+
+    public void ReleaseBookmark(Uri uri)
+    {
+        using var uriString = new AvnString(uri.AbsoluteUri);
+        _native.ReleaseBookmark(uriString);
+    }
+
+    public void Dispose()
+    {
+        _native.Dispose();
+    }
+
+    public async Task<IReadOnlyList<IStorageFile>> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options)
+    {
+        using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null);
+        var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
+
+        var results = await OpenDialogAsync(events =>
+        {
+            _native.OpenFileDialog((IAvnWindow?)topLevel?.Native,
+                events,
+                options.AllowMultiple.AsComBool(),
+                options.Title ?? string.Empty,
+                suggestedDirectory,
+                options.SuggestedFileName ?? string.Empty,
+                fileTypes);
+        });
+
+        return results.OfType<IStorageFile>().ToArray();
+    }
+
+    public async Task<IStorageFile?> SaveFileDialog(TopLevelImpl? topLevel, FilePickerSaveOptions options)
+    {
+        using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension);
+        var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
+
+        var results = await OpenDialogAsync(events =>
+        {
+            _native.SaveFileDialog((IAvnWindow?)topLevel?.Native,
+                events,
+                options.Title ?? string.Empty,
+                suggestedDirectory,
+                options.SuggestedFileName ?? string.Empty,
+                fileTypes);
+        }, create: true);
+
+        return results.OfType<IStorageFile>().FirstOrDefault();
+    }
+
+    public async Task<IReadOnlyList<IStorageFolder>> SelectFolderDialog(TopLevelImpl? topLevel, FolderPickerOpenOptions options)
+    {
+        var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
+
+        var results = await OpenDialogAsync(events =>
+        {
+            _native.SelectFolderDialog((IAvnWindow?)topLevel?.Native,
+                events,
+                options.AllowMultiple.AsComBool(),
+                options.Title ?? "",
+                suggestedDirectory);
+        });
+
+        return results.OfType<IStorageFolder>().ToArray();
+    }
+
+    public async Task<IEnumerable<IStorageItem>> OpenDialogAsync(Action<SystemDialogEvents> runDialog, bool create = false)
+    {
+        using var events = new SystemDialogEvents();
+        runDialog(events);
+        var result = await events.Task.ConfigureAwait(false);
+        return (result?
+            .Select(f => Uri.TryCreate(f, UriKind.Absolute, out var uri) ? TryGetStorageItem(uri, create) : null)
+            .Where(f => f is not null) ?? [])!;
+    }
+
+    internal class FilePickerFileTypesWrapper(
+        IReadOnlyList<FilePickerFileType>? types,
+        string? defaultExtension)
+        : NativeCallbackBase, IAvnFilePickerFileTypes
+    {
+        private readonly List<IDisposable> _disposables = new();
+
+        public int Count => types?.Count ?? 0;
+
+        public int IsDefaultType(int index) => (defaultExtension is not null &&
+            types![index].TryGetExtensions()?.Any(defaultExtension.EndsWith) == true).AsComBool();
+
+        public int IsAnyType(int index) =>
+            (types![index].Patterns?.Contains("*.*") == true || types[index].MimeTypes?.Contains("*.*") == true)
+            .AsComBool();
+
+        public IAvnString GetName(int index)
+        {
+            return EnsureDisposable(types![index].Name.ToAvnString());
+        }
+
+        public IAvnStringArray GetPatterns(int index)
+        {
+            return EnsureDisposable(new AvnStringArray(types![index].Patterns ?? Array.Empty<string>()));
+        }
+
+        public IAvnStringArray GetExtensions(int index)
+        {
+            return EnsureDisposable(new AvnStringArray(types![index].TryGetExtensions() ?? Array.Empty<string>()));
+        }
+
+        public IAvnStringArray GetMimeTypes(int index)
+        {
+            return EnsureDisposable(new AvnStringArray(types![index].MimeTypes ?? Array.Empty<string>()));
+        }
+
+        public IAvnStringArray GetAppleUniformTypeIdentifiers(int index)
+        {
+            return EnsureDisposable(new AvnStringArray(types![index].AppleUniformTypeIdentifiers ?? Array.Empty<string>()));
+        }
+
+        protected override void Destroyed()
+        {
+            foreach (var disposable in _disposables)
+            {
+                disposable.Dispose();
+            }
+        }
+
+        private T EnsureDisposable<T>(T input) where T : IDisposable
+        {
+            _disposables.Add(input);
+            return input;
+        }
+    }
+
+    internal class SystemDialogEvents : NativeCallbackBase, IAvnSystemDialogEvents
+    {
+        private readonly TaskCompletionSource<string[]> _tcs = new();
+
+        public Task<string[]> Task => _tcs.Task;
+
+        public void OnCompleted(IAvnStringArray? ppv)
+        {
+            using (ppv)
+            {
+                _tcs.SetResult(ppv?.ToStringArray() ?? []);
+            }
+        }
+    }
+}
diff --git a/src/Avalonia.Native/StorageProviderImpl.cs b/src/Avalonia.Native/StorageProviderImpl.cs
new file mode 100644
index 00000000000..2773eddf24f
--- /dev/null
+++ b/src/Avalonia.Native/StorageProviderImpl.cs
@@ -0,0 +1,63 @@
+﻿#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
+
+namespace Avalonia.Native;
+
+internal sealed class StorageProviderImpl(TopLevelImpl topLevel, StorageProviderApi native) : IStorageProvider
+{
+    public bool CanOpen => true;
+
+    public bool CanSave => true;
+
+    public bool CanPickFolder => true;
+
+    public Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
+    {
+        return native.OpenFileDialog(topLevel, options);
+    }
+
+    public Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
+    {
+        return native.SaveFileDialog(topLevel, options);
+    }
+
+    public Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
+    {
+        return native.SelectFolderDialog(topLevel, options);
+    }
+
+    public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
+    {
+        return Task.FromResult(native.TryGetStorageItem(native.ReadBookmark(bookmark, false)) as IStorageBookmarkFile);
+    }
+
+    public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
+    {
+        return Task.FromResult(native.TryGetStorageItem(native.ReadBookmark(bookmark, true)) as IStorageBookmarkFolder);
+    }
+
+    public Task<IStorageFile?> TryGetFileFromPathAsync(Uri fileUri)
+    {
+        return Task.FromResult(native.TryGetStorageItem(fileUri) as IStorageFile);
+    }
+
+    public Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
+    {
+        return Task.FromResult(native.TryGetStorageItem(folderPath) as IStorageFolder);
+    }
+
+    public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
+    {
+        if (BclStorageProvider.TryGetWellKnownFolderCore(wellKnownFolder) is { } directoryInfo)
+        {
+            return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directoryInfo));
+        }
+
+        return Task.FromResult<IStorageFolder?>(null);
+    }
+}
diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs
deleted file mode 100644
index bf898178814..00000000000
--- a/src/Avalonia.Native/SystemDialogs.cs
+++ /dev/null
@@ -1,181 +0,0 @@
-﻿#nullable enable
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Threading.Tasks;
-using Avalonia.Native.Interop;
-using Avalonia.Platform.Storage;
-using Avalonia.Platform.Storage.FileIO;
-
-namespace Avalonia.Native
-{
-    internal class SystemDialogs : BclStorageProvider
-    {
-        private readonly TopLevelImpl _topLevel;
-        private readonly IAvnSystemDialogs _native;
-
-        public SystemDialogs(TopLevelImpl topLevel, IAvnSystemDialogs native)
-        {
-            _topLevel = topLevel;
-            _native = native;
-        }
-
-        public override bool CanOpen => true;
-
-        public override bool CanSave => true;
-
-        public override bool CanPickFolder => true;
-
-        public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
-        {
-            using var events = new SystemDialogEvents();
-            using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null);
-
-            var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
-
-            _native.OpenFileDialog((IAvnWindow)_topLevel.Native,
-                                    events,
-                                    options.AllowMultiple.AsComBool(),
-                                    options.Title ?? string.Empty,
-                                    suggestedDirectory,
-                                    options.SuggestedFileName ?? string.Empty,
-                                    fileTypes);
-
-            var result = await events.Task.ConfigureAwait(false);
-
-            return result?.Select(f => new BclStorageFile(new FileInfo(f))).ToArray()
-                   ?? Array.Empty<IStorageFile>();
-        }
-
-        public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
-        {
-            using var events = new SystemDialogEvents();
-            using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension);
-
-            var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
-
-            _native.SaveFileDialog((IAvnWindow)_topLevel.Native,
-                        events,
-                        options.Title ?? string.Empty,
-                        suggestedDirectory,
-                        options.SuggestedFileName ?? string.Empty,
-                        fileTypes);
-
-            var result = await events.Task.ConfigureAwait(false);
-            return result.FirstOrDefault() is string file
-                ? new BclStorageFile(new FileInfo(file))
-                : null;
-        }
-
-        public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
-        {
-            using var events = new SystemDialogEvents();
-
-            var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
-
-            _native.SelectFolderDialog((IAvnWindow)_topLevel.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory);
-
-            var result = await events.Task.ConfigureAwait(false);
-            return result?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray()
-                   ?? Array.Empty<IStorageFolder>();
-        }
-    }
-
-    internal class FilePickerFileTypesWrapper : NativeCallbackBase, IAvnFilePickerFileTypes
-    {
-        private readonly IReadOnlyList<FilePickerFileType>? _types;
-        private readonly string? _defaultExtension;
-        private readonly List<IDisposable> _disposables;
-
-        public FilePickerFileTypesWrapper(
-            IReadOnlyList<FilePickerFileType>? types,
-            string? defaultExtension)
-        {
-            _types = types;
-            _defaultExtension = defaultExtension;
-            _disposables = new List<IDisposable>();
-        }
-
-        public int Count => _types?.Count ?? 0;
-
-        public int IsDefaultType(int index) => (_defaultExtension is not null &&
-            _types![index].TryGetExtensions()?.Any(ext => _defaultExtension.EndsWith(ext)) == true).AsComBool();
-
-        public int IsAnyType(int index) =>
-            (_types![index].Patterns?.Contains("*.*") == true || _types[index].MimeTypes?.Contains("*.*") == true)
-            .AsComBool();
-
-        public IAvnString GetName(int index)
-        {
-            return EnsureDisposable(_types![index].Name.ToAvnString());
-        }
-
-        public IAvnStringArray GetPatterns(int index)
-        {
-            return EnsureDisposable(new AvnStringArray(_types![index].Patterns ?? Array.Empty<string>()));
-        }
-
-        public IAvnStringArray GetExtensions(int index)
-        {
-            return EnsureDisposable(new AvnStringArray(_types![index].TryGetExtensions() ?? Array.Empty<string>()));
-        }
-
-        public IAvnStringArray GetMimeTypes(int index)
-        {
-            return EnsureDisposable(new AvnStringArray(_types![index].MimeTypes ?? Array.Empty<string>()));
-        }
-
-        public IAvnStringArray GetAppleUniformTypeIdentifiers(int index)
-        {
-            return EnsureDisposable(new AvnStringArray(_types![index].AppleUniformTypeIdentifiers ?? Array.Empty<string>()));
-        }
-
-        protected override void Destroyed()
-        {
-            foreach (var disposable in _disposables)
-            {
-                disposable.Dispose();
-            }
-        }
-
-        private T EnsureDisposable<T>(T input) where T : IDisposable
-        {
-            _disposables.Add(input);
-            return input;
-        }
-    }
-
-    internal unsafe class SystemDialogEvents : NativeCallbackBase, IAvnSystemDialogEvents
-    {
-        private readonly TaskCompletionSource<string[]> _tcs;
-
-        public SystemDialogEvents()
-        {
-            _tcs = new TaskCompletionSource<string[]>();
-        }
-
-        public Task<string[]> Task => _tcs.Task;
-
-        public void OnCompleted(int numResults, void* trFirstResultRef)
-        {
-            string[] results = new string[numResults];
-
-            unsafe
-            {
-                var ptr = (IntPtr*)trFirstResultRef;
-
-                for (int i = 0; i < numResults; i++)
-                {
-                    results[i] = Marshal.PtrToStringAnsi(*ptr) ?? string.Empty;
-
-                    ptr++;
-                }
-            }
-
-            _tcs.SetResult(results);
-        }
-    }
-}
diff --git a/src/Avalonia.Native/TopLevelImpl.cs b/src/Avalonia.Native/TopLevelImpl.cs
index c7d3305efdf..8c65a3bd760 100644
--- a/src/Avalonia.Native/TopLevelImpl.cs
+++ b/src/Avalonia.Native/TopLevelImpl.cs
@@ -65,7 +65,6 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
 {
     protected IInputRoot? _inputRoot;
     private NativeControlHostImpl? _nativeControlHost;
-    private IStorageProvider? _storageProvider;
     private PlatformBehaviorInhibition? _platformBehaviorInhibition;
 
     private readonly MouseDevice? _mouse;
@@ -98,7 +97,6 @@ internal virtual void Init(MacOSTopLevelHandle handle)
         _savedLogicalSize = ClientSize;
         _savedScaling = RenderScaling;
         _nativeControlHost = new NativeControlHostImpl(Native!.CreateNativeControlHost());
-        _storageProvider = new SystemDialogs(this, Factory.CreateSystemDialogs());
         _platformBehaviorInhibition = new PlatformBehaviorInhibition(Factory.CreatePlatformBehaviorInhibition());
         _surfaces = new object[] { new GlPlatformSurface(Native), new MetalPlatformSurface(Native), this };
         InputMethod = new AvaloniaNativeTextInputMethod(Native);
@@ -338,11 +336,6 @@ public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> tran
             return _nativeControlHost;
         }
 
-        if (featureType == typeof(IStorageProvider))
-        {
-            return _storageProvider;
-        }
-
         if (featureType == typeof(IPlatformBehaviorInhibition))
         {
             return _platformBehaviorInhibition;
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index 7e9d7f0b96c..902c477a9be 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -677,6 +677,7 @@ interface IAvaloniaNativeFactory : IUnknown
      HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv);
      HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnPopup** ppv);
      HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv);
+     HRESULT CreateStorageProvider(IAvnStorageProvider** ppv);
      HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv);
      HRESULT CreateScreens(IAvnScreenEvents* cb, IAvnScreens** ppv);
      HRESULT CreateClipboard(IAvnClipboard** ppv);
@@ -888,18 +889,18 @@ interface IAvnPlatformThreadingInterface : IUnknown
 [uuid(6c621a6e-e4c1-4ae3-9749-83eeeffa09b6)]
 interface IAvnSystemDialogEvents : IUnknown
 {
-     void OnCompleted(int numResults, void* ptrFirstResult);
+     void OnCompleted(IAvnStringArray*array);
 }
 
 [uuid(4d7a47db-a944-4061-abe7-62cb6aa0ffd5)]
-interface IAvnSystemDialogs : IUnknown
+interface IAvnStorageProvider : IUnknown
 {
      void SelectFolderDialog(IAvnWindow* parentWindowHandle,
                              IAvnSystemDialogEvents* events,
                              bool allowMultiple,
                              [const] char* title,
                              [const] char* initialPath);
-    
+
      void OpenFileDialog(IAvnWindow* parentWindowHandle,
                                  IAvnSystemDialogEvents* events,
                                  bool allowMultiple,
@@ -907,13 +908,21 @@ interface IAvnSystemDialogs : IUnknown
                                  [const] char* initialDirectory,
                                  [const] char* initialFile,
                                  IAvnFilePickerFileTypes* filters);
-    
+
      void SaveFileDialog(IAvnWindow* parentWindowHandle,
                                  IAvnSystemDialogEvents* events,
                                  [const] char* title,
                                  [const] char* initialDirectory,
                                  [const] char* initialFile,
                                  IAvnFilePickerFileTypes* filters);
+
+     HRESULT SaveBookmarkToBytes(IAvnString*fileUri, void**err, IAvnString**ppv);
+     HRESULT ReadBookmarkFromBytes(void* ptr, int len, IAvnString**ppv);
+
+     void ReleaseBookmark(IAvnString*fileUri);
+
+     bool OpenSecurityScope(IAvnString*fileUri);
+     void CloseSecurityScope(IAvnString*fileUri);         
 }
 
 [uuid(4d7ab7db-a111-406f-abeb-11cb6aa033d5)]
diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
index b594cad46ac..6a89a13e023 100644
--- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
+++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
@@ -3,6 +3,7 @@
 using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices.JavaScript;
+using System.Text;
 using System.Threading.Tasks;
 using Avalonia.Browser.Interop;
 using Avalonia.Platform.Storage;
@@ -12,6 +13,7 @@ namespace Avalonia.Browser.Storage;
 
 internal class BrowserStorageProvider : IStorageProvider
 {
+    internal static ReadOnlySpan<byte> BrowserBookmarkKey => "browser"u8;
     internal const string PickerCancelMessage = "The user aborted a request";
     internal const string NoPermissionsMessage = "Permissions denied";
 
@@ -104,16 +106,12 @@ public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPic
 
     public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
     {
-        await AvaloniaModule.ImportStorage();
-        var item = await StorageHelper.OpenBookmark(bookmark);
-        return item is not null ? new JSStorageFile(item) : null;
+        return await DecodeBookmark(bookmark) as IStorageBookmarkFile;
     }
-
+    
     public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
     {
-        await AvaloniaModule.ImportStorage();
-        var item = await StorageHelper.OpenBookmark(bookmark);
-        return item is not null ? new JSStorageFolder(item) : null;
+        return await DecodeBookmark(bookmark) as IStorageBookmarkFolder;
     }
 
     public Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
@@ -158,6 +156,24 @@ private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnum
 
         return (types, !includeAll);
     }
+
+    private async Task<IStorageBookmarkItem?> DecodeBookmark(string bookmark)
+    {
+        await AvaloniaModule.ImportStorage();
+        var item = StorageBookmarkHelper.TryDecodeBookmark(BrowserBookmarkKey, bookmark, out var bytes) switch
+        {
+            StorageBookmarkHelper.DecodeResult.Success => await StorageHelper.OpenBookmark(Encoding.UTF8.GetString(bytes!)),
+            // Attempt to decode 11.0 browser bookmarks
+            StorageBookmarkHelper.DecodeResult.InvalidFormat => await StorageHelper.OpenBookmark(bookmark),
+            _ => null
+        };
+        return item?.GetPropertyAsString("kind") switch
+        {
+            "directory" => new JSStorageFolder(item),
+            "file" => new JSStorageFile(item),
+            _ => null
+        };
+    }
 }
 
 internal abstract class JSStorageItem : IStorageBookmarkItem
@@ -188,14 +204,16 @@ public async Task<StorageItemProperties> GetBasicPropertiesAsync()
 
     public bool CanBookmark => StorageHelper.HasNativeFilePicker();
 
-    public Task<string?> SaveBookmarkAsync()
+    public async Task<string?> SaveBookmarkAsync()
     {
         if (!CanBookmark)
         {
-            return Task.FromResult<string?>(null);
+            return null;
         }
 
-        return StorageHelper.SaveBookmark(FileHandle);
+        var nativeBookmark = await StorageHelper.SaveBookmark(FileHandle);
+        return nativeBookmark is null ? null
+            : StorageBookmarkHelper.EncodeBookmark(BrowserStorageProvider.BrowserBookmarkKey, nativeBookmark);
     }
 
     public Task<IStorageFolder?> GetParentAsync()
diff --git a/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs b/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs
deleted file mode 100644
index 424ec7589a9..00000000000
--- a/src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-﻿using System.IO;
-
-using Foundation;
-
-using UIKit;
-
-#nullable enable
-
-namespace Avalonia.iOS.Storage;
-
-internal sealed class IOSSecurityScopedStream : Stream
-{
-    private readonly UIDocument _document;
-    private readonly FileStream _stream;
-    private readonly NSUrl _url;
-    private readonly NSUrl _securityScopedAncestorUrl;
-
-    internal IOSSecurityScopedStream(NSUrl url, NSUrl securityScopedAncestorUrl, FileAccess access)
-    {
-        _document = new UIDocument(url);
-        var path = _document.FileUrl.Path!;
-        _url = url;
-        _securityScopedAncestorUrl = securityScopedAncestorUrl;
-        _securityScopedAncestorUrl.StartAccessingSecurityScopedResource();
-        _stream = File.Open(path, FileMode.Open, access);
-    }
-
-    public override bool CanRead => _stream.CanRead;
-
-    public override bool CanSeek => _stream.CanSeek;
-
-    public override bool CanWrite => _stream.CanWrite;
-
-    public override long Length => _stream.Length;
-
-    public override long Position
-    {
-        get => _stream.Position;
-        set => _stream.Position = value;
-    }
-
-    public override void Flush() =>
-        _stream.Flush();
-
-    public override int Read(byte[] buffer, int offset, int count) =>
-        _stream.Read(buffer, offset, count);
-
-    public override long Seek(long offset, SeekOrigin origin) =>
-        _stream.Seek(offset, origin);
-
-    public override void SetLength(long value) =>
-        _stream.SetLength(value);
-
-    public override void Write(byte[] buffer, int offset, int count) =>
-        _stream.Write(buffer, offset, count);
-
-    protected override void Dispose(bool disposing)
-    {
-        base.Dispose(disposing);
-
-        if (disposing)
-        {
-            _stream.Dispose();
-            _document.Dispose();
-            _securityScopedAncestorUrl.StopAccessingSecurityScopedResource();
-        }
-    }
-}
diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
index cf124aa1012..fa086c7d61f 100644
--- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
+++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
@@ -5,6 +5,8 @@
 using System.Threading.Tasks;
 using Avalonia.Logging;
 using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
+using Avalonia.Reactive;
 using Foundation;
 
 using UIKit;
@@ -132,7 +134,7 @@ public Task ReleaseBookmarkAsync()
         return Task.CompletedTask;
     }
 
-    public Task<string?> SaveBookmarkAsync()
+    public unsafe Task<string?> SaveBookmarkAsync()
     {
         try
         {
@@ -141,7 +143,7 @@ public Task ReleaseBookmarkAsync()
                 return Task.FromResult<string?>(null);
             }
 
-            var newBookmark = Url.CreateBookmarkData(NSUrlBookmarkCreationOptions.SuitableForBookmarkFile, Array.Empty<string>(), null, out var bookmarkError);
+            using var newBookmark = Url.CreateBookmarkData(NSUrlBookmarkCreationOptions.SuitableForBookmarkFile, [], null, out var bookmarkError);
             if (bookmarkError is not null)
             {
                 Logger.TryGet(LogEventLevel.Error, LogArea.IOSPlatform)?.
@@ -149,8 +151,9 @@ public Task ReleaseBookmarkAsync()
                 return Task.FromResult<string?>(null);
             }
 
+            var bytes = new Span<byte>((void*)newBookmark.Bytes, (int)newBookmark.Length);
             return Task.FromResult<string?>(
-                newBookmark.GetBase64EncodedString(NSDataBase64EncodingOptions.None));
+                StorageBookmarkHelper.EncodeBookmark(IOSStorageProvider.PlatformKey, bytes));
         }
         finally
         {
@@ -171,12 +174,28 @@ public IOSStorageFile(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base
 
     public Task<Stream> OpenReadAsync()
     {
-        return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Read));
+        return Task.FromResult(CreateStream(FileAccess.Read));
     }
 
     public Task<Stream> OpenWriteAsync()
     {
-        return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Write));
+        return Task.FromResult(CreateStream(FileAccess.Write));
+    }
+
+    private Stream CreateStream(FileAccess fileAccess)
+    {
+        var document = new UIDocument(Url);
+        var path = document.FileUrl.Path!;
+        var scopeCreated = SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
+        var stream = File.Open(path, FileMode.Open, fileAccess);
+
+        return scopeCreated ?
+            new SecurityScopedStream(stream, Disposable.Create(() =>
+            {
+                document.Dispose();
+                SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
+            })) :
+            stream;
     }
 }
 
diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs
index 6d9d2b1e37c..4c1bf97c6fd 100644
--- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs
+++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs
@@ -10,11 +10,14 @@
 using UTTypeLegacy = MobileCoreServices.UTType;
 using UTType = UniformTypeIdentifiers.UTType;
 using System.Runtime.Versioning;
+using Avalonia.Platform.Storage.FileIO;
 
 namespace Avalonia.iOS.Storage;
 
 internal class IOSStorageProvider : IStorageProvider
 {
+    public static ReadOnlySpan<byte> PlatformKey => "ios"u8; 
+
     private readonly AvaloniaView _view;
     public IOSStorageProvider(AvaloniaView view)
     {
@@ -217,20 +220,41 @@ private Task<NSUrl[]> ShowPicker(UIViewController picker, TaskCompletionSource<N
         return tcs.Task;
     }
 
-    private NSUrl? GetBookmarkedUrl(string bookmark)
+    private unsafe NSUrl? GetBookmarkedUrl(string bookmark)
     {
-        var url = NSUrl.FromBookmarkData(new NSData(bookmark, NSDataBase64DecodingOptions.None),
-            NSUrlBookmarkResolutionOptions.WithoutUI, null, out var isStale, out var error);
-        if (isStale)
+        return StorageBookmarkHelper.TryDecodeBookmark(PlatformKey, bookmark, out var bytes) switch
+        {
+            StorageBookmarkHelper.DecodeResult.Success => DecodeFromBytes(bytes!),
+            // Attempt to decode 11.0 ios bookmarks
+            StorageBookmarkHelper.DecodeResult.InvalidFormat => DecodeFromNSData(new NSData(bookmark, NSDataBase64DecodingOptions.None)),
+            _ => null
+        };
+
+        NSUrl DecodeFromBytes(byte[] bytes)
         {
-            Logger.TryGet(LogEventLevel.Warning, LogArea.IOSPlatform)?.Log(this, "Stale bookmark detected");
+            fixed (byte* ptr = bytes)
+            {
+                using var data = new NSData(new IntPtr(ptr), new UIntPtr((uint)bytes.Length), null);
+                return DecodeFromNSData(data);
+            }
         }
-            
-        if (error != null)
+
+        NSUrl DecodeFromNSData(NSData nsData)
         {
-            throw new NSErrorException(error);
+            var url = NSUrl.FromBookmarkData(nsData,
+                NSUrlBookmarkResolutionOptions.WithoutUI, null, out var isStale, out var error);
+            if (isStale)
+            {
+                Logger.TryGet(LogEventLevel.Warning, LogArea.IOSPlatform)?.Log(this, "Stale bookmark detected");
+            }
+
+            if (error != null)
+            {
+                throw new NSErrorException(error);
+            }
+
+            return url;
         }
-        return url;
     }
 
     private static string[] FileTypesToUTTypeLegacy(IReadOnlyList<FilePickerFileType>? filePickerFileTypes)


Test patch:
diff --git a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs
index b648b0d1782..83380bf6a02 100644
--- a/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Utilities/UriExtensionsTests.cs
@@ -36,7 +36,7 @@ public void Assembly_Name_From_Empty_Query_Not_Parsed()
     [InlineData("C:\\\\Work\\Projects.txt")]
     public void Should_Convert_File_Path_To_Uri_And_Back(string path)
     {
-        var uri = StorageProviderHelpers.FilePathToUri(path);
+        var uri = StorageProviderHelpers.UriFromFilePath(path, false);
 
         Assert.Equal(path, uri.LocalPath);
     }
diff --git a/tests/Avalonia.Controls.UnitTests/Platform/StorageProviderHelperTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/StorageProviderHelperTests.cs
new file mode 100644
index 00000000000..301301f61dd
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Platform/StorageProviderHelperTests.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+using System.Text;
+using Avalonia.Platform.Storage.FileIO;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Platform;
+
+public class StorageProviderHelperTests
+{
+    [Fact]
+    public void Can_Encode_And_Decode_Bookmark()
+    {
+        var platform = "test"u8;
+        var nativeBookmark = "bookmark"u8;
+
+        var bookmark = StorageBookmarkHelper.EncodeBookmark(platform, nativeBookmark);
+
+        Assert.NotNull(bookmark);
+
+        Assert.Equal(
+            StorageBookmarkHelper.DecodeResult.Success,
+            StorageBookmarkHelper.TryDecodeBookmark(platform, bookmark, out var nativeBookmarkRet));
+
+        Assert.NotNull(nativeBookmarkRet);
+
+        Assert.True(nativeBookmark.SequenceEqual(nativeBookmarkRet));
+    }
+    
+    [Theory]
+    [InlineData("C://file.txt", "YXZhLnYxLnRlc3QAAAAAAEM6Ly9maWxlLnR4dA==")]
+    public void Can_Encode_Bookmark(string nativeBookmark, string expectedEncodedBookmark)
+    {
+        var platform = "test"u8;
+
+        var bookmark = StorageBookmarkHelper.EncodeBookmark(platform, nativeBookmark);
+
+        Assert.Equal(expectedEncodedBookmark, bookmark);
+        Assert.NotNull(bookmark);
+    }
+
+    [Theory]
+    [InlineData("YXZhLnYxLnRlc3QAAAAAAEM6Ly9maWxlLnR4dA==", "C://file.txt")]
+    public void Can_Decode_Bookmark(string encodedBookmark, string expectedNativeBookmark)
+    {
+        var platform = "test"u8;
+        var expectedNativeBookmarkBytes = Encoding.UTF8.GetBytes(expectedNativeBookmark);
+
+        Assert.Equal(
+            StorageBookmarkHelper.DecodeResult.Success,
+            StorageBookmarkHelper.TryDecodeBookmark(platform, encodedBookmark, out var nativeBookmark));
+
+        Assert.Equal(expectedNativeBookmarkBytes, nativeBookmark);
+    }
+
+    [Theory]
+    [InlineData("YXZhLnYxLmJjbAAAAAAAAEM6Ly9maWxlLnR4dA==", "C://file.txt")]
+    [InlineData("C://file.txt", "C://file.txt")]
+    public void Can_Decode_Bcl_Bookmarks(string bookmark, string expected)
+    {
+        var a = StorageBookmarkHelper.EncodeBclBookmark(expected);
+        Assert.True(StorageBookmarkHelper.TryDecodeBclBookmark(bookmark, out var localPath));
+        Assert.Equal(expected, localPath);
+    }
+
+    [Theory]
+    [InlineData("YXZhLnYxLnRlc3QAAAAAAEM6Ly9maWxlLnR4dA==")] // "test" platform passed instead of "bcl"
+    [InlineData("ZYXasHKJASd87124")]
+    public void Fails_To_Decode_Invalid_Bcl_Bookmarks(string bookmark)
+    { 
+        Assert.False(StorageBookmarkHelper.TryDecodeBclBookmark(bookmark, out var localPath));
+        Assert.Null(localPath);
+    }
+}