Isolated storage is a .NET security feature that assigns each application a unique storage area that is fully isolated from other applications and essentially private. Isolated storage provides true isolation in the sense that the identity of an application or a component uniquely determines the root of a virtual, sandboxed file system that only that application or component can access. Such a form of data storage is well suited to partially trusted applications in general and ASP.NET applications in particular.

In .NET, not all applications run with the same set of security permissions. Security permissions represent the key to access critical system resources such as the file system, the registry, the clipboard, and the printer. Applications that attempt to access these resources without a valid authorization simply run into an exception. The mechanism behind security permissions is called Code Access Security (CAS) and represents the .NET answer to the growing demand for software security. CAS works by granting permissions to each assembly that is loaded in the CLR. The permission set for an assembly stems from the evidence that the assembly itself provides. The evidence defines the set of information that constitutes input for security decisions. This information includes any digital signatures the assembly may have and the location from which the assembly is accessed.

Just the location of download is a critical factor for applications. The set of permissions granted, in fact, changes considerably if the assembly is accessed locally or maybe remotely from a Web site. As you can guess, location dependent permissions pose a number of issues to developers that are going to write network-deployable classes. In this article, after a brief introduction to fully and partially trusted applications, I'll focus on isolated storage—namely, a feature of the .NET Framework that elegantly works around the file system limitations for network-based applications, including ASP.NET applications.

Trusted Applications

While loading a managed assembly on behalf of a running application, the CLR is called to determine the set of permissions associated with that assembly. The CLR bases any decision both on the privileges of the currently logged user and the location from which the assembly is going to be downloaded. As a result, the permissions allowed to a certain assembly may prevent the code to perform certain necessary but potentially harmful operations such as writing files or accessing the registry. Applications automatically limited in their functionality for security reasons are said to be partially trusted applications.

When applications attempt to perform an operation not allowed by the ongoing security policy, the event is signaled with an exception of type SecurityException. The CLR prompts the user with a dialog box like the one shown in Figure 1.

Figure 1: The application just attempted to perform an operation on a restricted system resource. The dialog box represents the default way that the CLR handles the event.

For example, all assemblies accessed over the network are normally considered partially trusted code and, as such, are subject to a number of limitations. The net effect of the code access security infrastructure is that the same piece of code can produce different results if run from different locations or if invoked from different clients.

The code access security mechanism is extremely flexible and highly configurable, however. Using the Code Access Security Policy editor from the Control Panel (see Figure 2), you, or more exactly your system administrator, can modify the trust level of a particular assembly adapting it to the needs. You can accomplish the same task through the caspol.exe command line utility.

Figure 2: The Code Access Security editor is integrated with the Control Panel. Using the above applet you can configure the level of trust of individual assemblies.

In .NET, only fully trusted applications can access all resources in the system and call into all classes in the Framework. What happens to your application should the CLR happen to run it as partially trusted code? If the action your code is executing is not permitted, a security exception is thrown. A common practice to handle this exception entails that you wrap any restricted operation in a Try-Catch block. When the exception occurs you gently inform the user that the operation failed because it was not allowed in that context.

An even better practice is to check in advance whether the application has enough permissions to execute restricted operation such as file I/O. In this case you could gracefully adjust the user interface upon loading and make it reflect the new operating context. For example, you could disable all menu items that fire restricted operations or set some internal variables that silently prevent or limit sensitive actions throughout the code.

Applications commonly need to access the file system, but, for example, ASP.NET applications can't just do that when run with the default settings. As a result, relatively safe actions such as persisting user settings or writing a temporary data file are prohibited to code that is network-deployed. Let's examine the case of ASP.NET applications in more detail.

The Case of ASP.NET Applications

Just a few weeks before releasing version 1.0 of the .NET Framework, Microsoft decided to change the default account for the ASP.NET runtime process. Unlike ASP, ASP.NET does not run applications within the context of the LOCALSYSTEM account. ASP.NET applications are considered partially trusted applications and, subsequently, pages accessing the file system explicitly need to be granted write permissions to operate safely and effectively. How can you work around this I/O limitation?

