Index: chrome/browser/chromeos/platform_keys/platform_keys_nss.cc |
diff --git a/chrome/browser/chromeos/platform_keys/platform_keys_nss.cc b/chrome/browser/chromeos/platform_keys/platform_keys_nss.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b8e311bf7ea4a39b8a600b5287bb32472efb4ffc |
--- /dev/null |
+++ b/chrome/browser/chromeos/platform_keys/platform_keys_nss.cc |
@@ -0,0 +1,577 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include "chrome/browser/chromeos/platform_keys/platform_keys.h" |
+ |
+#include <cryptohi.h> |
+ |
+#include "base/bind.h" |
+#include "base/bind_helpers.h" |
+#include "base/callback.h" |
+#include "base/compiler_specific.h" |
+#include "base/location.h" |
+#include "base/logging.h" |
+#include "base/macros.h" |
+#include "base/single_thread_task_runner.h" |
+#include "base/thread_task_runner_handle.h" |
+#include "base/threading/worker_pool.h" |
+#include "chrome/browser/extensions/api/enterprise_platform_keys/enterprise_platform_keys_api.h" |
+#include "chrome/browser/net/nss_context.h" |
+#include "chrome/browser/profiles/profile.h" |
+#include "content/public/browser/browser_thread.h" |
+#include "crypto/rsa_private_key.h" |
+#include "net/base/crypto_module.h" |
+#include "net/base/net_errors.h" |
+#include "net/cert/cert_database.h" |
+#include "net/cert/nss_cert_database.h" |
+#include "net/cert/x509_certificate.h" |
+ |
+using content::BrowserThread; |
+ |
+namespace { |
+const char kErrorInternal[] = "Internal Error."; |
+const char kErrorKeyNotFound[] = "Key not found."; |
+const char kErrorCertificateNotFound[] = "Certificate could not be found."; |
+const char kErrorAlgorithmNotSupported[] = "Algorithm not supported."; |
+ |
+// The current maximal RSA modulus length that ChromeOS's TPM supports for key |
+// generation. |
+const unsigned int kMaxRSAModulusLength = 2048; |
+} |
+ |
+namespace chromeos { |
+ |
+namespace platform_keys { |
+ |
+namespace { |
+ |
+// Base class to store state that is common to all NSS database operations and |
+// to provide convenience methods to call back. |
+// Keeps track of the originating task runner. |
+class NSSOperationState { |
+ public: |
+ explicit NSSOperationState(Profile* profile); |
+ virtual ~NSSOperationState() {} |
+ |
+ // Called if an error occurred during the execution of the NSS operation |
+ // described by this object. |
+ virtual void OnError(const tracked_objects::Location& from, |
+ const std::string& error_message) = 0; |
+ |
+ Profile* profile_; |
+ crypto::ScopedPK11Slot slot_; |
+ |
+ // The task runner on which the NSS operation was called. Any reply must be |
+ // posted to this runner. |
+ scoped_refptr<base::SingleThreadTaskRunner> origin_task_runner_; |
+ |
+ private: |
+ DISALLOW_COPY_AND_ASSIGN(NSSOperationState); |
+}; |
+ |
+typedef base::Callback<void(net::NSSCertDatabase* cert_db)> GetCertDBCallback; |
+ |
+// Called back with the NSSCertDatabase associated to the given |token_id|. |
+// Calls |callback| if the database was successfully retrieved. Used by |
+// GetCertDatabaseOnIOThread. |
+void DidGetCertDBOnIOThread(const GetCertDBCallback& callback, |
+ NSSOperationState* state, |
+ net::NSSCertDatabase* cert_db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ if (!cert_db) { |
+ LOG(ERROR) << "Couldn't get NSSCertDatabase."; |
+ state->OnError(FROM_HERE, kErrorInternal); |
+ return; |
+ } |
+ |
+ state->slot_ = cert_db->GetPrivateSlot(); |
+ if (!state->slot_) { |
+ LOG(ERROR) << "No private slot"; |
+ state->OnError(FROM_HERE, kErrorInternal); |
+ return; |
+ } |
+ |
+ callback.Run(cert_db); |
+} |
+ |
+// Retrieves the NSSCertDatabase from |context|. Must be called on the IO |
+// thread. |
+void GetCertDatabaseOnIOThread(content::ResourceContext* context, |
+ const GetCertDBCallback& callback, |
+ NSSOperationState* state) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ net::NSSCertDatabase* cert_db = GetNSSCertDatabaseForResourceContext( |
+ context, |
+ base::Bind(&DidGetCertDBOnIOThread, callback, state)); |
+ |
+ if (cert_db) |
+ DidGetCertDBOnIOThread(callback, state, cert_db); |
+} |
+ |
+// Asynchronously fetches the NSSCertDatabase for |token_id| and passes it to |
+// |callback|. Will run |callback| on the IO thread. |
+void GetCertDatabase(const std::string& token_id, |
+ const GetCertDBCallback& callback, |
+ NSSOperationState* state) { |
+ // TODO(pneubeck): Decide which DB to retrieve depending on |token_id|. |
+ BrowserThread::PostTask(BrowserThread::IO, |
+ FROM_HERE, |
+ base::Bind(&GetCertDatabaseOnIOThread, |
+ state->profile_->GetResourceContext(), |
+ callback, |
+ state)); |
+} |
+ |
+class GenerateRSAKeyState : public NSSOperationState { |
+ public: |
+ GenerateRSAKeyState(unsigned int modulus_length, |
+ const GenerateKeyCallback& callback, |
+ Profile* profile); |
+ virtual ~GenerateRSAKeyState() {} |
+ |
+ virtual void OnError(const tracked_objects::Location& from, |
+ const std::string& error_message) OVERRIDE { |
+ CallBack(from, std::string() /* no public key */, error_message); |
+ } |
+ |
+ void CallBack(const tracked_objects::Location& from, |
+ const std::string& public_key_spki_der, |
+ const std::string& error_message) { |
+ origin_task_runner_->PostTask( |
+ from, base::Bind(callback_, public_key_spki_der, error_message)); |
+ } |
+ |
+ unsigned int modulus_length_; |
+ |
+ private: |
+ // Must be called on origin thread, use CallBack() therefore. |
+ GenerateKeyCallback callback_; |
+}; |
+ |
+class SignState : public NSSOperationState { |
+ public: |
+ SignState(const std::string& public_key, |
+ const std::string& data, |
+ const SignCallback& callback, |
+ Profile* profile); |
+ virtual ~SignState() {} |
+ |
+ virtual void OnError(const tracked_objects::Location& from, |
+ const std::string& error_message) OVERRIDE { |
+ CallBack(from, std::string() /* no signature */, error_message); |
+ } |
+ |
+ void CallBack(const tracked_objects::Location& from, |
+ const std::string& signature, |
+ const std::string& error_message) { |
+ origin_task_runner_->PostTask( |
+ from, base::Bind(callback_, signature, error_message)); |
+ } |
+ |
+ std::string public_key_; |
+ std::string data_; |
+ |
+ private: |
+ // Must be called on origin thread, use CallBack() therefore. |
+ SignCallback callback_; |
+}; |
+ |
+class GetCertificatesState : public NSSOperationState { |
+ public: |
+ GetCertificatesState(const GetCertificatesCallback& callback, |
+ Profile* profile); |
+ virtual ~GetCertificatesState() {} |
+ |
+ virtual void OnError(const tracked_objects::Location& from, |
+ const std::string& error_message) OVERRIDE { |
+ CallBack(from, |
+ scoped_ptr<net::CertificateList>() /* no certificates */, |
+ error_message); |
+ } |
+ |
+ void CallBack(const tracked_objects::Location& from, |
+ scoped_ptr<net::CertificateList> certs, |
+ const std::string& error_message) { |
+ origin_task_runner_->PostTask( |
+ from, base::Bind(callback_, base::Passed(&certs), error_message)); |
+ } |
+ |
+ scoped_ptr<net::CertificateList> certs_; |
+ |
+ private: |
+ // Must be called on origin thread, use CallBack() therefore. |
+ GetCertificatesCallback callback_; |
+}; |
+ |
+class ImportCertificateState : public NSSOperationState { |
+ public: |
+ ImportCertificateState(scoped_refptr<net::X509Certificate> certificate, |
+ const ImportCertificateCallback& callback, |
+ Profile* profile); |
+ virtual ~ImportCertificateState() {} |
+ |
+ virtual void OnError(const tracked_objects::Location& from, |
+ const std::string& error_message) OVERRIDE { |
+ CallBack(from, error_message); |
+ } |
+ |
+ void CallBack(const tracked_objects::Location& from, |
+ const std::string& error_message) { |
+ origin_task_runner_->PostTask(from, base::Bind(callback_, error_message)); |
+ } |
+ |
+ scoped_refptr<net::X509Certificate> certificate_; |
+ |
+ private: |
+ // Must be called on origin thread, use CallBack() therefore. |
+ ImportCertificateCallback callback_; |
+}; |
+ |
+class RemoveCertificateState : public NSSOperationState { |
+ public: |
+ RemoveCertificateState(scoped_refptr<net::X509Certificate> certificate, |
+ const RemoveCertificateCallback& callback, |
+ Profile* profile); |
+ virtual ~RemoveCertificateState() {} |
+ |
+ virtual void OnError(const tracked_objects::Location& from, |
+ const std::string& error_message) OVERRIDE { |
+ CallBack(from, error_message); |
+ } |
+ |
+ void CallBack(const tracked_objects::Location& from, |
+ const std::string& error_message) { |
+ origin_task_runner_->PostTask(from, base::Bind(callback_, error_message)); |
+ } |
+ |
+ scoped_refptr<net::X509Certificate> certificate_; |
+ |
+ private: |
+ // Must be called on origin thread, use CallBack() therefore. |
+ RemoveCertificateCallback callback_; |
+}; |
+ |
+NSSOperationState::NSSOperationState(Profile* profile) |
+ : profile_(profile), |
+ origin_task_runner_(base::ThreadTaskRunnerHandle::Get()) { |
+} |
+ |
+GenerateRSAKeyState::GenerateRSAKeyState(unsigned int modulus_length, |
+ const GenerateKeyCallback& callback, |
+ Profile* profile) |
+ : NSSOperationState(profile), |
+ modulus_length_(modulus_length), |
+ callback_(callback) { |
+} |
+ |
+SignState::SignState(const std::string& public_key, |
+ const std::string& data, |
+ const SignCallback& callback, |
+ Profile* profile) |
+ : NSSOperationState(profile), |
+ public_key_(public_key), |
+ data_(data), |
+ callback_(callback) { |
+} |
+ |
+GetCertificatesState::GetCertificatesState( |
+ const GetCertificatesCallback& callback, |
+ Profile* profile) |
+ : NSSOperationState(profile), callback_(callback) { |
+} |
+ |
+ImportCertificateState::ImportCertificateState( |
+ scoped_refptr<net::X509Certificate> certificate, |
+ const ImportCertificateCallback& callback, |
+ Profile* profile) |
eroman
2014/05/19 23:27:45
The ownership model here seems ill defined.
There
pneubeck (no reviews)
2014/05/20 09:29:21
I fixed Profile*, by passing it explicitly. It's n
|
+ : NSSOperationState(profile), |
+ certificate_(certificate), |
+ callback_(callback) { |
+} |
+ |
+RemoveCertificateState::RemoveCertificateState( |
+ scoped_refptr<net::X509Certificate> certificate, |
+ const RemoveCertificateCallback& callback, |
+ Profile* profile) |
+ : NSSOperationState(profile), |
+ certificate_(certificate), |
+ callback_(callback) { |
+} |
+ |
+// Does the actual key generation on a worker thread. Used by |
+// GenerateRSAKeyWithDB(). |
+void GenerateRSAKeyOnWorkerThread(scoped_ptr<GenerateRSAKeyState> state) { |
+ scoped_ptr<crypto::RSAPrivateKey> rsa_key( |
+ crypto::RSAPrivateKey::CreateSensitive(state->slot_.get(), |
+ state->modulus_length_)); |
+ if (!rsa_key) { |
+ LOG(ERROR) << "Couldn't create key."; |
+ state->OnError(FROM_HERE, kErrorInternal); |
+ return; |
+ } |
+ |
+ std::vector<uint8> public_key_spki_der; |
+ if (!rsa_key->ExportPublicKey(&public_key_spki_der)) { |
+ // TODO(pneubeck): Remove rsa_key from storage. |
+ LOG(ERROR) << "Couldn't export public key."; |
+ state->OnError(FROM_HERE, kErrorInternal); |
+ return; |
+ } |
+ state->CallBack( |
+ FROM_HERE, |
+ std::string(public_key_spki_der.begin(), public_key_spki_der.end()), |
+ std::string() /* no error */); |
+} |
+ |
+// Continues generating a RSA key with the obtained NSSCertDatabase. Used by |
+// GenerateRSAKey(). |
+void GenerateRSAKeyWithDB(scoped_ptr<GenerateRSAKeyState> state, |
+ net::NSSCertDatabase* cert_db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ // Only the slot and not the NSSCertDatabase is required. Ignore |cert_db|. |
+ base::WorkerPool::PostTask( |
+ FROM_HERE, |
+ base::Bind(&GenerateRSAKeyOnWorkerThread, base::Passed(&state)), |
+ true /*task is slow*/); |
+} |
+ |
+// Does the actual signing on a worker thread. Used by RSASignWithDB(). |
+void RSASignOnWorkerThread(scoped_ptr<SignState> state) { |
+ const uint8* public_key_uint8 = |
+ reinterpret_cast<const uint8*>(state->public_key_.data()); |
+ std::vector<uint8> public_key_vector( |
+ public_key_uint8, public_key_uint8 + state->public_key_.size()); |
+ |
+ // TODO(pneubeck): This searches all slots. Change to look only at |slot_|. |
+ scoped_ptr<crypto::RSAPrivateKey> rsa_key( |
+ crypto::RSAPrivateKey::FindFromPublicKeyInfo(public_key_vector)); |
+ if (!rsa_key || rsa_key->key()->pkcs11Slot != state->slot_) { |
+ state->OnError(FROM_HERE, kErrorKeyNotFound); |
+ return; |
+ } |
+ |
+ SECItem sign_result = {siBuffer, NULL, 0}; |
+ if (SEC_SignData(&sign_result, |
+ reinterpret_cast<const unsigned char*>(state->data_.data()), |
+ state->data_.size(), |
+ rsa_key->key(), |
+ SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION) != SECSuccess) { |
+ LOG(ERROR) << "Couldn't sign."; |
+ state->OnError(FROM_HERE, kErrorInternal); |
+ return; |
+ } |
+ |
+ std::string signature(reinterpret_cast<const char*>(sign_result.data), |
+ sign_result.len); |
+ state->CallBack(FROM_HERE, signature, std::string() /* no error */); |
+} |
+ |
+// Continues signing with the obtained NSSCertDatabase. Used by Sign(). |
+void RSASignWithDB(scoped_ptr<SignState> state, net::NSSCertDatabase* cert_db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ // Only the slot and not the NSSCertDatabase is required. Ignore |cert_db|. |
+ base::WorkerPool::PostTask( |
+ FROM_HERE, |
+ base::Bind(&RSASignOnWorkerThread, base::Passed(&state)), |
+ true /*task is slow*/); |
+} |
+ |
+// Filters the obtained certificates on a worker thread. Used by |
+// DidGetCertificates(). |
+void FilterCertificatesOnWorkerThread(scoped_ptr<GetCertificatesState> state) { |
+ scoped_ptr<net::CertificateList> client_certs(new net::CertificateList); |
+ for (net::CertificateList::const_iterator it = state->certs_->begin(); |
+ it != state->certs_->end(); |
+ ++it) { |
+ net::X509Certificate::OSCertHandle cert_handle = (*it)->os_cert_handle(); |
+ crypto::ScopedPK11Slot cert_slot(PK11_KeyForCertExists(cert_handle, |
+ NULL, // keyPtr |
+ NULL)); // wincx |
+ |
+ // Keep only user certificates, i.e. certs for which the private key is |
+ // present and stored in the queried slot. |
+ if (cert_slot != state->slot_) |
+ continue; |
+ |
+ client_certs->push_back(*it); |
+ } |
+ |
+ state->CallBack(FROM_HERE, client_certs.Pass(), std::string() /* no error */); |
+} |
+ |
+// Passes the obtained certificates to the worker thread for filtering. Used by |
+// GetCertificatesWithDB(). |
+void DidGetCertificates(scoped_ptr<GetCertificatesState> state, |
+ scoped_ptr<net::CertificateList> all_certs) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ state->certs_ = all_certs.Pass(); |
+ base::WorkerPool::PostTask( |
+ FROM_HERE, |
+ base::Bind(&FilterCertificatesOnWorkerThread, base::Passed(&state)), |
+ true /*task is slow*/); |
+} |
+ |
+// Continues getting certificates with the obtained NSSCertDatabase. Used by |
+// GetCertificates(). |
+void GetCertificatesWithDB(scoped_ptr<GetCertificatesState> state, |
+ net::NSSCertDatabase* cert_db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ // Get the pointer to slot before base::Passed releases |state|. |
+ PK11SlotInfo* slot = state->slot_.get(); |
+ cert_db->ListCertsInSlot( |
+ base::Bind(&DidGetCertificates, base::Passed(&state)), slot); |
+} |
+ |
+// Does the actual certificate importing on the IO thread. Used by |
+// ImportCertificate(). |
+void ImportCertificateWithDB(scoped_ptr<ImportCertificateState> state, |
+ net::NSSCertDatabase* cert_db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ // TODO(pneubeck): Use |state->slot_| to verify that we're really importing to |
+ // the correct token. |
+ // |cert_db| is not required, ignore it. |
+ net::CertDatabase* db = net::CertDatabase::GetInstance(); |
+ |
+ const net::Error cert_status = db->CheckUserCert(state->certificate_); |
+ if (cert_status == net::ERR_NO_PRIVATE_KEY_FOR_CERT) { |
+ state->OnError(FROM_HERE, kErrorKeyNotFound); |
+ return; |
+ } else if (cert_status != net::OK) { |
+ state->OnError(FROM_HERE, net::ErrorToString(cert_status)); |
+ return; |
+ } |
+ |
+ const net::Error import_status = db->AddUserCert(state->certificate_.get()); |
+ if (import_status != net::OK) { |
+ LOG(ERROR) << "Could not import certificate."; |
+ state->OnError(FROM_HERE, net::ErrorToString(import_status)); |
+ return; |
+ } |
+ |
+ state->CallBack(FROM_HERE, std::string() /* no error */); |
+} |
+ |
+// Called on IO thread after the certificate removal is finished. |
+void DidRemoveCertificate(scoped_ptr<RemoveCertificateState> state, |
+ bool certificate_found, |
+ bool success) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ // CertificateNotFound error has precedence over an internal error. |
+ if (!certificate_found) { |
+ state->OnError(FROM_HERE, kErrorCertificateNotFound); |
+ return; |
+ } |
+ if (!success) { |
+ state->OnError(FROM_HERE, kErrorInternal); |
+ return; |
+ } |
+ |
+ state->CallBack(FROM_HERE, std::string() /* no error */); |
+} |
+ |
+// Does the actual certificate removal on the IO thread. Used by |
+// RemoveCertificate(). |
+void RemoveCertificateWithDB(scoped_ptr<RemoveCertificateState> state, |
+ net::NSSCertDatabase* cert_db) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ // Get the pointer before base::Passed clears |state|. |
+ scoped_refptr<net::X509Certificate> certificate = state->certificate_; |
+ bool certificate_found = certificate->os_cert_handle()->isperm; |
+ cert_db->DeleteCertAndKeyAsync( |
+ certificate, |
+ base::Bind( |
+ &DidRemoveCertificate, base::Passed(&state), certificate_found)); |
+} |
+ |
+} // namespace |
+ |
+void GenerateRSAKey(const std::string& token_id, |
+ unsigned int modulus_length, |
+ const GenerateKeyCallback& callback, |
+ Profile* profile) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ scoped_ptr<GenerateRSAKeyState> state( |
+ new GenerateRSAKeyState(modulus_length, callback, profile)); |
+ |
+ if (modulus_length > kMaxRSAModulusLength) { |
+ state->OnError(FROM_HERE, kErrorAlgorithmNotSupported); |
+ return; |
+ } |
+ |
+ // Get the pointer to |state| before base::Passed releases |state|. |
+ NSSOperationState* state_ptr = state.get(); |
+ GetCertDatabase(token_id, |
+ base::Bind(&GenerateRSAKeyWithDB, base::Passed(&state)), |
+ state_ptr); |
+} |
+ |
+void Sign(const std::string& token_id, |
+ const std::string& public_key, |
+ const std::string& data, |
+ const SignCallback& callback, |
+ Profile* profile) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ scoped_ptr<SignState> state( |
+ new SignState(public_key, data, callback, profile)); |
+ // Get the pointer to |state| before base::Passed releases |state|. |
+ NSSOperationState* state_ptr = state.get(); |
+ |
+ // The NSSCertDatabase object is not required. But in case it's not available |
+ // we would get more informative error messages and we can double check that |
+ // we use a key of the correct token. |
+ GetCertDatabase( |
+ token_id, base::Bind(&RSASignWithDB, base::Passed(&state)), state_ptr); |
+} |
+ |
+void GetCertificates(const std::string& token_id, |
+ const GetCertificatesCallback& callback, |
+ Profile* profile) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ scoped_ptr<GetCertificatesState> state( |
+ new GetCertificatesState(callback, profile)); |
+ // Get the pointer to |state| before base::Passed releases |state|. |
+ NSSOperationState* state_ptr = state.get(); |
+ GetCertDatabase(token_id, |
+ base::Bind(&GetCertificatesWithDB, base::Passed(&state)), |
+ state_ptr); |
+} |
+ |
+void ImportCertificate(const std::string& token_id, |
+ scoped_refptr<net::X509Certificate> certificate, |
+ const ImportCertificateCallback& callback, |
+ Profile* profile) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ scoped_ptr<ImportCertificateState> state( |
+ new ImportCertificateState(certificate, callback, profile)); |
+ // Get the pointer to |state| before base::Passed releases |state|. |
+ NSSOperationState* state_ptr = state.get(); |
+ |
+ // The NSSCertDatabase object is not required. But in case it's not available |
+ // we would get more informative error messages and we can double check that |
+ // we use a key of the correct token. |
+ GetCertDatabase(token_id, |
+ base::Bind(&ImportCertificateWithDB, base::Passed(&state)), |
+ state_ptr); |
+} |
+ |
+void RemoveCertificate(const std::string& token_id, |
+ scoped_refptr<net::X509Certificate> certificate, |
+ const RemoveCertificateCallback& callback, |
+ Profile* profile) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ scoped_ptr<RemoveCertificateState> state( |
+ new RemoveCertificateState(certificate, callback, profile)); |
+ // Get the pointer to |state| before base::Passed releases |state|. |
+ NSSOperationState* state_ptr = state.get(); |
+ |
+ // The NSSCertDatabase object is not required. But in case it's not available |
+ // we would get more informative error messages. |
+ GetCertDatabase(token_id, |
+ base::Bind(&RemoveCertificateWithDB, base::Passed(&state)), |
+ state_ptr); |
+} |
+ |
+} // namespace platform_keys |
+ |
+} // namespace chromeos |