| Index: remoting/protocol/spake2_authenticator.cc
|
| diff --git a/remoting/protocol/spake2_authenticator.cc b/remoting/protocol/spake2_authenticator.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..d7c0a6c5550b945cf71a539da116b805862bd12d
|
| --- /dev/null
|
| +++ b/remoting/protocol/spake2_authenticator.cc
|
| @@ -0,0 +1,317 @@
|
| +// Copyright 2016 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 "remoting/protocol/spake2_authenticator.h"
|
| +
|
| +#include <utility>
|
| +
|
| +#include "base/base64.h"
|
| +#include "base/logging.h"
|
| +#include "base/sys_byteorder.h"
|
| +#include "crypto/hmac.h"
|
| +#include "crypto/secure_util.h"
|
| +#include "remoting/base/constants.h"
|
| +#include "remoting/base/rsa_key_pair.h"
|
| +#include "remoting/protocol/ssl_hmac_channel_authenticator.h"
|
| +#include "third_party/boringssl/src/include/openssl/curve25519.h"
|
| +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h"
|
| +
|
| +namespace remoting {
|
| +namespace protocol {
|
| +
|
| +namespace {
|
| +
|
| +// Each peer sends 2 messages: <spake-message> and <verification-hash>. The
|
| +// content of <spake-message> is the output of SPAKE2_generate_msg() and must
|
| +// be passed to SPAKE2_process_msg() on the other end. This is enough to
|
| +// generate authentication key. <verification-hash> is sent to confirm that both
|
| +// ends get the same authentication key (which means they both know the
|
| +// password). This verification hash is calculated in
|
| +// CalculateVerificationHash() as follows:
|
| +// HMAC_SHA256(auth_key, ("host"|"client") + local_jid.length() + local_jid +
|
| +// remote_jid.length() + remote_jid)
|
| +// where auth_key is the key produced by SPAKE2.
|
| +
|
| +const buzz::StaticQName kSpakeMessageTag = {kChromotingXmlNamespace,
|
| + "spake-message"};
|
| +const buzz::StaticQName kVerificationHashTag = {kChromotingXmlNamespace,
|
| + "verification-hash"};
|
| +const buzz::StaticQName kCertificateTag = {kChromotingXmlNamespace,
|
| + "certificate"};
|
| +
|
| +scoped_ptr<buzz::XmlElement> EncodeBinaryValueToXml(
|
| + const buzz::StaticQName& qname,
|
| + const std::string& content) {
|
| + std::string content_base64;
|
| + base::Base64Encode(content, &content_base64);
|
| +
|
| + scoped_ptr<buzz::XmlElement> result(new buzz::XmlElement(qname));
|
| + result->SetBodyText(content_base64);
|
| + return result;
|
| +}
|
| +
|
| +// Finds tag named |qname| in base_message and decodes it from base64 and stores
|
| +// in |data|. If the element is not present then found is set to false otherwise
|
| +// it's set to true. If the element is there and it's content cound't be decoded
|
| +// then false is returned.
|
| +bool DecodeBinaryValueFromXml(const buzz::XmlElement* message,
|
| + const buzz::QName& qname,
|
| + bool* found,
|
| + std::string* data) {
|
| + const buzz::XmlElement* element = message->FirstNamed(qname);
|
| + *found = element != nullptr;
|
| + if (!*found)
|
| + return true;
|
| +
|
| + if (!base::Base64Decode(element->BodyText(), data)) {
|
| + LOG(WARNING) << "Failed to parse " << qname.LocalPart();
|
| + return false;
|
| + }
|
| +
|
| + return !data->empty();
|
| +}
|
| +
|
| +std::string PrefixWithLength(const std::string& str) {
|
| + uint32_t length = base::HostToNet32(str.size());
|
| + return std::string(reinterpret_cast<char*>(&length), sizeof(length)) + str;
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +// static
|
| +scoped_ptr<Authenticator> Spake2Authenticator::CreateForClient(
|
| + const std::string& local_id,
|
| + const std::string& remote_id,
|
| + const std::string& shared_secret,
|
| + Authenticator::State initial_state) {
|
| + return make_scoped_ptr(new Spake2Authenticator(
|
| + local_id, remote_id, shared_secret, false, initial_state));
|
| +}
|
| +
|
| +// static
|
| +scoped_ptr<Authenticator> Spake2Authenticator::CreateForHost(
|
| + const std::string& local_id,
|
| + const std::string& remote_id,
|
| + const std::string& shared_secret,
|
| + const std::string& local_cert,
|
| + scoped_refptr<RsaKeyPair> key_pair,
|
| + Authenticator::State initial_state) {
|
| + scoped_ptr<Spake2Authenticator> result(new Spake2Authenticator(
|
| + local_id, remote_id, shared_secret, true, initial_state));
|
| + result->local_cert_ = local_cert;
|
| + result->local_key_pair_ = key_pair;
|
| + return std::move(result);
|
| +}
|
| +
|
| +Spake2Authenticator::Spake2Authenticator(const std::string& local_id,
|
| + const std::string& remote_id,
|
| + const std::string& shared_secret,
|
| + bool is_host,
|
| + Authenticator::State initial_state)
|
| + : local_id_(local_id),
|
| + remote_id_(remote_id),
|
| + shared_secret_(shared_secret),
|
| + is_host_(is_host),
|
| + state_(initial_state) {
|
| + spake2_context_ = SPAKE2_CTX_new(
|
| + is_host ? spake2_role_bob : spake2_role_alice,
|
| + reinterpret_cast<const uint8_t*>(local_id_.data()), local_id_.size(),
|
| + reinterpret_cast<const uint8_t*>(remote_id_.data()), remote_id_.size());
|
| +
|
| + // Generate first message and push it to |pending_messages_|.
|
| + uint8_t message[SPAKE2_MAX_MSG_SIZE];
|
| + size_t message_size;
|
| + int result = SPAKE2_generate_msg(
|
| + spake2_context_, message, &message_size, sizeof(message),
|
| + reinterpret_cast<const uint8_t*>(shared_secret_.data()),
|
| + shared_secret_.size());
|
| + CHECK(result);
|
| + local_spake_message_.assign(reinterpret_cast<char*>(message), message_size);
|
| +}
|
| +
|
| +Spake2Authenticator::~Spake2Authenticator() {
|
| + SPAKE2_CTX_free(spake2_context_);
|
| +}
|
| +
|
| +Authenticator::State Spake2Authenticator::state() const {
|
| + if (state_ == ACCEPTED && !outgoing_verification_hash_.empty())
|
| + return MESSAGE_READY;
|
| + return state_;
|
| +}
|
| +
|
| +bool Spake2Authenticator::started() const {
|
| + return started_;
|
| +}
|
| +
|
| +Authenticator::RejectionReason Spake2Authenticator::rejection_reason() const {
|
| + DCHECK_EQ(state(), REJECTED);
|
| + return rejection_reason_;
|
| +}
|
| +
|
| +void Spake2Authenticator::ProcessMessage(const buzz::XmlElement* message,
|
| + const base::Closure& resume_callback) {
|
| + ProcessMessageInternal(message);
|
| + resume_callback.Run();
|
| +}
|
| +
|
| +void Spake2Authenticator::ProcessMessageInternal(
|
| + const buzz::XmlElement* message) {
|
| + DCHECK_EQ(state(), WAITING_MESSAGE);
|
| +
|
| + // Parse the certificate.
|
| + bool cert_present;
|
| + if (!DecodeBinaryValueFromXml(message, kCertificateTag, &cert_present,
|
| + &remote_cert_)) {
|
| + state_ = REJECTED;
|
| + rejection_reason_ = PROTOCOL_ERROR;
|
| + return;
|
| + }
|
| +
|
| + // Client always expects certificate in the first message.
|
| + if (!is_host_ && remote_cert_.empty()) {
|
| + LOG(WARNING) << "No valid host certificate.";
|
| + state_ = REJECTED;
|
| + rejection_reason_ = PROTOCOL_ERROR;
|
| + return;
|
| + }
|
| +
|
| + bool spake_message_present = false;
|
| + std::string spake_message;
|
| + bool verification_hash_present = false;
|
| + std::string verification_hash;
|
| + if (!DecodeBinaryValueFromXml(message, kSpakeMessageTag,
|
| + &spake_message_present, &spake_message) ||
|
| + !DecodeBinaryValueFromXml(message, kVerificationHashTag,
|
| + &verification_hash_present,
|
| + &verification_hash)) {
|
| + state_ = REJECTED;
|
| + rejection_reason_ = PROTOCOL_ERROR;
|
| + return;
|
| + }
|
| +
|
| + // |auth_key_| is generated when <spake-message> is received.
|
| + if (auth_key_.empty()) {
|
| + if (!spake_message_present) {
|
| + LOG(WARNING) << "<spake-message> not found.";
|
| + state_ = REJECTED;
|
| + rejection_reason_ = PROTOCOL_ERROR;
|
| + return;
|
| + }
|
| + uint8_t key[SPAKE2_MAX_KEY_SIZE];
|
| + size_t key_size;
|
| + started_ = true;
|
| + int result = SPAKE2_process_msg(
|
| + spake2_context_, key, &key_size, sizeof(key),
|
| + reinterpret_cast<const uint8_t*>(spake_message.data()),
|
| + spake_message.size());
|
| + if (!result) {
|
| + state_ = REJECTED;
|
| + rejection_reason_ = INVALID_CREDENTIALS;
|
| + return;
|
| + }
|
| + CHECK(key_size);
|
| + auth_key_.assign(reinterpret_cast<char*>(key), key_size);
|
| +
|
| + outgoing_verification_hash_ =
|
| + CalculateVerificationHash(is_host_, local_id_, remote_id_);
|
| + expected_verification_hash_ =
|
| + CalculateVerificationHash(!is_host_, remote_id_, local_id_);
|
| + } else if (spake_message_present) {
|
| + LOG(WARNING) << "Received duplicate <spake-message>.";
|
| + state_ = REJECTED;
|
| + rejection_reason_ = PROTOCOL_ERROR;
|
| + return;
|
| + }
|
| +
|
| + if (spake_message_sent_ && !verification_hash_present) {
|
| + LOG(WARNING) << "Didn't receive <verification-hash> when expected.";
|
| + state_ = REJECTED;
|
| + rejection_reason_ = PROTOCOL_ERROR;
|
| + return;
|
| + }
|
| +
|
| + if (verification_hash_present) {
|
| + if (verification_hash.size() != expected_verification_hash_.size() ||
|
| + !crypto::SecureMemEqual(verification_hash.data(),
|
| + expected_verification_hash_.data(),
|
| + verification_hash.size())) {
|
| + state_ = REJECTED;
|
| + rejection_reason_ = INVALID_CREDENTIALS;
|
| + return;
|
| + }
|
| + state_ = ACCEPTED;
|
| + return;
|
| + }
|
| +
|
| + state_ = MESSAGE_READY;
|
| +}
|
| +
|
| +scoped_ptr<buzz::XmlElement> Spake2Authenticator::GetNextMessage() {
|
| + DCHECK_EQ(state(), MESSAGE_READY);
|
| +
|
| + scoped_ptr<buzz::XmlElement> message = CreateEmptyAuthenticatorMessage();
|
| +
|
| + if (!spake_message_sent_) {
|
| + if (!local_cert_.empty()) {
|
| + message->AddElement(
|
| + EncodeBinaryValueToXml(kCertificateTag, local_cert_).release());
|
| + }
|
| +
|
| + message->AddElement(
|
| + EncodeBinaryValueToXml(kSpakeMessageTag, local_spake_message_)
|
| + .release());
|
| +
|
| + spake_message_sent_ = true;
|
| + }
|
| +
|
| + if (!outgoing_verification_hash_.empty()) {
|
| + message->AddElement(EncodeBinaryValueToXml(kVerificationHashTag,
|
| + outgoing_verification_hash_)
|
| + .release());
|
| + outgoing_verification_hash_.clear();
|
| + }
|
| +
|
| + if (state_ != ACCEPTED) {
|
| + state_ = WAITING_MESSAGE;
|
| + }
|
| + return message;
|
| +}
|
| +
|
| +const std::string& Spake2Authenticator::GetAuthKey() const {
|
| + return auth_key_;
|
| +}
|
| +
|
| +scoped_ptr<ChannelAuthenticator>
|
| +Spake2Authenticator::CreateChannelAuthenticator() const {
|
| + DCHECK_EQ(state(), ACCEPTED);
|
| + CHECK(!auth_key_.empty());
|
| +
|
| + if (is_host_) {
|
| + return SslHmacChannelAuthenticator::CreateForHost(
|
| + local_cert_, local_key_pair_, auth_key_);
|
| + } else {
|
| + return SslHmacChannelAuthenticator::CreateForClient(remote_cert_,
|
| + auth_key_);
|
| + }
|
| +}
|
| +
|
| +std::string Spake2Authenticator::CalculateVerificationHash(
|
| + bool from_host,
|
| + const std::string& local_id,
|
| + const std::string& remote_id) {
|
| + std::string message = (from_host ? "host" : "client") +
|
| + PrefixWithLength(local_id) +
|
| + PrefixWithLength(remote_id);
|
| + crypto::HMAC hmac(crypto::HMAC::SHA256);
|
| + std::string result(hmac.DigestLength(), '\0');
|
| + if (!hmac.Init(auth_key_) ||
|
| + !hmac.Sign(message, reinterpret_cast<uint8_t*>(&result[0]),
|
| + result.length())) {
|
| + LOG(FATAL) << "Failed to calculate HMAC.";
|
| + }
|
| + return result;
|
| +}
|
| +
|
| +} // namespace protocol
|
| +} // namespace remoting
|
|
|