diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /security/manager/pki | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/manager/pki')
34 files changed, 4182 insertions, 0 deletions
diff --git a/security/manager/pki/components.conf b/security/manager/pki/components.conf new file mode 100644 index 0000000000..e381806925 --- /dev/null +++ b/security/manager/pki/components.conf @@ -0,0 +1,21 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{518e071f-1dd2-11b2-937e-c45f14def778}', + 'contract_ids': [ + '@mozilla.org/nsCertificateDialogs;1', + '@mozilla.org/nsClientAuthDialogs;1', + '@mozilla.org/nsGeneratingKeypairInfoDialogs;1', + '@mozilla.org/nsTokenDialogs;1', + '@mozilla.org/nsTokenPasswordDialogs;1', + ], + 'type': 'nsNSSDialogs', + 'headers': ['/security/manager/pki/nsNSSDialogs.h'], + 'init_method': 'Init', + }, +] diff --git a/security/manager/pki/moz.build b/security/manager/pki/moz.build new file mode 100644 index 0000000000..73d3d09eba --- /dev/null +++ b/security/manager/pki/moz.build @@ -0,0 +1,36 @@ +# -*- 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/. + +DIRS += ["resources"] + +UNIFIED_SOURCES += [ + "nsNSSDialogHelper.cpp", + "nsNSSDialogs.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +LOCAL_INCLUDES += [ + "!/dist/public/nss", +] + +FINAL_LIBRARY = "xul" + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += [ + "-Wextra", + # -Wextra enables this warning, but it's too noisy to be useful. + "-Wno-missing-field-initializers", + ] + + # Gecko headers aren't warning-free enough for us to enable these warnings. + CXXFLAGS += [ + "-Wno-unused-parameter", + ] + +REQUIRES_UNIFIED_BUILD = True diff --git a/security/manager/pki/nsIASN1Tree.idl b/security/manager/pki/nsIASN1Tree.idl new file mode 100644 index 0000000000..b44362e5b2 --- /dev/null +++ b/security/manager/pki/nsIASN1Tree.idl @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#include "nsISupports.idl" +#include "nsITreeView.idl" +#include "nsIX509Cert.idl" + +[scriptable, uuid(de142307-7b88-4e0a-b232-250f310e25d8)] +interface nsIASN1Tree : nsITreeView { + [must_use] + void loadASN1Structure(in nsIASN1Object asn1Object); + + [must_use] + AString getDisplayData(in unsigned long index); +}; + +%{C++ + +#define NS_ASN1TREE_CONTRACTID "@mozilla.org/security/nsASN1Tree;1" + +%} diff --git a/security/manager/pki/nsNSSDialogHelper.cpp b/security/manager/pki/nsNSSDialogHelper.cpp new file mode 100644 index 0000000000..4b204af85d --- /dev/null +++ b/security/manager/pki/nsNSSDialogHelper.cpp @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#include "nsNSSDialogHelper.h" + +#include "mozilla/dom/ScriptSettings.h" +#include "nsCOMPtr.h" +#include "nsIWindowWatcher.h" +#include "nsServiceManagerUtils.h" + +static const char kOpenDialogParam[] = "centerscreen,chrome,modal,titlebar"; +static const char kOpenWindowParam[] = "centerscreen,chrome,titlebar"; + +nsresult nsNSSDialogHelper::openDialog(mozIDOMWindowProxy* window, + const char* url, nsISupports* params, + bool modal) { + nsresult rv; + nsCOMPtr<nsIWindowWatcher> windowWatcher = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<mozIDOMWindowProxy> parent = window; + + if (!parent) { + windowWatcher->GetActiveWindow(getter_AddRefs(parent)); + } + + // We're loading XUL into this window, and it's happening on behalf of the + // system, not on behalf of content. Make sure the initial about:blank window + // gets a system principal, otherwise we'll bork when trying to wrap the + // nsIKeyGenThread |arguments| property into the unprivileged scoope. + MOZ_ASSERT(!strncmp("chrome://", url, strlen("chrome://"))); + mozilla::dom::AutoNoJSAPI nojsapi; + + nsCOMPtr<mozIDOMWindowProxy> newWindow; + rv = windowWatcher->OpenWindow( + parent, nsDependentCString(url), "_blank"_ns, + nsDependentCString(modal ? kOpenDialogParam : kOpenWindowParam), params, + getter_AddRefs(newWindow)); + return rv; +} diff --git a/security/manager/pki/nsNSSDialogHelper.h b/security/manager/pki/nsNSSDialogHelper.h new file mode 100644 index 0000000000..a7f3d5e474 --- /dev/null +++ b/security/manager/pki/nsNSSDialogHelper.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#ifndef nsNSSDialogHelper_h +#define nsNSSDialogHelper_h + +#include "nsError.h" + +class mozIDOMWindowProxy; +class nsISupports; + +/** + * Helper class that uses the window watcher service to open a standard dialog, + * with or without a parent context. + */ +class nsNSSDialogHelper { + public: + /** + * Opens a XUL dialog. + * + * @param window + * Parent window of the dialog, or nullptr to signal no parent. + * @param url + * URL to the XUL dialog. + * @param params + * Parameters to pass to the dialog. Same semantics as the + * nsIWindowWatcher.openWindow() |aArguments| parameter. + * @param modal + * true if the dialog should be modal, false otherwise. + * @return The result of opening the dialog. + */ + static nsresult openDialog(mozIDOMWindowProxy* window, const char* url, + nsISupports* params, bool modal = true); +}; + +#endif // nsNSSDialogHelper_h diff --git a/security/manager/pki/nsNSSDialogs.cpp b/security/manager/pki/nsNSSDialogs.cpp new file mode 100644 index 0000000000..54705052da --- /dev/null +++ b/security/manager/pki/nsNSSDialogs.cpp @@ -0,0 +1,313 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +/* + * Dialog services for PIP. + */ + +#include "nsNSSDialogs.h" + +#include "mozIDOMWindow.h" +#include "nsArray.h" +#include "nsComponentManagerUtils.h" +#include "nsEmbedCID.h" +#include "nsHashPropertyBag.h" +#include "nsIDialogParamBlock.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPK11Token.h" +#include "nsIPromptService.h" +#include "nsIWindowWatcher.h" +#include "nsIX509CertDB.h" +#include "nsIX509Cert.h" +#include "nsNSSDialogHelper.h" +#include "nsPromiseFlatString.h" +#include "nsString.h" +#include "nsVariant.h" + +#define PIPSTRING_BUNDLE_URL "chrome://pippki/locale/pippki.properties" + +nsNSSDialogs::nsNSSDialogs() = default; + +nsNSSDialogs::~nsNSSDialogs() = default; + +NS_IMPL_ISUPPORTS(nsNSSDialogs, nsITokenPasswordDialogs, nsICertificateDialogs, + nsIClientAuthDialogs) + +nsresult nsNSSDialogs::Init() { + nsresult rv; + + nsCOMPtr<nsIStringBundleService> service = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = service->CreateBundle(PIPSTRING_BUNDLE_URL, + getter_AddRefs(mPIPStringBundle)); + return rv; +} + +NS_IMETHODIMP +nsNSSDialogs::SetPassword(nsIInterfaceRequestor* ctx, nsIPK11Token* token, + /*out*/ bool* canceled) { + // |ctx| is allowed to be null. + NS_ENSURE_ARG(canceled); + + *canceled = false; + + // Get the parent window for the dialog + nsCOMPtr<mozIDOMWindowProxy> parent = do_GetInterface(ctx); + + nsCOMPtr<nsIDialogParamBlock> block = + do_CreateInstance(NS_DIALOGPARAMBLOCK_CONTRACTID); + if (!block) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIMutableArray> objects = nsArrayBase::Create(); + if (!objects) { + return NS_ERROR_FAILURE; + } + nsresult rv = objects->AppendElement(token); + if (NS_FAILED(rv)) { + return rv; + } + rv = block->SetObjects(objects); + if (NS_FAILED(rv)) { + return rv; + } + + rv = nsNSSDialogHelper::openDialog( + parent, "chrome://pippki/content/changepassword.xhtml", block); + + if (NS_FAILED(rv)) return rv; + + int32_t status; + + rv = block->GetInt(1, &status); + if (NS_FAILED(rv)) return rv; + + *canceled = (status == 0); + + return rv; +} + +NS_IMETHODIMP +nsNSSDialogs::ConfirmDownloadCACert(nsIInterfaceRequestor* ctx, + nsIX509Cert* cert, + /*out*/ uint32_t* trust, + /*out*/ bool* importConfirmed) { + // |ctx| is allowed to be null. + NS_ENSURE_ARG(cert); + NS_ENSURE_ARG(trust); + NS_ENSURE_ARG(importConfirmed); + + nsCOMPtr<nsIMutableArray> argArray = nsArrayBase::Create(); + if (!argArray) { + return NS_ERROR_FAILURE; + } + + nsresult rv = argArray->AppendElement(cert); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIWritablePropertyBag2> retVals = new nsHashPropertyBag(); + rv = argArray->AppendElement(retVals); + if (NS_FAILED(rv)) { + return rv; + } + + // Get the parent window for the dialog + nsCOMPtr<mozIDOMWindowProxy> parent = do_GetInterface(ctx); + rv = nsNSSDialogHelper::openDialog( + parent, "chrome://pippki/content/downloadcert.xhtml", argArray); + if (NS_FAILED(rv)) { + return rv; + } + + rv = retVals->GetPropertyAsBool(u"importConfirmed"_ns, importConfirmed); + if (NS_FAILED(rv)) { + return rv; + } + + *trust = nsIX509CertDB::UNTRUSTED; + if (!*importConfirmed) { + return NS_OK; + } + + bool trustForSSL = false; + rv = retVals->GetPropertyAsBool(u"trustForSSL"_ns, &trustForSSL); + if (NS_FAILED(rv)) { + return rv; + } + bool trustForEmail = false; + rv = retVals->GetPropertyAsBool(u"trustForEmail"_ns, &trustForEmail); + if (NS_FAILED(rv)) { + return rv; + } + + *trust |= trustForSSL ? nsIX509CertDB::TRUSTED_SSL : 0; + *trust |= trustForEmail ? nsIX509CertDB::TRUSTED_EMAIL : 0; + + return NS_OK; +} + +NS_IMETHODIMP +nsNSSDialogs::ChooseCertificate(const nsACString& hostname, int32_t port, + const nsACString& organization, + const nsACString& issuerOrg, nsIArray* certList, + /*out*/ uint32_t* selectedIndex, + /*out*/ bool* rememberClientAuthCertificate, + /*out*/ bool* certificateChosen) { + NS_ENSURE_ARG_POINTER(certList); + NS_ENSURE_ARG_POINTER(selectedIndex); + NS_ENSURE_ARG_POINTER(rememberClientAuthCertificate); + NS_ENSURE_ARG_POINTER(certificateChosen); + + *certificateChosen = false; + *rememberClientAuthCertificate = false; + + nsCOMPtr<nsIMutableArray> argArray = nsArrayBase::Create(); + if (!argArray) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIWritableVariant> hostnameVariant = new nsVariant(); + nsresult rv = hostnameVariant->SetAsAUTF8String(hostname); + if (NS_FAILED(rv)) { + return rv; + } + rv = argArray->AppendElement(hostnameVariant); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIWritableVariant> organizationVariant = new nsVariant(); + rv = organizationVariant->SetAsAUTF8String(organization); + if (NS_FAILED(rv)) { + return rv; + } + rv = argArray->AppendElement(organizationVariant); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIWritableVariant> issuerOrgVariant = new nsVariant(); + rv = issuerOrgVariant->SetAsAUTF8String(issuerOrg); + if (NS_FAILED(rv)) { + return rv; + } + rv = argArray->AppendElement(issuerOrgVariant); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIWritableVariant> portVariant = new nsVariant(); + rv = portVariant->SetAsInt32(port); + if (NS_FAILED(rv)) { + return rv; + } + rv = argArray->AppendElement(portVariant); + if (NS_FAILED(rv)) { + return rv; + } + + rv = argArray->AppendElement(certList); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIWritablePropertyBag2> retVals = new nsHashPropertyBag(); + rv = argArray->AppendElement(retVals); + if (NS_FAILED(rv)) { + return rv; + } + + rv = nsNSSDialogHelper::openDialog( + nullptr, "chrome://pippki/content/clientauthask.xhtml", argArray); + if (NS_FAILED(rv)) { + return rv; + } + + rv = retVals->GetPropertyAsBool(u"rememberSelection"_ns, + rememberClientAuthCertificate); + if (NS_FAILED(rv)) { + return rv; + } + + rv = retVals->GetPropertyAsBool(u"certChosen"_ns, certificateChosen); + if (NS_FAILED(rv)) { + return rv; + } + if (*certificateChosen) { + rv = retVals->GetPropertyAsUint32(u"selectedIndex"_ns, selectedIndex); + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsNSSDialogs::SetPKCS12FilePassword(nsIInterfaceRequestor* ctx, + /*out*/ nsAString& password, + /*out*/ bool* confirmedPassword) { + // |ctx| is allowed to be null. + NS_ENSURE_ARG(confirmedPassword); + + // Get the parent window for the dialog + nsCOMPtr<mozIDOMWindowProxy> parent = do_GetInterface(ctx); + nsCOMPtr<nsIWritablePropertyBag2> retVals = new nsHashPropertyBag(); + nsresult rv = nsNSSDialogHelper::openDialog( + parent, "chrome://pippki/content/setp12password.xhtml", retVals); + if (NS_FAILED(rv)) { + return rv; + } + + rv = retVals->GetPropertyAsBool(u"confirmedPassword"_ns, confirmedPassword); + if (NS_FAILED(rv)) { + return rv; + } + + if (!*confirmedPassword) { + return NS_OK; + } + + return retVals->GetPropertyAsAString(u"password"_ns, password); +} + +NS_IMETHODIMP +nsNSSDialogs::GetPKCS12FilePassword(nsIInterfaceRequestor* ctx, + nsAString& _password, bool* _retval) { + *_retval = false; + + nsCOMPtr<nsIPromptService> promptSvc( + do_GetService(NS_PROMPTSERVICE_CONTRACTID)); + if (!promptSvc) { + return NS_ERROR_FAILURE; + } + + nsAutoString msg; + nsresult rv = + mPIPStringBundle->GetStringFromName("getPKCS12FilePasswordMessage", msg); + if (NS_FAILED(rv)) { + return rv; + } + + // Get the parent window for the dialog + nsCOMPtr<mozIDOMWindowProxy> parent = do_GetInterface(ctx); + char16_t* pwTemp = nullptr; + rv = promptSvc->PromptPassword(parent, nullptr, msg.get(), &pwTemp, _retval); + if (NS_FAILED(rv)) { + return rv; + } + + if (*_retval) { + _password.Assign(pwTemp); + free(pwTemp); + } + + return NS_OK; +} diff --git a/security/manager/pki/nsNSSDialogs.h b/security/manager/pki/nsNSSDialogs.h new file mode 100644 index 0000000000..3c687ac667 --- /dev/null +++ b/security/manager/pki/nsNSSDialogs.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#ifndef nsNSSDialogs_h +#define nsNSSDialogs_h + +#include "nsCOMPtr.h" +#include "nsICertificateDialogs.h" +#include "nsIClientAuthDialogs.h" +#include "nsIStringBundle.h" +#include "nsITokenPasswordDialogs.h" + +#define NS_NSSDIALOGS_CID \ + { \ + 0x518e071f, 0x1dd2, 0x11b2, { \ + 0x93, 0x7e, 0xc4, 0x5f, 0x14, 0xde, 0xf7, 0x78 \ + } \ + } + +class nsNSSDialogs : public nsICertificateDialogs, + public nsIClientAuthDialogs, + public nsITokenPasswordDialogs { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITOKENPASSWORDDIALOGS + NS_DECL_NSICERTIFICATEDIALOGS + NS_DECL_NSICLIENTAUTHDIALOGS + nsNSSDialogs(); + + nsresult Init(); + + protected: + virtual ~nsNSSDialogs(); + nsCOMPtr<nsIStringBundle> mPIPStringBundle; +}; + +#endif // nsNSSDialogs_h diff --git a/security/manager/pki/resources/content/certManager.css b/security/manager/pki/resources/content/certManager.css new file mode 100644 index 0000000000..e943c721da --- /dev/null +++ b/security/manager/pki/resources/content/certManager.css @@ -0,0 +1,31 @@ +/* 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/. */ + +/* Good enough support for equalsize=always for the cert manager use cases. + * You probably shouldn't use this as-is elsewhere, this selector is somewhat + * slow, it relies on stuff having display: -moz-box, and you probably can use + * something simpler if you need this */ +[equalsize="always"] > * { + -moz-box-flex: 1; + contain: inline-size; + /* contain: inline-size should be enough, unless you have a legacy XUL layout + * box, which don't support it. */ + width: 0; +} + +#certmanager { + /* This prevents horizontal scrollbars due to <tree> and non-XUL layout + * interactions */ + padding: 0; +} + +/* This matches the <tree> height from dialog.css */ +richlistbox { + height: 15em; +} + +richlistbox, +richlistitem { + min-height: 30px; +} diff --git a/security/manager/pki/resources/content/certManager.js b/security/manager/pki/resources/content/certManager.js new file mode 100644 index 0000000000..57d0075add --- /dev/null +++ b/security/manager/pki/resources/content/certManager.js @@ -0,0 +1,824 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +const gCertFileTypes = "*.p7b; *.crt; *.cert; *.cer; *.pem; *.der"; + +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +var key; + +var certdialogs = Cc["@mozilla.org/nsCertificateDialogs;1"].getService( + Ci.nsICertificateDialogs +); + +/** + * List of certs currently selected in the active tab. + * + * @type {nsIX509Cert[]} + */ +var selected_certs = []; +var selected_tree_items = []; +var selected_index = []; +var certdb; + +/** + * Cert tree for the "Authorities" tab. + * + * @type {nsICertTree} + */ +var caTreeView; +/** + * Cert tree for the "Servers" tab. + * + * @type {nsICertTree} + */ +var serverTreeView; + +var overrideService; + +function createRichlistItem(item) { + let innerHbox = document.createXULElement("hbox"); + innerHbox.setAttribute("align", "center"); + innerHbox.setAttribute("flex", "1"); + + let row = document.createXULElement("label"); + row.setAttribute("flex", "1"); + row.setAttribute("crop", "right"); + row.setAttribute("style", "margin-inline-start: 15px;"); + if ("raw" in item) { + row.setAttribute("value", item.raw); + } else { + document.l10n.setAttributes(row, item.l10nid); + } + row.setAttribute("ordinal", "1"); + innerHbox.appendChild(row); + + return innerHbox; +} + +var serverRichList = { + richlist: undefined, + + buildRichList() { + let overrides = overrideService.getOverrides().map(item => { + let cert = null; + if (item.dbKey !== "") { + cert = certdb.findCertByDBKey(item.dbKey); + } + return { + hostPort: item.hostPort, + dbKey: item.dbKey, + asciiHost: item.asciiHost, + port: item.port, + originAttributes: item.originAttributes, + isTemporary: item.isTemporary, + displayName: cert !== null ? cert.displayName : "", + }; + }); + overrides.sort((a, b) => { + let criteria = ["hostPort", "displayName"]; + for (let c of criteria) { + let res = a[c].localeCompare(b[c]); + if (res !== 0) { + return res; + } + } + return 0; + }); + + this.richlist.textContent = ""; + this.richlist.clearSelection(); + + let frag = document.createDocumentFragment(); + for (let override of overrides) { + let richlistitem = this._richBoxAddItem(override); + frag.appendChild(richlistitem); + } + this.richlist.appendChild(frag); + + this._setButtonState(); + this.richlist.addEventListener("select", () => this._setButtonState()); + }, + + _richBoxAddItem(item) { + let richlistitem = document.createXULElement("richlistitem"); + + richlistitem.setAttribute("dbKey", item.dbKey); + richlistitem.setAttribute("host", item.asciiHost); + richlistitem.setAttribute("port", item.port); + richlistitem.setAttribute("hostPort", item.hostPort); + richlistitem.setAttribute( + "originAttributes", + JSON.stringify(item.originAttributes) + ); + + let hbox = document.createXULElement("hbox"); + hbox.setAttribute("flex", "1"); + hbox.setAttribute("equalsize", "always"); + + hbox.appendChild(createRichlistItem({ raw: item.hostPort })); + hbox.appendChild( + createRichlistItem( + item.displayName !== "" + ? { raw: item.displayName } + : { l10nid: "no-cert-stored-for-override" } + ) + ); + hbox.appendChild( + createRichlistItem({ + l10nid: item.isTemporary ? "temporary-override" : "permanent-override", + }) + ); + + richlistitem.appendChild(hbox); + + return richlistitem; + }, + + deleteSelectedRichListItem() { + let selectedItem = this.richlist.selectedItem; + if (!selectedItem) { + return; + } + + let retVals = { + deleteConfirmed: false, + }; + window.browsingContext.topChromeWindow.openDialog( + "chrome://pippki/content/deletecert.xhtml", + "", + "chrome,centerscreen,modal", + "websites_tab", + [ + { + hostPort: selectedItem.attributes.hostPort.value, + }, + ], + retVals + ); + + if (retVals.deleteConfirmed) { + overrideService.clearValidityOverride( + selectedItem.attributes.host.value, + selectedItem.attributes.port.value, + JSON.parse(selectedItem.attributes.originAttributes.value) + ); + this.buildRichList(); + } + }, + + viewSelectedRichListItem() { + let selectedItem = this.richlist.selectedItem; + if (!selectedItem) { + return; + } + + let dbKey = selectedItem.getAttribute("dbKey"); + if (dbKey) { + let cert = certdb.findCertByDBKey(dbKey); + viewCertHelper(window, cert); + } + }, + + exportSelectedRichListItem() { + let selectedItem = this.richlist.selectedItem; + if (!selectedItem) { + return; + } + + let dbKey = selectedItem.getAttribute("dbKey"); + if (dbKey) { + let cert = certdb.findCertByDBKey(dbKey); + exportToFile(window, cert); + } + }, + + addException() { + let retval = { + exceptionAdded: false, + }; + window.browsingContext.topChromeWindow.openDialog( + "chrome://pippki/content/exceptionDialog.xhtml", + "", + "chrome,centerscreen,modal", + retval + ); + if (retval.exceptionAdded) { + this.buildRichList(); + } + }, + + _setButtonState() { + let websiteViewButton = document.getElementById("websites_viewButton"); + let websiteExportButton = document.getElementById("websites_exportButton"); + let websiteDeleteButton = document.getElementById("websites_deleteButton"); + + let certKey = this.richlist.selectedItem?.getAttribute("dbKey"); + let cert = certKey && certdb.findCertByDBKey(certKey); + + websiteDeleteButton.disabled = this.richlist.selectedIndex < 0; + websiteExportButton.disabled = !cert; + websiteViewButton.disabled = websiteExportButton.disabled; + }, +}; +/** + * Cert tree for the "People" tab. + * + * @type {nsICertTree} + */ +var emailTreeView; +/** + * Cert tree for the "Your Certificates" tab. + * + * @type {nsICertTree} + */ +var userTreeView; + +var clientAuthRememberService; + +var rememberedDecisionsRichList = { + richlist: undefined, + + buildRichList() { + let rememberedDecisions = clientAuthRememberService.getDecisions(); + + let oldItems = this.richlist.querySelectorAll("richlistitem"); + for (let item of oldItems) { + item.remove(); + } + + let frag = document.createDocumentFragment(); + for (let decision of rememberedDecisions) { + let richlistitem = this._richBoxAddItem(decision); + frag.appendChild(richlistitem); + } + this.richlist.appendChild(frag); + + this.richlist.addEventListener("select", () => this.setButtonState()); + }, + + _richBoxAddItem(item) { + let richlistitem = document.createXULElement("richlistitem"); + + richlistitem.setAttribute("entryKey", item.entryKey); + richlistitem.setAttribute("dbKey", item.dbKey); + + let hbox = document.createXULElement("hbox"); + hbox.setAttribute("flex", "1"); + hbox.setAttribute("equalsize", "always"); + + hbox.appendChild(createRichlistItem({ raw: item.asciiHost })); + if (item.dbKey == "") { + hbox.appendChild( + createRichlistItem({ l10nid: "send-no-client-certificate" }) + ); + + hbox.appendChild(createRichlistItem({ raw: "" })); + } else { + let tmpCert = certdb.findCertByDBKey(item.dbKey); + // The certificate corresponding to this item's dbKey may not be + // available (for example, if it was stored on a token that's been + // removed, or if it was deleted). + if (tmpCert) { + hbox.appendChild(createRichlistItem({ raw: tmpCert.commonName })); + hbox.appendChild(createRichlistItem({ raw: tmpCert.serialNumber })); + } else { + hbox.appendChild( + createRichlistItem({ l10nid: "certificate-not-available" }) + ); + hbox.appendChild( + createRichlistItem({ l10nid: "certificate-not-available" }) + ); + } + } + + richlistitem.appendChild(hbox); + + return richlistitem; + }, + + deleteSelectedRichListItem() { + let selectedItem = this.richlist.selectedItem; + let index = this.richlist.selectedIndex; + if (index < 0) { + return; + } + + clientAuthRememberService.forgetRememberedDecision( + selectedItem.attributes.entryKey.value + ); + + this.buildRichList(); + this.setButtonState(); + }, + + viewSelectedRichListItem() { + let selectedItem = this.richlist.selectedItem; + let index = this.richlist.selectedIndex; + if (index < 0) { + return; + } + + if (selectedItem.attributes.dbKey.value != "") { + let cert = certdb.findCertByDBKey(selectedItem.attributes.dbKey.value); + viewCertHelper(window, cert); + } + }, + + setButtonState() { + let rememberedDeleteButton = document.getElementById( + "remembered_deleteButton" + ); + let rememberedViewButton = document.getElementById("remembered_viewButton"); + + rememberedDeleteButton.disabled = this.richlist.selectedIndex < 0; + rememberedViewButton.disabled = + this.richlist.selectedItem == null + ? true + : this.richlist.selectedItem.attributes.dbKey.value == ""; + }, +}; + +function LoadCerts() { + certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + var certcache = certdb.getCerts(); + + caTreeView = Cc["@mozilla.org/security/nsCertTree;1"].createInstance( + Ci.nsICertTree + ); + caTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.CA_CERT); + document.getElementById("ca-tree").view = caTreeView; + + emailTreeView = Cc["@mozilla.org/security/nsCertTree;1"].createInstance( + Ci.nsICertTree + ); + emailTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.EMAIL_CERT); + document.getElementById("email-tree").view = emailTreeView; + + userTreeView = Cc["@mozilla.org/security/nsCertTree;1"].createInstance( + Ci.nsICertTree + ); + userTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.USER_CERT); + document.getElementById("user-tree").view = userTreeView; + + clientAuthRememberService = Cc[ + "@mozilla.org/security/clientAuthRememberService;1" + ].getService(Ci.nsIClientAuthRememberService); + + overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService + ); + + rememberedDecisionsRichList.richlist = document.getElementById( + "rememberedList" + ); + serverRichList.richlist = document.getElementById("serverList"); + + rememberedDecisionsRichList.buildRichList(); + serverRichList.buildRichList(); + + rememberedDecisionsRichList.setButtonState(); + + enableBackupAllButton(); +} + +function enableBackupAllButton() { + let backupAllButton = document.getElementById("mine_backupAllButton"); + backupAllButton.disabled = userTreeView.rowCount < 1; +} + +function getSelectedCerts() { + var ca_tab = document.getElementById("ca_tab"); + var mine_tab = document.getElementById("mine_tab"); + var others_tab = document.getElementById("others_tab"); + var items = null; + if (ca_tab.selected) { + items = caTreeView.selection; + } else if (mine_tab.selected) { + items = userTreeView.selection; + } else if (others_tab.selected) { + items = emailTreeView.selection; + } + selected_certs = []; + var cert = null; + var nr = 0; + if (items != null) { + nr = items.getRangeCount(); + } + if (nr > 0) { + for (let i = 0; i < nr; i++) { + var o1 = {}; + var o2 = {}; + items.getRangeAt(i, o1, o2); + var min = o1.value; + var max = o2.value; + for (let j = min; j <= max; j++) { + if (ca_tab.selected) { + cert = caTreeView.getCert(j); + } else if (mine_tab.selected) { + cert = userTreeView.getCert(j); + } else if (others_tab.selected) { + cert = emailTreeView.getCert(j); + } + if (cert) { + var sc = selected_certs.length; + selected_certs[sc] = cert; + selected_index[sc] = j; + } + } + } + } +} + +function getSelectedTreeItems() { + var ca_tab = document.getElementById("ca_tab"); + var mine_tab = document.getElementById("mine_tab"); + var others_tab = document.getElementById("others_tab"); + var items = null; + if (ca_tab.selected) { + items = caTreeView.selection; + } else if (mine_tab.selected) { + items = userTreeView.selection; + } else if (others_tab.selected) { + items = emailTreeView.selection; + } + selected_certs = []; + selected_tree_items = []; + selected_index = []; + var tree_item = null; + var nr = 0; + if (items != null) { + nr = items.getRangeCount(); + } + if (nr > 0) { + for (let i = 0; i < nr; i++) { + var o1 = {}; + var o2 = {}; + items.getRangeAt(i, o1, o2); + var min = o1.value; + var max = o2.value; + for (let j = min; j <= max; j++) { + if (ca_tab.selected) { + tree_item = caTreeView.getTreeItem(j); + } else if (mine_tab.selected) { + tree_item = userTreeView.getTreeItem(j); + } else if (others_tab.selected) { + tree_item = emailTreeView.getTreeItem(j); + } + if (tree_item) { + var sc = selected_tree_items.length; + selected_tree_items[sc] = tree_item; + selected_index[sc] = j; + } + } + } + } +} + +/** + * Returns true if nothing in the given cert tree is selected or if the + * selection includes a container. Returns false otherwise. + * + * @param {nsICertTree} certTree + * @returns {boolean} + */ +function nothingOrContainerSelected(certTree) { + var certTreeSelection = certTree.selection; + var numSelectionRanges = certTreeSelection.getRangeCount(); + + if (numSelectionRanges == 0) { + return true; + } + + for (var i = 0; i < numSelectionRanges; i++) { + var o1 = {}; + var o2 = {}; + certTreeSelection.getRangeAt(i, o1, o2); + var minIndex = o1.value; + var maxIndex = o2.value; + for (var j = minIndex; j <= maxIndex; j++) { + if (certTree.isContainer(j)) { + return true; + } + } + } + + return false; +} + +async function promptError(aErrorCode) { + if (aErrorCode != Ci.nsIX509CertDB.Success) { + let msgName = "pkcs12-unknown-err"; + switch (aErrorCode) { + case Ci.nsIX509CertDB.ERROR_PKCS12_NOSMARTCARD_EXPORT: + msgName = "pkcs12-info-no-smartcard-backup"; + break; + case Ci.nsIX509CertDB.ERROR_PKCS12_RESTORE_FAILED: + msgName = "pkcs12-unknown-err-restore"; + break; + case Ci.nsIX509CertDB.ERROR_PKCS12_BACKUP_FAILED: + msgName = "pkcs12-unknown-err-backup"; + break; + case Ci.nsIX509CertDB.ERROR_PKCS12_CERT_COLLISION: + case Ci.nsIX509CertDB.ERROR_PKCS12_DUPLICATE_DATA: + msgName = "pkcs12-dup-data"; + break; + case Ci.nsIX509CertDB.ERROR_BAD_PASSWORD: + msgName = "pk11-bad-password"; + break; + case Ci.nsIX509CertDB.ERROR_DECODE_ERROR: + msgName = "pkcs12-decode-err"; + break; + default: + break; + } + let [message] = await document.l10n.formatValues([{ id: msgName }]); + let prompter = Services.ww.getNewPrompter(window); + prompter.alert(null, message); + } +} + +/** + * Enables or disables buttons corresponding to a cert tree depending on what + * is selected in the cert tree. + * + * @param {nsICertTree} certTree + * @param {Array} idList A list of string identifiers for button elements to + * enable or disable. + */ +function enableButtonsForCertTree(certTree, idList) { + let disableButtons = nothingOrContainerSelected(certTree); + + for (let id of idList) { + document.getElementById(id).setAttribute("disabled", disableButtons); + } +} + +function ca_enableButtons() { + let idList = [ + "ca_viewButton", + "ca_editButton", + "ca_exportButton", + "ca_deleteButton", + ]; + enableButtonsForCertTree(caTreeView, idList); +} + +function mine_enableButtons() { + let idList = ["mine_viewButton", "mine_backupButton", "mine_deleteButton"]; + enableButtonsForCertTree(userTreeView, idList); +} + +function email_enableButtons() { + let idList = ["email_viewButton", "email_exportButton", "email_deleteButton"]; + enableButtonsForCertTree(emailTreeView, idList); +} + +async function backupCerts() { + getSelectedCerts(); + var numcerts = selected_certs.length; + if (numcerts == 0) { + return; + } + + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [backupFileDialog, filePkcs12Spec] = await document.l10n.formatValues([ + { id: "choose-p12-backup-file-dialog" }, + { id: "file-browse-pkcs12-spec" }, + ]); + fp.init(window, backupFileDialog, Ci.nsIFilePicker.modeSave); + fp.appendFilter(filePkcs12Spec, "*.p12"); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.defaultExtension = "p12"; + fp.open(rv => { + if ( + rv == Ci.nsIFilePicker.returnOK || + rv == Ci.nsIFilePicker.returnReplace + ) { + let password = {}; + if (certdialogs.setPKCS12FilePassword(window, password)) { + let errorCode = certdb.exportPKCS12File( + fp.file, + selected_certs, + password.value + ); + promptError(errorCode); + } + } + }); +} + +function backupAllCerts() { + // Select all rows, then call doBackup() + userTreeView.selection.selectAll(); + backupCerts(); +} + +function editCerts() { + getSelectedCerts(); + + for (let cert of selected_certs) { + window.browsingContext.topChromeWindow.openDialog( + "chrome://pippki/content/editcacert.xhtml", + "", + "chrome,centerscreen,modal", + cert + ); + } +} + +async function restoreCerts() { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [ + restoreFileDialog, + filePkcs12Spec, + fileCertSpec, + ] = await document.l10n.formatValues([ + { id: "choose-p12-restore-file-dialog" }, + { id: "file-browse-pkcs12-spec" }, + { id: "file-browse-certificate-spec" }, + ]); + fp.init(window, restoreFileDialog, Ci.nsIFilePicker.modeOpen); + fp.appendFilter(filePkcs12Spec, "*.p12; *.pfx"); + fp.appendFilter(fileCertSpec, gCertFileTypes); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + + // If this is an X509 user certificate, import it as one. + + var isX509FileType = false; + var fileTypesList = gCertFileTypes.slice(1).split("; *"); + for (var type of fileTypesList) { + if (fp.file.path.endsWith(type)) { + isX509FileType = true; + break; + } + } + + if (isX509FileType) { + let fstream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fstream.init(fp.file, -1, 0, 0); + let dataString = NetUtil.readInputStreamToString( + fstream, + fstream.available() + ); + let dataArray = []; + for (let i = 0; i < dataString.length; i++) { + dataArray.push(dataString.charCodeAt(i)); + } + fstream.close(); + let prompter = Services.ww.getNewPrompter(window); + let interfaceRequestor = { + getInterface() { + return prompter; + }, + }; + certdb.importUserCertificate( + dataArray, + dataArray.length, + interfaceRequestor + ); + } else { + // Otherwise, assume it's a PKCS12 file and import it that way. + let password = {}; + let errorCode = Ci.nsIX509CertDB.ERROR_BAD_PASSWORD; + while ( + errorCode == Ci.nsIX509CertDB.ERROR_BAD_PASSWORD && + certdialogs.getPKCS12FilePassword(window, password) + ) { + errorCode = certdb.importPKCS12File(fp.file, password.value); + if ( + errorCode == Ci.nsIX509CertDB.ERROR_BAD_PASSWORD && + !password.value.length + ) { + // It didn't like empty string password, try no password. + errorCode = certdb.importPKCS12File(fp.file, null); + } + promptError(errorCode); + } + } + + var certcache = certdb.getCerts(); + userTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.USER_CERT); + userTreeView.selection.clearSelection(); + caTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.CA_CERT); + caTreeView.selection.clearSelection(); + enableBackupAllButton(); + }); +} + +async function exportCerts() { + getSelectedCerts(); + + for (let cert of selected_certs) { + await exportToFile(window, cert); + } +} + +/** + * Deletes the selected certs in the active tab. + */ +function deleteCerts() { + getSelectedTreeItems(); + let numcerts = selected_tree_items.length; + if (numcerts == 0) { + return; + } + + const treeViewMap = { + mine_tab: userTreeView, + ca_tab: caTreeView, + others_tab: emailTreeView, + }; + let selTab = document.getElementById("certMgrTabbox").selectedItem; + let selTabID = selTab.getAttribute("id"); + + if (!(selTabID in treeViewMap)) { + return; + } + + let retVals = { + deleteConfirmed: false, + }; + window.browsingContext.topChromeWindow.openDialog( + "chrome://pippki/content/deletecert.xhtml", + "", + "chrome,centerscreen,modal", + selTabID, + selected_tree_items, + retVals + ); + + if (retVals.deleteConfirmed) { + let treeView = treeViewMap[selTabID]; + + for (let t = numcerts - 1; t >= 0; t--) { + treeView.deleteEntryObject(selected_index[t]); + } + + selected_tree_items = []; + selected_index = []; + treeView.selection.clearSelection(); + if (selTabID == "mine_tab") { + enableBackupAllButton(); + } + } +} + +function viewCerts() { + getSelectedCerts(); + + for (let cert of selected_certs) { + viewCertHelper(window, cert); + } +} + +async function addCACerts() { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [importCa, fileCertSpec] = await document.l10n.formatValues([ + { id: "import-ca-certs-prompt" }, + { id: "file-browse-certificate-spec" }, + ]); + fp.init(window, importCa, Ci.nsIFilePicker.modeOpen); + fp.appendFilter(fileCertSpec, gCertFileTypes); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.open(rv => { + if (rv == Ci.nsIFilePicker.returnOK) { + certdb.importCertsFromFile(fp.file, Ci.nsIX509Cert.CA_CERT); + let certcache = certdb.getCerts(); + caTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.CA_CERT); + caTreeView.selection.clearSelection(); + } + }); +} + +async function addEmailCert() { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [importEmail, fileCertSpec] = await document.l10n.formatValues([ + { id: "import-email-cert-prompt" }, + { id: "file-browse-certificate-spec" }, + ]); + fp.init(window, importEmail, Ci.nsIFilePicker.modeOpen); + fp.appendFilter(fileCertSpec, gCertFileTypes); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.open(rv => { + if (rv == Ci.nsIFilePicker.returnOK) { + certdb.importCertsFromFile(fp.file, Ci.nsIX509Cert.EMAIL_CERT); + var certcache = certdb.getCerts(); + emailTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.EMAIL_CERT); + emailTreeView.selection.clearSelection(); + caTreeView.loadCertsFromCache(certcache, Ci.nsIX509Cert.CA_CERT); + caTreeView.selection.clearSelection(); + } + }); +} diff --git a/security/manager/pki/resources/content/certManager.xhtml b/security/manager/pki/resources/content/certManager.xhtml new file mode 100644 index 0000000000..6e90b57b1d --- /dev/null +++ b/security/manager/pki/resources/content/certManager.xhtml @@ -0,0 +1,225 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://pippki/content/certManager.css" type="text/css"?> + +<!DOCTYPE window> + +<window windowtype="mozilla:certmanager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="certmgr-title" + onload="LoadCerts();" + persist="screenX screenY width height"> +<dialog id="certmanager" + buttons="accept"> + + <linkset> + <html:link rel="localization" href="security/certificates/certManager.ftl"/> + </linkset> + + <script src="chrome://pippki/content/pippki.js"/> + <script src="chrome://pippki/content/certManager.js"/> + + <tabbox id="certmanagertabs" flex="1" persist="selectedIndex"> + <tabs id="certMgrTabbox"> + <tab id="mine_tab" data-l10n-id="certmgr-tab-mine"/> + <tab id="remembered_tab" data-l10n-id="certmgr-tab-remembered"/> + <tab id="others_tab" data-l10n-id="certmgr-tab-people"/> + <tab id="websites_tab" data-l10n-id="certmgr-tab-servers"/> + <tab id="ca_tab" data-l10n-id="certmgr-tab-ca" selected="true"/> + </tabs> + <tabpanels flex="1"> + <vbox id="myCerts" flex="1"> + <description data-l10n-id="certmgr-mine"></description> + <separator class="thin"/> + <tree id="user-tree" flex="1" enableColumnDrag="true" + onselect="mine_enableButtons()"> + <treecols> + <!-- + The below code may suggest that 'ordinal' is still a supported XUL + XUL attribute. It is not. This is a crutch so that we can + continue persisting the CSS -moz-box-ordinal-group attribute, + which is the appropriate replacement for the ordinal attribute + but cannot yet be easily persisted. The code that synchronizes + the attribute with the CSS lives in + toolkit/content/widget/tree.js and is specific to tree elements. + --> + <treecol id="certcol" data-l10n-id="certmgr-cert-name" primary="true" + persist="hidden width ordinal" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="tokencol" data-l10n-id="certmgr-token-name" + persist="hidden width ordinal" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="serialnumcol" data-l10n-id="certmgr-serial" + persist="hidden width ordinal" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="issuedcol" data-l10n-id="certmgr-begins-label" + hidden="true" persist="hidden width ordinal" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="expiredcol" data-l10n-id="certmgr-expires-label" + persist="hidden width ordinal" flex="1"/> + </treecols> + <treechildren ondblclick="viewCerts();"/> + </tree> + + <separator class="thin"/> + + <hbox> + <button id="mine_viewButton" class="normal" + data-l10n-id="certmgr-view" + disabled="true" oncommand="viewCerts();"/> + <button id="mine_backupButton" class="normal" + data-l10n-id="certmgr-backup" + disabled="true" oncommand="backupCerts();"/> + <button id="mine_backupAllButton" class="normal" + data-l10n-id="certmgr-backup-all" + oncommand="backupAllCerts();"/> + <button id="mine_restoreButton" class="normal" + data-l10n-id="certmgr-restore" + oncommand="restoreCerts();"/> + <button id="mine_deleteButton" class="normal" + data-l10n-id="certmgr-delete" + disabled="true" oncommand="deleteCerts();"/> + </hbox> + </vbox> + <vbox id="rememberedCerts" flex="1"> + <description data-l10n-id="certmgr-remembered"></description> + <separator class="thin"/> + + <listheader equalsize="always"> + <treecol id="hostcol" data-l10n-id="certmgr-cert-host" primary="true" + persist="hidden width ordinal" flex="1"/> + <treecol id="certcol" data-l10n-id="certmgr-cert-name" primary="true" + persist="hidden width ordinal" flex="1"/> + <treecol id="serialnumcol" data-l10n-id="certmgr-serial" + persist="hidden width ordinal" flex="1"/> + </listheader> + <richlistbox id="rememberedList" flex="1" selected="false"/> + + <separator class="thin"/> + + <hbox> + <button id="remembered_deleteButton" class="normal" + data-l10n-id="certmgr-delete" + + oncommand="rememberedDecisionsRichList.deleteSelectedRichListItem()"/> + + <button id="remembered_viewButton" class="normal" + data-l10n-id="certmgr-view" + + oncommand="rememberedDecisionsRichList.viewSelectedRichListItem()"/> + </hbox> + </vbox> + <vbox id="othersCerts" flex="1"> + <description data-l10n-id="certmgr-people"></description> + <separator class="thin"/> + <tree id="email-tree" flex="1" + onselect="email_enableButtons()"> + <treecols> + <treecol id="certcol" data-l10n-id="certmgr-cert-name" primary="true" + flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="emailcol" data-l10n-id="certmgr-email" + flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="expiredcol" data-l10n-id="certmgr-expires-label" + flex="1"/> + </treecols> + <treechildren flex="1" ondblclick="viewCerts();"/> + </tree> + + <separator class="thin"/> + + <hbox> + <button id="email_viewButton" + data-l10n-id="certmgr-view" + disabled="true" oncommand="viewCerts();"/> + <button id="email_addButton" + data-l10n-id="certmgr-restore" + oncommand="addEmailCert();"/> + <button id="email_exportButton" + data-l10n-id="certmgr-export" + disabled="true" oncommand="exportCerts();"/> + <button id="email_deleteButton" + data-l10n-id="certmgr-delete" + disabled="true" oncommand="deleteCerts();"/> + </hbox> + </vbox> + + <vbox id="webCerts" flex="1"> + <description data-l10n-id="certmgr-server"></description> + <separator class="thin"/> + + <listheader equalsize="always"> + <treecol id="sitecol" data-l10n-id="certmgr-cert-server" primary="true" flex="1"/> + <treecol id="certcol" data-l10n-id="certmgr-cert-name" flex="1"/> + <treecol id="lifetimecol" data-l10n-id="certmgr-override-lifetime" flex="1"/> + </listheader> + <richlistbox ondblclick="serverRichList.viewSelectedRichListItem();" id="serverList" flex="1" selected="false"/> + + <separator class="thin"/> + + <hbox> + <button id="websites_viewButton" + data-l10n-id="certmgr-view" oncommand="serverRichList.viewSelectedRichListItem();"/> + <button id="websites_exportButton" + data-l10n-id="certmgr-export" oncommand="serverRichList.exportSelectedRichListItem();"/> + <button id="websites_deleteButton" + data-l10n-id="certmgr-delete" oncommand="serverRichList.deleteSelectedRichListItem();"/> + <button id="websites_exceptionButton" + data-l10n-id="certmgr-add-exception" + oncommand="serverRichList.addException();"/> + </hbox> + </vbox> + <vbox id="CACerts" flex="1"> + <description data-l10n-id="certmgr-ca"></description> + <separator class="thin"/> + <tree id="ca-tree" flex="1" enableColumnDrag="true" + onselect="ca_enableButtons()"> + <treecols> + <!-- + The below code may suggest that 'ordinal' is still a supported XUL + XUL attribute. It is not. This is a crutch so that we can + continue persisting the CSS -moz-box-ordinal-group attribute, + which is the appropriate replacement for the ordinal attribute + but cannot yet be easily persisted. The code that synchronizes + the attribute with the CSS lives in + toolkit/content/widget/tree.js and is specific to tree elements. + --> + <treecol id="certcol" data-l10n-id="certmgr-cert-name" primary="true" + persist="hidden width ordinal" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="tokencol" data-l10n-id="certmgr-token-name" + persist="hidden width ordinal" flex="1"/> + </treecols> + <treechildren ondblclick="viewCerts();"/> + </tree> + + <separator class="thin"/> + + <hbox> + <button id="ca_viewButton" + data-l10n-id="certmgr-view" + disabled="true" oncommand="viewCerts();"/> + <button id="ca_editButton" + data-l10n-id="certmgr-edit" + disabled="true" oncommand="editCerts();"/> + <button id="ca_addButton" + data-l10n-id="certmgr-restore" + oncommand="addCACerts();"/> + <button id="ca_exportButton" + data-l10n-id="certmgr-export" + disabled="true" oncommand="exportCerts();"/> + <button id="ca_deleteButton" + data-l10n-id="certmgr-delete-builtin" + disabled="true" oncommand="deleteCerts();"/> + </hbox> + </vbox> + </tabpanels> + </tabbox> +</dialog> +</window> diff --git a/security/manager/pki/resources/content/changepassword.js b/security/manager/pki/resources/content/changepassword.js new file mode 100644 index 0000000000..e3d6b118bf --- /dev/null +++ b/security/manager/pki/resources/content/changepassword.js @@ -0,0 +1,212 @@ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyGetter( + this, + "l10n", + () => new Localization(["security/pippki/pippki.ftl"], true) +); + +var params; +var token; +var pw1; + +function doPrompt(messageL10nId) { + let msg = l10n.formatValueSync(messageL10nId); + Services.prompt.alert(window, null, msg); +} + +function onLoad() { + document.getElementById("set_password").getButton("accept").disabled = true; + document.addEventListener("dialogaccept", setPassword); + + pw1 = document.getElementById("pw1"); + params = window.arguments[0].QueryInterface(Ci.nsIDialogParamBlock); + token = params.objects.GetElementAt(0).QueryInterface(Ci.nsIPK11Token); + + document.l10n.setAttributes( + document.getElementById("tokenName"), + "change-password-token", + { tokenName: token.tokenName } + ); + process(); +} + +function process() { + let bundle = document.getElementById("pippki_bundle"); + let oldpwbox = document.getElementById("oldpw"); + let msgBox = document.getElementById("message"); + // If the token is unitialized, don't use the old password box. + // Otherwise, do. + if ((token.needsLogin() && token.needsUserInit) || !token.needsLogin()) { + oldpwbox.hidden = true; + msgBox.setAttribute("value", bundle.getString("password_not_set")); + msgBox.hidden = false; + + if (!token.needsLogin()) { + oldpwbox.setAttribute("inited", "empty"); + } else { + oldpwbox.setAttribute("inited", "true"); + } + + // Select first password field + document.getElementById("pw1").focus(); + } else { + // Select old password field + oldpwbox.hidden = false; + msgBox.hidden = true; + oldpwbox.setAttribute("inited", "false"); + oldpwbox.focus(); + } + + // Return value 0 means "canceled" + params.SetInt(1, 0); + + checkPasswords(); +} + +function setPassword(event) { + var oldpwbox = document.getElementById("oldpw"); + var initpw = oldpwbox.getAttribute("inited"); + + var success = false; + + if (initpw == "false" || initpw == "empty") { + try { + var oldpw = ""; + var passok = 0; + + if (initpw == "empty") { + passok = 1; + } else { + oldpw = oldpwbox.value; + passok = token.checkPassword(oldpw); + } + + if (passok) { + if (initpw == "empty" && pw1.value == "") { + // checkPasswords() should have prevented this path from being reached. + } else { + if (pw1.value == "") { + var secmoddb = Cc[ + "@mozilla.org/security/pkcs11moduledb;1" + ].getService(Ci.nsIPKCS11ModuleDB); + if (secmoddb.isFIPSEnabled) { + // empty passwords are not allowed in FIPS mode + doPrompt("pippki-pw-change2empty-in-fips-mode"); + passok = 0; + } + } + if (passok) { + token.changePassword(oldpw, pw1.value); + if (pw1.value == "") { + doPrompt("pippki-pw-erased-ok"); + } else { + doPrompt("pippki-pw-change-ok"); + } + success = true; + } + } + } else { + oldpwbox.focus(); + oldpwbox.setAttribute("value", ""); + doPrompt("pippki-incorrect-pw"); + } + } catch (e) { + doPrompt("pippki-failed-pw-change"); + } + } else { + token.initPassword(pw1.value); + if (pw1.value == "") { + doPrompt("pippki-pw-not-wanted"); + } + success = true; + } + + if (success && params) { + // Return value 1 means "successfully executed ok" + params.SetInt(1, 1); + } + + // Terminate dialog + if (!success) { + event.preventDefault(); + } +} + +function setPasswordStrength() { + // We weigh the quality of the password by checking the number of: + // - Characters + // - Numbers + // - Non-alphanumeric chars + // - Upper and lower case characters + + let pw = document.getElementById("pw1").value; + + let pwlength = pw.length; + if (pwlength > 5) { + pwlength = 5; + } + + let numnumeric = pw.replace(/[0-9]/g, ""); + let numeric = pw.length - numnumeric.length; + if (numeric > 3) { + numeric = 3; + } + + let symbols = pw.replace(/\W/g, ""); + let numsymbols = pw.length - symbols.length; + if (numsymbols > 3) { + numsymbols = 3; + } + + let numupper = pw.replace(/[A-Z]/g, ""); + let upper = pw.length - numupper.length; + if (upper > 3) { + upper = 3; + } + + let pwstrength = + pwlength * 10 - 20 + numeric * 10 + numsymbols * 15 + upper * 10; + + // Clamp strength to [0, 100]. + if (pwstrength < 0) { + pwstrength = 0; + } + if (pwstrength > 100) { + pwstrength = 100; + } + + let meter = document.getElementById("pwmeter"); + meter.setAttribute("value", pwstrength); +} + +function checkPasswords() { + let pw1 = document.getElementById("pw1").value; + let pw2 = document.getElementById("pw2").value; + + var oldpwbox = document.getElementById("oldpw"); + if (oldpwbox) { + var initpw = oldpwbox.getAttribute("inited"); + + if (initpw == "empty" && pw1 == "") { + // The token has already been initialized, therefore this dialog + // was called with the intention to change the password. + // The token currently uses an empty password. + // We will not allow changing the password from empty to empty. + document + .getElementById("set_password") + .getButton("accept").disabled = true; + return; + } + } + + document.getElementById("set_password").getButton("accept").disabled = + pw1 != pw2; +} diff --git a/security/manager/pki/resources/content/changepassword.xhtml b/security/manager/pki/resources/content/changepassword.xhtml new file mode 100644 index 0000000000..850fb01108 --- /dev/null +++ b/security/manager/pki/resources/content/changepassword.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window + data-l10n-id="change-device-password-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();"> +<dialog id="set_password" + buttons="accept,cancel"> + +<linkset> + <html:link rel="localization" href="security/pippki/pippki.ftl"/> +</linkset> + +<stringbundle id="pippki_bundle" src="chrome://pippki/locale/pippki.properties"/> + +<script src="chrome://global/content/globalOverlay.js"/> +<script src="chrome://global/content/editMenuOverlay.js"/> + +<script src="chrome://pippki/content/changepassword.js"/> + +<hbox align="center"> + <label id="tokenName" data-l10n-id="change-password-token" data-l10n-args='{"tokenName":""}'/> +</hbox> + +<separator/> + +<vbox> + <hbox class="input-row"> + <label flex="1" data-l10n-id="change-password-old" /> + <html:input id="oldpw" type="password"/> + <!-- This textbox is inserted as a workaround to the fact that making the 'type' + & 'disabled' property of the 'oldpw' textbox toggle between ['password' & + 'false'] and ['text' & 'true'] - as would be necessary if the menu has more + than one tokens, some initialized and some not - does not work properly. So, + either the textbox 'oldpw' or the textbox 'message' would be displayed, + depending on the state of the token selected + --> + <html:input id="message" disabled="true" /> + </hbox> + <hbox class="input-row"> + <label flex="1" data-l10n-id="change-password-new" /> + <html:input id="pw1" type="password" + oninput="setPasswordStrength(); checkPasswords();"/> + </hbox> + <hbox class="input-row"> + <label flex="1" data-l10n-id="change-password-reenter" /> + <html:input id="pw2" type="password" oninput="checkPasswords();"/> + </hbox> +</vbox> + +<vbox style="margin: 6px;"> + <label for="pwmeter" style="display: -moz-box;" data-l10n-id="password-quality-meter" /> + <html:progress id="pwmeter" value="0" max="100"/> +</vbox> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/clientauthask.js b/security/manager/pki/resources/content/clientauthask.js new file mode 100644 index 0000000000..da9fc07abc --- /dev/null +++ b/security/manager/pki/resources/content/clientauthask.js @@ -0,0 +1,200 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- + * + * 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-globals-from pippki.js */ +"use strict"; + +const { asn1js } = ChromeUtils.import( + "chrome://global/content/certviewer/asn1js_bundle.jsm" +); +const { pkijs } = ChromeUtils.import( + "chrome://global/content/certviewer/pkijs_bundle.jsm" +); +const { pvutils } = ChromeUtils.import( + "chrome://global/content/certviewer/pvutils_bundle.jsm" +); + +const { Integer, fromBER } = asn1js.asn1js; +const { Certificate } = pkijs.pkijs; +const { fromBase64, stringToArrayBuffer } = pvutils.pvutils; + +const { certDecoderInitializer } = ChromeUtils.import( + "chrome://global/content/certviewer/certDecoder.jsm" +); +const { parse, pemToDER } = certDecoderInitializer( + Integer, + fromBER, + Certificate, + fromBase64, + stringToArrayBuffer, + crypto +); + +/** + * @file Implements the functionality of clientauthask.xhtml: a dialog that allows + * a user pick a client certificate for TLS client authentication. + * @param {string} window.arguments.0 + * The hostname of the server requesting client authentication. + * @param {string} window.arguments.1 + * The Organization of the server cert. + * @param {string} window.arguments.2 + * The Organization of the issuer of the server cert. + * @param {number} window.arguments.3 + * The port of the server. + * @param {nsISupports} window.arguments.4 + * List of certificates the user can choose from, queryable to + * nsIArray<nsIX509Cert>. + * @param {nsISupports} window.arguments.5 + * Object to set the return values of calling the dialog on, queryable + * to the underlying type of ClientAuthAskReturnValues. + */ + +/** + * @typedef ClientAuthAskReturnValues + * @type {nsIWritablePropertyBag2} + * @property {boolean} certChosen + * Set to true if the user chose a cert and accepted the dialog, false + * otherwise. + * @property {boolean} rememberSelection + * Set to true if the user wanted their cert selection to be + * remembered, false otherwise. + * @property {number} selectedIndex + * The index the chosen cert is at for the given cert list. Undefined + * value if |certChosen| is not true. + */ + +/** + * The pippki <stringbundle> element. + * + * @type {stringbundle} + * @see {@link toolkit/content/widgets/stringbundle.js} + */ +var bundle; +/** + * The array of certs the user can choose from. + * + * @type {nsIArray<nsIX509Cert>} + */ +var certArray; +/** + * The checkbox storing whether the user wants to remember the selected cert. + * + * @type {HTMLInputElement} Element checkbox, has to have |checked| property. + */ +var rememberBox; + +async function onLoad() { + bundle = document.getElementById("pippki_bundle"); + let rememberSetting = Services.prefs.getBoolPref( + "security.remember_cert_checkbox_default_setting" + ); + + rememberBox = document.getElementById("rememberBox"); + rememberBox.label = bundle.getString("clientAuthRemember"); + rememberBox.checked = rememberSetting; + + let hostname = window.arguments[0]; + let org = window.arguments[1]; + let issuerOrg = window.arguments[2]; + let port = window.arguments[3]; + let formattedOrg = bundle.getFormattedString("clientAuthMessage1", [org]); + let formattedIssuerOrg = bundle.getFormattedString("clientAuthMessage2", [ + issuerOrg, + ]); + let formattedHostnameAndPort = bundle.getFormattedString( + "clientAuthHostnameAndPort", + [hostname, port.toString()] + ); + setText("hostname", formattedHostnameAndPort); + setText("organization", formattedOrg); + setText("issuer", formattedIssuerOrg); + + let selectElement = document.getElementById("nicknames"); + certArray = window.arguments[4].QueryInterface(Ci.nsIArray); + for (let i = 0; i < certArray.length; i++) { + let menuItemNode = document.createXULElement("menuitem"); + let cert = certArray.queryElementAt(i, Ci.nsIX509Cert); + let nickAndSerial = bundle.getFormattedString("clientAuthNickAndSerial", [ + cert.displayName, + cert.serialNumber, + ]); + menuItemNode.setAttribute("value", i); + menuItemNode.setAttribute("label", nickAndSerial); // This is displayed. + selectElement.menupopup.appendChild(menuItemNode); + if (i == 0) { + selectElement.selectedItem = menuItemNode; + } + } + + await setDetails(); + document.addEventListener("dialogaccept", doOK); + document.addEventListener("dialogcancel", doCancel); + + Services.obs.notifyObservers( + document.getElementById("certAuthAsk"), + "cert-dialog-loaded" + ); +} + +/** + * Populates the details section with information concerning the selected cert. + */ +async function setDetails() { + let index = parseInt(document.getElementById("nicknames").value); + let cert = certArray.queryElementAt(index, Ci.nsIX509Cert); + + const formatter = new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "long", + }); + let detailLines = [ + bundle.getFormattedString("clientAuthIssuedTo", [cert.subjectName]), + bundle.getFormattedString("clientAuthSerial", [cert.serialNumber]), + bundle.getFormattedString("clientAuthValidityPeriod", [ + formatter.format(new Date(cert.validity.notBefore / 1000)), + formatter.format(new Date(cert.validity.notAfter / 1000)), + ]), + ]; + let parsedCert = await parse(pemToDER(cert.getBase64DERString())); + let keyUsages = parsedCert.ext.keyUsages; + if (keyUsages && keyUsages.purposes.length) { + detailLines.push( + bundle.getFormattedString("clientAuthKeyUsages", [keyUsages.purposes]) + ); + } + let emailAddresses = cert.getEmailAddresses(); + if (emailAddresses.length) { + let joinedAddresses = emailAddresses.join(", "); + detailLines.push( + bundle.getFormattedString("clientAuthEmailAddresses", [joinedAddresses]) + ); + } + detailLines.push( + bundle.getFormattedString("clientAuthIssuedBy", [cert.issuerName]) + ); + detailLines.push( + bundle.getFormattedString("clientAuthStoredOn", [cert.tokenName]) + ); + + document.getElementById("details").value = detailLines.join("\n"); +} + +async function onCertSelected() { + await setDetails(); +} + +function doOK() { + let retVals = window.arguments[5].QueryInterface(Ci.nsIWritablePropertyBag2); + retVals.setPropertyAsBool("certChosen", true); + let index = parseInt(document.getElementById("nicknames").value); + retVals.setPropertyAsUint32("selectedIndex", index); + retVals.setPropertyAsBool("rememberSelection", rememberBox.checked); +} + +function doCancel() { + let retVals = window.arguments[5].QueryInterface(Ci.nsIWritablePropertyBag2); + retVals.setPropertyAsBool("certChosen", false); + retVals.setPropertyAsBool("rememberSelection", rememberBox.checked); +} diff --git a/security/manager/pki/resources/content/clientauthask.xhtml b/security/manager/pki/resources/content/clientauthask.xhtml new file mode 100644 index 0000000000..94645e7ba0 --- /dev/null +++ b/security/manager/pki/resources/content/clientauthask.xhtml @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window data-l10n-id="client-auth-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();"> +<dialog id="certAuthAsk" + buttons="accept,cancel"> + +<linkset> + <html:link rel="localization" href="security/pippki/pippki.ftl"/> +</linkset> + +<stringbundleset id="stringbundleset"> + <stringbundle id="pippki_bundle" src="chrome://pippki/locale/pippki.properties"/> +</stringbundleset> + +<script src="chrome://pippki/content/pippki.js"/> +<script src="chrome://pippki/content/clientauthask.js"/> +<script src="chrome://global/content/globalOverlay.js"/> +<script src="chrome://global/content/editMenuOverlay.js"/> + +<description style="font-weight: bold;" data-l10n-id="client-auth-site-description"></description> +<description id="hostname"/> +<description id="organization"/> +<description id="issuer"/> + +<description style="font-weight: bold;" data-l10n-id="client-auth-choose-cert"></description> +<!-- The items in this menulist must never be sorted, + but remain in the order filled by the application +--> +<menulist id="nicknames" oncommand="onCertSelected();" native="true"> + <menupopup/> +</menulist> +<description data-l10n-id="client-auth-cert-details"></description> +<html:textarea readonly="readonly" id="details" style="height: 11em;"/> +<checkbox id="rememberBox" checked="true" native="true"/> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/deletecert.js b/security/manager/pki/resources/content/deletecert.js new file mode 100644 index 0000000000..7d926863ed --- /dev/null +++ b/security/manager/pki/resources/content/deletecert.js @@ -0,0 +1,121 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +/** + * @file Implements the functionality of deletecert.xhtml: a dialog that allows a + * user to confirm whether to delete certain certificates. + * @param {string} window.arguments.0 + * One of the tab IDs listed in certManager.xhtml. + * @param {object[]} window.arguments.1 + * An array of objects representing the certs to delete. + * Each must have a 'cert' property or a 'hostPort' property. + * @param {DeleteCertReturnValues} window.arguments.2 + * Object holding the return values of calling the dialog. + */ + +/** + * @typedef DeleteCertReturnValues + * @type {object} + * @property {boolean} deleteConfirmed + * Set to true if the user confirmed deletion of the given certs, + * false otherwise. + */ + +/** + * Returns the element to represent the given cert to delete. + * + * @param {object} certToDelete + * The item to represent. + * @returns {Element} + * A element of each cert tree item. + */ +function getLabelForCertToDelete(certToDelete) { + let element = document.createXULElement("label"); + let cert = certToDelete.cert; + if (!cert) { + element.setAttribute("value", certToDelete.hostPort); + return element; + } + + const attributes = [ + cert.commonName, + cert.organizationalUnit, + cert.organization, + cert.subjectName, + ]; + for (let attribute of attributes) { + if (attribute) { + element.setAttribute("value", attribute); + return element; + } + } + + document.l10n.setAttributes(element, "cert-with-serial", { + serialNumber: cert.serialNumber, + }); + return element; +} + +/** + * onload() handler. + */ +function onLoad() { + let typeFlag = window.arguments[0]; + let confirm = document.getElementById("confirm"); + let impact = document.getElementById("impact"); + let prefixForType; + switch (typeFlag) { + case "mine_tab": + prefixForType = "delete-user-cert-"; + break; + case "websites_tab": + prefixForType = "delete-ssl-override-"; + break; + case "ca_tab": + prefixForType = "delete-ca-cert-"; + break; + case "others_tab": + prefixForType = "delete-email-cert-"; + break; + default: + return; + } + + document.l10n.setAttributes( + document.documentElement, + prefixForType + "title" + ); + document.l10n.setAttributes(confirm, prefixForType + "confirm"); + document.l10n.setAttributes(impact, prefixForType + "impact"); + + document.addEventListener("dialogaccept", onDialogAccept); + document.addEventListener("dialogcancel", onDialogCancel); + + let box = document.getElementById("certlist"); + let certsToDelete = window.arguments[1]; + for (let certToDelete of certsToDelete) { + let listItem = document.createXULElement("richlistitem"); + let label = getLabelForCertToDelete(certToDelete); + listItem.appendChild(label); + box.appendChild(listItem); + } +} + +/** + * ondialogaccept() handler. + */ +function onDialogAccept() { + let retVals = window.arguments[2]; + retVals.deleteConfirmed = true; +} + +/** + * ondialogcancel() handler. + */ +function onDialogCancel() { + let retVals = window.arguments[2]; + retVals.deleteConfirmed = false; +} diff --git a/security/manager/pki/resources/content/deletecert.xhtml b/security/manager/pki/resources/content/deletecert.xhtml new file mode 100644 index 0000000000..fffff3c27d --- /dev/null +++ b/security/manager/pki/resources/content/deletecert.xhtml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window data-l10n-id="certmgr-delete-cert2" + data-l10n-attrs="style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();"> +<dialog id="deleteCertificate" + buttons="accept,cancel"> + + <linkset> + <html:link rel="localization" href="security/certificates/certManager.ftl"/> + </linkset> + + <script src="pippki.js" /> + <script src="chrome://pippki/content/deletecert.js"/> + + <description id="confirm" style="width: 400px;"/> + <richlistbox id="certlist" class="box-padded" flex="1" + style="min-height: 8em; height: 8em; min-width: 35em;"/> + <description id="impact" style="width: 400px;"/> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/device_manager.js b/security/manager/pki/resources/content/device_manager.js new file mode 100644 index 0000000000..deb58d2ff3 --- /dev/null +++ b/security/manager/pki/resources/content/device_manager.js @@ -0,0 +1,433 @@ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +var secmoddb; +var skip_enable_buttons = false; + +/* Do the initial load of all PKCS# modules and list them. */ +function LoadModules() { + secmoddb = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService( + Ci.nsIPKCS11ModuleDB + ); + RefreshDeviceList(); +} + +async function doPrompt(l10n_id) { + let [msg] = await document.l10n.formatValues([{ id: l10n_id }]); + Services.prompt.alert(window, null, msg); +} + +async function doConfirm(l10n_id) { + let [msg] = await document.l10n.formatValues([{ id: l10n_id }]); + return Services.prompt.confirm(window, null, msg); +} + +function RefreshDeviceList() { + for (let module of secmoddb.listModules()) { + let slots = module.listSlots(); + AddModule(module, slots); + } + + // Set the text on the FIPS button. + SetFIPSButton(); +} + +function SetFIPSButton() { + var fipsButton = document.getElementById("fipsbutton"); + if (secmoddb.isFIPSEnabled) { + document.l10n.setAttributes(fipsButton, "devmgr-button-disable-fips"); + } else { + document.l10n.setAttributes(fipsButton, "devmgr-button-enable-fips"); + } + + var can_toggle = secmoddb.canToggleFIPS; + if (can_toggle) { + fipsButton.removeAttribute("disabled"); + } else { + fipsButton.setAttribute("disabled", "true"); + } +} + +/* Add a module to the tree. slots is the array of slots in the module, + * to be represented as children. + */ +function AddModule(module, slots) { + var tree = document.getElementById("device_list"); + var item = document.createXULElement("treeitem"); + var row = document.createXULElement("treerow"); + var cell = document.createXULElement("treecell"); + cell.setAttribute("label", module.name); + row.appendChild(cell); + item.appendChild(row); + var parent = document.createXULElement("treechildren"); + for (let slot of slots) { + var child_item = document.createXULElement("treeitem"); + var child_row = document.createXULElement("treerow"); + var child_cell = document.createXULElement("treecell"); + child_cell.setAttribute("label", slot.name); + child_row.appendChild(child_cell); + child_item.appendChild(child_row); + child_item.setAttribute("pk11kind", "slot"); + // 'slot' is an attribute on any HTML element, hence 'slotObject' instead. + child_item.slotObject = slot; + parent.appendChild(child_item); + } + item.appendChild(parent); + item.setAttribute("pk11kind", "module"); + item.module = module; + item.setAttribute("open", "true"); + item.setAttribute("container", "true"); + tree.appendChild(item); +} + +var selected_slot; +var selected_module; + +/* get the slot selected by the user (can only be one-at-a-time) */ +function getSelectedItem() { + let tree = document.getElementById("device_tree"); + if (tree.currentIndex < 0) { + return; + } + let item = tree.view.getItemAtIndex(tree.currentIndex); + selected_slot = null; + selected_module = null; + if (item) { + let kind = item.getAttribute("pk11kind"); + if (kind == "slot") { + selected_slot = item.slotObject; + } else { + // (kind == "module") + selected_module = item.module; + } + } +} + +function enableButtons() { + if (skip_enable_buttons) { + return; + } + + var login_toggle = "true"; + var logout_toggle = "true"; + var pw_toggle = "true"; + var unload_toggle = "true"; + getSelectedItem(); + if (selected_module) { + unload_toggle = "false"; + showModuleInfo(); + } else if (selected_slot) { + // here's the workaround - login functions are all with token, + // so grab the token type + var selected_token = selected_slot.getToken(); + if (selected_token != null) { + if (selected_token.needsLogin() || !selected_token.needsUserInit) { + pw_toggle = "false"; + if (selected_token.needsLogin()) { + if (selected_token.isLoggedIn()) { + logout_toggle = "false"; + } else { + login_toggle = "false"; + } + } + } + + if ( + !Services.policies.isAllowed("createMasterPassword") && + selected_token.isInternalKeyToken && + !selected_token.hasPassword + ) { + pw_toggle = "true"; + } + } + showSlotInfo(); + } + document + .getElementById("login_button") + .setAttribute("disabled", login_toggle); + document + .getElementById("logout_button") + .setAttribute("disabled", logout_toggle); + document + .getElementById("change_pw_button") + .setAttribute("disabled", pw_toggle); + document + .getElementById("unload_button") + .setAttribute("disabled", unload_toggle); +} + +// clear the display of information for the slot +function ClearInfoList() { + let infoList = document.getElementById("info_list"); + while (infoList.hasChildNodes()) { + infoList.firstChild.remove(); + } +} + +function ClearDeviceList() { + ClearInfoList(); + + skip_enable_buttons = true; + var tree = document.getElementById("device_tree"); + tree.view.selection.clearSelection(); + skip_enable_buttons = false; + + // Remove the existing listed modules so that a refresh doesn't display the + // module that just changed. + let deviceList = document.getElementById("device_list"); + while (deviceList.hasChildNodes()) { + deviceList.firstChild.remove(); + } +} + +// show a list of info about a slot +function showSlotInfo() { + var present = true; + ClearInfoList(); + switch (selected_slot.status) { + case Ci.nsIPKCS11Slot.SLOT_DISABLED: + AddInfoRow( + "devinfo-status", + { l10nID: "devinfo-status-disabled" }, + "tok_status" + ); + present = false; + break; + case Ci.nsIPKCS11Slot.SLOT_NOT_PRESENT: + AddInfoRow( + "devinfo-status", + { l10nID: "devinfo-status-not-present" }, + "tok_status" + ); + present = false; + break; + case Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED: + AddInfoRow( + "devinfo-status", + { l10nID: "devinfo-status-uninitialized" }, + "tok_status" + ); + break; + case Ci.nsIPKCS11Slot.SLOT_NOT_LOGGED_IN: + AddInfoRow( + "devinfo-status", + { l10nID: "devinfo-status-not-logged-in" }, + "tok_status" + ); + break; + case Ci.nsIPKCS11Slot.SLOT_LOGGED_IN: + AddInfoRow( + "devinfo-status", + { l10nID: "devinfo-status-logged-in" }, + "tok_status" + ); + break; + case Ci.nsIPKCS11Slot.SLOT_READY: + AddInfoRow( + "devinfo-status", + { l10nID: "devinfo-status-ready" }, + "tok_status" + ); + break; + default: + return; + } + AddInfoRow("devinfo-desc", { label: selected_slot.desc }, "slot_desc"); + AddInfoRow("devinfo-man-id", { label: selected_slot.manID }, "slot_manID"); + AddInfoRow( + "devinfo-hwversion", + { label: selected_slot.HWVersion }, + "slot_hwv" + ); + AddInfoRow( + "devinfo-fwversion", + { label: selected_slot.FWVersion }, + "slot_fwv" + ); + if (present) { + showTokenInfo(); + } +} + +function showModuleInfo() { + ClearInfoList(); + AddInfoRow("devinfo-modname", { label: selected_module.name }, "module_name"); + AddInfoRow( + "devinfo-modpath", + { label: selected_module.libName }, + "module_path" + ); +} + +// add a row to the info list, as [col1 col2] (ex.: ["status" "logged in"]) +function AddInfoRow(l10nID, col2, cell_id) { + var tree = document.getElementById("info_list"); + var item = document.createXULElement("treeitem"); + var row = document.createXULElement("treerow"); + var cell1 = document.createXULElement("treecell"); + document.l10n.setAttributes(cell1, l10nID); + cell1.setAttribute("crop", "never"); + row.appendChild(cell1); + var cell2 = document.createXULElement("treecell"); + if (col2.l10nID) { + document.l10n.setAttributes(cell2, col2.l10nID); + } else { + cell2.setAttribute("label", col2.label); + } + cell2.setAttribute("crop", "never"); + cell2.setAttribute("id", cell_id); + row.appendChild(cell2); + item.appendChild(row); + tree.appendChild(item); +} + +// log in to a slot +function doLogin() { + getSelectedItem(); + // here's the workaround - login functions are with token + var selected_token = selected_slot.getToken(); + try { + selected_token.login(false); + var tok_status = document.getElementById("tok_status"); + if (selected_token.isLoggedIn()) { + document.l10n.setAttributes(tok_status, "devinfo-status-logged-in"); + } else { + document.l10n.setAttributes(tok_status, "devinfo-status-not-logged-in"); + } + } catch (e) { + doPrompt("login-failed"); + } + enableButtons(); +} + +// log out of a slot +function doLogout() { + getSelectedItem(); + // here's the workaround - login functions are with token + var selected_token = selected_slot.getToken(); + try { + selected_token.logoutAndDropAuthenticatedResources(); + var tok_status = document.getElementById("tok_status"); + if (selected_token.isLoggedIn()) { + document.l10n.setAttributes(tok_status, "devinfo-status-logged-in"); + } else { + document.l10n.setAttributes(tok_status, "devinfo-status-not-logged-in"); + } + } catch (e) {} + enableButtons(); +} + +// load a new device +function doLoad() { + window.browsingContext.topChromeWindow.open( + "load_device.xhtml", + "loaddevice", + "chrome,centerscreen,modal" + ); + ClearDeviceList(); + RefreshDeviceList(); +} + +async function deleteSelected() { + getSelectedItem(); + if (selected_module && (await doConfirm("del-module-warning"))) { + try { + secmoddb.deleteModule(selected_module.name); + } catch (e) { + doPrompt("del-module-error"); + return false; + } + selected_module = null; + return true; + } + return false; +} + +async function doUnload() { + if (await deleteSelected()) { + ClearDeviceList(); + RefreshDeviceList(); + } +} + +function changePassword() { + getSelectedItem(); + let params = Cc["@mozilla.org/embedcomp/dialogparam;1"].createInstance( + Ci.nsIDialogParamBlock + ); + let objects = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + objects.appendElement(selected_slot.getToken()); + params.objects = objects; + window.browsingContext.topChromeWindow.openDialog( + "changepassword.xhtml", + "", + "chrome,centerscreen,modal", + params + ); + showSlotInfo(); + enableButtons(); +} + +// ------------------------------------- Old code + +function showTokenInfo() { + var selected_token = selected_slot.getToken(); + AddInfoRow("devinfo-label", { label: selected_token.tokenName }, "tok_label"); + AddInfoRow( + "devinfo-man-id", + { label: selected_token.tokenManID }, + "tok_manID" + ); + AddInfoRow( + "devinfo-serialnum", + { label: selected_token.tokenSerialNumber }, + "tok_sNum" + ); + AddInfoRow( + "devinfo-hwversion", + { label: selected_token.tokenHWVersion }, + "tok_hwv" + ); + AddInfoRow( + "devinfo-fwversion", + { label: selected_token.tokenFWVersion }, + "tok_fwv" + ); +} + +function toggleFIPS() { + if (!secmoddb.isFIPSEnabled) { + // A restriction of FIPS mode is, the password must be set + // In FIPS mode the password must be non-empty. + // This is different from what we allow in NON-Fips mode. + + var tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].getService( + Ci.nsIPK11TokenDB + ); + var internal_token = tokendb.getInternalKeyToken(); // nsIPK11Token + if (!internal_token.hasPassword) { + // Token has either no or an empty password. + doPrompt("fips-nonempty-primary-password-required"); + return; + } + } + + try { + secmoddb.toggleFIPSMode(); + } catch (e) { + doPrompt("unable-to-toggle-fips"); + return; + } + + // Remove the existing listed modules so that a refresh doesn't display the + // module that just changed. + ClearDeviceList(); + + RefreshDeviceList(); +} diff --git a/security/manager/pki/resources/content/device_manager.xhtml b/security/manager/pki/resources/content/device_manager.xhtml new file mode 100644 index 0000000000..1c697d52e3 --- /dev/null +++ b/security/manager/pki/resources/content/device_manager.xhtml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE dialog> + +<window windowtype="mozilla:devicemanager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="devmgr-window" + data-l10n-attrs="style" + persist="screenX screenY width height" + onload="LoadModules();"> +<dialog id="devicemanager" + buttons="accept"> + +<linkset> + <html:link rel="localization" href="security/certificates/deviceManager.ftl"/> +</linkset> + +<script src="chrome://pippki/content/device_manager.js"/> + +<hbox flex="1" style="margin:5px"> + <!-- List of devices --> + <tree id="device_tree" seltype="single" + onselect="enableButtons();" hidecolumnpicker="true" + flex="1" style="min-width: 15em"> + <treecols> + <treecol id="deviceCol" flex="1" primary="true" data-l10n-id="devmgr-devlist"/> + </treecols> + <treechildren id="device_list"/> + </tree> + <!-- / List of devices --> + <!-- Device status --> + <tree id="info_tree" seltype="single" hidecolumnpicker="true" + style="-moz-box-flex: 3; min-width: 10em"> + <treecols> + <treecol id="title1Col" style="-moz-box-flex: 5" primary="true" data-l10n-id="devmgr-header-details"/> + <treecol id="title2Col" style="-moz-box-flex: 7" data-l10n-id="devmgr-header-value"/> + </treecols> + <treechildren id="info_list"/> + </tree> + <!-- / Device status --> + <vbox> <!-- Buttons for manipulating devices --> + <button id="login_button" + data-l10n-id="devmgr-button-login" + oncommand="doLogin();" disabled="true"/> + <button id="logout_button" + data-l10n-id="devmgr-button-logout" + oncommand="doLogout();" disabled="true"/> + <button id="change_pw_button" + data-l10n-id="devmgr-button-changepw" + oncommand="changePassword();" disabled="true"/> + <button id="load_button" + data-l10n-id="devmgr-button-load" + oncommand="doLoad();"/> + <button id="unload_button" + data-l10n-id="devmgr-button-unload" + oncommand="doUnload();" disabled="true"/> + <button id="fipsbutton" + data-l10n-id="devmgr-button-enable-fips" + oncommand="toggleFIPS();"/> + </vbox> <!-- / Buttons for manipulating devices --> +</hbox> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/downloadcert.js b/security/manager/pki/resources/content/downloadcert.js new file mode 100644 index 0000000000..8451997441 --- /dev/null +++ b/security/manager/pki/resources/content/downloadcert.js @@ -0,0 +1,83 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +/** + * @file Implements the functionality of downloadcert.xhtml: a dialog that allows + * a user to confirm whether to import a certificate, and if so what trust + * to give it. + * @param {nsISupports} window.arguments.0 + * Certificate to confirm import of, queryable to nsIX509Cert. + * @param {nsISupports} window.arguments.1 + * Object to set the return values of calling the dialog on, queryable + * to the underlying type of DownloadCertReturnValues. + */ + +/** + * @typedef DownloadCertReturnValues + * @type {nsIWritablePropertyBag2} + * @property {boolean} importConfirmed + * Set to true if the user confirmed import of the cert and accepted + * the dialog, false otherwise. + * @property {boolean} trustForSSL + * Set to true if the cert should be trusted for SSL, false otherwise. + * Undefined value if |importConfirmed| is not true. + * @property {boolean} trustForEmail + * Set to true if the cert should be trusted for e-mail, false + * otherwise. Undefined value if |importConfirmed| is not true. + */ + +/** + * The cert to potentially import. + * + * @type {nsIX509Cert} + */ +var gCert; + +/** + * onload() handler. + */ +function onLoad() { + gCert = window.arguments[0].QueryInterface(Ci.nsIX509Cert); + + document.addEventListener("dialogaccept", onDialogAccept); + document.addEventListener("dialogcancel", onDialogCancel); + + let bundle = document.getElementById("pippki_bundle"); + let caName = gCert.commonName; + if (!caName.length) { + caName = bundle.getString("unnamedCA"); + } + + setText("trustHeader", bundle.getFormattedString("newCAMessage1", [caName])); +} + +/** + * Handler for the "View Cert" button. + */ +function viewCert() { + viewCertHelper(window, gCert, "window"); +} + +/** + * ondialogaccept() handler. + */ +function onDialogAccept() { + let checkSSL = document.getElementById("trustSSL"); + let checkEmail = document.getElementById("trustEmail"); + + let retVals = window.arguments[1].QueryInterface(Ci.nsIWritablePropertyBag2); + retVals.setPropertyAsBool("importConfirmed", true); + retVals.setPropertyAsBool("trustForSSL", checkSSL.checked); + retVals.setPropertyAsBool("trustForEmail", checkEmail.checked); +} + +/** + * ondialogcancel() handler. + */ +function onDialogCancel() { + let retVals = window.arguments[1].QueryInterface(Ci.nsIWritablePropertyBag2); + retVals.setPropertyAsBool("importConfirmed", false); +} diff --git a/security/manager/pki/resources/content/downloadcert.xhtml b/security/manager/pki/resources/content/downloadcert.xhtml new file mode 100644 index 0000000000..bec39af4a7 --- /dev/null +++ b/security/manager/pki/resources/content/downloadcert.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window data-l10n-id="download-cert-window2" + data-l10n-attrs="title, style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();"> +<dialog id="download_cert" + buttons="accept,cancel"> + +<linkset> + <html:link rel="localization" href="security/pippki/pippki.ftl"/> +</linkset> + +<stringbundle id="pippki_bundle" src="chrome://pippki/locale/pippki.properties"/> + +<script src="chrome://pippki/content/pippki.js"/> +<script src="chrome://pippki/content/downloadcert.js"/> + + <!-- Let 'em know what they're doing --> + <vbox> + <description data-l10n-id="download-cert-message"></description> + </vbox> + + <separator/> + + <!-- checkboxes for trust bits + - "do you want to?" + - * trust for SSL + - * trust for email + --> + <vbox> + <description id="trustHeader"/> + <checkbox data-l10n-id="download-cert-trust-ssl" + id="trustSSL"/> + <checkbox data-l10n-id="download-cert-trust-email" + id="trustEmail"/> + </vbox> + + <separator/> + + <vbox> + <description data-l10n-id="download-cert-message-desc"></description> + <separator/> + <hbox> + <button id="viewC-button" + data-l10n-id="download-cert-view-cert" + oncommand="viewCert();"/> + <description style="margin: 4px;" data-l10n-id="download-cert-view-text"></description> + </hbox> + </vbox> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/editcacert.js b/security/manager/pki/resources/content/editcacert.js new file mode 100644 index 0000000000..c482bd6e0a --- /dev/null +++ b/security/manager/pki/resources/content/editcacert.js @@ -0,0 +1,55 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +var gCertDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); +/** + * Cert to edit the trust of. + * + * @type {nsIX509Cert} + */ +var gCert; + +/** + * onload() handler. + */ +function onLoad() { + gCert = window.arguments[0]; + + document.addEventListener("dialogaccept", onDialogAccept); + + let certMsg = document.getElementById("certmsg"); + document.l10n.setAttributes(certMsg, "edit-trust-ca", { + certName: gCert.commonName, + }); + + let sslCheckbox = document.getElementById("trustSSL"); + sslCheckbox.checked = gCertDB.isCertTrusted( + gCert, + Ci.nsIX509Cert.CA_CERT, + Ci.nsIX509CertDB.TRUSTED_SSL + ); + + let emailCheckbox = document.getElementById("trustEmail"); + emailCheckbox.checked = gCertDB.isCertTrusted( + gCert, + Ci.nsIX509Cert.CA_CERT, + Ci.nsIX509CertDB.TRUSTED_EMAIL + ); +} + +/** + * ondialogaccept() handler. + */ +function onDialogAccept() { + let sslCheckbox = document.getElementById("trustSSL"); + let emailCheckbox = document.getElementById("trustEmail"); + let trustSSL = sslCheckbox.checked ? Ci.nsIX509CertDB.TRUSTED_SSL : 0; + let trustEmail = emailCheckbox.checked ? Ci.nsIX509CertDB.TRUSTED_EMAIL : 0; + + gCertDB.setCertTrust(gCert, Ci.nsIX509Cert.CA_CERT, trustSSL | trustEmail); +} diff --git a/security/manager/pki/resources/content/editcacert.xhtml b/security/manager/pki/resources/content/editcacert.xhtml new file mode 100644 index 0000000000..745f05fdf3 --- /dev/null +++ b/security/manager/pki/resources/content/editcacert.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window data-l10n-id="certmgr-edit-ca-cert2" + data-l10n-attrs="style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();"> +<dialog id="editCaCert" + buttons="accept,cancel"> + + <linkset> + <html:link rel="localization" href="security/certificates/certManager.ftl"/> + </linkset> + + <script src="chrome://pippki/content/pippki.js"/> + <script src="chrome://pippki/content/editcacert.js"/> + + <description id="certmsg"/> + <separator/> + <description data-l10n-id="certmgr-edit-cert-edit-trust"></description> + <vbox align="start"> + <checkbox data-l10n-id="certmgr-edit-cert-trust-ssl" + id="trustSSL"/> + <checkbox data-l10n-id="certmgr-edit-cert-trust-email" + id="trustEmail"/> + </vbox> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/exceptionDialog.css b/security/manager/pki/resources/content/exceptionDialog.css new file mode 100644 index 0000000000..0aaf917330 --- /dev/null +++ b/security/manager/pki/resources/content/exceptionDialog.css @@ -0,0 +1,38 @@ +/* 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/. */ + +:root { + max-width: 40em; +} + +#warningSupplemental { + font-weight: bold; +} + +.description { + font-weight: bold; +} + +.longDescription { + /* 40em is the max-width of the dialog defined above. + 18px is the horizontal padding of the dialog. */ + width: calc(40em - 18px); + padding-bottom: 1em; +} + +#warningText, +#warningSupplemental, +#headerDescription, +.longDescription { + white-space: pre-wrap; +} + +.description:empty, +.longDescription:empty { + display: none; +} + +#locationTextBox { + -moz-box-flex: 1; +} diff --git a/security/manager/pki/resources/content/exceptionDialog.js b/security/manager/pki/resources/content/exceptionDialog.js new file mode 100644 index 0000000000..1953cce8fa --- /dev/null +++ b/security/manager/pki/resources/content/exceptionDialog.js @@ -0,0 +1,334 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +var gDialog; +var gSecInfo; +var gCert; +var gChecking; +var gBroken; +var gNeedReset; + +const { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); + +function initExceptionDialog() { + gNeedReset = false; + gDialog = document.getElementById("exceptiondialog"); + let warningText = document.getElementById("warningText"); + document.l10n.setAttributes(warningText, "add-exception-branded-warning"); + let confirmButton = gDialog.getButton("extra1"); + let l10nUpdatedElements = [confirmButton, warningText]; + confirmButton.disabled = true; + + var args = window.arguments; + if (args && args[0]) { + if (args[0].location) { + // We were pre-seeded with a location. + document.getElementById("locationTextBox").value = args[0].location; + document.getElementById("checkCertButton").disabled = false; + + if (args[0].securityInfo) { + gSecInfo = args[0].securityInfo; + gCert = gSecInfo.serverCert; + gBroken = true; + l10nUpdatedElements = l10nUpdatedElements.concat(updateCertStatus()); + } else if (args[0].prefetchCert) { + // We can optionally pre-fetch the certificate too. Don't do this + // synchronously, since it would prevent the window from appearing + // until the fetch is completed, which could be multiple seconds. + // Instead, let's use a timer to spawn the actual fetch, but update + // the dialog to "checking..." state right away, so that the UI + // is appropriately responsive. Bug 453855 + document.getElementById("checkCertButton").disabled = true; + gChecking = true; + l10nUpdatedElements = l10nUpdatedElements.concat(updateCertStatus()); + + window.setTimeout(checkCert, 0); + } + } + + // Set out parameter to false by default + args[0].exceptionAdded = false; + } + + for (let id of [ + "warningSupplemental", + "certLocationLabel", + "checkCertButton", + "statusDescription", + "statusLongDescription", + "viewCertButton", + "permanent", + ]) { + let element = document.getElementById(id); + l10nUpdatedElements.push(element); + } + + document.l10n + .translateElements(l10nUpdatedElements) + .then(() => window.sizeToContent()); + + document.addEventListener("dialogextra1", addException); + document.addEventListener("dialogextra2", checkCert); +} + +/** + * Helper function for checkCert. Set as the onerror/onload callbacks for an + * XMLHttpRequest. Sets gSecInfo, gCert, gBroken, and gChecking according to + * the load information from the request. Probably should not be used directly. + * + * @param {XMLHttpRequest} req + * The XMLHttpRequest created and sent by checkCert. + * @param {Event} evt + * The load or error event. + */ +function grabCert(req, evt) { + if (req.channel && req.channel.securityInfo) { + gSecInfo = req.channel.securityInfo; + gCert = gSecInfo ? gSecInfo.serverCert : null; + } + gBroken = evt.type == "error"; + gChecking = false; + document.l10n + .translateElements(updateCertStatus()) + .then(() => window.sizeToContent()); +} + +/** + * Attempt to download the certificate for the location specified, and populate + * the Certificate Status section with the result. + */ +async function checkCert() { + gCert = null; + gSecInfo = null; + gChecking = true; + gBroken = false; + await document.l10n.translateElements(updateCertStatus()); + window.sizeToContent(); + + let uri = getURI(); + + if (uri) { + let req = new XMLHttpRequest(); + req.open("GET", uri.prePath); + req.onerror = grabCert.bind(this, req); + req.onload = grabCert.bind(this, req); + req.send(null); + } else { + gChecking = false; + await document.l10n.translateElements(updateCertStatus()); + window.sizeToContent(); + } +} + +/** + * Build and return a URI, based on the information supplied in the + * Certificate Location fields + * + * @returns {nsIURI} + * URI constructed from the information supplied on success, null + * otherwise. + */ +function getURI() { + // Use fixup service instead of just ioservice's newURI since it's quite + // likely that the host will be supplied without a protocol prefix, resulting + // in malformed uri exceptions being thrown. + let locationTextBox = document.getElementById("locationTextBox"); + let { preferredURI: uri } = Services.uriFixup.getFixupURIInfo( + locationTextBox.value + ); + + if (!uri) { + return null; + } + + let mutator = uri.mutate(); + if (uri.scheme == "http") { + mutator.setScheme("https"); + } + + if (uri.port == -1) { + mutator.setPort(443); + } + + return mutator.finalize(); +} + +function resetDialog() { + document.getElementById("viewCertButton").disabled = true; + document.getElementById("permanent").disabled = true; + gDialog.getButton("extra1").disabled = true; + setText("headerDescription", ""); + setText("statusDescription", ""); + setText("statusLongDescription", ""); + setText("status2Description", ""); + setText("status2LongDescription", ""); + setText("status3Description", ""); + setText("status3LongDescription", ""); + window.sizeToContent(); +} + +/** + * Called by input textboxes to manage UI state + */ +function handleTextChange() { + var checkCertButton = document.getElementById("checkCertButton"); + checkCertButton.disabled = !document.getElementById("locationTextBox").value; + if (gNeedReset) { + gNeedReset = false; + resetDialog(); + } +} + +function updateCertStatus() { + var shortDesc, longDesc; + let l10nUpdatedElements = []; + if (gCert) { + if (gBroken) { + var mms = "add-exception-domain-mismatch-short"; + var mml = "add-exception-domain-mismatch-long"; + var exs = "add-exception-expired-short"; + var exl = "add-exception-expired-long"; + var uts = "add-exception-unverified-or-bad-signature-short"; + var utl = "add-exception-unverified-or-bad-signature-long"; + if ( + gSecInfo.overridableErrorCategory == + Ci.nsITransportSecurityInfo.ERROR_TRUST + ) { + shortDesc = uts; + longDesc = utl; + } else if ( + gSecInfo.overridableErrorCategory == + Ci.nsITransportSecurityInfo.ERROR_DOMAIN + ) { + shortDesc = mms; + longDesc = mml; + } else if ( + gSecInfo.overridableErrorCategory == + Ci.nsITransportSecurityInfo.ERROR_TIME + ) { + shortDesc = exs; + longDesc = exl; + } + // In these cases, we do want to enable the "Add Exception" button + gDialog.getButton("extra1").disabled = false; + + // If the Private Browsing service is available and the mode is active, + // don't store permanent exceptions, since they would persist after + // private browsing mode was disabled. + var inPrivateBrowsing = inPrivateBrowsingMode(); + var pe = document.getElementById("permanent"); + pe.disabled = inPrivateBrowsing; + pe.checked = !inPrivateBrowsing; + + let headerDescription = document.getElementById("headerDescription"); + document.l10n.setAttributes( + headerDescription, + "add-exception-invalid-header" + ); + l10nUpdatedElements.push(headerDescription); + } else { + shortDesc = "add-exception-valid-short"; + longDesc = "add-exception-valid-long"; + gDialog.getButton("extra1").disabled = true; + document.getElementById("permanent").disabled = true; + } + + // We're done checking the certificate, so allow the user to check it again. + document.getElementById("checkCertButton").disabled = false; + document.getElementById("viewCertButton").disabled = false; + + // Notify observers about the availability of the certificate + Services.obs.notifyObservers(null, "cert-exception-ui-ready"); + } else if (gChecking) { + shortDesc = "add-exception-checking-short"; + longDesc = "add-exception-checking-long"; + // We're checking the certificate, so we disable the Get Certificate + // button to make sure that the user can't interrupt the process and + // trigger another certificate fetch. + document.getElementById("checkCertButton").disabled = true; + document.getElementById("viewCertButton").disabled = true; + gDialog.getButton("extra1").disabled = true; + document.getElementById("permanent").disabled = true; + } else { + shortDesc = "add-exception-no-cert-short"; + longDesc = "add-exception-no-cert-long"; + // We're done checking the certificate, so allow the user to check it again. + document.getElementById("checkCertButton").disabled = false; + document.getElementById("viewCertButton").disabled = true; + gDialog.getButton("extra1").disabled = true; + document.getElementById("permanent").disabled = true; + } + let statusDescription = document.getElementById("statusDescription"); + let statusLongDescription = document.getElementById("statusLongDescription"); + document.l10n.setAttributes(statusDescription, shortDesc); + document.l10n.setAttributes(statusLongDescription, longDesc); + l10nUpdatedElements.push(statusDescription); + l10nUpdatedElements.push(statusLongDescription); + + gNeedReset = true; + return l10nUpdatedElements; +} + +/** + * Handle user request to display certificate details + */ +function viewCertButtonClick() { + if (gCert) { + viewCertHelper(this, gCert); + } +} + +/** + * Handle user request to add an exception for the specified cert + */ +function addException() { + if (!gCert || !gSecInfo) { + return; + } + + var overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( + Ci.nsICertOverrideService + ); + var flags = 0; + if (gSecInfo.isUntrusted) { + flags |= overrideService.ERROR_UNTRUSTED; + } + if (gSecInfo.isDomainMismatch) { + flags |= overrideService.ERROR_MISMATCH; + } + if (gSecInfo.isNotValidAtThisTime) { + flags |= overrideService.ERROR_TIME; + } + + var permanentCheckbox = document.getElementById("permanent"); + var shouldStorePermanently = + permanentCheckbox.checked && !inPrivateBrowsingMode(); + var uri = getURI(); + overrideService.rememberValidityOverride( + uri.asciiHost, + uri.port, + {}, + gCert, + flags, + !shouldStorePermanently + ); + + let args = window.arguments; + if (args && args[0]) { + args[0].exceptionAdded = true; + } + + gDialog.acceptDialog(); +} + +/** + * @returns {boolean} Whether this dialog is in private browsing mode. + */ +function inPrivateBrowsingMode() { + return PrivateBrowsingUtils.isWindowPrivate(window); +} diff --git a/security/manager/pki/resources/content/exceptionDialog.xhtml b/security/manager/pki/resources/content/exceptionDialog.xhtml new file mode 100644 index 0000000000..56be4bc5cb --- /dev/null +++ b/security/manager/pki/resources/content/exceptionDialog.xhtml @@ -0,0 +1,88 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://pippki/content/exceptionDialog.css" type="text/css"?> + +<!DOCTYPE window> + +<window windowtype="mozilla:exceptiondialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="exception-mgr" + onload="initExceptionDialog();"> +<dialog id="exceptiondialog" + buttonidextra1="exception-mgr-extra-button" + buttons="cancel,extra1,extra2" + defaultButton="extra2"> + + <linkset> + <html:link rel="localization" href="branding/brand.ftl"/> + <html:link rel="localization" href="security/certificates/certManager.ftl"/> + </linkset> + + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + + <script src="chrome://pippki/content/pippki.js"/> + <script src="chrome://pippki/content/exceptionDialog.js"/> + + <hbox> + <vbox> +#ifdef MOZ_WIDGET_GTK + <image src="moz-icon://stock/gtk-dialog-warning?size=dialog"/> +#else + <image src="chrome://global/skin/icons/warning-large.png"/> +#endif + <spacer flex="1"/> + </vbox> + <vbox flex="1"> + <!-- Note that because of the styling, there must be no whitespace within + the description tags --> + <description id="warningText"/> + <description id="warningSupplemental" + data-l10n-id="exception-mgr-supplemental-warning"/> + </vbox> + </hbox> + + <hbox align="center"> + <label control="locationTextBox" + id="certLocationLabel" + data-l10n-id="exception-mgr-cert-location-url"/> + <html:input id="locationTextBox" + oninput="handleTextChange();" + value="https://" + class="uri-element"/> + <button id="checkCertButton" + disabled="true" + dlgtype="extra2" + data-l10n-id="exception-mgr-cert-location-download"/> + </hbox> + + <hbox align="center"> + <description id="headerDescription" + flex="1"/> + <button id="viewCertButton" + data-l10n-id="exception-mgr-cert-status-view-cert" + disabled="true" + oncommand="viewCertButtonClick();"/> + </hbox> + <description id="statusDescription" + class="description"/> + <description id="statusLongDescription" + class="longDescription"/> + <description id="status2Description" + class="description"/> + <description id="status2LongDescription" + class="longDescription"/> + <description id="status3Description" + class="description"/> + <description id="status3LongDescription" + class="longDescription"/> + <checkbox id="permanent" + disabled="true" + data-l10n-id="exception-mgr-permanent"/> +</dialog> +</window> diff --git a/security/manager/pki/resources/content/load_device.js b/security/manager/pki/resources/content/load_device.js new file mode 100644 index 0000000000..0f77e1f1bd --- /dev/null +++ b/security/manager/pki/resources/content/load_device.js @@ -0,0 +1,75 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +document.addEventListener("dialogaccept", onDialogAccept); + +/** + * @file Implements the functionality of load_device.xhtml: a dialog that allows + * a PKCS #11 module to be loaded into Firefox. + */ + +async function onBrowseBtnPress() { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let [loadPK11ModuleFilePickerTitle] = await document.l10n.formatValues([ + { id: "load-pk11-module-file-picker-title" }, + ]); + fp.init(window, loadPK11ModuleFilePickerTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + fp.open(rv => { + if (rv == Ci.nsIFilePicker.returnOK) { + document.getElementById("device_path").value = fp.file.path; + } + + // This notification gets sent solely for test purposes. It should not be + // used by production code. + Services.obs.notifyObservers(window, "LoadPKCS11Module:FilePickHandled"); + }); +} + +/** + * ondialogaccept() handler. + * + * @param {object} event + * The event causing this handler function to be called. + */ +function onDialogAccept(event) { + let nameBox = document.getElementById("device_name"); + let pathBox = document.getElementById("device_path"); + let pkcs11ModuleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService( + Ci.nsIPKCS11ModuleDB + ); + + try { + pkcs11ModuleDB.addModule(nameBox.value, pathBox.value, 0, 0); + } catch (e) { + addModuleFailure("add-module-failure"); + event.preventDefault(); + } +} + +async function addModuleFailure(l10nID) { + let [AddModuleFailure] = await document.l10n.formatValues([{ id: l10nID }]); + alertPromptService(null, AddModuleFailure); +} + +function validateModuleName() { + let name = document.getElementById("device_name").value; + let helpText = document.getElementById("helpText"); + helpText.value = ""; + let dialogNode = document.querySelector("dialog"); + dialogNode.removeAttribute("buttondisabledaccept"); + if (name == "") { + document.l10n.setAttributes(helpText, "load-module-help-empty-module-name"); + dialogNode.setAttribute("buttondisabledaccept", true); + } + if (name == "Root Certs") { + document.l10n.setAttributes( + helpText, + "load-module-help-root-certs-module-name" + ); + dialogNode.setAttribute("buttondisabledaccept", true); + } +} diff --git a/security/manager/pki/resources/content/load_device.xhtml b/security/manager/pki/resources/content/load_device.xhtml new file mode 100644 index 0000000000..3f43b9e65e --- /dev/null +++ b/security/manager/pki/resources/content/load_device.xhtml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="load-device"> +<dialog id="loaddevice" + buttons="accept,cancel"> + + <linkset> + <html:link rel="localization" href="security/certificates/deviceManager.ftl"/> + </linkset> + + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + + <script src="chrome://pippki/content/pippki.js"/> + <script src="chrome://pippki/content/load_device.js"/> + + <html:style> + #device_name, + #device_path { + -moz-box-flex: 1; + } + </html:style> + + <description data-l10n-id="load-device-info"></description> + <hbox align="center"> + <label data-l10n-id="load-device-modname" + control="device_name"/> + <html:input id="device_name" + data-l10n-id="load-device-modname-default" + data-l10n-attrs="value" + onchange="validateModuleName();"/> + </hbox> + <hbox align="center"> + <label data-l10n-id="load-device-filename" + control="device_path"/> + <html:input id="device_path" /> + <button id="browse" flex="1" + data-l10n-id="load-device-browse" + oncommand="onBrowseBtnPress();"/> + </hbox> + <label id="helpText" value=""/> + +</dialog> +</window> diff --git a/security/manager/pki/resources/content/pippki.js b/security/manager/pki/resources/content/pippki.js new file mode 100644 index 0000000000..7c2f8f3255 --- /dev/null +++ b/security/manager/pki/resources/content/pippki.js @@ -0,0 +1,301 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * + * 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"; + +/* + * These are helper functions to be included + * pippki UI js files. + */ + +function setText(id, value) { + let element = document.getElementById(id); + if (!element) { + return; + } + if (element.hasChildNodes()) { + element.firstChild.remove(); + } + element.appendChild(document.createTextNode(value)); +} + +async function viewCertHelper(parent, cert, openingOption = "tab") { + if (!cert) { + return; + } + + let win = Services.wm.getMostRecentBrowserWindow(); + let results = await asyncDetermineUsages(cert); + let chain = getBestChain(results); + if (!chain) { + chain = [cert]; + } + let certs = chain.map(elem => encodeURIComponent(elem.getBase64DERString())); + let certsStringURL = certs.map(elem => `cert=${elem}`); + certsStringURL = certsStringURL.join("&"); + let url = `about:certificate?${certsStringURL}`; + let opened = win.switchToTabHavingURI(url, false, {}); + if (!opened) { + win.openTrustedLinkIn(url, openingOption); + } +} + +function getPKCS7Array(certArray) { + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + let pkcs7String = certdb.asPKCS7Blob(certArray); + let pkcs7Array = new Uint8Array(pkcs7String.length); + for (let i = 0; i < pkcs7Array.length; i++) { + pkcs7Array[i] = pkcs7String.charCodeAt(i); + } + return pkcs7Array; +} + +function getPEMString(cert) { + var derb64 = cert.getBase64DERString(); + // Wrap the Base64 string into lines of 64 characters with CRLF line breaks + // (as specified in RFC 1421). + var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n"); + return ( + "-----BEGIN CERTIFICATE-----\r\n" + + wrapped + + "\r\n-----END CERTIFICATE-----\r\n" + ); +} + +function alertPromptService(title, message) { + // XXX Bug 1425832 - Using Services.prompt here causes tests to report memory + // leaks. + // eslint-disable-next-line mozilla/use-services + var ps = Cc["@mozilla.org/prompter;1"].getService(Ci.nsIPromptService); + ps.alert(window, title, message); +} + +const DEFAULT_CERT_EXTENSION = "crt"; + +/** + * Generates a filename for a cert suitable to set as the |defaultString| + * attribute on an Ci.nsIFilePicker. + * + * @param {nsIX509Cert} cert + * The cert to generate a filename for. + * @returns {string} + * Generated filename. + */ +function certToFilename(cert) { + let filename = cert.displayName; + + // Remove unneeded and/or unsafe characters. + filename = filename + .replace(/\s/g, "") + .replace(/\./g, "_") + .replace(/\\/g, "") + .replace(/\//g, ""); + + // Ci.nsIFilePicker.defaultExtension is more of a suggestion to some + // implementations, so we include the extension in the file name as well. This + // is what the documentation for Ci.nsIFilePicker.defaultString says we should do + // anyways. + return `${filename}.${DEFAULT_CERT_EXTENSION}`; +} + +async function exportToFile(parent, cert) { + if (!cert) { + return; + } + + let results = await asyncDetermineUsages(cert); + let chain = getBestChain(results); + if (!chain) { + chain = [cert]; + } + + let formats = { + base64: "*.crt; *.pem", + "base64-chain": "*.crt; *.pem", + der: "*.der", + pkcs7: "*.p7c", + "pkcs7-chain": "*.p7c", + }; + let [saveCertAs, ...formatLabels] = await document.l10n.formatValues( + [ + "save-cert-as", + ...Object.keys(formats).map(f => "cert-format-" + f), + ].map(id => ({ id })) + ); + + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(parent, saveCertAs, Ci.nsIFilePicker.modeSave); + fp.defaultString = certToFilename(cert); + fp.defaultExtension = DEFAULT_CERT_EXTENSION; + for (let format of Object.values(formats)) { + fp.appendFilter(formatLabels.shift(), format); + } + fp.appendFilters(Ci.nsIFilePicker.filterAll); + let filePickerResult = await new Promise(resolve => { + fp.open(resolve); + }); + + if ( + filePickerResult != Ci.nsIFilePicker.returnOK && + filePickerResult != Ci.nsIFilePicker.returnReplace + ) { + return; + } + + var content = ""; + switch (fp.filterIndex) { + case 1: + content = getPEMString(cert); + for (let i = 1; i < chain.length; i++) { + content += getPEMString(chain[i]); + } + break; + case 2: + // IOUtils.write requires a typed array. + // nsIX509Cert.getRawDER() returns an array (not a typed array), so we + // convert it here. + content = Uint8Array.from(cert.getRawDER()); + break; + case 3: + // getPKCS7Array returns a typed array already, so no conversion is + // necessary. + content = getPKCS7Array([cert]); + break; + case 4: + content = getPKCS7Array(chain); + break; + case 0: + default: + content = getPEMString(cert); + break; + } + + if (typeof content === "string") { + content = new TextEncoder().encode(content); + } + + try { + await IOUtils.write(fp.file.path, content); + } catch (ex) { + let title = await document.l10n.formatValue("write-file-failure"); + alertPromptService(title, ex.toString()); + } + if (Cu.isInAutomation) { + Services.obs.notifyObservers(null, "cert-export-finished"); + } +} + +const PRErrorCodeSuccess = 0; + +// Certificate usages we care about in the certificate viewer. +const certificateUsageSSLClient = 0x0001; +const certificateUsageSSLServer = 0x0002; +const certificateUsageSSLCA = 0x0008; +const certificateUsageEmailSigner = 0x0010; +const certificateUsageEmailRecipient = 0x0020; + +// A map from the name of a certificate usage to the value of the usage. +// Useful for printing debugging information and for enumerating all supported +// usages. +const certificateUsages = { + certificateUsageSSLClient, + certificateUsageSSLServer, + certificateUsageSSLCA, + certificateUsageEmailSigner, + certificateUsageEmailRecipient, +}; + +/** + * Returns a promise that will resolve with a results array consisting of what + * usages the given certificate successfully verified for. + * + * @param {nsIX509Cert} cert + * The certificate to determine valid usages for. + * @returns {Promise} + * A promise that will resolve with the results of the verifications. + */ +function asyncDetermineUsages(cert) { + let promises = []; + let now = Date.now() / 1000; + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + Object.keys(certificateUsages).forEach(usageString => { + promises.push( + new Promise((resolve, reject) => { + let usage = certificateUsages[usageString]; + certdb.asyncVerifyCertAtTime( + cert, + usage, + 0, + null, + now, + (aPRErrorCode, aVerifiedChain, aHasEVPolicy) => { + resolve({ + usageString, + errorCode: aPRErrorCode, + chain: aVerifiedChain, + }); + } + ); + }) + ); + }); + return Promise.all(promises); +} + +/** + * Given a results array, returns the "best" verified certificate chain. Since + * the primary use case is for TLS server certificates in Firefox, such a + * verified chain will be returned if present. Otherwise, the priority is: TLS + * client certificate, email signer, email recipient, CA. Returns null if no + * usage verified successfully. + * + * @param {Array} results + * An array of results from `asyncDetermineUsages`. See `displayUsages`. + * @returns {Array} An array of `nsIX509Cert` representing the verified + * certificate chain for the given usage, or null if there is none. + */ +function getBestChain(results) { + let usages = [ + certificateUsageSSLServer, + certificateUsageSSLClient, + certificateUsageEmailSigner, + certificateUsageEmailRecipient, + certificateUsageSSLCA, + ]; + for (let usage of usages) { + let chain = getChainForUsage(results, usage); + if (chain) { + return chain; + } + } + return null; +} + +/** + * Given a results array, returns the chain corresponding to the desired usage, + * if verifying for that usage succeeded. Returns null otherwise. + * + * @param {Array} results + * An array of results from `asyncDetermineUsages`. See `displayUsages`. + * @param {number} usage + * A numerical value corresponding to a usage. See `certificateUsages`. + * @returns {Array} An array of `nsIX509Cert` representing the verified + * certificate chain for the given usage, or null if there is none. + */ +function getChainForUsage(results, usage) { + for (let result of results) { + if ( + certificateUsages[result.usageString] == usage && + result.errorCode == PRErrorCodeSuccess + ) { + return result.chain; + } + } + return null; +} diff --git a/security/manager/pki/resources/content/resetpassword.js b/security/manager/pki/resources/content/resetpassword.js new file mode 100644 index 0000000000..30db349794 --- /dev/null +++ b/security/manager/pki/resources/content/resetpassword.js @@ -0,0 +1,28 @@ +/* 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-globals-from pippki.js */ +"use strict"; + +document.addEventListener("dialogaccept", resetPassword); + +function resetPassword() { + var pk11db = Cc["@mozilla.org/security/pk11tokendb;1"].getService( + Ci.nsIPK11TokenDB + ); + var token = pk11db.getInternalKeyToken(); + token.reset(); + + try { + Services.logins.removeAllUserFacingLogins(); + } catch (e) {} + + let l10n = new Localization(["security/pippki/pippki.ftl"], true); + if (l10n) { + Services.prompt.alert( + window, + l10n.formatValueSync("pippki-reset-password-confirmation-title"), + l10n.formatValueSync("pippki-reset-password-confirmation-message") + ); + } +} diff --git a/security/manager/pki/resources/content/resetpassword.xhtml b/security/manager/pki/resources/content/resetpassword.xhtml new file mode 100644 index 0000000000..397ee1ab27 --- /dev/null +++ b/security/manager/pki/resources/content/resetpassword.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window data-l10n-id="reset-primary-password-window2" + data-l10n-attrs="title, style" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> +<dialog id="reset_password" + buttons="accept,cancel" + buttonidaccept="reset-password-button-label" + defaultButton="cancel"> + +<linkset> + <html:link rel="localization" href="security/pippki/pippki.ftl"/> +</linkset> + + <stringbundle id="pippki_bundle" src="chrome://pippki/locale/pippki.properties"/> + + <script src="chrome://pippki/content/pippki.js"/> + <script src="chrome://pippki/content/resetpassword.js"/> + + <hbox flex="1"> + <vbox> + <image class="alert-icon" style="margin: 5px;"/> + </vbox> + <vbox style="margin: 5px;" flex="1"> + <hbox flex="1"> + <vbox flex="1"> + <description data-l10n-id="reset-primary-password-text"></description> + </vbox> + </hbox> + </vbox> + </hbox> +</dialog> +</window> diff --git a/security/manager/pki/resources/content/setp12password.js b/security/manager/pki/resources/content/setp12password.js new file mode 100644 index 0000000000..14200c36ce --- /dev/null +++ b/security/manager/pki/resources/content/setp12password.js @@ -0,0 +1,127 @@ +/* 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"; + +/** + * @file Implements the functionality of setp12password.xhtml: a dialog that lets + * the user confirm the password to set on a PKCS #12 file. + * @param {nsISupports} window.arguments.0 + * Object to set the return values of calling the dialog on, queryable + * to the underlying type of SetP12PasswordReturnValues. + */ + +/** + * @typedef SetP12PasswordReturnValues + * @type {nsIWritablePropertyBag2} + * @property {boolean} confirmedPassword + * Set to true if the user entered two matching passwords and + * confirmed the dialog. + * @property {string} password + * The password the user entered. Undefined value if + * |confirmedPassword| is not true. + */ + +/** + * onload() handler. + */ +function onLoad() { + // Ensure the first password textbox has focus. + document.getElementById("pw1").focus(); + document.addEventListener("dialogaccept", onDialogAccept); + document.addEventListener("dialogcancel", onDialogCancel); +} + +/** + * ondialogaccept() handler. + */ +function onDialogAccept() { + let password = document.getElementById("pw1").value; + + let retVals = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2); + retVals.setPropertyAsBool("confirmedPassword", true); + retVals.setPropertyAsAString("password", password); +} + +/** + * ondialogcancel() handler. + */ +function onDialogCancel() { + let retVals = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2); + retVals.setPropertyAsBool("confirmedPassword", false); +} + +/** + * Calculates the strength of the given password, suitable for use in updating + * a progress bar that represents said strength. + * + * The strength of the password is calculated by checking the number of: + * - Characters + * - Numbers + * - Non-alphanumeric chars + * - Upper case characters + * + * @param {string} password + * The password to calculate the strength of. + * @returns {number} + * The strength of the password in the range [0, 100]. + */ +function getPasswordStrength(password) { + let lengthStrength = password.length; + if (lengthStrength > 5) { + lengthStrength = 5; + } + + let nonNumericChars = password.replace(/[0-9]/g, ""); + let numericStrength = password.length - nonNumericChars.length; + if (numericStrength > 3) { + numericStrength = 3; + } + + let nonSymbolChars = password.replace(/\W/g, ""); + let symbolStrength = password.length - nonSymbolChars.length; + if (symbolStrength > 3) { + symbolStrength = 3; + } + + let nonUpperAlphaChars = password.replace(/[A-Z]/g, ""); + let upperAlphaStrength = password.length - nonUpperAlphaChars.length; + if (upperAlphaStrength > 3) { + upperAlphaStrength = 3; + } + + let strength = + lengthStrength * 10 - + 20 + + numericStrength * 10 + + symbolStrength * 15 + + upperAlphaStrength * 10; + if (strength < 0) { + strength = 0; + } + if (strength > 100) { + strength = 100; + } + + return strength; +} + +/** + * oninput() handler for both password textboxes. + * + * @param {boolean} recalculatePasswordStrength + * Whether to recalculate the strength of the first password. + */ +function onPasswordInput(recalculatePasswordStrength) { + let pw1 = document.getElementById("pw1").value; + + if (recalculatePasswordStrength) { + document.getElementById("pwmeter").value = getPasswordStrength(pw1); + } + + // Disable the accept button if the two passwords don't match, and enable it + // if the passwords do match. + let pw2 = document.getElementById("pw2").value; + document.getElementById("setp12password").getButton("accept").disabled = + pw1 != pw2; +} diff --git a/security/manager/pki/resources/content/setp12password.xhtml b/security/manager/pki/resources/content/setp12password.xhtml new file mode 100644 index 0000000000..b521f028ef --- /dev/null +++ b/security/manager/pki/resources/content/setp12password.xhtml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<!DOCTYPE window> + +<window data-l10n-id="set-password-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + style="width: 48em;" + onload="onLoad();"> +<dialog id="setp12password" + buttons="accept,cancel"> + +<linkset> + <html:link rel="localization" href="security/pippki/pippki.ftl"/> +</linkset> + + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + + <script src="chrome://pippki/content/setp12password.js"/> + + <description data-l10n-id="set-password-message"></description> + <separator /> + <vbox> + <hbox class="input-row"> + <label flex="1" data-l10n-id="set-password-backup-pw"/> + <html:input id="pw1" type="password" oninput="onPasswordInput(true);"/> + </hbox> + <hbox class="input-row"> + <label flex="1" data-l10n-id="set-password-repeat-backup-pw"/> + <html:input id="pw2" type="password" oninput="onPasswordInput(false);"/> + </hbox> + </vbox> + <separator/> + <description data-l10n-id="set-password-reminder"></description> + <separator/> + + <vbox style="margin: 6px;"> + <html:label for="pwmeter" style="display: -moz-box;" data-l10n-id="password-quality-meter"></html:label> + <html:progress id="pwmeter" value="0" max="100"/> + </vbox> +</dialog> +</window> diff --git a/security/manager/pki/resources/jar.mn b/security/manager/pki/resources/jar.mn new file mode 100644 index 0000000000..c884037838 --- /dev/null +++ b/security/manager/pki/resources/jar.mn @@ -0,0 +1,31 @@ +# 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/. + +pippki.jar: +% content pippki %content/pippki/ + content/pippki/certManager.js (content/certManager.js) + content/pippki/certManager.css (content/certManager.css) + content/pippki/certManager.xhtml (content/certManager.xhtml) + content/pippki/changepassword.js (content/changepassword.js) + content/pippki/changepassword.xhtml (content/changepassword.xhtml) + content/pippki/clientauthask.js (content/clientauthask.js) + content/pippki/clientauthask.xhtml (content/clientauthask.xhtml) + content/pippki/deletecert.js (content/deletecert.js) + content/pippki/deletecert.xhtml (content/deletecert.xhtml) + content/pippki/device_manager.js (content/device_manager.js) + content/pippki/device_manager.xhtml (content/device_manager.xhtml) + content/pippki/downloadcert.js (content/downloadcert.js) + content/pippki/downloadcert.xhtml (content/downloadcert.xhtml) + content/pippki/editcacert.js (content/editcacert.js) + content/pippki/editcacert.xhtml (content/editcacert.xhtml) + content/pippki/exceptionDialog.css (content/exceptionDialog.css) + content/pippki/exceptionDialog.js (content/exceptionDialog.js) +* content/pippki/exceptionDialog.xhtml (content/exceptionDialog.xhtml) + content/pippki/load_device.js (content/load_device.js) + content/pippki/load_device.xhtml (content/load_device.xhtml) + content/pippki/pippki.js (content/pippki.js) + content/pippki/resetpassword.js (content/resetpassword.js) + content/pippki/resetpassword.xhtml (content/resetpassword.xhtml) + content/pippki/setp12password.js (content/setp12password.js) + content/pippki/setp12password.xhtml (content/setp12password.xhtml) diff --git a/security/manager/pki/resources/moz.build b/security/manager/pki/resources/moz.build new file mode 100644 index 0000000000..d988c0ff9b --- /dev/null +++ b/security/manager/pki/resources/moz.build @@ -0,0 +1,7 @@ +# -*- 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/. + +JAR_MANIFESTS += ["jar.mn"] |