/* 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 "LockstoreService.h" #include "mozilla/ErrorResult.h" #include "mozilla/RefPtr.h" #include "mozilla/Result.h" #include "mozilla/Services.h" #include "mozilla/dom/Promise.h" #include "nsAppDirectoryServiceDefs.h" #include "nsCOMPtr.h" #include "nsDirectoryServiceUtils.h" #include "nsIFile.h" #include "nsIGlobalObject.h" #include "nsIObserverService.h" #include "nsProxyRelease.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "xpcpublic.h" #include #include namespace mozilla::security::lockstore { using dom::Promise; NS_IMPL_ISUPPORTS(LockstoreService, nsILockstore, nsIObserver) LockstoreService::LockstoreService() : mMutex("LockstoreService::mMutex"), mKeystore(nullptr), mShutdown(false) {} LockstoreService::~LockstoreService() { // No mutex needed: the destructor runs only after the last refcount // drops, so no other thread can be racing the in-flight FFI callbacks // (they each hold a `RefPtr`). if (mKeystore) { keystore_close(mKeystore); mKeystore = nullptr; } } nsresult LockstoreService::Init() { // Resolve the profile path on the main thread and cache it. // `nsIDirectoryService::Get` asserts main-thread, and every sync // `Do*` method runs off-main, so the directory lookup must happen // here at construction time. nsCOMPtr profileDir; MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profileDir))); nsAutoString widePath; MOZ_TRY(profileDir->GetPath(widePath)); CopyUTF16toUTF8(widePath, mProfilePath); nsCOMPtr os = services::GetObserverService(); if (os) { os->AddObserver(this, "profile-before-change", false); os->AddObserver(this, "xpcom-shutdown", false); } return NS_OK; } // static already_AddRefed LockstoreService::GetSingleton() { nsCOMPtr svc = do_GetService("@mozilla.org/security/lockstore;1"); if (!svc) { return nullptr; } // The component is registered as a singleton in components.conf and // LockstoreService is the only implementer of nsILockstore in tree, so // a downcast is safe. RefPtr ls = static_cast(svc.get()); return ls.forget(); } nsresult LockstoreService::EnsureOpenLocked() { mMutex.AssertCurrentThreadOwns(); if (mShutdown) { return NS_ERROR_NOT_AVAILABLE; } if (mKeystore) { return NS_OK; } // `Init()` runs unconditionally as the component's `init_method` // before any FFI dispatch can reach this point. MOZ_ASSERT(!mProfilePath.IsEmpty(), "Init() must have run first"); return keystore_open(&mProfilePath, &mKeystore); } // --------------------------------------------------------------------------- // nsIObserver // --------------------------------------------------------------------------- NS_IMETHODIMP LockstoreService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { // mMutex is held across every FFI call by the sync `Do*` tier, so // acquiring it here implicitly drains any in-flight background work: // a running encrypt/decrypt holds the mutex and blocks us until it // finishes; a runnable that hasn't yet started will see mShutdown == // true and fail via `EnsureOpenLocked`. MutexAutoLock lock(mMutex); if (mShutdown) { return NS_OK; } mShutdown = true; if (mKeystore) { keystore_close(mKeystore); mKeystore = nullptr; } return NS_OK; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- namespace { nsresult NewDOMPromise(JSContext* aCx, RefPtr& aOut) { nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); if (NS_WARN_IF(!global)) { return NS_ERROR_UNEXPECTED; } ErrorResult err; aOut = Promise::Create(global, err); if (NS_WARN_IF(err.Failed())) { return err.StealNSResult(); } return NS_OK; } // Dispatches a sync `Do*` method onto a background task and bridges // the result to a DOM Promise. `Result` is the return type of the // method — either `nsresult` (resolves with undefined) or // `mozilla::Result` (resolves with T). One template // covers every method shape; the constexpr branch picks the right // resolution at the bridge. template nsresult ImplXpcomMethod(LockstoreService* aLockstore, JSContext* aCx, Promise** aPromise, Result (LockstoreService::*aMethod)(Args...), Storages... aArgs) { MOZ_RELEASE_ASSERT(NS_IsMainThread()); RefPtr domPromise; MOZ_TRY(NewDOMPromise(aCx, domPromise)); // Wrap the DOM promise so its addref / release stay on the main // thread: the lambdas below run on a background task and a // main-thread runnable, but capturing a bare `RefPtr` // would bump the refcount on the background thread when the inner // lambda is constructed. nsMainThreadPtrHandle domHandle(new nsMainThreadPtrHolder( "LockstoreService::ImplXpcomMethod::DOMPromise", domPromise)); nsresult rv = NS_DispatchBackgroundTask(NS_NewRunnableFunction( "LockstoreService::ImplXpcomMethod", [self = RefPtr{aLockstore}, aMethod, domHandle, ... args = std::forward(aArgs)]() mutable { auto result = (self.get()->*aMethod)(args...); NS_DispatchToMainThread(NS_NewRunnableFunction( "LockstoreService::ImplXpcomMethod::Resolve", [domHandle = std::move(domHandle), result = std::move(result)]() mutable { if constexpr (std::is_same_v) { if (NS_FAILED(result)) { domHandle->MaybeReject(result); } else { domHandle->MaybeResolveWithUndefined(); } } else { if (result.isErr()) { domHandle->MaybeReject(result.unwrapErr()); } else { domHandle->MaybeResolve(std::move(result.unwrap())); } } })); })); if (NS_FAILED(rv)) { // Dispatch failed; surface the failure to JS rather than leaving // the promise pending forever. domPromise->MaybeReject(rv); } domPromise.forget(aPromise); return NS_OK; } } // namespace // --------------------------------------------------------------------------- // Synchronous nsILockstore methods (cheap in-memory state only) // --------------------------------------------------------------------------- NS_IMETHODIMP LockstoreService::IsKekUnlocked(const nsACString& aKekRef, bool* aOut) { MutexAutoLock lock(mMutex); MOZ_TRY(EnsureOpenLocked()); return keystore_is_kek_unlocked(mKeystore, &aKekRef, aOut); } // --------------------------------------------------------------------------- // Synchronous C++ tier — invokes the FFI on the calling thread under // `mMutex`. Asserts off-main-thread in debug builds. // --------------------------------------------------------------------------- #define LOCKSTORE_SYNC_PREAMBLE \ MOZ_ASSERT(!NS_IsMainThread(), \ "Synchronous Lockstore I/O is forbidden on the main " \ "thread; dispatch onto a background task instead."); \ MutexAutoLock lock(mMutex); \ MOZ_TRY(EnsureOpenLocked()) nsresult LockstoreService::DoUnlockKek(const nsACString& aKekRef, const nsACString& aSecret, uint32_t aTimeoutMs) { LOCKSTORE_SYNC_PREAMBLE; return keystore_unlock_kek(mKeystore, &aKekRef, &aSecret, aTimeoutMs); } nsresult LockstoreService::DoLockKek(const nsACString& aKekRef) { LOCKSTORE_SYNC_PREAMBLE; return keystore_lock_kek(mKeystore, &aKekRef); } nsresult LockstoreService::DoLock() { LOCKSTORE_SYNC_PREAMBLE; return keystore_lock(mKeystore); } nsresult LockstoreService::DoCreateDek(const nsACString& aCollection, const nsACString& aKekRef, bool aExtractable) { LOCKSTORE_SYNC_PREAMBLE; return keystore_create_dek(mKeystore, &aCollection, &aKekRef, aExtractable); } nsresult LockstoreService::DoImportDek(const nsACString& aCollection, const nsACString& aKekRef, const nsTArray& aDekBytes, bool aExtractable) { LOCKSTORE_SYNC_PREAMBLE; return keystore_import_dek(mKeystore, &aCollection, &aKekRef, aDekBytes.Elements(), aDekBytes.Length(), aExtractable); } Result LockstoreService::DoIsDekExtractable( const nsACString& aCollection) { LOCKSTORE_SYNC_PREAMBLE; bool out = false; MOZ_TRY(keystore_is_dek_extractable(mKeystore, &aCollection, &out)); return out; } nsresult LockstoreService::DoDeleteDek(const nsACString& aCollection) { LOCKSTORE_SYNC_PREAMBLE; return keystore_delete_dek(mKeystore, &aCollection); } nsresult LockstoreService::DoAddKek(const nsACString& aCollection, const nsACString& aFromKekRef, const nsACString& aToKekRef) { LOCKSTORE_SYNC_PREAMBLE; return keystore_add_kek(mKeystore, &aCollection, &aFromKekRef, &aToKekRef); } nsresult LockstoreService::DoRemoveKek(const nsACString& aCollection, const nsACString& aKekRef) { LOCKSTORE_SYNC_PREAMBLE; return keystore_remove_kek(mKeystore, &aCollection, &aKekRef); } nsresult LockstoreService::DoSwitchKek(const nsACString& aCollection, const nsACString& aOldKekRef, const nsACString& aNewKekRef) { LOCKSTORE_SYNC_PREAMBLE; return keystore_switch_kek(mKeystore, &aCollection, &aOldKekRef, &aNewKekRef); } Result, nsresult> LockstoreService::DoListDeks() { LOCKSTORE_SYNC_PREAMBLE; nsTArray out; MOZ_TRY(keystore_list_deks(mKeystore, &out)); return out; } Result, nsresult> LockstoreService::DoListKeks( const nsACString& aDekName) { LOCKSTORE_SYNC_PREAMBLE; nsTArray out; MOZ_TRY(keystore_list_keks(mKeystore, &aDekName, &out)); return out; } Result, nsresult> LockstoreService::DoEncrypt( const nsACString& aCollection, const nsACString& aKekRef, const nsTArray& aPlaintext) { LOCKSTORE_SYNC_PREAMBLE; nsTArray out; MOZ_TRY(keystore_encrypt(mKeystore, &aCollection, &aKekRef, aPlaintext.Elements(), aPlaintext.Length(), &out)); return out; } Result, nsresult> LockstoreService::DoDecrypt( const nsACString& aCollection, const nsACString& aKekRef, const nsTArray& aCiphertext) { LOCKSTORE_SYNC_PREAMBLE; nsTArray out; MOZ_TRY(keystore_decrypt(mKeystore, &aCollection, &aKekRef, aCiphertext.Elements(), aCiphertext.Length(), &out)); return out; } Result, nsresult> LockstoreService::DoGetDek( const nsACString& aCollection, const nsACString& aKekRef) { LOCKSTORE_SYNC_PREAMBLE; nsTArray out; MOZ_TRY(keystore_get_dek(mKeystore, &aCollection, &aKekRef, &out)); return out; } Result LockstoreService::DoCreateKek( const nsACString& aKekType, const nsACString& aIdentifier, const nsACString& aSecret, uint32_t aCacheTimeoutMs) { LOCKSTORE_SYNC_PREAMBLE; nsCString out; MOZ_TRY(keystore_create_kek(mKeystore, &aKekType, &aIdentifier, &aSecret, aCacheTimeoutMs, &out)); return out; } nsresult LockstoreService::DoDeleteKek(const nsACString& aKekRef) { LOCKSTORE_SYNC_PREAMBLE; return keystore_delete_kek(mKeystore, &aKekRef); } #undef LOCKSTORE_SYNC_PREAMBLE // --------------------------------------------------------------------------- // nsILockstore async tier — one-line adapters over the sync C++ tier // via `ImplXpcomMethod`. // --------------------------------------------------------------------------- NS_IMETHODIMP LockstoreService::UnlockKek(const nsACString& aKekRef, const nsACString& aSecret, uint32_t aTimeoutMs, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoUnlockKek, nsCString{aKekRef}, nsCString{aSecret}, aTimeoutMs); } NS_IMETHODIMP LockstoreService::LockKek(const nsACString& aKekRef, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoLockKek, nsCString{aKekRef}); } NS_IMETHODIMP LockstoreService::Lock(JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoLock); } NS_IMETHODIMP LockstoreService::CreateDek(const nsACString& aCollection, const nsACString& aKekRef, bool aExtractable, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoCreateDek, nsCString{aCollection}, nsCString{aKekRef}, aExtractable); } NS_IMETHODIMP LockstoreService::ImportDek(const nsACString& aCollection, const nsACString& aKekRef, const nsTArray& aDekBytes, bool aExtractable, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoImportDek, nsCString{aCollection}, nsCString{aKekRef}, aDekBytes.Clone(), aExtractable); } NS_IMETHODIMP LockstoreService::IsDekExtractable(const nsACString& aCollection, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoIsDekExtractable, nsCString{aCollection}); } NS_IMETHODIMP LockstoreService::DeleteDek(const nsACString& aCollection, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoDeleteDek, nsCString{aCollection}); } NS_IMETHODIMP LockstoreService::AddKek(const nsACString& aCollection, const nsACString& aFromKekRef, const nsACString& aToKekRef, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoAddKek, nsCString{aCollection}, nsCString{aFromKekRef}, nsCString{aToKekRef}); } NS_IMETHODIMP LockstoreService::RemoveKek(const nsACString& aCollection, const nsACString& aKekRef, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoRemoveKek, nsCString{aCollection}, nsCString{aKekRef}); } NS_IMETHODIMP LockstoreService::SwitchKek(const nsACString& aCollection, const nsACString& aOldKekRef, const nsACString& aNewKekRef, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoSwitchKek, nsCString{aCollection}, nsCString{aOldKekRef}, nsCString{aNewKekRef}); } NS_IMETHODIMP LockstoreService::ListDeks(JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoListDeks); } NS_IMETHODIMP LockstoreService::ListKeks(const nsACString& aDekName, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoListKeks, nsCString{aDekName}); } NS_IMETHODIMP LockstoreService::Encrypt(const nsACString& aCollection, const nsACString& aKekRef, const nsTArray& aPlaintext, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoEncrypt, nsCString{aCollection}, nsCString{aKekRef}, aPlaintext.Clone()); } NS_IMETHODIMP LockstoreService::Decrypt(const nsACString& aCollection, const nsACString& aKekRef, const nsTArray& aCiphertext, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoDecrypt, nsCString{aCollection}, nsCString{aKekRef}, aCiphertext.Clone()); } NS_IMETHODIMP LockstoreService::GetDek(const nsACString& aCollection, const nsACString& aKekRef, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoGetDek, nsCString{aCollection}, nsCString{aKekRef}); } NS_IMETHODIMP LockstoreService::CreateKek(const nsACString& aKekType, const nsACString& aIdentifier, const nsACString& aSecret, uint32_t aCacheTimeoutMs, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoCreateKek, nsCString{aKekType}, nsCString{aIdentifier}, nsCString{aSecret}, aCacheTimeoutMs); } NS_IMETHODIMP LockstoreService::DeleteKek(const nsACString& aKekRef, JSContext* aCx, Promise** aPromise) { return ImplXpcomMethod(this, aCx, aPromise, &LockstoreService::DoDeleteKek, nsCString{aKekRef}); } } // namespace mozilla::security::lockstore