// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.storage.EncryptedStorageTest');
goog.setTestOnly('goog.storage.EncryptedStorageTest');

goog.require('goog.json');
goog.require('goog.storage.EncryptedStorage');
goog.require('goog.storage.ErrorCode');
goog.require('goog.storage.RichStorage');
goog.require('goog.storage.collectableStorageTester');
goog.require('goog.storage.storageTester');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PseudoRandom');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.storage.FakeMechanism');

function getEncryptedWrapper(storage, key) {
  return JSON.parse(storage.mechanism.get(storage.hashKeyWithSecret_(key)));
}

function getEncryptedData(storage, key) {
  return getEncryptedWrapper(storage, key)[goog.storage.RichStorage.DATA_KEY];
}

function decryptWrapper(storage, key, wrapper) {
  return JSON.parse(storage.decryptValue_(
      wrapper[goog.storage.EncryptedStorage.SALT_KEY], key,
      wrapper[goog.storage.RichStorage.DATA_KEY]));
}

function hammingDistance(a, b) {
  if (a.length != b.length) {
    throw new Error('Lengths must be the same for Hamming distance');
  }
  var distance = 0;
  for (var i = 0; i < a.length; ++i) {
    if (a.charAt(i) != b.charAt(i)) {
      ++distance;
    }
  }
  return distance;
}


function testBasicOperations() {
  var mechanism = new goog.testing.storage.FakeMechanism();
  var storage = new goog.storage.EncryptedStorage(mechanism, 'secret');
  goog.storage.storageTester.runBasicTests(storage);
}


function testExpiredKeyCollection() {
  var mechanism = new goog.testing.storage.FakeMechanism();
  var clock = new goog.testing.MockClock(true);
  var storage = new goog.storage.EncryptedStorage(mechanism, 'secret');

  goog.storage.collectableStorageTester.runBasicTests(
      mechanism, clock, storage);
}


function testEncryption() {
  var mechanism = new goog.testing.storage.FakeMechanism();
  var clock = new goog.testing.MockClock(true);
  var storage = new goog.storage.EncryptedStorage(mechanism, 'secret');
  var mallory = new goog.storage.EncryptedStorage(mechanism, 'guess');

  // Simple Objects.
  storage.set('first', 'Hello world!');
  storage.set('second', ['one', 'two', 'three'], 1000);
  storage.set('third', {'a': 97, 'b': 98});

  // Wrong secret can't find keys.
  assertNull(mechanism.get('first'));
  assertNull(mechanism.get('second'));
  assertNull(mechanism.get('third'));
  assertUndefined(mallory.get('first'));
  assertUndefined(mallory.get('second'));
  assertUndefined(mallory.get('third'));

  // Wrong secret can't overwrite keys.
  mallory.set('first', 'Ho ho ho!');
  assertObjectEquals('Ho ho ho!', mallory.get('first'));
  assertObjectEquals('Hello world!', storage.get('first'));
  mallory.remove('first');

  // Correct key decrypts properly.
  assertObjectEquals('Hello world!', storage.get('first'));
  assertObjectEquals(['one', 'two', 'three'], storage.get('second'));
  assertObjectEquals({'a': 97, 'b': 98}, storage.get('third'));

  // Wrong secret can't decode values even if the key is revealed.
  var encryptedWrapper = getEncryptedWrapper(storage, 'first');
  assertObjectEquals(
      'Hello world!', decryptWrapper(storage, 'first', encryptedWrapper));
  assertThrows(function() {
    decryptWrapper(mallory, 'first', encryptedWrapper);
  });

  // If the value is overwritten, it can't be decrypted.
  encryptedWrapper[goog.storage.RichStorage.DATA_KEY] = 'kaboom';
  mechanism.set(
      storage.hashKeyWithSecret_('first'),
      goog.json.serialize(encryptedWrapper));
  assertEquals(
      goog.storage.ErrorCode.DECRYPTION_ERROR, assertThrows(function() {
        storage.get('first');
      }));

  // Test garbage collection.
  storage.collect();
  assertNotNull(getEncryptedWrapper(storage, 'first'));
  assertObjectEquals(['one', 'two', 'three'], storage.get('second'));
  assertObjectEquals({'a': 97, 'b': 98}, storage.get('third'));
  clock.tick(2000);
  storage.collect();
  assertNotNull(getEncryptedWrapper(storage, 'first'));
  assertUndefined(storage.get('second'));
  assertObjectEquals({'a': 97, 'b': 98}, storage.get('third'));
  mechanism.set(storage.hashKeyWithSecret_('first'), '"kaboom"');
  storage.collect();
  assertNotNull(getEncryptedWrapper(storage, 'first'));
  assertObjectEquals({'a': 97, 'b': 98}, storage.get('third'));
  storage.collect(true);
  assertUndefined(storage.get('first'));
  assertObjectEquals({'a': 97, 'b': 98}, storage.get('third'));

  // Clean up.
  storage.remove('third');
  assertUndefined(storage.get('third'));
  clock.uninstall();
}

function testSalting() {
  var mechanism = new goog.testing.storage.FakeMechanism();
  var randomMock = new goog.testing.PseudoRandom(0, true);
  var storage = new goog.storage.EncryptedStorage(mechanism, 'secret');

  // Same value under two different keys should appear very different,
  // even with the same salt.
  storage.set('one', 'Hello world!');
  randomMock.seed(0);  // Reset the generator so we get the same salt.
  storage.set('two', 'Hello world!');
  var golden = getEncryptedData(storage, 'one');
  assertRoughlyEquals(
      'Ciphertext did not change with keys', golden.length,
      hammingDistance(golden, getEncryptedData(storage, 'two')), 2);

  // Same key-value pair written second time should appear very different.
  storage.set('one', 'Hello world!');
  assertRoughlyEquals(
      'Salting seems to have failed', golden.length,
      hammingDistance(golden, getEncryptedData(storage, 'one')), 2);

  // Clean up.
  storage.remove('1');
  storage.remove('2');
  randomMock.uninstall();
}
