diff --git a/src/providers/crypto-wrappers.js b/src/providers/crypto-wrappers.js new file mode 100644 index 0000000..fcd5a03 --- /dev/null +++ b/src/providers/crypto-wrappers.js @@ -0,0 +1,125 @@ +define(function(require) { + var FILE_SYSTEM_NAME = require('src/constants').FILE_SYSTEM_NAME; + + // AES encryption, see http://code.google.com/p/crypto-js/#AES + require("crypto-js/rollups/aes"); + // DES, Triple DES, see http://code.google.com/p/crypto-js/#DES,_Triple_DES + require("crypto-js/rollups/tripledes"); + // Rabbit, see http://code.google.com/p/crypto-js/#Rabbi + require("crypto-js/rollups/rabbit"); + + function CryptoWrappedContext(context, encrypt, decrypt) { + this.context = context; + this.encrypt = encrypt; + this.decrypt = decrypt; + } + CryptoWrappedContext.prototype.clear = function(callback) { + this.context.clear(callback); + }; + CryptoWrappedContext.prototype.get = function(key, callback) { + var that = this; + var encryptedKey = this.encrypt(key); + this.context.get(encryptedKey, function(err, value) { + if(err) { + callback(err); + return; + } + if(value) { + value = that.decrypt(value); + } + callback(null, value); + }); + }; + CryptoWrappedContext.prototype.put = function(key, value, callback) { + var encryptedKey = this.encrypt(key); + var encryptedValue = this.encrypt(value); + this.context.put(encryptedKey, encryptedValue, callback); + }; + CryptoWrappedContext.prototype.delete = function(key, callback) { + var encryptedKey = this.encrypt(key); + this.context.delete(encryptedKey, callback); + }; + + // Custom formatting for encryption objects <-> strings + var formatter = { + stringify: function(cipherParams) { + // create json object with ciphertext + var jsonObj = { + ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64) + }; + + // cache iv and salt + if (cipherParams.iv) { + jsonObj.iv = cipherParams.iv.toString(); + } + if (cipherParams.salt) { + jsonObj.s = cipherParams.salt.toString(); + } + + // stringify json object + return JSON.stringify(jsonObj); + }, + + parse: function(jsonString) { + // parse json string + var jsonObj; + try { + jsonObj = JSON.parse(jsonString); + } catch(e) { + throw e; + } + + // extract ciphertext from json object, and create cipher params object + var cipherParams = CryptoJS.lib.CipherParams.create({ + ciphertext: CryptoJS.enc.Base64.parse(jsonObj.ct) + }); + + // extract iv and salt + if (jsonObj.iv) { + cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv); + } + if (jsonObj.s) { + cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.s); + } + + return cipherParams; + } + }; + + function buildCryptoWrapper(encryptionType) { + // It is up to the app using this wrapper how the passphrase is acquired, probably by + // prompting the user to enter it when the file system is being opened. + function CryptoWrappedProvider(passphrase, provider) { + this.provider = provider; + this.encrypt = function(plain) { + console.log('encrypt', plain, CryptoJS[encryptionType].encrypt(plain, passphrase).toString()); + return CryptoJS[encryptionType].encrypt(plain, passphrase, {format: formatter}).toString(); + }; + this.decrypt = function(encrypted) { + console.log('decrypt', encrypted, CryptoJS[encryptionType].decrypt(encrypted, passphrase).toString()); + return CryptoJS[encryptionType].decrypt(encrypted, passphrase, {format: formatter}).toString(); + }; + } + CryptoWrappedProvider.isSupported = function() { + return true; + }; + + CryptoWrappedProvider.prototype.open = function(callback) { + this.provider.open(callback); + }; + CryptoWrappedProvider.prototype.getReadOnlyContext = function() { + return new CryptoWrappedContext(this.provider.getReadOnlyContext(), this.encrypt, this.decrypt); + }; + CryptoWrappedProvider.prototype.getReadWriteContext = function() { + return new CryptoWrappedContext(this.provider.getReadWriteContext(), this.encrypt, this.decrypt); + }; + + return CryptoWrappedProvider; + } + + return { + AESWrapper: buildCryptoWrapper('AES'), + TripleDESWrapper: buildCryptoWrapper('TripleDES'), + RabbitWrapper: buildCryptoWrapper('Rabbit') + }; +}); diff --git a/src/providers/providers.js b/src/providers/providers.js index edc6c53..f73b4a7 100644 --- a/src/providers/providers.js +++ b/src/providers/providers.js @@ -3,12 +3,32 @@ define(function(require) { var IndexedDB = require('src/providers/indexeddb'); var WebSQL = require('src/providers/websql'); var Memory = require('src/providers/memory'); + var CryptoWrappers = require('src/providers/crypto-wrappers'); return { IndexedDB: IndexedDB, WebSQL: WebSQL, Memory: Memory, + + /** + * Wrappers for composing various types of providers + */ + + // Encryption Wrappers + AESWrapper: CryptoWrappers.AESWrapper, + TripleDESWrapper: CryptoWrappers.TripleDESWrapper, + RabbitWrapper: CryptoWrappers.RabbitWrapper, + // Convenience encryption wrapper + EncryptionWrapper: CryptoWrappers.AESWrapper, + + + /** + * Convenience Provider references + */ + + // The default provider to use when none is specified Default: IndexedDB, + // The Fallback provider does automatic fallback checks Fallback: (function() { if(IndexedDB.isSupported()) { diff --git a/tests/spec/providers/providers.crypto.spec.js b/tests/spec/providers/providers.crypto.spec.js new file mode 100644 index 0000000..5df21fd --- /dev/null +++ b/tests/spec/providers/providers.crypto.spec.js @@ -0,0 +1,192 @@ +define(["IDBFS"], function(IDBFS) { + + // We reuse the same set of tests for all crypto providers. + // buildTestsFor() creates a set of tests bound to a crypto + // provider, and uses a Memory() provider internally. + + function buildTestsFor(wrapperName) { + var passphrase = '' + Date.now(); + + function createProvider() { + var memoryProvider = new IDBFS.FileSystem.providers.Memory(); + return new IDBFS.FileSystem.providers[wrapperName](passphrase, memoryProvider); + } + + describe("IDBFS.FileSystem.providers" + wrapperName, function() { + it("is supported -- if it isn't, none of these tests can run.", function() { + expect(IDBFS.FileSystem.providers[wrapperName].isSupported()).toEqual(true); + }); + + it("has open, getReadOnlyContext, and getReadWriteContext instance methods", function() { + var indexedDBProvider = createProvider(); + expect(typeof indexedDBProvider.open).toEqual('function'); + expect(typeof indexedDBProvider.getReadOnlyContext).toEqual('function'); + expect(typeof indexedDBProvider.getReadWriteContext).toEqual('function'); + }); + + describe("open an Memory provider", function() { + it("should open a new Memory database", function() { + var complete = false; + var _error, _result; + + var provider = createProvider(); + provider.open(function(err, firstAccess) { + _error = err; + _result = firstAccess; + complete = true; + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result).toEqual(true); + }); + }); + }); + + describe("Read/Write operations on an Memory provider", function() { + it("should allow put() and get()", function() { + var complete = false; + var _error, _result; + + var provider = createProvider(); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadWriteContext(); + context.put("key", "value", function(err, result) { + _error = _error || err; + context.get("key", function(err, result) { + _error = _error || err; + _result = result; + + complete = true; + }); + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result).toEqual("value"); + }); + }); + + it("should allow delete()", function() { + var complete = false; + var _error, _result; + + var provider = createProvider(); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadWriteContext(); + context.put("key", "value", function(err, result) { + _error = _error || err; + context.delete("key", function(err, result) { + _error = _error || err; + context.get("key", function(err, result) { + _error = _error || err; + _result = result; + + complete = true; + }); + }); + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result).toEqual(null); + }); + }); + + it("should allow clear()", function() { + var complete = false; + var _error, _result1, _result2; + + var provider = createProvider(); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadWriteContext(); + context.put("key1", "value1", function(err, result) { + _error = _error || err; + context.put("key2", "value2", function(err, result) { + _error = _error || err; + + context.clear(function(err) { + _error = _error || err; + + context.get("key1", function(err, result) { + _error = _error || err; + _result1 = result; + + context.get("key2", function(err, result) { + _error = _error || err; + _result2 = result; + + complete = true; + }); + }); + }); + }); + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toEqual(null); + expect(_result1).toEqual(null); + expect(_result2).toEqual(null); + }); + }); + + it("should fail when trying to write on ReadOnlyContext", function() { + var complete = false; + var _error, _result; + + var provider = createProvider(); + provider.open(function(err, firstAccess) { + _error = err; + + var context = provider.getReadOnlyContext(); + context.put("key1", "value1", function(err, result) { + _error = _error || err; + _result = result; + + complete = true; + }); + }); + + waitsFor(function() { + return complete; + }, 'test to complete', DEFAULT_TIMEOUT); + + runs(function() { + expect(_error).toBeDefined(); + expect(_result).toEqual(null); + }); + }); + }); + }); + } + + buildTestsFor('AESWrapper'); + buildTestsFor('TripleDESWrapper'); + buildTestsFor('RabbitWrapper'); + +}); diff --git a/tests/spec/providers/providers.spec.js b/tests/spec/providers/providers.spec.js index fbdde86..507e8c1 100644 --- a/tests/spec/providers/providers.spec.js +++ b/tests/spec/providers/providers.spec.js @@ -16,6 +16,18 @@ define(["IDBFS"], function(IDBFS) { expect(typeof IDBFS.FileSystem.providers.Memory).toEqual('function'); }); + it("has an AESWrapper constructor", function() { + expect(typeof IDBFS.FileSystem.providers.AESWrapper).toEqual('function'); + }); + + it("has a TripleDESWrapper constructor", function() { + expect(typeof IDBFS.FileSystem.providers.TripleDESWrapper).toEqual('function'); + }); + + it("has a RabbitWrapper constructor", function() { + expect(typeof IDBFS.FileSystem.providers.RabbitWrapper).toEqual('function'); + }); + it("has a Default constructor", function() { expect(typeof IDBFS.FileSystem.providers.Default).toEqual('function'); }); diff --git a/tests/test-manifest.js b/tests/test-manifest.js index 47e9d84..e91b842 100644 --- a/tests/test-manifest.js +++ b/tests/test-manifest.js @@ -33,6 +33,7 @@ define([ "spec/providers/providers.memory.spec", "spec/providers/providers.indexeddb.spec", "spec/providers/providers.websql.spec", + "spec/providers/providers.crypto.spec", // Ported node.js tests (filenames match names in https://github.com/joyent/node/tree/master/test) "spec/node-js/simple/test-fs-mkdir",