diff options
Diffstat (limited to '')
13 files changed, 2803 insertions, 0 deletions
diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.css b/toolkit/components/aboutwebauthn/content/aboutWebauthn.css new file mode 100644 index 0000000000..5654e4276b --- /dev/null +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.css @@ -0,0 +1,99 @@ +/* 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/. */ + +@import url("chrome://global/skin/in-content/common.css"); + +html { + height: 100%; +} + +body { + display: flex; + align-items: stretch; + height: 100%; +} + +label { + display: block; +} + +#info-text-div { + padding: 20px; +} + +#ctap-listen-div { + padding-top: 15px; +} +#ctap-listen-result { + font-weight: 600; + font-size: 1.5em; + padding-inline-start: 15px; + height: 1.5em; + + &.success { + background-color: var(--green-50); + } + + &.error { + background-color: var(--red-50); + } +} + +.category { + cursor: pointer; + /* Center category names */ + display: flex; + align-items: center; +} + +.optional-category { + display: none; +} + +.disabled-category { + pointer-events: none; +} + +@media (max-width: 830px){ + #categories > .category { + padding-inline-start: 5px; + margin-inline-start: 0; + } +} + +#main-content { + flex: 1; +} + +.token-info-flex-box { + display: flex; +} + +.token-info-flex-child { + flex: 1; +} + +.token-info-flex-child#authenticator-options { + margin-inline-end: 2px; +} + +.bio-enrollment-sample { + display: flex; + gap: 0.5em; +} + +.button-row { + display: inline-block; + margin-inline-end: 5px; +} + +.delete-icon { + margin-inline-end: 5px; + -moz-context-properties: fill; + fill: currentColor; +} + +.delete-button { + display: inline-flex; +} diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.html b/toolkit/components/aboutwebauthn/content/aboutWebauthn.html new file mode 100644 index 0000000000..5c8485b61f --- /dev/null +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.html @@ -0,0 +1,322 @@ +<!DOCTYPE html> + +<!-- 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/. --> + +<html> + <head> + <meta + http-equiv="Content-Security-Policy" + content="default-src chrome:; style-src chrome:; object-src 'none'" + /> + <meta charset="utf-8" /> + <title id="page-title" data-l10n-id="about-webauthn-page-title"></title> + <link rel="stylesheet" href="chrome://global/content/aboutWebauthn.css" /> + <script src="chrome://global/content/aboutWebauthn.js"></script> + <link rel="localization" href="toolkit/about/aboutWebauthn.ftl" /> + </head> + + <body id="body"> + <div id="categories" role="tablist" aria-labelledby="page-title"> + <div + id="info-tab-button" + class="category" + selected="true" + role="tab" + tabindex="0" + aria-selected="true" + aria-controls="token-info-section" + > + <span + class="tablinks" + data-l10n-id="about-webauthn-info-section-title" + ></span> + </div> + <div + id="pin-tab-button" + class="category optional-category" + role="tab" + tabindex="-1" + aria-selected="false" + aria-controls="set-change-pin-section" + > + <span + class="tablinks" + data-l10n-id="about-webauthn-pin-section-title" + ></span> + </div> + <div + id="credentials-tab-button" + class="category optional-category" + role="tab" + tabindex="-1" + aria-selected="false" + aria-controls="credential-management-section" + > + <span + class="tablinks" + data-l10n-id="about-webauthn-credential-management-section-title" + ></span> + </div> + <div + id="bio-enrollments-tab-button" + class="category optional-category" + role="tab" + tabindex="-1" + aria-selected="false" + aria-controls="bio-enrollment-section" + > + <span + class="tablinks" + data-l10n-id="about-webauthn-bio-enrollment-section-title" + ></span> + </div> + </div> + + <div id="main-content"> + <div id="ctap-listen-div"> + <label id="ctap-listen-result"></label> + </div> + + <div + class="tabcontent token-info-section" + id="token-info-section" + role="tabpanel" + aria-labelledby="info-tab-button" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-info-section-title" + ></h2> + + <div id="info-text-div"> + <label + id="info-text-field" + data-l10n-id="about-webauthn-text-connect-device" + ></label> + </div> + + <div id="ctap2-token-info" class="token-info-flex-box" display="none"> + <div id="ctap2-token-info-options" class="token-info-flex-child"> + <h3 data-l10n-id="about-webauthn-options-subsection-title"></h3> + <table id="authenticator-options"></table> + </div> + <div id="ctap2-token-info-info" class="token-info-flex-child"> + <h3 data-l10n-id="about-webauthn-info-subsection-title"></h3> + <table id="authenticator-info"></table> + </div> + </div> + </div> + + <div + hidden + class="tabcontent pin-section" + id="set-change-pin-section" + role="tabpanel" + aria-labelledby="pin-tab-button" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-pin-section-title" + ></h2> + <div id="new-pin-div"> + <label + for="new-pin" + data-l10n-id="about-webauthn-new-pin-label" + ></label> + <input type="password" id="new-pin" name="new-pin" required /> + <label + for="new-pin-repeat" + data-l10n-id="about-webauthn-repeat-pin-label" + ></label> + <input + type="password" + id="new-pin-repeat" + name="new-pin-repeat" + required + /> + </div> + <div id="current-pin-div"> + <label + for="current-pin" + data-l10n-id="about-webauthn-current-pin-label" + ></label> + <input type="password" id="current-pin" name="current-pin" required /> + </div> + <button + disabled + id="set-pin-button" + data-l10n-id="about-webauthn-current-set-pin-button" + ></button> + <button + disabled + id="change-pin-button" + data-l10n-id="about-webauthn-current-change-pin-button" + ></button> + <label id="set-change-pin-result" class="ctap-result"></label> + </div> + + <div + hidden + class="tabcontent credential-management-section" + id="credential-management-section" + role="tabpanel" + aria-labelledby="credentials-tab-button" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-credential-management-section-title" + ></h2> + <div + hidden + id="credential-list-subsection" + class="token-info-flex-child" + > + <h3 + data-l10n-id="about-webauthn-credential-list-subsection-title" + ></h3> + <div hidden id="credential-list-empty-label"> + <label + hidden + data-l10n-id="about-webauthn-credential-list-empty" + ></label> + </div> + <table id="credential-list"></table> + </div> + <button + class="credentials-button" + id="list-credentials-button" + data-l10n-id="about-webauthn-list-credentials-button" + ></button> + </div> + + <div + hidden + class="tabcontent bio-enrollment-section" + id="bio-enrollment-section" + role="tabpanel" + aria-labelledby="bio-enrollments-tab-button" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-bio-enrollment-section-title" + ></h2> + <div + hidden + id="bio-enrollment-list-subsection" + class="token-info-flex-child" + > + <h3 + data-l10n-id="about-webauthn-bio-enrollment-list-subsection-title" + ></h3> + <div hidden id="bio-enrollment-list-empty-label"> + <label + hidden + data-l10n-id="about-webauthn-enrollment-list-empty" + ></label> + </div> + <table id="bio-enrollment-list"></table> + </div> + <button + class="bio-enrollment-button button-row" + id="list-bio-enrollments-button" + data-l10n-id="about-webauthn-list-bio-enrollments-button" + ></button> + <button + class="bio-enrollment-button button-row" + id="add-bio-enrollment-button" + data-l10n-id="about-webauthn-add-bio-enrollment-button" + ></button> + </div> + <div + hidden + class="tabcontent add-bio-enrollment-section" + id="add-bio-enrollment-section" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-add-bio-enrollment-section-title" + ></h2> + <label + for="enrollment-name" + data-l10n-id="about-webauthn-enrollment-name-label" + ></label> + <input id="enrollment-name" name="enrollment-name" autofocus /> + <button + class="bio-enrollment-button button-row" + id="start-enrollment-button" + data-l10n-id="about-webauthn-start-enrollment-button" + ></button> + <button + id="cancel-enrollment-button" + class="button-row" + data-l10n-id="about-webauthn-cancel-button" + ></button> + <div id="enrollment-update"></div> + </div> + + <div + hidden + class="tabcontent pin-required-section" + id="pin-required-section" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-pin-required-section-title" + ></h2> + <div id="pin-div"> + <label + for="pin-required" + data-l10n-id="about-webauthn-pin-required-label" + ></label> + <input + type="password" + id="pin-required" + name="pin-required" + required + autofocus + /> + </div> + <button + id="send-pin-button" + class="button-row" + data-l10n-id="about-webauthn-send-pin-button" + ></button> + <button + id="cancel-send-pin-button" + class="button-row" + data-l10n-id="about-webauthn-cancel-button" + ></button> + </div> + + <div + hidden + class="tabcontent confirm-deletion-section" + id="confirm-deletion-section" + > + <h2 + class="categoryTitle" + data-l10n-id="about-webauthn-confirm-deletion-section-title" + ></h2> + <div id="confirmation-div"> + <label + for="confirmation-context" + data-l10n-id="about-webauthn-confirm-deletion-label" + ></label> + <label id="confirmation-context"></label> + </div> + <button + id="confirm-deletion-button" + class="button-row" + data-l10n-id="about-webauthn-delete-button" + ></button> + <button + id="cancel-confirmation-button" + class="button-row" + data-l10n-id="about-webauthn-cancel-button" + ></button> + </div> + </div> + </body> +</html> diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.js b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js new file mode 100644 index 0000000000..9142ed198a --- /dev/null +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js @@ -0,0 +1,866 @@ +/* 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"; + +let AboutWebauthnService = null; + +var AboutWebauthnManagerJS = { + _topic: "about-webauthn-prompt", + _initialized: false, + _l10n: null, + _bio_l10n: null, + _curr_data: null, + _current_tab: "", + _previous_tab: "", + + init() { + if (this._initialized) { + return; + } + this._l10n = new Localization(["toolkit/about/aboutWebauthn.ftl"], true); + this._bio_l10n = new Map(); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpGood", + "about-webauthn-ctap2-enroll-feedback-good" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooHigh", + "about-webauthn-ctap2-enroll-feedback-too-high" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooLow", + "about-webauthn-ctap2-enroll-feedback-too-low" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooLeft", + "about-webauthn-ctap2-enroll-feedback-too-left" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooRight", + "about-webauthn-ctap2-enroll-feedback-too-right" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooFast", + "about-webauthn-ctap2-enroll-feedback-too-fast" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooSlow", + "about-webauthn-ctap2-enroll-feedback-too-slow" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpPoorQuality", + "about-webauthn-ctap2-enroll-feedback-poor-quality" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooSkewed", + "about-webauthn-ctap2-enroll-feedback-too-skewed" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpTooShort", + "about-webauthn-ctap2-enroll-feedback-too-short" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpMergeFailure", + "about-webauthn-ctap2-enroll-feedback-merge-failure" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackFpExists", + "about-webauthn-ctap2-enroll-feedback-exists" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackNoUserActivity", + "about-webauthn-ctap2-enroll-feedback-no-user-activity" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackNoUserPresenceTransition", + "about-webauthn-ctap2-enroll-feedback-no-user-presence-transition" + ); + this._bio_l10n.set( + "Ctap2EnrollFeedbackOther", + "about-webauthn-ctap2-enroll-feedback-other" + ); + + Services.obs.addObserver(this, this._topic); + this._initialized = true; + reset_page(); + }, + + uninit() { + Services.obs.removeObserver(this, this._topic); + this._initialized = false; + this._l10n = null; + this._current_tab = ""; + this._previous_tab = ""; + }, + + observe(aSubject, aTopic, aData) { + let data = JSON.parse(aData); + + // We have token + if (data.type == "selected-device") { + this._curr_data = data.auth_info; + this.show_ui_based_on_authenticator_info(data); + fake_click_event_for_id("info-tab-button"); + } else if (data.type == "select-device") { + set_info_text("about-webauthn-text-select-device"); + } else if (data.type == "pin-required") { + open_pin_required_tab(); + } else if (data.type == "pin-invalid") { + let retries = data.retries ? data.retries : 0; + show_results_banner( + "error", + "about-webauthn-results-pin-invalid-error", + JSON.stringify({ retriesLeft: retries }) + ); + open_pin_required_tab(); + } else if (data.type == "bio-enrollment-update") { + if (data.result.EnrollmentList) { + show_results_banner("success", "about-webauthn-results-success"); + this.show_enrollment_list(data.result.EnrollmentList); + bio_enrollment_in_progress(false); + } else if (data.result.DeleteSuccess || data.result.AddSuccess) { + show_results_banner("success", "about-webauthn-results-success"); + clear_bio_enrollment_samples(); + // Update AuthenticatorInfo + this._curr_data = data.result.DeleteSuccess ?? data.result.AddSuccess; + fake_click_event_for_id("bio-enrollments-tab-button"); + bio_enrollment_in_progress(false); + // If we still have some enrollments to show, update the list + // otherwise, remove it. + if ( + this._curr_data.options.bioEnroll === true || + this._curr_data.options.userVerificationMgmtPreview === true + ) { + list_bio_enrollments(); + } else { + // Hide the list, because it's empty + document.getElementById( + "bio-enrollment-list-subsection" + ).hidden = true; + } + } else if (data.result.UpdateSuccess) { + fake_click_event_for_id("bio-enrollments-tab-button"); + list_bio_enrollments(); + } else if (data.result.SampleStatus) { + show_add_bio_enrollment_section(); + let up = document.getElementById("enrollment-update"); + let sample_update = document.createElement("div"); + let new_line = document.createElement("label"); + new_line.classList.add("sample"); + new_line.setAttribute( + "data-l10n-id", + this._bio_l10n.get(data.result.SampleStatus[0]) + ); + sample_update.appendChild(new_line); + let samples_needed_line = document.createElement("label"); + samples_needed_line.setAttribute( + "data-l10n-id", + "about-webauthn-samples-still-needed" + ); + samples_needed_line.setAttribute( + "data-l10n-args", + JSON.stringify({ repeatCount: data.result.SampleStatus[1] }) + ); + sample_update.classList.add("bio-enrollment-sample"); + sample_update.appendChild(samples_needed_line); + up.appendChild(sample_update); + } + } else if (data.type == "credential-management-update") { + credential_management_in_progress(false); + if (data.result.CredentialList) { + show_results_banner("success", "about-webauthn-results-success"); + this.show_credential_list(data.result.CredentialList.credential_list); + } else { + // DeleteSuccess or UpdateSuccess + show_results_banner("success", "about-webauthn-results-success"); + list_credentials(); + } + } else if (data.type == "listen-success") { + reset_page(); + // Show results + show_results_banner("success", "about-webauthn-results-success"); + this._reset_in_progress = ""; + AboutWebauthnService.listen(); + } else if (data.type == "listen-error") { + reset_page(); + + if (!data.error) { + show_results_banner("error", "about-webauthn-results-general-error"); + } else if (data.error.type == "pin-auth-blocked") { + show_results_banner( + "error", + "about-webauthn-results-pin-auth-blocked-error" + ); + } else if (data.error.type == "pin-not-set") { + show_results_banner( + "error", + "about-webauthn-results-pin-not-set-error" + ); + } else if (data.error.type == "device-blocked") { + show_results_banner( + "error", + "about-webauthn-results-pin-blocked-error" + ); + } else if (data.error.type == "pin-is-too-short") { + show_results_banner( + "error", + "about-webauthn-results-pin-too-short-error" + ); + } else if (data.error.type == "pin-is-too-long") { + show_results_banner( + "error", + "about-webauthn-results-pin-too-long-error" + ); + } else if (data.error.type == "pin-invalid") { + let retries = data.error.retries + ? JSON.stringify({ retriesLeft: data.error.retries }) + : null; + show_results_banner( + "error", + "about-webauthn-results-pin-invalid-error", + retries + ); + } else if (data.error.type == "cancel") { + show_results_banner( + "error", + "about-webauthn-results-cancelled-by-user-error" + ); + } else { + show_results_banner("error", "about-webauthn-results-general-error"); + } + AboutWebauthnService.listen(); + } + }, + + show_authenticator_options(options, element, l10n_base) { + let table = document.getElementById(element); + var empty_table = document.createElement("table"); + empty_table.id = element; + table.parentNode.replaceChild(empty_table, table); + table = document.getElementById(element); + for (let key in options) { + if (key == "options") { + continue; + } + // Create an empty <tr> element and add it to the 1st position of the table: + var row = table.insertRow(0); + + // Insert new cells (<td> elements) at the 1st and 2nd position of the "new" <tr> element: + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + + // Add some text to the new cells: + let key_text = this._l10n.formatValueSync( + l10n_base + "-" + key.toLowerCase().replace(/_/g, "-") + ); + var key_node = document.createTextNode(key_text); + cell1.appendChild(key_node); + var raw_value = JSON.stringify(options[key]); + var value = raw_value; + if (["true", "false", "null"].includes(raw_value)) { + value = this._l10n.formatValueSync(l10n_base + "-" + raw_value); + } + var value_node = document.createTextNode(value); + cell2.appendChild(value_node); + } + }, + + show_ui_based_on_authenticator_info(data) { + // Hide the "Please plug in a token"-message + document.getElementById("info-text-div").hidden = true; + // Show options, based on what the token supports + if (data.auth_info) { + document.getElementById("ctap2-token-info").style.display = "flex"; + this.show_authenticator_options( + data.auth_info.options, + "authenticator-options", + "about-webauthn-auth-option" + ); + this.show_authenticator_options( + data.auth_info, + "authenticator-info", + "about-webauthn-auth-info" + ); + // Check if token supports PINs + if (data.auth_info.options.clientPin != null) { + document.getElementById("pin-tab-button").style.display = "flex"; + if (data.auth_info.options.clientPin === true) { + // It has a Pin set + document.getElementById("change-pin-button").style.display = "block"; + document.getElementById("set-pin-button").style.display = "none"; + document.getElementById("current-pin-div").hidden = false; + } else { + // It does not have a Pin set yet + document.getElementById("change-pin-button").style.display = "none"; + document.getElementById("set-pin-button").style.display = "block"; + document.getElementById("current-pin-div").hidden = true; + } + } else { + document.getElementById("pin-tab-button").style.display = "none"; + } + + if ( + data.auth_info.options.credMgmt || + data.auth_info.options.credentialMgmtPreview + ) { + document.getElementById("credentials-tab-button").style.display = + "flex"; + } else { + document.getElementById("credentials-tab-button").style.display = + "none"; + } + + if ( + data.auth_info.options.bioEnroll != null || + data.auth_info.options.userVerificationMgmtPreview != null + ) { + document.getElementById("bio-enrollments-tab-button").style.display = + "flex"; + } else { + document.getElementById("bio-enrollments-tab-button").style.display = + "none"; + } + } else { + // Currently auth-rs doesn't send this, because it filters out ctap2-devices. + // U2F / CTAP1 tokens can't be managed + set_info_text("about-webauthn-text-non-ctap2-device"); + } + }, + + show_credential_list(credential_list) { + // We may have temporarily hidden the tab when asking the user for a PIN + // so we have to show it again. + fake_click_event_for_id("credentials-tab-button"); + document.getElementById("credential-list-subsection").hidden = false; + let table = document.getElementById("credential-list"); + var empty_table = document.createElement("table"); + empty_table.id = "credential-list"; + table.parentNode.replaceChild(empty_table, table); + if (!credential_list.length) { + document.getElementById("credential-list-empty-label").hidden = false; + return; + } + document.getElementById("credential-list-empty-label").hidden = true; + table = document.getElementById("credential-list"); + credential_list.forEach(rp => { + // Add some text to the new cells: + let key_text = rp.rp.id; + rp.credentials.forEach(cred => { + let value_text = cred.user.name; + // Create an empty <tr> element and add it to the 1st position of the table: + var row = table.insertRow(0); + var key_node = document.createTextNode(key_text); + var value_node = document.createTextNode(value_text); + row.insertCell(0).appendChild(key_node); + row.insertCell(1).appendChild(value_node); + var delete_button = document.createElement("button"); + delete_button.classList.add("delete-button"); + delete_button.classList.add("credentials-button"); + let garbage_icon = document.createElement("img"); + garbage_icon.setAttribute( + "src", + "chrome://global/skin/icons/delete.svg" + ); + garbage_icon.classList.add("delete-icon"); + delete_button.appendChild(garbage_icon); + let delete_text = document.createElement("span"); + delete_text.setAttribute( + "data-l10n-id", + "about-webauthn-delete-button" + ); + delete_button.appendChild(delete_text); + delete_button.addEventListener("click", function () { + let context = document.getElementById("confirmation-context"); + context.textContent = key_text + " - " + value_text; + credential_management_in_progress(true); + let cmd = { + CredentialManagement: { DeleteCredential: cred.credential_id }, + }; + context.setAttribute("data-ctap-command", JSON.stringify(cmd)); + open_delete_confirmation_tab(); + }); + row.insertCell(2).appendChild(delete_button); + }); + }); + }, + + show_enrollment_list(enrollment_list) { + // We may have temporarily hidden the tab when asking the user for a PIN + // so we have to show it again. + fake_click_event_for_id("bio-enrollments-tab-button"); + document.getElementById("bio-enrollment-list-subsection").hidden = false; + let table = document.getElementById("bio-enrollment-list"); + var empty_table = document.createElement("table"); + empty_table.id = "bio-enrollment-list"; + table.parentNode.replaceChild(empty_table, table); + if (!enrollment_list.length) { + document.getElementById("bio-enrollment-list-empty-label").hidden = false; + return; + } + document.getElementById("bio-enrollment-list-empty-label").hidden = true; + table = document.getElementById("bio-enrollment-list"); + enrollment_list.forEach(enrollment => { + let key_text = enrollment.template_friendly_name ?? "<unnamed>"; + var row = table.insertRow(0); + var key_node = document.createTextNode(key_text); + row.insertCell(0).appendChild(key_node); + var delete_button = document.createElement("button"); + delete_button.classList.add("delete-button"); + delete_button.classList.add("bio-enrollment-button"); + let garbage_icon = document.createElement("img"); + garbage_icon.setAttribute("src", "chrome://global/skin/icons/delete.svg"); + garbage_icon.classList.add("delete-icon"); + delete_button.appendChild(garbage_icon); + let delete_text = document.createElement("span"); + delete_text.setAttribute("data-l10n-id", "about-webauthn-delete-button"); + delete_button.appendChild(delete_text); + delete_button.addEventListener("click", function () { + let context = document.getElementById("confirmation-context"); + context.textContent = key_text; + bio_enrollment_in_progress(true); + let cmd = { + BioEnrollment: { DeleteEnrollment: enrollment.template_id }, + }; + context.setAttribute("data-ctap-command", JSON.stringify(cmd)); + open_delete_confirmation_tab(); + }); + row.insertCell(1).appendChild(delete_button); + }); + }, +}; + +function set_info_text(l10nId) { + document.getElementById("info-text-div").hidden = false; + let field = document.getElementById("info-text-field"); + field.setAttribute("data-l10n-id", l10nId); + document.getElementById("ctap2-token-info").style.display = "none"; +} + +function show_results_banner(result, l10n, l10n_args) { + let ctap_result = document.getElementById("ctap-listen-result"); + ctap_result.setAttribute("data-l10n-id", l10n); + ctap_result.classList.add(result); + if (l10n_args) { + ctap_result.setAttribute("data-l10n-args", l10n_args); + } +} + +function hide_results_banner() { + let res_banner = document.getElementById("ctap-listen-result"); + let res_div = document.getElementById("ctap-listen-div"); + let empty_banner = document.createElement("label"); + empty_banner.id = "ctap-listen-result"; + res_div.replaceChild(empty_banner, res_banner); +} + +function operation_in_progress(name, in_progress) { + let buttons = Array.from(document.getElementsByClassName(name)); + buttons.forEach(button => { + button.disabled = in_progress; + }); +} + +function credential_management_in_progress(in_progress) { + operation_in_progress("credentials-button", in_progress); +} + +function bio_enrollment_in_progress(in_progress) { + operation_in_progress("bio-enrollment-button", in_progress); +} + +function clear_bio_enrollment_samples() { + // Remove all previous status updates + let up = document.getElementById("enrollment-update"); + while (up.firstChild) { + up.removeChild(up.lastChild); + } + document.getElementById("enrollment-name").value = ""; +} + +function fake_click_event_for_id(id) { + // Not using document.getElementById(id).click(); + // here, because we have to add additional data, so we don't + // hide the results-div here, if there is any. 'Normal' clicking + // by the user will hide it. + const evt = new CustomEvent("click", { + detail: { skip_results_clearing: true }, + }); + document.getElementById(id).dispatchEvent(evt); +} + +function reset_page() { + // Hide everything that needs a device to know if it should be displayed + document.getElementById("ctap2-token-info").style.display = "none"; + Array.from(document.getElementsByClassName("optional-category")).forEach( + div => { + div.style.display = "none"; + } + ); + + // Only display the "please connect a device" - text + set_info_text("about-webauthn-text-connect-device"); + + // Clear results and input fields + hide_results_banner(); + var divs = Array.from(document.getElementsByTagName("input")); + divs.forEach(div => { + div.value = ""; + }); + + sidebar_set_disabled(false); + + // ListCredentials + credential_management_in_progress(false); + document.getElementById("credential-list-subsection").hidden = true; + + // BioEnrollment + clear_bio_enrollment_samples(); + document.getElementById("bio-enrollment-list-subsection").hidden = true; + bio_enrollment_in_progress(false); + + AboutWebauthnManagerJS._previous_tab = ""; + AboutWebauthnManagerJS._current_tab = ""; + + // Not using `document.getElementById("info-tab-button").click();` + // here, because if we were focused on a category-button that got removed (e.g. + // when unplugging the device), we have to reset the ARIA-related attributes + // first, before we can click on the button, otherwise the a11y-tests + // will complain. So we fake the click again. + const evt = { + detail: {}, + currentTarget: document.getElementById("info-tab-button"), + }; + open_info_tab(evt); +} + +function sidebar_set_disabled(disabled) { + var cats = Array.from(document.getElementsByClassName("category")); + cats.forEach(cat => { + if (disabled) { + cat.classList.add("disabled-category"); + } else { + cat.classList.remove("disabled-category"); + } + }); +} + +function check_pin_repeat_is_correct(button) { + let pin = document.getElementById("new-pin"); + let pin_repeat = document.getElementById("new-pin-repeat"); + let has_current_pin = !document.getElementById("current-pin-div").hidden; + let current_pin = document.getElementById("current-pin"); + let can_enable_button = + pin.value != null && pin.value != "" && pin.value == pin_repeat.value; + if (has_current_pin && !current_pin.value) { + can_enable_button = false; + } + if (!can_enable_button) { + pin.classList.add("different"); + pin_repeat.classList.add("different"); + document.getElementById("set-pin-button").disabled = true; + document.getElementById("change-pin-button").disabled = true; + return false; + } + pin.classList.remove("different"); + pin_repeat.classList.remove("different"); + document.getElementById("set-pin-button").disabled = false; + document.getElementById("change-pin-button").disabled = false; + return true; +} + +function send_pin() { + close_temporary_overlay_tab(); + let pin = document.getElementById("pin-required").value; + AboutWebauthnService.pinCallback(0, pin); +} + +function set_pin() { + let pin = document.getElementById("new-pin").value; + let cmd = { SetPIN: pin }; + AboutWebauthnService.runCommand(JSON.stringify(cmd)); +} + +function change_pin() { + let curr_pin = document.getElementById("current-pin").value; + let new_pin = document.getElementById("new-pin").value; + let cmd = { ChangePIN: [curr_pin, new_pin] }; + AboutWebauthnService.runCommand(JSON.stringify(cmd)); +} + +function list_credentials() { + credential_management_in_progress(true); + let cmd = { CredentialManagement: "GetCredentials" }; + AboutWebauthnService.runCommand(JSON.stringify(cmd)); +} + +function list_bio_enrollments() { + bio_enrollment_in_progress(true); + let cmd = { BioEnrollment: "GetEnrollments" }; + AboutWebauthnService.runCommand(JSON.stringify(cmd)); +} + +function show_add_bio_enrollment_section() { + const evt = new CustomEvent("click", { + detail: { temporary_overlay: true }, + }); + open_tab(evt, "add-bio-enrollment-section"); + document.getElementById("enrollment-name").focus(); +} + +function start_bio_enrollment() { + bio_enrollment_in_progress(true); + let name = document.getElementById("enrollment-name").value; + if (!name) { + name = null; // Empty means "Not set" + } + let cmd = { BioEnrollment: { StartNewEnrollment: name } }; + AboutWebauthnService.runCommand(JSON.stringify(cmd)); +} + +function cancel_transaction() { + credential_management_in_progress(false); + bio_enrollment_in_progress(false); + AboutWebauthnService.cancel(0); +} + +function confirm_deletion() { + let context = document.getElementById("confirmation-context"); + let cmd = context.getAttribute("data-ctap-command"); + AboutWebauthnService.runCommand(cmd); +} + +function cancel_confirmation() { + credential_management_in_progress(false); + bio_enrollment_in_progress(false); + close_temporary_overlay_tab(); +} + +async function onLoad() { + document.getElementById("set-pin-button").addEventListener("click", set_pin); + document + .getElementById("change-pin-button") + .addEventListener("click", change_pin); + document + .getElementById("list-credentials-button") + .addEventListener("click", list_credentials); + document + .getElementById("list-bio-enrollments-button") + .addEventListener("click", list_bio_enrollments); + document + .getElementById("add-bio-enrollment-button") + .addEventListener("click", show_add_bio_enrollment_section); + document + .getElementById("start-enrollment-button") + .addEventListener("click", start_bio_enrollment); + document + .getElementById("new-pin") + .addEventListener("input", check_pin_repeat_is_correct); + document + .getElementById("new-pin-repeat") + .addEventListener("input", check_pin_repeat_is_correct); + document + .getElementById("current-pin") + .addEventListener("input", check_pin_repeat_is_correct); + let info_button = document.getElementById("info-tab-button"); + info_button.addEventListener("click", open_info_tab); + info_button.addEventListener("keydown", handle_keydowns); + let pin_button = document.getElementById("pin-tab-button"); + pin_button.addEventListener("click", open_pin_tab); + pin_button.addEventListener("keydown", handle_keydowns); + let credentials_button = document.getElementById("credentials-tab-button"); + credentials_button.addEventListener("click", open_credentials_tab); + credentials_button.addEventListener("keydown", handle_keydowns); + let bio_enrollments_button = document.getElementById( + "bio-enrollments-tab-button" + ); + bio_enrollments_button.addEventListener("click", open_bio_enrollments_tab); + bio_enrollments_button.addEventListener("keydown", handle_keydowns); + document + .getElementById("send-pin-button") + .addEventListener("click", send_pin); + document + .getElementById("cancel-send-pin-button") + .addEventListener("click", cancel_transaction); + document + .getElementById("cancel-enrollment-button") + .addEventListener("click", cancel_transaction); + document + .getElementById("cancel-confirmation-button") + .addEventListener("click", cancel_confirmation); + document + .getElementById("confirm-deletion-button") + .addEventListener("click", confirm_deletion); + AboutWebauthnManagerJS.init(); + try { + AboutWebauthnService.listen(); + } catch (ex) { + set_info_text("about-webauthn-text-not-available"); + AboutWebauthnManagerJS.uninit(); + } +} + +function handle_keydowns(event) { + let index; + let event_was_handled = true; + let tabs = Array.from(document.getElementsByClassName("category")); + if (tabs.length <= 0) { + return; + } + + switch (event.key) { + case "ArrowLeft": + case "ArrowUp": + if (event.currentTarget === tabs[0]) { + event.currentTarget.focus(); + } else { + index = tabs.indexOf(event.currentTarget); + tabs[index - 1].focus(); + } + break; + + case "ArrowRight": + case "ArrowDown": + if (event.currentTarget === tabs[tabs.length - 1]) { + event.currentTarget.focus(); + } else { + index = tabs.indexOf(event.currentTarget); + tabs[index + 1].focus(); + } + break; + + case "Home": + tabs[0].focus(); + break; + + case "End": + tabs[tabs.length - 1].focus(); + break; + + case "Enter": + case " ": + event.currentTarget.click(); + break; + + default: + event_was_handled = false; + break; + } + + if (event_was_handled) { + event.stopPropagation(); + event.preventDefault(); + } +} + +function open_tab(evt, tabName) { + var tabcontent, tablinks; + // Hide all others + tabcontent = Array.from(document.getElementsByClassName("tabcontent")); + tabcontent.forEach(tab => { + tab.style.display = "none"; + }); + // Display the one we selected + document.getElementById(tabName).style.display = "block"; + + // If this is a temporary overlay, like pin-required, we don't + // touch the sidebar and which button is selected. + if (!evt.detail.temporary_overlay) { + tablinks = Array.from(document.getElementsByClassName("category")); + tablinks.forEach(tablink => { + tablink.removeAttribute("selected"); + tablink.setAttribute("aria-selected", "false"); + tablink.setAttribute("tabindex", "-1"); + tablink.disabled = false; + }); + evt.currentTarget.setAttribute("selected", "true"); + evt.currentTarget.setAttribute("tabindex", "0"); + evt.currentTarget.setAttribute("aria-selected", "true"); + } + + if (!evt.detail.skip_results_clearing) { + hide_results_banner(); + } + sidebar_set_disabled(false); + AboutWebauthnManagerJS._previous_tab = AboutWebauthnManagerJS._current_tab; + AboutWebauthnManagerJS._current_tab = tabName; +} + +function open_info_tab(evt) { + open_tab(evt, "token-info-section"); +} +function open_pin_tab(evt) { + open_tab(evt, "set-change-pin-section"); +} +function open_credentials_tab(evt) { + open_tab(evt, "credential-management-section"); +} +function open_bio_enrollments_tab(evt) { + // We can only list, if there are any registered already + if ( + AboutWebauthnManagerJS._curr_data.options.bioEnroll === true || + AboutWebauthnManagerJS._curr_data.options.userVerificationMgmtPreview === + true + ) { + document.getElementById("list-bio-enrollments-button").style.display = + "inline-block"; + } else { + document.getElementById("list-bio-enrollments-button").style.display = + "none"; + } + open_tab(evt, "bio-enrollment-section"); +} +function open_reset_tab(evt) { + open_tab(evt, "reset-token-section"); +} +function open_pin_required_tab() { + // Remove any old value we might have had + document.getElementById("pin-required").value = ""; + const evt = new CustomEvent("click", { + detail: { + temporary_overlay: true, + skip_results_clearing: true, // We might be called multiple times, if PIN was invalid + }, + }); + open_tab(evt, "pin-required-section"); + document.getElementById("pin-required").focus(); + // This is a temporary overlay, so we don't want the + // user to click away from it, unless via the Cancel-button. + sidebar_set_disabled(true); +} +function close_temporary_overlay_tab() { + const evt = new CustomEvent("click", { + detail: { temporary_overlay: true }, + }); + open_tab(evt, AboutWebauthnManagerJS._previous_tab); + sidebar_set_disabled(false); +} +function open_delete_confirmation_tab() { + const evt = new CustomEvent("click", { + detail: { + temporary_overlay: true, + }, + }); + open_tab(evt, "confirm-deletion-section"); + // This is a temporary overlay, so we don't want the + // user to click away from it, unless via the Cancel-button. + sidebar_set_disabled(true); +} + +try { + AboutWebauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + document.addEventListener("DOMContentLoaded", onLoad); + window.addEventListener("beforeunload", event => { + AboutWebauthnManagerJS.uninit(); + if (AboutWebauthnService) { + AboutWebauthnService.cancel(0); + } + }); +} catch (ex) { + // Do nothing if we fail to create a singleton instance, + // showing the default no-module message. + console.error(ex); +} diff --git a/toolkit/components/aboutwebauthn/jar.mn b/toolkit/components/aboutwebauthn/jar.mn new file mode 100644 index 0000000000..daf0ff5aaa --- /dev/null +++ b/toolkit/components/aboutwebauthn/jar.mn @@ -0,0 +1,8 @@ +# 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/. + +toolkit.jar: + content/global/aboutWebauthn.css (content/aboutWebauthn.css) + content/global/aboutWebauthn.html (content/aboutWebauthn.html) + content/global/aboutWebauthn.js (content/aboutWebauthn.js) diff --git a/toolkit/components/aboutwebauthn/moz.build b/toolkit/components/aboutwebauthn/moz.build new file mode 100644 index 0000000000..4cf77ef103 --- /dev/null +++ b/toolkit/components/aboutwebauthn/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Web Authentication") + +if CONFIG["MOZ_WIDGET_TOOLKIT"] not in ("windows", "android"): + BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser.toml b/toolkit/components/aboutwebauthn/tests/browser/browser.toml new file mode 100644 index 0000000000..a95e23b956 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +head = "head.js" +prefs = [ + "security.webauth.webauthn=true", + "security.webauth.webauthn_enable_softtoken=false", + "security.webauth.webauthn_enable_android_fido2=false", + "security.webauth.webauthn_enable_usbtoken=false", + "security.webauthn.ctap2=true", +] + +["browser_aboutwebauthn_aria_keycontrols.js"] + +["browser_aboutwebauthn_bio.js"] + +["browser_aboutwebauthn_credentials.js"] + +["browser_aboutwebauthn_info.js"] + +["browser_aboutwebauthn_no_token.js"] + +["browser_aboutwebauthn_pin.js"] diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js new file mode 100644 index 0000000000..44e7f3dcd3 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js @@ -0,0 +1,195 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; + let ops = { + credMgmt: true, + clientPin: true, + bioEnroll: true, + }; + send_auth_info_and_check_categories(doc, ops); +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function moving_up_and_down() { + doc.getElementById("info-tab-button").focus(); + [ + ["UP", "DOWN"], + ["LEFT", "RIGHT"], + ].forEach(dirs => { + let up = dirs[0]; + let down = dirs[1]; + EventUtils.sendKey(down); + is( + doc.activeElement, + doc.getElementById("pin-tab-button"), + "Wrong element active" + ); + EventUtils.sendKey(down); + is( + doc.activeElement, + doc.getElementById("credentials-tab-button"), + "Wrong element active" + ); + EventUtils.sendKey(down); + is( + doc.activeElement, + doc.getElementById("bio-enrollments-tab-button"), + "Wrong element active" + ); + // Trying to go down further should do nothing + EventUtils.sendKey(down); + is( + doc.activeElement, + doc.getElementById("bio-enrollments-tab-button"), + "Wrong element active" + ); + EventUtils.sendKey(down); + is( + doc.activeElement, + doc.getElementById("bio-enrollments-tab-button"), + "Wrong element active" + ); + + // Going back up again + EventUtils.sendKey(up); + is( + doc.activeElement, + doc.getElementById("credentials-tab-button"), + "Wrong element active" + ); + EventUtils.sendKey(up); + is( + doc.activeElement, + doc.getElementById("pin-tab-button"), + "Wrong element active" + ); + EventUtils.sendKey(up); + is( + doc.activeElement, + doc.getElementById("info-tab-button"), + "Wrong element active" + ); + // Trying to go up further should do nothing + EventUtils.sendKey(up); + is( + doc.activeElement, + doc.getElementById("info-tab-button"), + "Wrong element active" + ); + EventUtils.sendKey(up); + is( + doc.activeElement, + doc.getElementById("info-tab-button"), + "Wrong element active" + ); + }); +}); + +add_task(async function switching_sections() { + doc.getElementById("info-tab-button").focus(); + EventUtils.sendKey("DOWN"); // Pin section + EventUtils.sendKey("DOWN"); // Credentials section + EventUtils.sendKey("RETURN"); + + let credentials_section = doc.getElementById("credential-management-section"); + isnot( + credentials_section.style.display, + "none", + "credentials section not visible" + ); + + EventUtils.sendKey("UP"); // PIN section + EventUtils.sendKey("RETURN"); + + let pin_section = doc.getElementById("set-change-pin-section"); + isnot(pin_section.style.display, "none", "pin section not visible"); + + EventUtils.sendKey("DOWN"); // Credentials section + EventUtils.sendChar(" "); // Try using Space instead of Return + + isnot( + credentials_section.style.display, + "none", + "credentials section not visible" + ); +}); + +add_task(async function jumping_sections() { + doc.getElementById("info-tab-button").focus(); + EventUtils.sendKey("DOWN"); // Pin section + EventUtils.sendKey("DOWN"); // Credentials section + is( + doc.activeElement, + doc.getElementById("credentials-tab-button"), + "Wrong element active" + ); + + EventUtils.sendKey("HOME"); + is( + doc.activeElement, + doc.getElementById("info-tab-button"), + "Wrong element active" + ); + + // Another hit of the Home-key should change nothing + EventUtils.sendKey("HOME"); + is( + doc.activeElement, + doc.getElementById("info-tab-button"), + "Wrong element active" + ); + + EventUtils.sendKey("END"); + is( + doc.activeElement, + doc.getElementById("bio-enrollments-tab-button"), + "Wrong element active" + ); + + EventUtils.sendKey("HOME"); + is( + doc.activeElement, + doc.getElementById("info-tab-button"), + "Wrong element active" + ); + + EventUtils.sendKey("DOWN"); // Pin section + EventUtils.sendKey("DOWN"); // Credentials section + is( + doc.activeElement, + doc.getElementById("credentials-tab-button"), + "Wrong element active" + ); + + EventUtils.sendKey("END"); + is( + doc.activeElement, + doc.getElementById("bio-enrollments-tab-button"), + "Wrong element active" + ); + + // Another hit of the "End"-key should change nothing + EventUtils.sendKey("END"); + is( + doc.activeElement, + doc.getElementById("bio-enrollments-tab-button"), + "Wrong element active" + ); +}); diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js new file mode 100644 index 0000000000..79ce5723d1 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js @@ -0,0 +1,351 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +function send_bio_enrollment_command(state) { + let msg = JSON.stringify({ + type: "bio-enrollment-update", + result: state, + }); + Services.obs.notifyObservers(null, "about-webauthn-prompt", msg); +} + +function check_bio_buttons_disabled(disabled) { + let buttons = Array.from(doc.getElementsByClassName("bio-enrollment-button")); + buttons.forEach(button => { + is(button.disabled, disabled); + }); + return buttons; +} + +function check_tab_buttons_aria(button_id) { + let button = doc.getElementById(button_id); + is(button.role, "tab", button_id + " in the sidebar is a tab"); + ok( + button.hasAttribute("aria-controls"), + button_id + " in the sidebar is a tab with an assigned tablist" + ); + ok( + button.hasAttribute("aria-selected"), + button_id + " in the sidebar is a tab that can be or is selected" + ); + ok( + button.hasAttribute("tabindex"), + button_id + " in the sidebar is a tab with keyboard focusability provided" + ); +} + +async function send_authinfo_and_open_bio_section(ops) { + reset_about_page(doc); + send_auth_info_and_check_categories(doc, ops); + + ["pin-tab-button", "credentials-tab-button"].forEach(button_id => { + let button = doc.getElementById(button_id); + is(button.style.display, "none", button_id + " in the sidebar not hidden"); + check_tab_buttons_aria(button_id); + }); + + if (ops.bioEnroll !== null || ops.userVerificationMgmtPreview !== null) { + let bio_enrollments_tab_button = doc.getElementById( + "bio-enrollments-tab-button" + ); + // Check if bio enrollment section is visible + isnot( + bio_enrollments_tab_button.style.display, + "none", + "bio enrollment button in the sidebar not visible" + ); + check_tab_buttons_aria("bio-enrollments-tab-button"); + + // Click the section and wait for it to open + let bio_enrollment_section = doc.getElementById("bio-enrollment-section"); + bio_enrollments_tab_button.click(); + isnot( + bio_enrollment_section.style.display, + "none", + "bio enrollment section not visible" + ); + is( + bio_enrollments_tab_button.getAttribute("selected"), + "true", + "bio enrollment section button not selected" + ); + } +} + +add_task(async function bio_enrollment_not_supported() { + send_authinfo_and_open_bio_section({ + bioEnroll: null, + userVerificationMgmtPreview: null, + }); + let bio_enrollments_tab_button = doc.getElementById( + "bio-enrollments-tab-button" + ); + is( + bio_enrollments_tab_button.style.display, + "none", + "bio enrollments button in the sidebar visible" + ); +}); + +add_task(async function bio_enrollment_supported() { + // Setting bioEnroll should show the button in the sidebar + // The function is checking this for us. + send_authinfo_and_open_bio_section({ + bioEnroll: true, + userVerificationMgmtPreview: null, + }); + // Setting Preview should show the button in the sidebar + send_authinfo_and_open_bio_section({ + bioEnroll: null, + userVerificationMgmtPreview: true, + }); + // Setting both should also work + send_authinfo_and_open_bio_section({ + bioEnroll: true, + userVerificationMgmtPreview: true, + }); +}); + +add_task(async function bio_enrollment_empty_list() { + send_authinfo_and_open_bio_section({ bioEnroll: true }); + + let list_bio_enrollments_button = doc.getElementById( + "list-bio-enrollments-button" + ); + let add_bio_enrollment_button = doc.getElementById( + "add-bio-enrollment-button" + ); + isnot( + list_bio_enrollments_button.style.display, + "none", + "List bio enrollments button in the sidebar not visible" + ); + isnot( + add_bio_enrollment_button.style.display, + "none", + "Add bio enrollment button in the sidebar not visible" + ); + + // Bio list should initially not be visible + let bio_enrollment_list = doc.getElementById( + "bio-enrollment-list-subsection" + ); + is(bio_enrollment_list.hidden, true, "bio enrollment list visible"); + + list_bio_enrollments_button.click(); + is( + list_bio_enrollments_button.disabled, + true, + "List bio enrollments button not disabled while op in progress" + ); + is( + add_bio_enrollment_button.disabled, + true, + "Add bio enrollment button not disabled while op in progress" + ); + + send_bio_enrollment_command({ EnrollmentList: [] }); + is( + list_bio_enrollments_button.disabled, + false, + "List bio enrollments button disabled" + ); + is( + add_bio_enrollment_button.disabled, + false, + "Add bio enrollment button disabled" + ); + + is(bio_enrollment_list.hidden, false, "bio enrollments list visible"); + let bio_enrollment_list_empty_label = doc.getElementById( + "bio-enrollment-list-empty-label" + ); + is( + bio_enrollment_list_empty_label.hidden, + false, + "bio enrollments list empty label not visible" + ); +}); + +add_task(async function bio_enrollment_real_data() { + send_authinfo_and_open_bio_section({ bioEnroll: true }); + let list_bio_enrollments_button = doc.getElementById( + "list-bio-enrollments-button" + ); + let add_bio_enrollment_button = doc.getElementById( + "add-bio-enrollment-button" + ); + list_bio_enrollments_button.click(); + send_bio_enrollment_command({ EnrollmentList: REAL_DATA }); + is( + list_bio_enrollments_button.disabled, + false, + "List bio enrollments button disabled" + ); + is( + add_bio_enrollment_button.disabled, + false, + "Add bio enrollment button disabled" + ); + let bio_enrollment_list = doc.getElementById("bio-enrollment-list"); + is( + bio_enrollment_list.rows.length, + 2, + "bio enrollment list table doesn't contain 2 bio enrollments" + ); + is(bio_enrollment_list.rows[0].cells[0].textContent, "right-middle"); + is(bio_enrollment_list.rows[1].cells[0].textContent, "right-index"); + let buttons = check_bio_buttons_disabled(false); + // 2 for each bio + 1 for the list button + 1 for the add button + hidden "start enroll" + is(buttons.length, 5); + + list_bio_enrollments_button.click(); + buttons = check_bio_buttons_disabled(true); +}); + +add_task(async function bio_enrollment_add_new() { + send_authinfo_and_open_bio_section({ bioEnroll: true }); + let add_bio_enrollment_button = doc.getElementById( + "add-bio-enrollment-button" + ); + add_bio_enrollment_button.click(); + let add_bio_enrollment_section = doc.getElementById( + "add-bio-enrollment-section" + ); + isnot( + add_bio_enrollment_section.style.display, + "none", + "Add bio enrollment section invisible" + ); + let enrollment_name = doc.getElementById("enrollment-name"); + enrollment_name.value = "Test123"; + + let start_enrollment_button = doc.getElementById("start-enrollment-button"); + start_enrollment_button.click(); + + check_bio_buttons_disabled(true); + send_bio_enrollment_command({ + SampleStatus: ["Ctap2EnrollFeedbackFpGood", 3], + }); + + let enrollment_update = doc.getElementById("enrollment-update"); + is(enrollment_update.children.length, 1); + check_bio_buttons_disabled(true); + + send_bio_enrollment_command({ + SampleStatus: ["Ctap2EnrollFeedbackFpGood", 2], + }); + send_bio_enrollment_command({ + SampleStatus: ["Ctap2EnrollFeedbackFpGood", 1], + }); + send_bio_enrollment_command({ + SampleStatus: ["Ctap2EnrollFeedbackFpGood", 0], + }); + is(enrollment_update.children.length, 4); + check_bio_buttons_disabled(true); + // We tell the about-page that we still have enrollments (bioEnroll: true). + // So it will automatically request a refreshed list from the authenticator, disabling + // all the buttons again. + send_bio_enrollment_command({ AddSuccess: { options: { bioEnroll: true } } }); + is(enrollment_update.children.length, 0); + is(enrollment_name.value, "", "Enrollment name field did not get cleared"); + check_bio_buttons_disabled(true); +}); + +add_task(async function bio_enrollment_delete() { + send_authinfo_and_open_bio_section({ bioEnroll: true }); + let list_bio_enrollments_button = doc.getElementById( + "list-bio-enrollments-button" + ); + list_bio_enrollments_button.click(); + send_bio_enrollment_command({ EnrollmentList: REAL_DATA }); + let buttons = check_bio_buttons_disabled(false); + buttons[1].click(); + check_bio_buttons_disabled(true); + // Tell the about page, we still have enrollments + send_bio_enrollment_command({ + DeleteSuccess: { options: { bioEnroll: true } }, + }); + // This will cause it to automatically request the new list, thus disabling all buttons + check_bio_buttons_disabled(true); + send_bio_enrollment_command({ EnrollmentList: REAL_DATA }); + buttons = check_bio_buttons_disabled(false); + buttons[1].click(); + + // Confirmation dialog should pop open + let confirmation_section = doc.getElementById("confirm-deletion-section"); + isnot( + confirmation_section.style.display, + "none", + "Confirmation section did not open." + ); + + // Check if the label displays the correct data + let confirmation_context = doc.getElementById("confirmation-context"); + // Items get listed in reverse order (inserRow(0) in a loop), so we use [0] instead of [1] here + is( + confirmation_context.textContent, + REAL_DATA[0].template_friendly_name, + "Deletion context show wrong credential name" + ); + // Check if the delete-button has the correct context-data + let cmd = { + BioEnrollment: { + DeleteEnrollment: REAL_DATA[0].template_id, + }, + }; + is( + confirmation_context.getAttribute("data-ctap-command"), + JSON.stringify(cmd), + "Confirm-button has the wrong context data" + ); + + let cancel_button = doc.getElementById("cancel-confirmation-button"); + cancel_button.click(); + is( + confirmation_section.style.display, + "none", + "Confirmation section did not close." + ); + check_bio_buttons_disabled(false); + + // Click the delete-button again + buttons[1].click(); + + // Now we tell it that we deleted all enrollments (bioEnroll: false) + send_bio_enrollment_command({ + DeleteSuccess: { options: { bioEnroll: false } }, + }); + // Thus, all buttons should get re-enabled and the subsection should be hidden + check_bio_buttons_disabled(false); + + let bio_enrollment_subsection = doc.getElementById( + "bio-enrollment-list-subsection" + ); + is(bio_enrollment_subsection.hidden, true, "Bio Enrollment List not hidden."); +}); + +const REAL_DATA = [ + { template_id: [248, 82], template_friendly_name: "right-index" }, + { template_id: [14, 163], template_friendly_name: "right-middle" }, +]; diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js new file mode 100644 index 0000000000..ff45ea2962 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js @@ -0,0 +1,395 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +function send_credential_list(credlist) { + let num_of_creds = 0; + credlist.forEach(domain => { + domain.credentials.forEach(c => { + num_of_creds += 1; + }); + }); + let msg = JSON.stringify({ + type: "credential-management-update", + result: { + CredentialList: { + existing_resident_credentials_count: num_of_creds, + max_possible_remaining_resident_credentials_count: 20, + credential_list: credlist, + }, + }, + }); + Services.obs.notifyObservers(null, "about-webauthn-prompt", msg); +} + +function check_cred_buttons_disabled(disabled) { + let buttons = Array.from(doc.getElementsByClassName("credentials-button")); + buttons.forEach(button => { + is(button.disabled, disabled); + }); + return buttons; +} + +async function send_authinfo_and_open_cred_section(ops) { + reset_about_page(doc); + send_auth_info_and_check_categories(doc, ops); + + ["pin-tab-button", "bio-enrollments-tab-button"].forEach(button_id => { + let button = doc.getElementById(button_id); + is(button.style.display, "none", button_id + " in the sidebar not hidden"); + }); + + if (ops.credMgmt !== null || ops.credentialMgmtPreview !== null) { + let credentials_tab_button = doc.getElementById("credentials-tab-button"); + // Check if credentials section is visible + isnot( + credentials_tab_button.style.display, + "none", + "credentials button in the sidebar not visible" + ); + + // Click the section and wait for it to open + let credentials_section = doc.getElementById( + "credential-management-section" + ); + credentials_tab_button.click(); + isnot( + credentials_section.style.display, + "none", + "credentials section not visible" + ); + is( + credentials_tab_button.getAttribute("selected"), + "true", + "credentials section button not selected" + ); + } +} + +add_task(async function cred_mgmt_not_supported() { + // Not setting credMgmt at all should lead to not showing it in the sidebar + send_authinfo_and_open_cred_section({ + credMgmt: null, + credentialMgmtPreview: null, + }); + let credentials_tab_button = doc.getElementById("credentials-tab-button"); + is( + credentials_tab_button.style.display, + "none", + "credentials button in the sidebar visible" + ); +}); + +add_task(async function cred_mgmt_supported() { + // Setting credMgmt should show the button in the sidebar + // The function is checking this for us. + send_authinfo_and_open_cred_section({ + credMgmt: true, + credentialMgmtPreview: null, + }); + // Setting credMgmtPreview should show the button in the sidebar + send_authinfo_and_open_cred_section({ + credMgmt: null, + credentialMgmtPreview: true, + }); + // Setting both should also work + send_authinfo_and_open_cred_section({ + credMgmt: true, + credentialMgmtPreview: true, + }); +}); + +add_task(async function cred_mgmt_empty_list() { + send_authinfo_and_open_cred_section({ credMgmt: true }); + + let list_credentials_button = doc.getElementById("list-credentials-button"); + isnot( + list_credentials_button.style.display, + "none", + "credentials button in the sidebar not visible" + ); + + // Credential list should initially not be visible + let credential_list = doc.getElementById("credential-list-subsection"); + is(credential_list.hidden, true, "credentials list visible"); + + list_credentials_button.click(); + is( + list_credentials_button.disabled, + true, + "credentials button not disabled while op in progress" + ); + + send_credential_list([]); + is(list_credentials_button.disabled, false, "credentials button disabled"); + + is(credential_list.hidden, false, "credentials list visible"); + let credential_list_empty_label = doc.getElementById( + "credential-list-empty-label" + ); + is( + credential_list_empty_label.hidden, + false, + "credentials list empty label not visible" + ); +}); + +add_task(async function cred_mgmt_real_data() { + send_authinfo_and_open_cred_section({ credMgmt: true }); + let list_credentials_button = doc.getElementById("list-credentials-button"); + list_credentials_button.click(); + send_credential_list(REAL_DATA); + is(list_credentials_button.disabled, false, "credentials button disabled"); + let credential_list = doc.getElementById("credential-list"); + is( + credential_list.rows.length, + 4, + "credential list table doesn't contain 4 credentials" + ); + is(credential_list.rows[0].cells[0].textContent, "webauthn.io"); + is(credential_list.rows[0].cells[1].textContent, "fasdfasd"); + is(credential_list.rows[1].cells[0].textContent, "webauthn.io"); + is(credential_list.rows[1].cells[1].textContent, "twetwetw"); + is(credential_list.rows[2].cells[0].textContent, "webauthn.io"); + is(credential_list.rows[2].cells[1].textContent, "hhhhhg"); + is(credential_list.rows[3].cells[0].textContent, "example.com"); + is(credential_list.rows[3].cells[1].textContent, "A. Nother"); + let buttons = check_cred_buttons_disabled(false); + // 4 for each cred + 1 for the list button + is(buttons.length, 5); + + list_credentials_button.click(); + buttons = check_cred_buttons_disabled(true); + send_credential_list(REAL_DATA); + buttons[2].click(); + check_cred_buttons_disabled(true); + + // Confirmation section should now be open + let credential_section = doc.getElementById("credential-management-section"); + is( + credential_section.style.display, + "none", + "credential section still visible" + ); + let confirmation_section = doc.getElementById("confirm-deletion-section"); + isnot( + confirmation_section.style.display, + "none", + "Confirmation section did not open." + ); + + // Check if the label displays the correct data + let confirmation_context = doc.getElementById("confirmation-context"); + is( + confirmation_context.textContent, + "webauthn.io - hhhhhg", + "Deletion context show wrong credential name" + ); + // Check if the delete-button has the correct context-data + let cmd = { + CredentialManagement: { + DeleteCredential: REAL_DATA[1].credentials[0].credential_id, + }, + }; + is( + confirmation_context.getAttribute("data-ctap-command"), + JSON.stringify(cmd), + "Confirm-button has the wrong context data" + ); + + let cancel_button = doc.getElementById("cancel-confirmation-button"); + cancel_button.click(); + isnot( + credential_section.style.display, + "none", + "credential section still visible" + ); + is( + confirmation_section.style.display, + "none", + "Confirmation section did not open." + ); + check_cred_buttons_disabled(false); +}); + +const REAL_DATA = [ + { + rp: { id: "example.com" }, + rp_id_hash: [ + 163, 121, 166, 246, 238, 175, 185, 165, 94, 55, 140, 17, 128, 52, 226, + 117, 30, 104, 47, 171, 159, 45, 48, 171, 19, 210, 18, 85, 134, 206, 25, + 71, + ], + credentials: [ + { + user: { + id: [65, 46, 32, 78, 111, 116, 104, 101, 114], + name: "A. Nother", + }, + credential_id: { + id: [ + 195, 239, 221, 151, 76, 77, 255, 242, 217, 50, 87, 144, 238, 79, + 199, 120, 234, 148, 142, 69, 163, 133, 189, 254, 74, 138, 119, 140, + 197, 171, 36, 215, 191, 176, 36, 111, 113, 158, 204, 147, 101, 200, + 20, 239, 191, 174, 51, 15, + ], + type: "public-key", + }, + public_key: { + 1: 2, + 3: -7, + "-1": 1, + "-2": [ + 195, 239, 221, 151, 76, 77, 255, 242, 217, 50, 87, 144, 238, 235, + 230, 51, 155, 142, 121, 60, 136, 63, 80, 184, 41, 238, 217, 61, 1, + 206, 253, 141, + ], + "-3": [ + 15, 81, 111, 204, 199, 48, 18, 121, 134, 243, 26, 49, 6, 244, 25, + 156, 188, 71, 245, 122, 93, 47, 218, 235, 25, 222, 191, 116, 20, 14, + 195, 114, + ], + }, + cred_protect: 1, + large_blob_key: [ + 223, 32, 77, 171, 223, 133, 38, 175, 229, 40, 85, 216, 203, 79, 194, + 223, 32, 191, 119, 241, 115, 6, 101, 180, 92, 194, 208, 193, 181, 163, + 164, 64, + ], + }, + ], + }, + { + rp: { id: "webauthn.io" }, + rp_id_hash: [ + 116, 166, 234, 146, 19, 201, 156, 47, 116, 178, 36, 146, 179, 32, 207, 64, + 38, 42, 148, 193, 169, 80, 160, 57, 127, 41, 37, 11, 96, 132, 30, 240, + ], + credentials: [ + { + user: { id: [97, 71, 104, 111, 97, 71, 104, 110], name: "hhhhhg" }, + credential_id: { + id: [ + 64, 132, 129, 5, 185, 62, 86, 253, 199, 113, 219, 14, 207, 113, 145, + 78, 177, 198, 130, 217, 122, 105, 225, 111, 32, 227, 237, 209, 6, + 220, 202, 234, 144, 227, 246, 42, 73, 68, 37, 142, 95, 139, 224, 36, + 156, 168, 118, 181, + ], + type: "public-key", + }, + public_key: { + 1: 2, + 3: -7, + "-1": 1, + "-2": [ + 64, 132, 129, 5, 185, 62, 86, 253, 199, 113, 219, 14, 207, 22, 10, + 241, 230, 152, 5, 204, 35, 94, 22, 191, 213, 2, 247, 220, 227, 62, + 76, 182, + ], + "-3": [ + 13, 30, 30, 149, 170, 118, 78, 115, 101, 218, 245, 52, 154, 242, 67, + 146, 17, 184, 112, 225, 51, 47, 242, 157, 195, 80, 76, 101, 147, + 161, 3, 185, + ], + }, + cred_protect: 1, + large_blob_key: [ + 10, 67, 27, 233, 8, 115, 69, 191, 105, 213, 77, 123, 210, 118, 193, + 234, 3, 12, 234, 228, 215, 106, 24, 228, 102, 247, 255, 156, 99, 196, + 215, 230, + ], + }, + { + user: { + id: [100, 72, 100, 108, 100, 72, 100, 108, 100, 72, 99], + name: "twetwetw", + }, + credential_id: { + id: [ + 29, 8, 25, 57, 66, 234, 22, 27, 227, 141, 77, 93, 233, 234, 251, 61, + 100, 199, 176, 97, 112, 48, 172, 118, 145, 0, 156, 76, 156, 215, 18, + 25, 118, 32, 241, 127, 13, 177, 249, 101, 26, 209, 142, 116, 74, 95, + 117, 29, + ], + type: "public-key", + }, + public_key: { + 1: 2, + 3: -7, + "-1": 1, + "-2": [ + 29, 8, 25, 57, 66, 234, 22, 27, 227, 141, 77, 93, 233, 154, 113, + 177, 251, 161, 54, 76, 150, 15, 6, 143, 117, 214, 232, 215, 118, 41, + 116, 19, + ], + "-3": [ + 201, 6, 43, 178, 3, 249, 175, 123, 149, 81, 127, 20, 116, 152, 238, + 84, 52, 113, 3, 165, 176, 105, 200, 137, 209, 0, 141, 50, 42, 192, + 174, 26, + ], + }, + cred_protect: 1, + large_blob_key: [ + 125, 175, 155, 1, 14, 247, 182, 241, 234, 66, 115, 236, 200, 223, 176, + 88, 88, 88, 202, 173, 147, 217, 9, 193, 114, 7, 29, 169, 224, 179, + 187, 188, + ], + }, + { + user: { + id: [90, 109, 70, 122, 90, 71, 90, 104, 99, 50, 81], + name: "fasdfasd", + }, + credential_id: { + id: [ + 58, 174, 92, 116, 17, 108, 28, 203, 233, 192, 182, 60, 80, 236, 133, + 196, 98, 32, 103, 53, 107, 48, 46, 236, 228, 166, 21, 33, 228, 75, + 85, 191, 71, 18, 214, 177, 56, 254, 89, 28, 187, 220, 241, 7, 21, + 11, 45, 151, + ], + type: "public-key", + }, + public_key: { + 1: 2, + 3: -7, + "-1": 1, + "-2": [ + 58, 174, 92, 116, 17, 108, 28, 203, 233, 192, 182, 60, 80, 111, 150, + 192, 102, 255, 211, 156, 5, 186, 29, 105, 154, 79, 14, 2, 106, 159, + 57, 156, + ], + "-3": [ + 182, 164, 251, 221, 237, 36, 239, 109, 146, 184, 146, 29, 143, 16, + 35, 188, 84, 148, 247, 83, 181, 40, 88, 111, 245, 13, 254, 206, 242, + 164, 234, 159, + ], + }, + cred_protect: 1, + large_blob_key: [ + 113, 154, 217, 69, 45, 108, 115, 20, 104, 43, 214, 120, 253, 93, 223, + 204, 125, 234, 220, 148, 98, 118, 98, 157, 175, 41, 154, 97, 87, 233, + 208, 171, + ], + }, + ], + }, +]; diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js new file mode 100644 index 0000000000..f66f5929fc --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +function send_auth_data_and_check(auth_data) { + Services.obs.notifyObservers( + null, + "about-webauthn-prompt", + JSON.stringify({ type: "selected-device", auth_info: auth_data }) + ); + + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, true, "Start prompt not hidden"); + + let info_section = doc.getElementById("token-info-section"); + isnot(info_section.style.display, "none", "Info section hidden"); +} + +add_task(async function multiple_devices() { + Services.obs.notifyObservers( + null, + "about-webauthn-prompt", + JSON.stringify({ type: "select-device" }) + ); + + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, false, "Start prompt hidden"); + let field = doc.getElementById("info-text-field"); + is( + field.getAttribute("data-l10n-id"), + "about-webauthn-text-select-device", + "Field does not prompt user to touch device for selection" + ); +}); + +add_task(async function multiple_devices() { + send_auth_data_and_check(REAL_AUTH_INFO_1); + reset_about_page(doc); + send_auth_data_and_check(REAL_AUTH_INFO_2); + reset_about_page(doc); + send_auth_data_and_check(REAL_AUTH_INFO_3); + reset_about_page(doc); +}); + +// Yubikey BIO +const REAL_AUTH_INFO_1 = { + versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE", "FIDO_2_1"], + extensions: [ + "credProtect", + "hmac-secret", + "largeBlobKey", + "credBlob", + "minPinLength", + ], + aaguid: [ + 216, 82, 45, 159, 87, 91, 72, 102, 136, 169, 186, 153, 250, 2, 243, 91, + ], + options: { + plat: false, + rk: true, + clientPin: true, + up: true, + uv: true, + pinUvAuthToken: true, + noMcGaPermissionsWithClientPin: null, + largeBlobs: true, + ep: null, + bioEnroll: true, + userVerificationMgmtPreview: true, + uvBioEnroll: null, + authnrCfg: true, + uvAcfg: null, + credMgmt: true, + credentialMgmtPreview: true, + setMinPINLength: true, + makeCredUvNotRqd: true, + alwaysUv: false, + }, + max_msg_size: 1200, + pin_protocols: [2, 1], + max_credential_count_in_list: 8, + max_credential_id_length: 128, + transports: ["usb"], + algorithms: [ + { alg: -7, type: "public-key" }, + { alg: -8, type: "public-key" }, + ], + max_ser_large_blob_array: 1024, + force_pin_change: false, + min_pin_length: 4, + firmware_version: 328966, + max_cred_blob_length: 32, + max_rpids_for_set_min_pin_length: 1, + preferred_platform_uv_attempts: 3, + uv_modality: 2, + certifications: null, + remaining_discoverable_credentials: 20, + vendor_prototype_config_commands: null, +}; + +// Yubikey 5 +const REAL_AUTH_INFO_2 = { + versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"], + extensions: ["credProtect", "hmac-secret"], + aaguid: [ + 47, 192, 87, 159, 129, 19, 71, 234, 177, 22, 187, 90, 141, 185, 32, 42, + ], + options: { + plat: false, + rk: true, + clientPin: true, + up: true, + uv: null, + pinUvAuthToken: null, + noMcGaPermissionsWithClientPin: null, + largeBlobs: null, + ep: null, + bioEnroll: null, + userVerificationMgmtPreview: null, + uvBioEnroll: null, + authnrCfg: null, + uvAcfg: null, + credMgmt: null, + credentialMgmtPreview: true, + setMinPINLength: null, + makeCredUvNotRqd: null, + alwaysUv: null, + }, + max_msg_size: 1200, + pin_protocols: [1], + max_credential_count_in_list: 8, + max_credential_id_length: 128, + transports: ["nfc", "usb"], + algorithms: [ + { alg: -7, type: "public-key" }, + { alg: -8, type: "public-key" }, + ], + max_ser_large_blob_array: null, + force_pin_change: null, + min_pin_length: null, + firmware_version: null, + max_cred_blob_length: null, + max_rpids_for_set_min_pin_length: null, + preferred_platform_uv_attempts: null, + uv_modality: null, + certifications: null, + remaining_discoverable_credentials: null, + vendor_prototype_config_commands: null, +}; + +// Nitrokey 3 +const REAL_AUTH_INFO_3 = { + versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"], + extensions: ["credProtect", "hmac-secret"], + aaguid: [ + 47, 192, 87, 159, 129, 19, 71, 234, 177, 22, 187, 90, 141, 185, 32, 42, + ], + options: { + plat: false, + rk: true, + clientPin: true, + up: true, + uv: null, + pinUvAuthToken: null, + noMcGaPermissionsWithClientPin: null, + largeBlobs: null, + ep: null, + bioEnroll: null, + userVerificationMgmtPreview: null, + uvBioEnroll: null, + authnrCfg: null, + uvAcfg: null, + credMgmt: null, + credentialMgmtPreview: true, + setMinPINLength: null, + makeCredUvNotRqd: null, + alwaysUv: null, + }, + max_msg_size: 1200, + pin_protocols: [1], + max_credential_count_in_list: 8, + max_credential_id_length: 128, + transports: ["nfc", "usb"], + algorithms: [ + { alg: -7, type: "public-key" }, + { alg: -8, type: "public-key" }, + ], + max_ser_large_blob_array: null, + force_pin_change: null, + min_pin_length: null, + firmware_version: null, + max_cred_blob_length: null, + max_rpids_for_set_min_pin_length: null, + preferred_platform_uv_attempts: null, + uv_modality: null, + certifications: null, + remaining_discoverable_credentials: null, + vendor_prototype_config_commands: null, +}; diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js new file mode 100644 index 0000000000..f6da6c7146 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function verify_page_no_token() { + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, false, "info-text-div should be visible"); + let categories = doc.getElementById("categories"); + is(categories.hidden, false, "categories-sidebar should be invisible"); + let dev_info = doc.getElementById("info-tab-button"); + is( + dev_info.getAttribute("selected"), + "true", + "token-info-section not selected by default" + ); + let ctap2_info = doc.getElementById("ctap2-token-info"); + is(ctap2_info.style.display, "none", "ctap2-info-table is visible"); +}); + +add_task(async function verify_no_auth_info() { + let field = doc.getElementById("info-text-field"); + let promise = BrowserTestUtils.waitForMutationCondition( + field, + { attributes: true, attributeFilter: ["data-l10n-id"] }, + () => + field.getAttribute("data-l10n-id") === + "about-webauthn-text-non-ctap2-device" + ); + Services.obs.notifyObservers( + null, + "about-webauthn-prompt", + JSON.stringify({ type: "selected-device", auth_info: null }) + ); + await promise; + + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, false); +}); diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js new file mode 100644 index 0000000000..0849b194a4 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ABOUT_URL = "about:webauthn"; + +var doc, tab; + +async function send_authinfo_and_open_pin_section(ops) { + reset_about_page(doc); + send_auth_info_and_check_categories(doc, ops); + + ["credentials-tab-button", "bio-enrollments-tab-button"].forEach( + button_id => { + let button = doc.getElementById(button_id); + is( + button.style.display, + "none", + button_id + " in the sidebar not hidden" + ); + } + ); + + if (ops.clientPin !== null) { + let pin_tab_button = doc.getElementById("pin-tab-button"); + // Check if PIN section is visible + isnot( + pin_tab_button.style.display, + "none", + "PIN button in the sidebar not visible" + ); + + // Click the section and wait for it to open + let pin_section = doc.getElementById("set-change-pin-section"); + pin_tab_button.click(); + isnot(pin_section.style.display, "none", "PIN section not visible"); + is( + pin_tab_button.getAttribute("selected"), + "true", + "PIN section button not selected" + ); + } +} + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function pin_not_supported() { + // Not setting clientPIN at all should lead to not showing it in the sidebar + send_authinfo_and_open_pin_section({ uv: true, clientPin: null }); + // Check if PIN section is invisible + let pin_tab_button = doc.getElementById("pin-tab-button"); + is(pin_tab_button.style.display, "none", "PIN button in the sidebar visible"); +}); + +add_task(async function pin_already_set() { + send_authinfo_and_open_pin_section({ clientPin: true }); + + let set_pin_button = doc.getElementById("set-pin-button"); + is(set_pin_button.style.display, "none", "Set PIN button visible"); + + let change_pin_button = doc.getElementById("change-pin-button"); + isnot( + change_pin_button.style.display, + "none", + "Change PIN button not visible" + ); + + let current_pin_div = doc.getElementById("current-pin-div"); + is(current_pin_div.hidden, false, "Current PIN field not visible"); + + // Test that the button is only active if both inputs are set the same + let new_pin = doc.getElementById("new-pin"); + let new_pin_repeat = doc.getElementById("new-pin-repeat"); + let current_pin = doc.getElementById("current-pin"); + + set_text(new_pin, "abcdefg"); + is(change_pin_button.disabled, true, "Change PIN button not disabled"); + set_text(new_pin_repeat, "abcde"); + is(change_pin_button.disabled, true, "Change PIN button not disabled"); + set_text(new_pin_repeat, "abcdefg"); + is(change_pin_button.disabled, true, "Change PIN button not disabled"); + set_text(current_pin, "1234567"); + is(change_pin_button.disabled, false, "Change PIN button disabled"); +}); + +add_task(async function pin_not_yet_set() { + send_authinfo_and_open_pin_section({ clientPin: false }); + + let set_pin_button = doc.getElementById("set-pin-button"); + isnot(set_pin_button.style.display, "none", "Set PIN button not visible"); + + let change_pin_button = doc.getElementById("change-pin-button"); + is(change_pin_button.style.display, "none", "Change PIN button visible"); + + let current_pin_div = doc.getElementById("current-pin-div"); + is(current_pin_div.hidden, true, "Current PIN field visible"); + + // Test that the button is only active if both inputs are set the same + let new_pin = doc.getElementById("new-pin"); + let new_pin_repeat = doc.getElementById("new-pin-repeat"); + + set_text(new_pin, "abcdefg"); + is(set_pin_button.disabled, true, "Set PIN button not disabled"); + set_text(new_pin_repeat, "abcde"); + is(set_pin_button.disabled, true, "Set PIN button not disabled"); + set_text(new_pin_repeat, "abcdefg"); + is(set_pin_button.disabled, false, "Set PIN button disabled"); +}); + +add_task(async function pin_switch_back_and_forth() { + // This will click the PIN section button + send_authinfo_and_open_pin_section({ clientPin: false }); + + let pin_tab_button = doc.getElementById("pin-tab-button"); + let pin_section = doc.getElementById("set-change-pin-section"); + let info_tab_button = doc.getElementById("info-tab-button"); + let info_section = doc.getElementById("token-info-section"); + + // a11y-tree is racy here, so we have to wait a tick for it to get up to date + await TestUtils.waitForTick(); + // Now click the "info"-button and verify the correct buttons are highlighted + info_tab_button.click(); + // await info_promise; + isnot(info_section.style.display, "none", "info section not visible"); + is( + info_tab_button.getAttribute("selected"), + "true", + "Info tab button not selected" + ); + isnot( + pin_tab_button.getAttribute("selected"), + "true", + "PIN tab button selected" + ); + + // Click back to the PIN section + pin_tab_button.click(); + isnot(pin_section.style.display, "none", "PIN section not visible"); + is( + pin_tab_button.getAttribute("selected"), + "true", + "PIN tab button not selected" + ); + isnot( + info_tab_button.getAttribute("selected"), + "true", + "Info button selected" + ); +}); + +add_task(async function invalid_pin() { + send_authinfo_and_open_pin_section({ clientPin: true }); + let pin_tab_button = doc.getElementById("pin-tab-button"); + // Click the section and wait for it to open + pin_tab_button.click(); + is( + pin_tab_button.getAttribute("selected"), + "true", + "PIN section button not selected" + ); + + let change_pin_button = doc.getElementById("change-pin-button"); + + // Test that the button is only active if both inputs are set the same + let new_pin = doc.getElementById("new-pin"); + let new_pin_repeat = doc.getElementById("new-pin-repeat"); + let current_pin = doc.getElementById("current-pin"); + + // Needed to activate change_pin_button + set_text(new_pin, "abcdefg"); + set_text(new_pin_repeat, "abcdefg"); + set_text(current_pin, "1234567"); + + // This should silently error out since we have no authenticator + change_pin_button.click(); + + // Now we fake a response from the authenticator, saying the PIN was invalid + let pin_required = doc.getElementById("pin-required-section"); + let msg = JSON.stringify({ type: "pin-required" }); + Services.obs.notifyObservers(null, "about-webauthn-prompt", msg); + isnot(pin_required.style.display, "none", "PIN required dialog not visible"); + + let info_tab_button = doc.getElementById("info-tab-button"); + is( + info_tab_button.classList.contains("disabled-category"), + true, + "Sidebar not disabled" + ); + + let pin_field = doc.getElementById("pin-required"); + let send_pin_button = doc.getElementById("send-pin-button"); + + set_text(pin_field, "654321"); + send_pin_button.click(); + + is( + pin_required.style.display, + "none", + "PIN required dialog did not disappear" + ); + let pin_section = doc.getElementById("set-change-pin-section"); + isnot(pin_section.style.display, "none", "PIN section did not reappear"); +}); diff --git a/toolkit/components/aboutwebauthn/tests/browser/head.js b/toolkit/components/aboutwebauthn/tests/browser/head.js new file mode 100644 index 0000000000..f6ea51ab3a --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/head.js @@ -0,0 +1,40 @@ +function set_text(field, text) { + field.value = text; + field.dispatchEvent(new Event("input")); +} + +async function reset_about_page(doc) { + let info_text = doc.getElementById("info-text-div"); + let msg = JSON.stringify({ type: "listen-success" }); + let promise = BrowserTestUtils.waitForMutationCondition( + info_text, + { attributes: true, attributeFilter: ["hidden"] }, + () => info_text.hidden !== false + ); + Services.obs.notifyObservers(null, "about-webauthn-prompt", msg); + await promise; +} + +async function send_auth_info_and_check_categories(doc, ops) { + let info_text = doc.getElementById("info-text-div"); + let msg = JSON.stringify({ + type: "selected-device", + auth_info: { options: ops }, + }); + + let promise = BrowserTestUtils.waitForMutationCondition( + info_text, + { attributes: true, attributeFilter: ["hidden"] }, + () => info_text.hidden + ); + Services.obs.notifyObservers(null, "about-webauthn-prompt", msg); + await promise; + + // Info should be shown always, so we use it as a canary + let info_tab_button = doc.getElementById("info-tab-button"); + isnot( + info_tab_button.style.display, + "none", + "Info button in the sidebar not visible" + ); +} |