One thing you can do is change the security permissions on the folder that contains the ASP.NET application. You must do this once during the setup of the application. Here's a step-by-step guide.

  • Open the folder with Windows Explorer.
  • Right-click and select the Properties menu item.
  • From the subsequent tabbed view, pick the Security page.
  • Add the ASP.NET account to the list of accounts enabled to operate on the folder by clicking the Add button and selecting ASPNET from the list of available users. (See Figure 3.)
  • Next, return to the previously selected Security tab, highlight the newly added ASPNET account and add write permissions. (See Figure 4.)
  • Figure 3: Select the ASPNET user account and click Add to be able to edit the default permissions.
    Figure 4: Select the ASPNET account and edit the permissions accordingly.

    After this treatment the ASP.NET application based in that physical folder will be able to create and update local files.

    Normally an ASP.NET application needs to create local files to park some data or to persist some settings. If the name of the file is known, not subject to change, and a default copy of the file is provided at setup time, you could also apply the algorithm above to the individual file rather than to the whole folder.

    In other situations ASP.NET applications need to create temporary files that don't survive the session or even the single request. In this case you can consider employing a simpler trick—using in-memory streams.

    The MemoryStream class works in much the same way that a disk file stream does. The key difference is that MemoryStream creates a stream whose backing store is memory rather than a disk file. Let's compare the code necessary to save a file to a file stream and to a memory stream.

    ' Use FileStream to create a file
    Dim file As StreamWriter
    file = New StreamWriter(fileName, _
       FileMode.CreateNew)
    WriteContents(file)
    file.Close()
    ' Use MemoryStream to create a memory buffer
    Dim ms As MemoryStream
    ms = New MemoryStream()
    WriteContents(ms)
    ms.Close()
    
    

    The MemoryStream class encapsulates its data into an array of bytes and makes it available only in that form. You can write strings to a MemoryStream object only after you convert them to an array of unsigned characters. Unfortunately no method on the String class provides that capability. To do this job you must resort to the GetBuffer method of the Encoding class.

    Dim s As String = "Hello, world!"
    ms.Write(Encoding.ASCII.GetBytes(s), _
       0, s.Length)
    
    

    It is also easy to read back strings from a MemoryStream object. In this case you use the GetString method of the Encoding class, as shown below.

    ' GetBuffer returns the contents of the stream
    Dim buf As String
    buf = Encoding.ASCII.GetString(ms.GetBuffer())
    
    

    While the previously discussed trick—changing the permissions on a file or a folder—is just a hack, memory streams represents an interesting option because they can reduce the need for temporary buffers and files in an application.

    Let's make a more general consideration about partially trusted applications. The CAS infrastructure prohibits them from accessing the file system to avoid the risk that they could perform malicious code. However, one thing is getting a full access to the file system and all another one is persisting some user-defined bytes to disk. The second option still requires that the application accesses the file system, but—that's the point—not necessarily the whole file system. This rather straightforward observation is at the root of the CAS feature called isolated storage.

    What's Isolated Storage, Anyway?

    Isolated storage represents an assembly-specific virtual file system that is managed using the facilities of the local file system. Access to the isolated storage is always restricted to the user who created it. The idea is that the code accesses paths relative to an isolated sub file system. The high-level code simply works with streams that take regular file names. Those streams, though, read and write files located in a sandboxed portion of the file system that the system transparently manages. Isolated storage doesn't accept absolute file names and manages to assign a distinct (and identity-based) file system subtree to each client. Figure 5 shows the architecture of isolated storage for ASP.NET applications.

    Figure 5: ASP.NET applications obtain a store and read and write files there. A store is a root directory located somewhere in the local computer.

    With isolated storage, partially trusted applications such as ASP.NET applications can persist data in accordance with the restrictions set by the current security policy. By default, in fact, security policies grant applications running from the Internet the permission to use isolated storage. On the other hand, isolated storage offers applications only a small and circumscribed portion of the disk to use.

    With isolated storage, data is always isolated by user and by assembly. In addition, programmers can manage to have it isolated by application domain. Each different logged user has his own isolated sandbox and each sandbox is distinct on a per-assembly basis. By default, the same assembly when loaded in different application domains accesses the same storage system. Remember that you can configure this option at will.

    Applications that take advantage of isolated storage are associated with a data compartment. The data compartment is a collection of local files called stores. Each store contains information about the user, the assembly, and the name of the file system directory that is the physical root of store. For Windows 2000 machines, the typical path used to create isolated stores is under C:\Document And Settings\user\Local Settings\Application Data.

    Classes That Make It Happen

    You need to know about two key classes: IsolatedStorageFile and IsolatedStorageFileStream. IsolatedStorageFile represents a separated storage area containing files and directories. IsolatedStorageFileStream inherits from FileStream and represents a Stream object you use to actually read and write file contents. IsolatedStorageFile has a static member called GetStore that is the key that opens your application's private storage system. You want to obtain a data store as your first step.

    Dim iso As IsolatedStorageFile
    iso = IsolatedStorageFile.GetStore( _
        IsolatedStorageScope.User Or _
        IsolatedStorageScope.Assembly, _
        Nothing, _
        Nothing)
    
    

    The GetStore method gets a new isolated store for the current assembly and saves it into a new instance of the IsolatedStorageFile class. GetStore has a few overloads. In the previous code snippet I used the simplest method to overload GetStore. The first parameter you need to pass determines the scope of the store. The scope consists of a bitwise combination of values (See Table 1) taken from the IsolatedStorageScope enumeration.

    The scope essentially sets parameters to determine on which basis the storage is isolated: by user, assembly, or application domain. The other two arguments are expected to be objects that contain evidence for the application domain and the code assembly identity, respectively. By using Nothing (null in C#) you indicate that you'll use the current domain and assembly. Other, more specific methods allow you to get the store for a predefined scope: GetUserStoreForAssembly and GetUserStoreForDomain.

    Once you hold a Store object you can start writing and reading files and directories. Although names retain the case for display purposes, file and folder names are case-insensitive for search.

    Using Isolated Streams

    The IsolatedStorageFile object exposes a few methods to manipulate files and directories. In particular, the CreateDirectory method lets you create a new directory in the store.

    iso.CreateDirectory("First\Second")
    
    

    You can indicate multiple levels of directories and CreateDirectory will ensure that the specified path exists when it returns. You read, write, and create files using a particular flavor of stream—the IsolatedStorageFileStream class. The file name must be relative and cannot contain any drive information. Look at this example:

    Dim stm As IsolatedStorageFileStream
    stm = New IsolatedStorageFileStream(file, _
       FileMode.Create, iso)
    Dim s As String = "Hello, world!"
    stm.Write(Encoding.ASCII.GetBytes(s), _
       0, s.Length)
    stm.Close()
    
    

    You use the file stream API to create a file. The file stream API is based on the programming interface of the FileStream class from which the IsolatedStorageFileStream class is derived. Once you hold an IsolatedStorageFileStream object you can use it to work with a StreamReader or a StreamWriter object, both of which provide an easier and more programmer-friendly set of methods. Using StreamReader and StreamWriter you can read and write to a file in a store as you would to any other file.

    Dim reader As StreamReader
    reader = New StreamReader(stm)
    Console.WriteLine(reader.ReadToEnd())
    reader.Close()
    
    

    Although the isolated storage is a miniaturized version of the operating system's file system, it does not provide a rich programming interface. Isolated storage provides for the key tasks but not always through powerful and easy-to-use tools. If you want to test to make sure a given file exists before you overwrite it you must run a For-Each statement on an array of strings. Here is how:

    Dim filenames As String()
    filenames = iso.GetFileNames("test.txt")
    Dim file As String
    For Each file In filenames
        If file = "test.txt" Then
           Console.WriteLine("Already exists!")
           Exit Sub
        End If
    Next
    
    

    GetFileNames on the IsolatedStorageFile class returns an array of strings filled with all the file names found in the specified pattern. GetFileNames takes an expression that may contain wildcard characters such as ? or *. Wildcards are permitted only in the file name portion of the expression. You cannot use wildcards to indicate a directory path. The returned array contains only the name of items found—that's why you can't perform your search on multiple paths. Similarly, GetDirectoryNames selects all directories in the isolated storage that match the pattern. Both methods require an instance of the Store object (they are not static methods) and don't work recursively on the subfolders of the specified folder.

    DeleteDirectory and DeleteFile are instance methods of the IsolatedStorageFile class that allow you to remove undesired storage directories and files. These Delete methods do not accept wildcards. To remove the whole store you use the Remove method. It is important that you remove unneeded files and directories because the disk reserved for isolated storage is not unlimited. Administrators must tune the quotas of disk storage allocated to each single store. Two read-only long properties—MaximumSize and CurrentSize—let developers foresee in advance whether a given call may fail due to space constraints. Notice that quotas are assigned on a per-evidence basis, which means that the same assembly can receive different quota if run from an intranet rather than from the Internet.

    Conclusion