Files
tubestation/dom/mls/MLS.cpp
Benjamin Beurdouche 4e1e4fe2c9 Bug 1900537 - Part 3: Web API for MLS. r=webidl,ipc-reviewers,smaug,nika
This patch contains an experimental Web API for the Messaging Layer Security (RFC 9420) protocol.
The API allows to securely generate cryptographic material and build large dynamic groups with state-of-the-art security.

The state (both public and secret) is stored in the profile and isolated by origin within dedicated databases.
No secret can be exfiltrated through the API, and privacy risks are minimized due to the selected isolation.

Differential Revision: https://phabricator.services.mozilla.com/D210568
2024-12-18 14:04:20 +00:00

636 lines
21 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "mozilla/dom/MLS.h"
#include "mozilla/dom/MLSGroupView.h"
#include "mozilla/dom/TypedArray.h"
#include "mozilla/dom/Promise.h"
#include "nsTArray.h"
#include "nsCOMPtr.h"
#include "nsIGlobalObject.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/dom/MLSTransactionChild.h"
#include "mozilla/dom/MLSTransactionMessage.h"
#include "mozilla/dom/PMLSTransaction.h"
#include "mozilla/ipc/Endpoint.h"
#include "mozilla/BasePrincipal.h"
#include "MLSGroupView.h"
#include "nsTArray.h"
#include "mozilla/Logging.h"
#include "mozilla/Span.h"
#include "nsDebug.h"
#include "MLSLogging.h"
#include "MLSTypeUtils.h"
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MLS, mGlobalObject)
NS_IMPL_CYCLE_COLLECTING_ADDREF(MLS)
NS_IMPL_CYCLE_COLLECTING_RELEASE(MLS)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MLS)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
// Setup logging
mozilla::LazyLogModule gMlsLog("MLS");
/* static */ already_AddRefed<MLS> MLS::Constructor(GlobalObject& aGlobalObject,
ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::Constructor()"));
nsCOMPtr<nsIGlobalObject> global(
do_QueryInterface(aGlobalObject.GetAsSupports()));
if (NS_WARN_IF(!global)) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
// Get the principal and perform some validation on it.
// We do not allow MLS in Private Browsing Mode for now.
nsIPrincipal* principal = global->PrincipalOrNull();
if (!principal || !principal->GetIsContentPrincipal() ||
principal->GetIsInPrivateBrowsing()) {
aRv.ThrowSecurityError("Cannot create MLS store for origin");
return nullptr;
}
// Create the endpoints for the MLS actor
mozilla::ipc::Endpoint<PMLSTransactionParent> parentEndpoint;
mozilla::ipc::Endpoint<PMLSTransactionChild> childEndpoint;
MOZ_ALWAYS_SUCCEEDS(
PMLSTransaction::CreateEndpoints(&parentEndpoint, &childEndpoint));
mozilla::ipc::PBackgroundChild* backgroundChild =
mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
if (!backgroundChild) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
// Bind the child actor, and send the parent endpoint.
RefPtr<MLSTransactionChild> actor = new MLSTransactionChild();
MOZ_ALWAYS_TRUE(childEndpoint.Bind(actor));
MOZ_ALWAYS_TRUE(backgroundChild->SendCreateMLSTransaction(
std::move(parentEndpoint), WrapNotNull(principal)));
return MakeAndAddRef<MLS>(global, actor);
}
MLS::MLS(nsIGlobalObject* aGlobalObject, MLSTransactionChild* aActor)
: mGlobalObject(aGlobalObject), mTransactionChild(aActor) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::MLS()"));
}
MLS::~MLS() {
if (mTransactionChild) {
mTransactionChild->Close();
}
}
JSObject* MLS::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
return MLS_Binding::Wrap(aCx, this, aGivenProto);
}
//
// API
//
already_AddRefed<Promise> MLS::DeleteState(ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::DeleteState()"));
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
mTransactionChild->SendRequestStateDelete(
[promise](bool result) {
if (result) {
promise->MaybeResolveWithUndefined();
} else {
promise->MaybeReject(NS_ERROR_FAILURE);
}
},
[promise](::mozilla::ipc::ResponseRejectReason) {
promise->MaybeRejectWithUnknownError("deleteState failed");
});
return promise.forget();
}
already_AddRefed<Promise> MLS::GenerateIdentity(ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::GenerateIdentity()"));
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
mTransactionChild->SendRequestGenerateIdentityKeypair()->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this}](Maybe<RawBytes>&& result) {
// Check if the value is Nothing
if (result.isNothing()) {
promise->MaybeRejectWithUnknownError(
"generateIdentityKeypair failed");
return;
}
// Get the context from the GlobalObject
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(self->mGlobalObject))) {
promise->MaybeRejectWithUnknownError(
"generateIdentityKeypair failed");
return;
}
JSContext* cx = jsapi.cx();
// Construct the Uint8Array object
ErrorResult error;
JS::Rooted<JSObject*> content(
cx, Uint8Array::Create(cx, result->data(), error));
error.WouldReportJSException();
if (error.Failed()) {
promise->MaybeReject(std::move(error));
return;
}
// Construct MLSBytes with the client identifer as content
RootedDictionary<MLSBytes> rvalue(cx);
rvalue.mType = MLSObjectType::Client_identifier;
rvalue.mContent.Init(content);
// Resolve the promise with the MLSBytes object
promise->MaybeResolve(rvalue);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
promise->MaybeRejectWithUnknownError("generateIdentity failed");
});
return promise.forget();
}
already_AddRefed<Promise> MLS::GenerateCredential(
const MLSBytesOrUint8ArrayOrUTF8String& aJsCredContent, ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug,
("MLS::GenerateCredentialBasic()"));
// Handle the credential content parameter
nsTArray<uint8_t> credContent = ExtractMLSBytesOrUint8ArrayOrUTF8String(
MLSObjectType::Credential_basic, aJsCredContent, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the credContent is empty
if (NS_WARN_IF(credContent.IsEmpty())) {
aRv.ThrowTypeError("The credential content must not be empty");
return nullptr;
}
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
mTransactionChild->SendRequestGenerateCredentialBasic(credContent)
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this}](Maybe<RawBytes>&& result) {
// Check if the value is Nothing
if (result.isNothing()) {
promise->MaybeRejectWithUnknownError(
"generateCredentialBasic failed");
return;
}
// Get the context from the GlobalObject
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(self->mGlobalObject))) {
promise->MaybeRejectWithUnknownError(
"generateCredentialBasic failed");
return;
}
JSContext* cx = jsapi.cx();
// Construct the Uint8Array object
ErrorResult error;
JS::Rooted<JSObject*> content(
cx, Uint8Array::Create(cx, result->data(), error));
error.WouldReportJSException();
if (error.Failed()) {
promise->MaybeReject(std::move(error));
return;
}
// Construct MLSBytes with the client identifer as content
RootedDictionary<MLSBytes> rvalue(cx);
rvalue.mType = MLSObjectType::Credential_basic;
rvalue.mContent.Init(content);
// Resolve the promise
promise->MaybeResolve(rvalue);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
promise->MaybeRejectWithUnknownError(
"generateCredentialBasic failed");
});
return promise.forget();
}
already_AddRefed<Promise> MLS::GenerateKeyPackage(
const MLSBytesOrUint8Array& aJsClientIdentifier,
const MLSBytesOrUint8Array& aJsCredential, ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::GenerateKeyPackage()"));
// Handle the client identifier parameter
nsTArray<uint8_t> clientIdentifier = ExtractMLSBytesOrUint8Array(
MLSObjectType::Client_identifier, aJsClientIdentifier, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the client identifier is empty
if (NS_WARN_IF(clientIdentifier.IsEmpty())) {
aRv.ThrowTypeError("The client identifier must not be empty");
return nullptr;
}
// Handle the credential parameter
nsTArray<uint8_t> credential = ExtractMLSBytesOrUint8Array(
MLSObjectType::Credential_basic, aJsCredential, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the credential is empty
if (NS_WARN_IF(credential.IsEmpty())) {
aRv.ThrowTypeError("The credential must not be empty");
return nullptr;
}
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Use the static method or instance to send the IPC message
mTransactionChild->SendRequestGenerateKeyPackage(clientIdentifier, credential)
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this}](Maybe<RawBytes>&& keyPackage) {
// Check if the value is Nothing
if (keyPackage.isNothing()) {
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
// Get the context from the GlobalObject
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(self->mGlobalObject))) {
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
JSContext* cx = jsapi.cx();
// Construct the Uint8Array object
ErrorResult error;
JS::Rooted<JSObject*> content(
cx, Uint8Array::Create(cx, keyPackage->data(), error));
error.WouldReportJSException();
if (error.Failed()) {
promise->MaybeReject(std::move(error));
return;
}
// Construct MLSBytes with the client identifer as content
RootedDictionary<MLSBytes> rvalue(cx);
rvalue.mType = MLSObjectType::Key_package;
rvalue.mContent.Init(content);
// Resolve the promise
promise->MaybeResolve(rvalue);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
promise->MaybeRejectWithUnknownError("generateKeyPackage failed");
});
return promise.forget();
}
already_AddRefed<Promise> MLS::GroupCreate(
const MLSBytesOrUint8Array& aJsClientIdentifier,
const MLSBytesOrUint8Array& aJsCredential, ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::GroupCreate()"));
// Handle the client identifier parameter
nsTArray<uint8_t> clientIdentifier = ExtractMLSBytesOrUint8Array(
MLSObjectType::Client_identifier, aJsClientIdentifier, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the client identifier is empty
if (NS_WARN_IF(clientIdentifier.IsEmpty())) {
aRv.ThrowTypeError("The client identifier must not be empty");
return nullptr;
}
// Handle the credential parameter
nsTArray<uint8_t> credential = ExtractMLSBytesOrUint8Array(
MLSObjectType::Credential_basic, aJsCredential, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the credential is empty
if (NS_WARN_IF(credential.IsEmpty())) {
aRv.ThrowTypeError("The credential must not be empty");
return nullptr;
}
// Log the hex of clientIdentifier
if (MOZ_LOG_TEST(gMlsLog, LogLevel::Debug)) {
nsAutoCString clientIdHex;
for (uint8_t byte : clientIdentifier) {
clientIdHex.AppendPrintf("%02X", byte);
}
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug,
("clientIdentifier in hex: %s\n", clientIdHex.get()));
}
// Initialize jsGroupIdentifier to one byte of value 0xFF.
// We do not want to allow choosing the GID at this point.
// This value not being of the correct length will be discarded
// internally and a fresh GID will be generated.
//
// In the future, the caller will allow choosing the GID.
AutoTArray<uint8_t, 1> groupIdentifier{0xFF};
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Use the static method or instance to send the IPC message
mTransactionChild
->SendRequestGroupCreate(clientIdentifier, credential, groupIdentifier)
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this},
clientIdentifier(std::move(clientIdentifier))](
Maybe<mozilla::security::mls::GkGroupIdEpoch>&&
groupIdEpoch) mutable {
// Check if the value is Nothing
if (groupIdEpoch.isNothing()) {
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
RefPtr<MLSGroupView> group =
new MLSGroupView(self, std::move(groupIdEpoch->group_id),
std::move(clientIdentifier));
promise->MaybeResolve(group);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Error,
("IPC message rejected with reason: %d",
static_cast<int>(aReason)));
promise->MaybeRejectWithUnknownError("groupCreate failed");
});
return promise.forget();
}
already_AddRefed<mozilla::dom::Promise> MLS::GroupGet(
const MLSBytesOrUint8Array& aJsGroupIdentifier,
const MLSBytesOrUint8Array& aJsClientIdentifier, ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::GroupGet()"));
// Handle the group identifier parameter
nsTArray<uint8_t> groupIdentifier = ExtractMLSBytesOrUint8Array(
MLSObjectType::Group_identifier, aJsGroupIdentifier, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the group identifier is empty
if (NS_WARN_IF(groupIdentifier.IsEmpty())) {
aRv.ThrowTypeError("The group identifier must not be empty");
return nullptr;
}
// Handle the client identifier parameter
nsTArray<uint8_t> clientIdentifier = ExtractMLSBytesOrUint8Array(
MLSObjectType::Client_identifier, aJsClientIdentifier, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the client identifier is empty
if (NS_WARN_IF(clientIdentifier.IsEmpty())) {
aRv.ThrowTypeError("The client identifier must not be empty");
return nullptr;
}
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Initialize label, context and len
// We pass this through IPC to be able to reuse the same code for different
// labels in the future
AutoTArray<uint8_t, 7> label{'l', 'i', 'v', 'e', 'n', 'e', 's', 's'};
AutoTArray<uint8_t, 1> context{0x00};
uint64_t len = 32;
// Use the static method or instance to send the IPC message
mTransactionChild
->SendRequestExportSecret(groupIdentifier, clientIdentifier, label,
context, len)
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this},
groupIdentifier(std::move(groupIdentifier)),
clientIdentifier(std::move(clientIdentifier))](
Maybe<mozilla::security::mls::GkExporterOutput>&&
exporterOutput) mutable {
// Check if the exporterOutput contains a value
if (exporterOutput.isNothing()) {
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
RefPtr<MLSGroupView> group =
new MLSGroupView(self, std::move(exporterOutput->group_id),
std::move(clientIdentifier));
promise->MaybeResolve(group);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
promise->MaybeRejectWithUnknownError("exportSecret failed");
});
return promise.forget();
}
already_AddRefed<Promise> MLS::GroupJoin(
const MLSBytesOrUint8Array& aJsClientIdentifier,
const MLSBytesOrUint8Array& aJsWelcome, ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::GroupJoin()"));
// Handle the client identifier parameter
nsTArray<uint8_t> clientIdentifier = ExtractMLSBytesOrUint8Array(
MLSObjectType::Client_identifier, aJsClientIdentifier, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the client identifier is empty
if (NS_WARN_IF(clientIdentifier.IsEmpty())) {
aRv.ThrowTypeError("The client identifier must not be empty");
return nullptr;
}
// Handle the welcome parameter
nsTArray<uint8_t> welcome =
ExtractMLSBytesOrUint8Array(MLSObjectType::Welcome, aJsWelcome, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the welcome is empty
if (NS_WARN_IF(welcome.IsEmpty())) {
aRv.ThrowTypeError("The welcome must not be empty");
return nullptr;
}
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
mTransactionChild->SendRequestGroupJoin(clientIdentifier, welcome)
->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this},
clientIdentifier(std::move(clientIdentifier))](
Maybe<mozilla::security::mls::GkGroupIdEpoch>&&
groupIdEpoch) mutable {
// Check if the value is Nothing
if (groupIdEpoch.isNothing()) {
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
// Returns groupId and epoch
RefPtr<MLSGroupView> group =
new MLSGroupView(self, std::move(groupIdEpoch->group_id),
std::move(clientIdentifier));
// Resolve the promise
promise->MaybeResolve(group);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
promise->MaybeRejectWithUnknownError("groupJoin failed");
});
return promise.forget();
}
already_AddRefed<Promise> MLS::GetGroupIdFromMessage(
const MLSBytesOrUint8Array& aJsMessage, ErrorResult& aRv) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug, ("MLS::GetGroupIdFromMessage()"));
// Handle the message parameter
nsTArray<uint8_t> message =
ExtractMLSBytesOrUint8ArrayWithUnknownType(aJsMessage, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Check if the message is empty
if (NS_WARN_IF(message.IsEmpty())) {
aRv.ThrowTypeError("The message must not be empty");
return nullptr;
}
// Create a new Promise object for the result
RefPtr<Promise> promise = Promise::Create(mGlobalObject, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
mTransactionChild->SendRequestGetGroupIdentifier(message)->Then(
GetCurrentSerialEventTarget(), __func__,
[promise, self = RefPtr{this},
message(std::move(message))](Maybe<RawBytes>&& result) {
// Check if the value is Nothing
if (result.isNothing()) {
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
// Get the context from the GlobalObject
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(self->mGlobalObject))) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Error,
("Failed to initialize JSAPI"));
promise->MaybeReject(NS_ERROR_FAILURE);
return;
}
JSContext* cx = jsapi.cx();
// Construct the Uint8Array objects based on the tag
ErrorResult error;
JS::Rooted<JSObject*> jsGroupId(
cx, Uint8Array::Create(cx, result->data(), error));
error.WouldReportJSException();
if (error.Failed()) {
promise->MaybeReject(std::move(error));
return;
}
// Construct the MLSBytes object for the groupId
RootedDictionary<MLSBytes> rvalue(cx);
rvalue.mType = MLSObjectType::Group_identifier;
rvalue.mContent.Init(jsGroupId);
// Log if in debug mode
if (MOZ_LOG_TEST(gMlsLog, LogLevel::Debug)) {
MOZ_LOG(gMlsLog, mozilla::LogLevel::Debug,
("Successfully constructed MLSBytes"));
}
// Resolve the promise
promise->MaybeResolve(rvalue);
},
[promise](::mozilla::ipc::ResponseRejectReason aReason) {
MOZ_LOG(
gMlsLog, mozilla::LogLevel::Error,
("IPC call rejected with reason: %d", static_cast<int>(aReason)));
promise->MaybeRejectWithUnknownError("getGroupIdFromMessage failed");
});
return promise.forget();
}
} // namespace mozilla::dom