Thursday, December 20, 2012

WinJS Data Storage and Protection in Windows 8 Store Apps

In my previous post I discussed about how to protect JavaScript source in Windows Store apps. Today I will discuss how to store and secure WinJS app data inside the client machine.

Data Storage and Access


The first question is how and where to save the data. This MSDN documentation is a good reference. There're a lot of options out there but you may end up using one of following methods to store configurations or settings in a Windows 8 machine:

1. WinJS.Application.local.writeText and WinJS.Application.local.readText. Both methods return Promise and they are used to write/read the specified text to/from the specified file. The saved content will be plain text and the content is physically stored in C:\Users\{UserName}\AppData\Local\Packages\{AppID}\LocalState folder. Code example:
    var appLocal = WinJS.Application.local;
    var mySettings = [ {Locale : "EN-CA"}, { Theme : "Classic" }, { LastAccessDate: "2012-12-12" }]; 
    appLocal.writeText("app.config", JSON.stringify(mySettings)).then(function () {
        appLocal.readText("app.config").then(function (data) {
            try {
                var retrievedSettings = JSON.parse(data);
                // do stuff
            }
            catch (ex) {
                // Exception handling 
            }
        });
    });
2. Windows.Storage.ApplicationData.current.localFolder. Same as using "ms-appdata:///Local/" protocol. The data storage location of this localFolder is actually the same as above but the API is different:
    var localFolder = Windows.Storage.ApplicationData.current.localFolder;
    var saveOption = Windows.Storage.CreationCollisionOption;
    var mySettings = {Locale : "EN-CA", Theme : "Classic", LastAccessDate: "2012-12-12"}; 
    localFolder.createFileAsync("myapp.config", saveOption.replaceExisting).then(
        function (file) {
            return Windows.Storage.FileIO.writeTextAsync(file, JSON.stringify(mySettings));
        }).done(function () { 
            localFolder.getFileAsync("myapp.config").then(function (file) {
                return Windows.Storage.FileIO.readTextAsync(file);
            }).done(function (data) {
                var retrievedSettings = JSON.parse(data);
                // do stuff
            });
        });
    }
3. Windows.Storage.ApplicationData.current.localSettings. This localSettings, a key/value pair container, is a bit easier to use because it's not implemented by Prmoise like above two methods. You can set and get values directly in a traditional JavaScript way. The data is stored in C:\Users\{UserName}\AppData\Local\Packages\{AppID}\Settings folder. As name suggested localSettings is ideal for saving settings or small amount of data but not good for big size of content:
    var localSettings = Windows.Storage.ApplicationData.current.localSettings;
    var mySettings = { Locale: "EN-CA", Theme: "Classic", LastAccessDate: "2012-12-12" };
    localSettings.values["myAppSetting"] = JSON.stringify(mySettings); // Assign value
    var data = localSettings.values["myAppSetting"]; // Retrieve value
    if (data) {
        try {
            var retrievedSettings = JSON.parse(data);
            // do stuff
        }
        catch (ex) {
            localSettings.values.remove("myAppSetting");
        }
    }
4. Windows.Storage.ApplicationData.current.roamingSettings/roamingFolder.. The roamingSettings API is pretty much the same as localSettings, and roamingFolder just like localFolder. The difference are:
  • Roaming store would automatically sync the local data to the user's profile in the cloud when the user login as a Microsoft account.
  • Roaming store's physical location is C:\Users\{UserName}\AppData\Local\Packages\{AppID}\RoamingState.
  • Roaming store's URI is "ms-appdata:///Roaming/"
  • Roaming store can only save maximum of 100K data.

Data Encryption and Decryption


In all above methods the saved data are not secure and they can be easily retrieved in other place. So how to protect sensitive data stored in the client machine? Encryption is straightforward answer. Encrypt your data if you don't want to expose them directly to the end user.

The asymmetric encryption is hard and not applicable for distributed single alone application due to the complexity of PKI system. Symmetric encryption such as AES is used in most cases. But client side symmetric encryption is not safe in general because the same encryption key is used in the client machine. The encrypted data can be decrypted in any other machine running the same app, and it's not hard to get the decrypted value. It would be more secure if the encryption key is associated with login user's identity such as SID in Windows machine. In that case the encrypted data can't be decrypted easily in other machine or different user in the same machine.

The problem is that the user SID is not achievable in JavaScript or Runtime component. Fortunately Windows Runtime environment provides Windows.Security.Cryptography library which includes mechanism to encrypt/decrypt data using a key associated with the current user, and the library is accessible from WinJS:
    var localSettings = Windows.Storage.ApplicationData.current.localSettings;
    var cryptography = Windows.Security.Cryptography;
    var cryptoBuffer = cryptography.CryptographicBuffer;
    var cryptoProvider = new cryptography.DataProtection.DataProtectionProvider("LOCAL=user");
            
    var mySettings = { Locale: "EN-CA", Theme: "Classic", LastAccessDate: "2012-12-12" };
    var bufferData = cryptoBuffer.convertStringToBinary(JSON.stringify(mySettings), cryptography.BinaryStringEncoding.utf8);

    cryptoProvider.protectAsync(bufferData).then(function (encryptedData) {
        var dataToBeSaved = cryptoBuffer.encodeToHexString(encryptedData);
        localSettings.values["appSecuredSetting"] = dataToBeSaved;
    }).then(function () {
        var encryptedHexData = localSettings.values["appSecuredSetting"];
        if (encryptedHexData) {
            try {
                var bufferData = cryptoBuffer.decodeFromHexString(encryptedHexData);
                cryptoProvider.unprotectAsync(bufferData).then(
                    function (decryptedBuffer) {
                        var decryptedData =
                            cryptoBuffer.convertBinaryToString(cryptography.BinaryStringEncoding.utf8, decryptedBuffer);
                        var retrievedSettings = JSON.parse(decryptedData);
                        // do stuff
                    },
                    function (err) {
                        // Decryption error handling
                    });
            } catch (ex) {
                // DecodeFromHexString error handling
            }
        }
    });;
The descriptor parameter of "LOCAL=user" passed to DataProtectionProvider is important. I tested other available parameters and this "LOCAL=user" parameter is the only one that would prevent other user from decrypting the data. When data is encrypted with this parameter, they can't be decrypted by any other user in the same machine or other machine. I also tried to decrypt the data in another machine by the same user name but it failed.

The real encryption key used by DataProtectionProvider is invisible to end user or developer. That's great because it makes super hard for attackers to reproduce that key. Conclusion is that by using Windows.Security.Cryptography library and passing "LOCAL=user" to DataProtectionProvider we could secure the data saved to a Windows 8 box.