// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";

// Tests various aspects of the nsISecretDecoderRing implementation.

do_get_profile();

let gSetPasswordShownCount = 0;

// Mock implementation of nsITokenPasswordDialogs.
const gTokenPasswordDialogs = {
  setPassword(ctx, tokenName) {
    gSetPasswordShownCount++;
    info(`setPassword() called; shown ${gSetPasswordShownCount} times`);
    info(`tokenName: ${tokenName}`);
    return false; // Returning false means "the user didn't cancel".
  },

  QueryInterface: ChromeUtils.generateQI(["nsITokenPasswordDialogs"]),
};

let gMockPrompter = {
  promptPassword(dialogTitle, text, password, checkMsg, checkValue) {
    // Returning false simulates the user canceling the password prompt.
    return false;
  },

  QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
};

// Mock nsIWindowWatcher. PSM calls getNewPrompter on this to get an nsIPrompt
// to call promptPassword. We return the mock one, above.
let gWindowWatcher = {
  getNewPrompter: () => gMockPrompter,
  QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]),
};

add_task(function setup() {
  let windowWatcherCID = MockRegistrar.register(
    "@mozilla.org/embedcomp/window-watcher;1",
    gWindowWatcher
  );
  registerCleanupFunction(() => {
    MockRegistrar.unregister(windowWatcherCID);
  });
});

add_task(function testEncryptString() {
  let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
    Ci.nsISecretDecoderRing
  );

  // Test valid inputs for encryptString() and decryptString().
  let inputs = [
    "",
    " ", // First printable latin1 character (code point 32).
    "foo",
    "1234567890`~!@#$%^&*()-_=+{[}]|\\:;'\",<.>/?",
    "¡äöüÿ", // Misc + last printable latin1 character (code point 255).
    "aaa 一二三", // Includes Unicode with code points outside [0, 255].
  ];
  for (let input of inputs) {
    let converter = Cc[
      "@mozilla.org/intl/scriptableunicodeconverter"
    ].createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    let convertedInput = converter.ConvertFromUnicode(input);
    convertedInput += converter.Finish();

    let encrypted = sdr.encryptString(convertedInput);

    notEqual(
      convertedInput,
      encrypted,
      "Encrypted input should not just be the input itself"
    );

    try {
      atob(encrypted);
    } catch (e) {
      ok(false, `encryptString() should have returned Base64: ${e}`);
    }

    equal(
      convertedInput,
      sdr.decryptString(encrypted),
      "decryptString(encryptString(input)) should return input"
    );
  }

  // Test invalid inputs for decryptString().
  throws(
    () => sdr.decryptString("*"),
    /NS_ERROR_ILLEGAL_VALUE/,
    "decryptString() should throw if given non-Base64 input"
  );

  // Test calling changePassword() pops up the appropriate dialog.
  // Note: On Android, nsITokenPasswordDialogs is apparently not implemented,
  //       which also seems to prevent us from mocking out the interface.
  if (AppConstants.platform != "android") {
    let tokenPasswordDialogsCID = MockRegistrar.register(
      "@mozilla.org/nsTokenPasswordDialogs;1",
      gTokenPasswordDialogs
    );
    registerCleanupFunction(() => {
      MockRegistrar.unregister(tokenPasswordDialogsCID);
    });

    equal(
      gSetPasswordShownCount,
      0,
      "changePassword() dialog should have been shown zero times"
    );
    sdr.changePassword();
    equal(
      gSetPasswordShownCount,
      1,
      "changePassword() dialog should have been shown exactly once"
    );
  }
});

