Programming .Net Security
Isolated storage is simple to use and requires knowledge of relatively few classes. The most important class is System.IO.IsolatedStorage.IsolatedStorageFile. IsolatedStorageFile objects represent individual stores and provide methods to manage the files and directories contained within the stores, as well as properties to access information, such as the store's isolation scope, current size, and maximum size. The IsolatedStorageFile class also provides static factory methods through which you initially obtain store objects. Also important is the System.IO.IsolatedStorage.IsolatedStorageFileStream class, which extends System.IO.FileStream and provides the mechanism through which you read and write data to files in isolated storage. In the following sections, we demonstrate how to use these classes to implement the use of isolated storage in your programs. First, we look at the code-access permissions used to control access to isolated storage. 11.2.1 Isolated Storage and Code-Access Security
The System.Security.Permissions.IsolatedStorageFilePermission code-access permission class controls access to isolated storage. Unlike the System.Security.Permissions.FileIOPermission class that controls direct access to the hard drive, the IsolatedStorageFilePermission class does not control the type of access (read or write) to specific directories or even specific stores. IsolatedStorageFilePermission controls access to isolated storage by restricting the scope of isolation at which code can obtain a store. Permission to obtain a store of a specified scope implies the ability for code to read and write data in the store. The IsolatedStorageFilePermission class also controls the maximum size that code can make a store.
When configuring IsolatedStorageFilePermission instances, the System.Security.Permissions.IsolatedStorageContainment enumeration represents the different isolation scopes available, as well as some levels of access that transcend the individual isolation scopes. Table 11-1 lists the members of the IsolatedStorageContainment enumeration and describes the access they represent.
Despite the relative safety of isolated storage, .NET's default security policy (discussed in Chapter 9) restricts the isolation scope of stores some code can obtain. In particular, code run from the Internet can obtain only a store isolated by user, assembly, and application domain. This minimizes the chance that applications downloaded from the Internet can share data between each other. If you intend to use isolated storage in your code, it is prudent to use a permission request, which we discussed in Chapter 7, to ensure the runtime grants your code the isolation scope it requires. The following code demonstrates the statements that request permission to create stores of some sample isolation scopes: # C# // Minimum permission request to obtain a non-roaming store isolated by // user, assembly, and application domain [assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, UsageAllowed = IsolatedStorageContainment.DomainIsolationByUser)] // Minimum permission request to obtain a roaming store isolated by // user and assembly with a quota of 2048 bytes [assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, UsageAllowed = IsolatedStorageContainment.AssemblyIsolationByRoamingUser, UserQuota = 2048)] // Minimum permission request for unrestricted access to isolated storage [assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, Unrestricted = true)] # Visual Basic .NET ' Minimum permission request to obtain a non-roaming store isolated by ' user, assembly, and application domain <Assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, _ UsageAllowed := IsolatedStorageContainment.DomainIsolationByUser)> ' Minimum permission request to obtain a roaming store isolated by ' user and assembly with a quota of 2048 bytes <Assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, _ UsageAllowed:=IsolatedStorageContainment.AssemblyIsolationByRoamingUser, _ UserQuota := 2048)> ' Minimum permission request for unrestricted access to isolated storage <Assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, _ Unrestricted := true)>
11.2.2 Obtaining a Store
Two static methods of the IsolatedStorageFile class provide the simplest and most common way of obtaining a store:
Both methods take no arguments and return an IsolatedStorageFile object representing the store with the appropriate identity for the current user and code. If a store with the necessary identity does not already exist, isolated storage creates a new one. The following statements demonstrate the use of both methods: # C# // Obtain a store isolated by user and assembly IsolateStorageFile iso1 = IsolatedStorageFile.GetUserStoreForAssembly( ); // Obtain a store isolated by user, assembly, and application domain IsolateStorageFile iso2 = IsolatedStorageFile.GetUserStoreForDomain( ); # Visual Basic .NET ' Obtain a store isolated by user and assembly Dim iso1 As IsolateStorageFile =_ IsolatedStorageFile.GetUserStoreForAssembly( ) ' Obtain a store isolated by user, assembly, and application domain Dim iso2 As IsolateStorageFile = _ IsolatedStorageFile.GetUserStoreForDomain( ) The static IsolatedStorageFile.GetStore method also obtains a store, but allows the caller to specify the scope and identity of the store to obtain. GetStore has three overloads that provide different ways to specify the identity of the store. We show the most commonly used signature here; refer to the .NET Framework documentation for a discussion of all available overloads: # C# public static IsolatedStorageFile GetStore { IsolatedStorageScope scope, object domainIdentity, object assemblyIdentity ); # Visual Basic .NET Overloads Public Shared Function GetStore( _ ByVal scope As IsolatedStorageScope, _ ByVal domainIdentity As Object, _ ByVal assemblyIdentity As Object _ ) As IsolatedStorageFile The domainIdentity and assemblyIdentity arguments represent the evidence objects for isolated storage to use when determining the identity of the store to obtain. Specifying null (C#) or Nothing (Visual Basic .NET) for both arguments means isolated storage will use its default approach to determine code identity, which we discussed earlier in Section 11.1.1.1. The scope argument takes a value of the System.IO.IsolatedStorage.IsolatedStorageScope enumeration that identifies the desired isolation scope for the store. The IsolatedStorageScope enumeration contains the following five values:
By themselves, these values serve no purpose; only by combining them using a logical Or operation do they have any meaning to the GetStore method. Table 11-2 lists the valid combination of values. The | symbol is the C# equivalent of the Visual Basic .NET Keyword Or.
GetStore is the only way to obtain stores backed by Windows-roaming profiles. If you use GetUserStoreForAssembly, GetUserStoreForDomain, or do not specify the Roaming flag when calling GetStore, the store will exist only on the machine on which it was created and will not be accessible to the user when she uses other machines. Because the GetStore method allows you to override the default evidence used to identify the store to obtain, you can obtain another application's store if you specify the correct evidence values a relatively easy task. This breaks the normal code-level isolation provided by isolated storage; therefore, the calling code must have the more highly trusted AdministerIsolatedStorageByUser element of the IsolatedStorageFilePermission to call GetStore with evidence arguments that are not null (C#) or Nothing (Visual Basic .NET). The following statements demonstrate the use of the GetStore method, including method calls that are equivalent to calling the GetUserStoreForAssembly and GetUserStoreForDomain methods: # C# // GetStore equivalent of GetUserStoreForAssembly IsolatedStorageFile iso1 = IsolatedStorageFile.GetStore( IsolatedStorageScope.User | IsolatedStorageScope. Assembly, null, null ); // GetStore equivalent of GetUserStoreForDomain IsolatedStorageFile iso2 = IsolatedStorageFile.GetStore( IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null ); // Get a roaming store isolated by user and assembly. Specify // new Url evidence for the assembly identity IsolatedStorageFile iso3 = IsolatedStorageFile.GetStore( IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Roaming, null, new Url(@"http://test.com") ); # Visual Basic .NET ' GetStore equivalent of GetUserStoreForAssembly Dim iso1 As IsolatedStorageFile = IsolatedStorageFile.GetStore( _ IsolatedStorageScope.User Or IsolatedStorageScope.Assembly, _ Nothing, Nothing) ' GetStore equivalent of GetUserStoreForDomain Dim iso2 As IsolatedStorageFile = IsolatedStorageFile.GetStore( _ IsolatedStorageScope.User Or IsolatedStorageScope.Assembly Or _ IsolatedStorageScope.Domain, Nothing, Nothing) ' Get a roaming store isolated by user and assembly. Specify ' new Url evidence for the assembly identity Dim iso3 As IsolatedStorageFile = IsolatedStorageFile.GetStore( _ IsolatedStorageScope.User Or IsolatedStorageScope.Assembly Or _ IsolatedStorageScope.Roaming, Nothing, New Url("http://test.com")) The final way to obtain a store is to enumerate all stores for the current user. This breaks the data isolation provided by isolated storage completely and requires the caller to have the AdministerIsolatedStorageByUser element of the IsolatedStorageFilePermission permission. The IsolatedStorageFile.GetEnumerator method has the following signature: # C# public static IEnumerator GetEnumerator( IsolatedStorageScope scope ); # Visual Basic .NET Public Shared Function GetEnumerator( _ ByVal scope As IsolatedStorageScope _ ) As IEnumerator The scope argument takes a value of the IsolatedStorageScope enumeration; Table 11-3 lists the valid member combinations.
You can read data from IsolatedStorageFile objects obtained through the GetEnumerator method, but you cannot write to them. Methods that write data to stores ensure that no write operation increases the size of the store beyond that specified by the IsolatedStorageFilePermission permission granted to the executing code. When code obtains a store through the GetStore, GetUserStoreForAssembly, or GetUserStoreForDomain methods, the code's permissions determine the maximum size of the store. This figure is stored in the IsolatedStorageFile.MaximumSize property. However, code that executes GetEnumerator has no defined quota and so no maximum size is set. The lack of a maximum size value does not, however, result in an unrestricted store quote. Any attempt by code that obtains a store using GetEnumerator to read or modify the MazimumSize property will cause a System.InvalidOperationException to be thrown. Example 11-1 demonstrates the use of the GetEnumerator method to obtain all nonroaming stores for the current user. We then step through the stores and display the stores' isolation scope, current size, and the evidence of the code that created the stores using the Scope, CurrentSize, AssemblyIdentity, and DomainIdentity properties: Example 11-1. Enumerating and displaying the characteristics of isolated stores
# C# using System; using System.Collections; using System.IO.IsolatedStorage; using System.Security.Permissions; // Ensure we have permission to enumerate stores [assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, UsageAllowed = IsolatedStorageContainment.AdministerIsolatedStorageByUser)] public class EnumerateStores { public static void Main( ) { // Obtain an IEnumerator across all non roaming stores for the // current user IEnumerator e = IsolatedStorageFile.GetEnumerator(IsolatedStorageScope.User); // Iterate through the enumerator while (e.MoveNext( )) { IsolatedStorageFile i = (IsolatedStorageFile)e.Current; // Display the scope of the current store Console.WriteLine("STORE SCOPE: {0}", i.Scope); // Display the maximum and current size for the store Console.WriteLine("Current size={0}", i.CurrentSize); // Display the assembly identity of the current store Console.WriteLine("Assembly ID:"); Console.WriteLine(i.AssemblyIdentity); // Display the domain identity of the current store if // the store is isolated by domain if ((i.Scope & IsolatedStorageScope.Domain) != 0) { Console.WriteLine("Domain ID:"); Console.WriteLine(i.DomainIdentity); } } } } # Visual Basic .NET Imports System Imports System.Collections Imports System.IO.IsolatedStorage Imports System.Security.Permissions ' Ensure we have permission to enumerate stores <assembly:IsolatedStorageFilePermission(SecurityAction.RequestMinimum, _ UsageAllowed := IsolatedStorageContainment.AdministerIsolatedStorageByUser _ )> _ Public Class EnumerateStores Public Shared Sub Main( ) ' Obtain an IEnumerator across all non roaming stores for the ' current user Dim e As IEnumerator = _ IsolatedStorageFile.GetEnumerator(IsolatedStorageScope.User) ' Iterate through the enumerator While e.MoveNext( ) Dim i As IsolatedStorageFile = _ CType(e.Current, IsolatedStorageFile) ' Display the scope of the current store Console.WriteLine("STORE SCOPE: {0}", i.Scope) ' Display the maximum and current size for the store Console.WriteLine("Current size={0}", i.CurrentSize) ' Display the assembly identity of the current store Console.WriteLine("Assembly ID:") Console.WriteLine(i.AssemblyIdentity) ' Display the domain identity of the current store if ' the store is isolated by domain If (i.Scope And IsolatedStorageScope.Domain) <> 0 Then Console.WriteLine("Domain ID:") Console.WriteLine(i.DomainIdentity) End If End While End Sub End Class 11.2.3 Creating Directories
Once you have an IsolatedStorageFile object representing a store, you can create files and directories in the store. .NET's isolated storage implementation does not provide an object that represents directories; this simplifies the isolated storage class structure but also forces you to constantly work with strings that represent directory paths. The CreateDirectory method, with the following signature, creates a directory with the specified fully qualified name: # C# public void CreateDirectory( string dir ); # Visual Basic .NET Public Sub CreateDirectory( _ ByVal dir As String _ ) You can create more than one level of the directory hierarchy in a single call. For example, the following statements create a directory named Dir1, as well as two subdirectories named Dir2 and Dir3 in the store represented by the iso object: # C# IsolatedStorageFile iso = IsolatedStorageFile.GetUserStoreForDomain( ); iso.CreateDirectory(@"Dir1\Dir2"); iso.CreateDirectory("Dir1/Dir3"); # Visual Basic .NET Dim iso As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForDomain( ) iso.CreateDirectory("Dir1\Dir2") iso.CreateDirectory("Dir1/Dir3") On the first call, CreateDirectory creates both the Dir1 and Dir2 directories. If you specify a directory name that already exists, as is the case with Dir1 in the second call to CreateDirectory, no error occurs. If you specify a directory name that contains illegal characters, CreateDirectory throws an IsolatedStorageException. As this code shows, you can use either the forward slash / or backward slash \ characters as the path separators. 11.2.4 Creating, Reading, and Writing Files
The IsolatedStorageFileStream class represents a file in a store. IsolatedStorageFileStream extends System.IO.FileStream and apart from its Handle property, supports all of the normal file I/O capabilities that you are familiar with, including asynchronous I/O. If you use the Handle property, it throws an IsolatedStorageException. You do not create an IsolatedStorageFileStream through an IsolatedStorageFile object as you may expect; instead, you pass the IsolatedStorageFile instance in which you want to create or open the file to one of the IsolatedStorageFileStream constructors. The IsolatedStorageFileStream class provides eight overloaded constructors. We show the most specific IsolatedStorageFileStream constructor signature here; the other seven variations take subsets of the arguments required by this version and provide defaults for the arguments not specified: # C# public IsolatedStorageFileStream( string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, IsolatedStorageFile isf ); # Visual Basic .NET Public Sub New( _ ByVal path As String, _ ByVal mode As FileMode, _ ByVal access As FileAccess, _ ByVal share As FileShare, _ ByVal bufferSize As Integer, _ ByVal isf As IsolatedStorageFile _ ) All of the arguments to the IsolatedStorageFileStream constructor serve the same purpose as those to the FileStream constructor, and we do not cover them in detail here. Of special interest are the first and last arguments: path and isf. The path argument is a String that contains the name of the file to create and must include the full path (within the store) of the file. If you create a file in a directory, the directory must exist before you create the file. The isf argument is a reference to the IsolatedStorageFile in which to create or open the file.
After creating or opening a file in a store, you can use the IsolatedStorageFileStream object as you would with any other FileStream object. Example 11-2 demonstrates two methods that demonstrate the simplicity with which you can read and write XML data to isolated storage a common approach when storing user and application data. In WriteConfig, we demonstrate the creation of an XML file named Config.xml, to which we write the contents of a System.Xml.XmlDocument. In the ReadConfig method, read the Config.xml file and return a new XmlDocument created from the files contents. Consult the .NET Framework SDK documentation for details of the XmlDocument class. Example 11-2. Reading and writing XML data to isolated storage
# C# public static void WriteConfig(IsolatedStorageFile store, XmlDocument xml) { //Ensure there is a config directory store.CreateDirectory("/config"); // Create or Open the Config.xml file IsolatedStorageFileStream str = new IsolatedStorageFileStream( "/config/Config.xml", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 100, store ); // Write the XmlDocument to the isolated storage file xml.Save(str); // Close the Config.xml file str.Close( ); } public static XmlDocument ReadConfig(IsolatedStorageFile store) { // Open the Config.xml file IsolatedStorageFileStream str = new IsolatedStorageFileStream( "config/Config.xml", FileMode.Open, FileAccess.Read, FileShare.None, 100, store ); // Create a new XmlDocument and load the config file XmlDocument xml = new XmlDocument( ); xml.Load(str); // Close the Config.xml file str.Close( ); // return the XmlDocument return xml; } # Visual Basic .NET Public Shared Sub WriteConfig(ByVal store As IsolatedStorageFile, _ ByVal xml As XmlDocument) 'Ensure there is a config directory store.CreateDirectory("/config") ' Create or Open the Config.xml file Dim str As IsolatedStorageFileStream = New IsolatedStorageFileStream( _ "/config/Config.xml",FileMode.OpenOrCreate,FileAccess.Write, _ FileShare.None,100,store) ' Write the XmlDocument to the isolated storage file xml.Save(str) ' Close the Config.xml file str.Close( ) End Sub Public Shared Function ReadConfig(ByVal store As IsolatedStorageFile) _ As XmlDocument ' Open the Config.xml file Dim str As IsolatedStorageFileStream = New IsolatedStorageFileStream( _ "config/Config.xml",FileMode.Open,FileAccess.Read,FileShare.None,100, _ store) ' Create a new XmlDocument and load the config file Dim xml As XmlDocument = New XmlDocument( ) xml.Load(str) ' Close the Config.xml file str.Close( ) ' return the XmlDocument Return xml End Function 11.2.5 Searching for Files and Directories
Isolated storage does not provide any mechanism through which you can work directly with objects that represent the individual directories within a store, but does provide search capabilities that simplify the discovery of files and directories. The GetDirectoryNames and GetFileNames methods both return a String array containing a list of files or directories contained in a store with names that match a specified pattern. Both methods take a single String argument containing the pattern for which to search. The GetDirectoryNames and GetFileNames methods have the following signatures: # C# public string[] GetDirectoryNames( string searchPattern ); public string[] GetFileNames( string searchPattern ); # Visual Basic .NET Public Function GetDirectoryNames( _ ByVal searchPattern As String _ ) As String( ) Public Function GetFileNames( _ ByVal searchPattern As String _ ) As String( ) The searchPattern argument must specify the full path of the directory in which you want to search and can include both single-character ("?") and multicharacter ("*") wildcards, but only in the final element. Table 11-4 lists some example values of searchPattern for both the GetDirectoryNames and GetFileNames methods and describes their effects.
We demonstrate the use of both the GetDirectoryNames and GetFileNames methods in the following section. 11.2.6 Deleting Files and Directories
You delete files and directories from a store through the methods of the IsolatedStorageFile object that represents the containing store. Both the DeleteDirectory and DeleteFile methods take a single String argument that specifies the fully qualified name of the file to delete: # C# public void DeleteDirectory( string dir ); public void DeleteFile( string file ); # Visual Basic .NET Public Sub DeleteDirectory( _ ByVal dir As String _ ) Public Sub DeleteFile( _ ByVal file As String _ ) If you try to delete a directory that does not exist or you include wildcard characters in the directory name, DeleteDirectory throws an IsolatedStorageFile exception. Attempting to delete a file that does not exist also results in an IsolatedStorageFile exception, but including wildcard characters in the filename will cause a System.Argument exception. The major difficulty in using DeleteDirectory is that the directory must be empty of files and subdirectories before you can delete it. Because you cannot specify wildcard characters to delete groups of files and directories, you have to implement the program logic necessary to delete all of the files in the directory. Example 11-3 contains a static method named DeleteDirectory, which demonstrates the techniques necessary to delete an isolated storage directory tree. DeleteDirectory takes two arguments: an IsolatedStorageFile object that represents the store containing the directory to delete, and a System.String, which specifies the fully qualified name of the directory to delete for example, "/config/gary": Example 11-3. Deleting an isolated storage directory tree
# C# public static void DeleteDirectory(IsolatedStorageFile store, String root) { // Ensure we have a directory name ending in a "/" character String dir = root.EndsWith("/") ? root : root + "/"; // Get a list of all files in the current directory, iterate // through them and delete them. foreach (String file in store.GetFileNames(dir + "*")) { store.DeleteFile(file); } // Get a list of all sub-directories in the current directory, // iterate through them and delete them. foreach (String subdir in store.GetDirectoryNames(dir + "*")) { DeleteDirectory(store, dir + subdir + "/"); } // Delete the current directory store.DeleteDirectory(dir); } # Visual Basic .NET Public Shared Sub DeleteDirectory(ByVal store As IsolatedStorageFile, _ ByVal root As String) ' Ensure we have a directory name ending in a "/" character Dim dir As String = IIf(root.EndsWith("/"), root, root & "/") ' Get a list of all files in the current directory, iterate ' through them and delete them. Dim file As String For Each file In store.GetFileNames(dir + "*") store.DeleteFile(file) Next ' Get a list of all sub-directories in the current directory, ' iterate through them and delete them. Dim subdir As String For Each subdir In store.GetDirectoryNames(dir + "*") DeleteDirectory(store, dir + subdir + "/") Next ' Delete the current directory store.DeleteDirectory(dir) End Sub 11.2.7 Deleting Stores
To delete a store, use the Remove method of the IsolatedStorageFile class. Remove has two overloads, with the signatures shown here: # C# public override void Remove( ); public static void Remove( IsolatedStorageScope scope ); # Visual Basic .NET Overrides Overloads Public Sub Remove( ) Overloads Public Shared Sub Remove( _ ByVal scope As IsolatedStorageScope _ ) The first overload is an instance method that takes no arguments and simply deletes the store represented by the current IsolatedStorageFile object. If you try to use the IsolatedStorageFile object after calling Remove, a System.InvalidOperation exception is thrown because the store is considered to be closed. The second Remove overload is a static method that takes an IsolatedStorageScope argument, which represents the isolation scope of the stores to delete. Table 11-5 lists the valid combination of IsolatedStorageScope values you can use for the scope argument.
|