/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const TEST_URL = "https://example.com/";

let expectNotSupportedError = expectError("NotSupported");
let expectInvalidStateError = expectError("InvalidState");
let expectSecurityError = expectError("Security");

function promiseU2FRegister(tab, app_id_) {
  let challenge_ = crypto.getRandomValues(new Uint8Array(16));
  challenge_ = bytesToBase64UrlSafe(challenge_);

  return SpecialPowers.spawn(
    tab.linkedBrowser,
    [[app_id_, challenge_]],
    function([app_id, challenge]) {
      return new Promise(resolve => {
        content.u2f.register(
          app_id,
          [{ version: "U2F_V2", challenge }],
          [],
          resolve
        );
      });
    }
  ).then(res => {
    is(res.errorCode, 0, "u2f.register() succeeded");
    let data = base64ToBytesUrlSafe(res.registrationData);
    is(data[0], 0x05, "Reserved byte is correct");
    return data.slice(67, 67 + data[66]);
  });
}

add_task(function test_setup() {
  Services.prefs.setBoolPref("security.webauth.u2f", true);
  Services.prefs.setBoolPref("security.webauth.webauthn", true);
  Services.prefs.setBoolPref(
    "security.webauth.webauthn_enable_android_fido2",
    false
  );
  Services.prefs.setBoolPref(
    "security.webauth.webauthn_enable_softtoken",
    true
  );
  Services.prefs.setBoolPref(
    "security.webauth.webauthn_enable_usbtoken",
    false
  );
});

add_task(async function test_appid() {
  // Open a new tab.
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);

  // Get a keyHandle for a FIDO AppId.
  let appid = "https://example.com/appId";
  let keyHandle = await promiseU2FRegister(tab, appid);

  // The FIDO AppId extension can't be used for MakeCredential.
  await promiseWebAuthnMakeCredential(tab, "none", { appid })
    .then(arrivingHereIsBad)
    .catch(expectNotSupportedError);

  // Using the keyHandle shouldn't work without the FIDO AppId extension.
  // This will be an invalid state, because the softtoken will consent without
  // having the correct "RP ID" via the FIDO extension.
  await promiseWebAuthnGetAssertion(tab, keyHandle)
    .then(arrivingHereIsBad)
    .catch(expectInvalidStateError);

  // Invalid app IDs (for the current origin) must be rejected.
  await promiseWebAuthnGetAssertion(tab, keyHandle, {
    appid: "https://bogus.com/appId",
  })
    .then(arrivingHereIsBad)
    .catch(expectSecurityError);

  // Non-matching app IDs must be rejected. Even when the user/softtoken
  // consents, leading to an invalid state.
  await promiseWebAuthnGetAssertion(tab, keyHandle, { appid: appid + "2" })
    .then(arrivingHereIsBad)
    .catch(expectInvalidStateError);

  let rpId = new TextEncoder("utf-8").encode(appid);
  let rpIdHash = await crypto.subtle.digest("SHA-256", rpId);

  // Succeed with the right fallback rpId.
  await promiseWebAuthnGetAssertion(tab, keyHandle, { appid }).then(
    ({ authenticatorData, clientDataJSON, extensions }) => {
      is(extensions.appid, true, "appid extension was acted upon");

      // Check that the correct rpIdHash is returned.
      let rpIdHashSign = authenticatorData.slice(0, 32);
      ok(memcmp(rpIdHash, rpIdHashSign), "rpIdHash is correct");

      let clientData = JSON.parse(buffer2string(clientDataJSON));
      is(clientData.clientExtensions.appid, appid, "appid extension sent");
    }
  );

  // Close tab.
  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_appid_unused() {
  // Open a new tab.
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);

  // Get a keyHandle for a FIDO AppId.
  let appid = "https://example.com/appId";

  let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab);
  let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj);

  // Make sure the RP ID hash matches what we calculate.
  await checkRpIdHash(authDataObj.rpIdHash, "example.com");

  // Get a new assertion.
  let {
    clientDataJSON,
    authenticatorData,
    signature,
    extensions,
  } = await promiseWebAuthnGetAssertion(tab, rawId, { appid });

  // Check the we can parse clientDataJSON.
  let clientData = JSON.parse(buffer2string(clientDataJSON));
  ok(
    "appid" in clientData.clientExtensions,
    `since it was passed, appid field should appear in the client data, but ` +
      `saw: ${JSON.stringify(clientData.clientExtensions)}`
  );

  ok(
    "appid" in extensions,
    `appid should be populated in the extensions data, but saw: ` +
      `${JSON.stringify(extensions)}`
  );
  is(extensions.appid, false, "appid extension should indicate it was unused");

  // Check auth data.
  let attestation = await webAuthnDecodeAuthDataArray(
    new Uint8Array(authenticatorData)
  );
  is(
    "" + attestation.flags,
    "" + flag_TUP,
    "Assertion's user presence byte set correctly"
  );

  // Verify the signature.
  let params = await deriveAppAndChallengeParam(
    "example.com",
    clientDataJSON,
    attestation
  );
  let signedData = await assembleSignedData(
    params.appParam,
    params.attestation.flags,
    params.attestation.counter,
    params.challengeParam
  );
  let valid = await verifySignature(
    authDataObj.publicKeyHandle,
    signedData,
    signature
  );
  ok(valid, "signature is valid");

  // Close tab.
  BrowserTestUtils.removeTab(tab);
});

add_task(function test_cleanup() {
  Services.prefs.clearUserPref("security.webauth.u2f");
  Services.prefs.clearUserPref("security.webauth.webauthn");
  Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
  Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
});