add_task(async function testAsyncEncryptStrings() {
  let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
    Ci.nsISecretDecoderRing
  );

  // Test valid inputs for encryptString() and decryptString().
  let inputs = [
    "",
    " ", // First printable latin1 character (code point 32).
    "foo",
    "1234567890`~!@#$%^&*()-_=+{[}]|\\:;'\",<.>/?",
    "¡äöüÿ", // Misc + last printable latin1 character (code point 255).
    "aaa 一二三", // Includes Unicode with code points outside [0, 255].
  ];

  let encrypteds = await sdr.asyncEncryptStrings(inputs);
  for (let i = 0; i < inputs.length; i++) {
    let encrypted = encrypteds[i];
    let input = inputs[i];
    let converter = Cc[
      "@mozilla.org/intl/scriptableunicodeconverter"
    ].createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    let convertedInput = converter.ConvertFromUnicode(input);
    convertedInput += converter.Finish();
    notEqual(
      convertedInput,
      encrypted,
      "Encrypted input should not just be the input itself"
    );

    try {
      atob(encrypted);
    } catch (e) {
      ok(false, `encryptString() should have returned Base64: ${e}`);
    }

    equal(
      convertedInput,
      sdr.decryptString(encrypted),
      "decryptString(encryptString(input)) should return input"
    );
  }
});

add_task(async function testAsyncDecryptStrings() {
  let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
    Ci.nsISecretDecoderRing
  );

  // Test valid inputs for encryptString() and decryptString().
  let testCases = [
    "",
    " ", // First printable latin1 character (code point 32).
    "foo",
    "1234567890`~!@#$%^&*()-_=+{[}]|\\:;'\",<.>/?",
    "¡äöüÿ", // Misc + last printable latin1 character (code point 255).
    "aaa 一二三", // Includes Unicode with code points outside [0, 255].
  ];

  let convertedTestCases = testCases.map(tc => {
    let converter = Cc[
      "@mozilla.org/intl/scriptableunicodeconverter"
    ].createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    let convertedInput = converter.ConvertFromUnicode(tc);
    convertedInput += converter.Finish();
    return convertedInput;
  });

  let encryptedStrings = convertedTestCases.map(tc => sdr.encryptString(tc));
  let decrypteds = await sdr.asyncDecryptStrings(encryptedStrings);
  for (let i = 0; i < encryptedStrings.length; i++) {
    let decrypted = decrypteds[i];

    equal(
      decrypted,
      testCases[i],
      "decrypted string should match expected value"
    );
    equal(
      sdr.decryptString(encryptedStrings[i]),
      convertedTestCases[i],
      "decryptString(encryptString(input)) should return the initial decrypted string value"
    );
  }
});

add_task(async function testAsyncDecryptInvalidStrings() {
  let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
    Ci.nsISecretDecoderRing
  );

  // Test invalid inputs for sdr.asyncDecryptStrings
  let testCases = [
    "~bmV0cGxheQ==", // invalid base64 encoding
    "bmV0cGxheQ==", // valid base64 characters but not encrypted
    "https://www.example.com", // website address from erroneous migration
  ];

  let decrypteds = await sdr.asyncDecryptStrings(testCases);
  equal(
    decrypteds.length,
    testCases.length,
    "each testcase should still return a response"
  );
  for (let i = 0; i < decrypteds.length; i++) {
    let decrypted = decrypteds[i];

    equal(
      decrypted,
      "",
      "decrypted string should be empty when trying to decrypt an invalid input with asyncDecryptStrings"
    );

    Assert.throws(
      () => sdr.decryptString(testCases[i]),
      /NS_ERROR_ILLEGAL_VALUE|NS_ERROR_FAILURE/,
      `Check testcase would have thrown: ${testCases[i]}`
    );
  }
});

add_task(async function testAsyncDecryptLoggedOut() {
  // Set a master password.
  let token = Cc["@mozilla.org/security/pk11tokendb;1"]
    .getService(Ci.nsIPK11TokenDB)
    .getInternalKeyToken();
  token.initPassword("password");
  token.logoutSimple();

  let sdr = Cc["@mozilla.org/security/sdr;1"].getService(
    Ci.nsISecretDecoderRing
  );

  await Assert.rejects(
    sdr.asyncDecryptStrings(["irrelevant"]),
    /NS_ERROR_NOT_AVAILABLE/,
    "Check error is thrown instead of returning empty strings"
  );

  token.reset();
  token.initPassword("");
});
