blob: 01005f6d9b36657251f2b05194ed9807cd1c32ca [file] [log] [blame]
// Copyright 2014 The Chromium OS 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 "privetd/security_manager.h"
#include <algorithm>
#include <limits>
#include <memory>
#include <set>
#include <base/bind.h>
#include <base/guid.h>
#include <base/logging.h>
#include <base/message_loop/message_loop.h>
#include <base/rand_util.h>
#include <base/stl_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <chromeos/data_encoding.h>
#include <chromeos/key_value_store.h>
#include <chromeos/strings/string_utils.h>
#include <crypto/p224_spake.h>
#include "privetd/constants.h"
#include "privetd/openssl_utils.h"
namespace privetd {
namespace {
const char kTokenDelimeter[] = ":";
const int kSessionExpirationTimeMinutes = 5;
const int kPairingExpirationTimeMinutes = 5;
const int kMaxAllowedPairingAttemts = 3;
const int kPairingBlockingTimeMinutes = 1;
const char kEmbeddedCode[] = "embedded_code";
// Returns "scope:time".
std::string CreateTokenData(AuthScope scope, const base::Time& time) {
return base::IntToString(static_cast<int>(scope)) + kTokenDelimeter +
base::Int64ToString(time.ToTimeT());
}
// Splits string of "scope:time" format.
AuthScope SplitTokenData(const std::string& token, base::Time* time) {
auto parts = chromeos::string_utils::SplitAtFirst(token, kTokenDelimeter);
int scope = 0;
if (!base::StringToInt(parts.first, &scope) ||
scope < static_cast<int>(AuthScope::kNone) ||
scope > static_cast<int>(AuthScope::kOwner)) {
return AuthScope::kNone;
}
int64_t timestamp = 0;
if (!base::StringToInt64(parts.second, &timestamp))
return AuthScope::kNone;
*time = base::Time::FromTimeT(timestamp);
return static_cast<AuthScope>(scope);
}
std::string LoadEmbeddedCode(const base::FilePath& path) {
std::string code;
chromeos::KeyValueStore store;
if (store.Load(path))
store.GetString(kEmbeddedCode, &code);
return code;
}
class Spakep224Exchanger : public SecurityManager::KeyExchanger {
public:
explicit Spakep224Exchanger(const std::string& password)
: spake_(crypto::P224EncryptedKeyExchange::kPeerTypeServer, password) {}
~Spakep224Exchanger() override = default;
// SecurityManager::KeyExchanger methods.
const std::string& GetMessage() override { return spake_.GetNextMessage(); }
bool ProcessMessage(const std::string& message,
chromeos::ErrorPtr* error) override {
switch (spake_.ProcessMessage(message)) {
case crypto::P224EncryptedKeyExchange::kResultPending:
return true;
case crypto::P224EncryptedKeyExchange::kResultFailed:
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kInvalidClientCommitment,
spake_.error());
return false;
default:
LOG(FATAL) << "SecurityManager uses only one round trip";
}
return false;
}
const std::string& GetKey() const override {
return spake_.GetUnverifiedKey();
}
private:
crypto::P224EncryptedKeyExchange spake_;
};
class UnsecureKeyExchanger : public SecurityManager::KeyExchanger {
public:
explicit UnsecureKeyExchanger(const std::string& password)
: password_(password) {}
~UnsecureKeyExchanger() override = default;
// SecurityManager::KeyExchanger methods.
const std::string& GetMessage() override { return password_; }
bool ProcessMessage(const std::string& message,
chromeos::ErrorPtr* error) override {
return true;
}
const std::string& GetKey() const override { return password_; }
private:
std::string password_;
};
} // namespace
SecurityManager::SecurityManager(const std::set<PairingType>& pairing_modes,
const base::FilePath& embedded_code_path,
bool disable_security)
: is_security_disabled_(disable_security),
pairing_modes_(pairing_modes),
embedded_code_path_(embedded_code_path),
secret_(kSha256OutputSize) {
base::RandBytes(secret_.data(), kSha256OutputSize);
CHECK_EQ(embedded_code_path_.empty(),
std::find(pairing_modes_.begin(), pairing_modes_.end(),
PairingType::kEmbeddedCode) == pairing_modes_.end());
}
SecurityManager::~SecurityManager() {
while (!pending_sessions_.empty())
ClosePendingSession(pending_sessions_.begin()->first);
}
// Returns "base64([hmac]scope:time)".
std::string SecurityManager::CreateAccessToken(AuthScope scope,
const base::Time& time) const {
chromeos::SecureBlob data(CreateTokenData(scope, time));
chromeos::Blob hash(HmacSha256(secret_, data));
return chromeos::data_encoding::Base64Encode(chromeos::SecureBlob::Combine(
chromeos::SecureBlob(hash.begin(), hash.end()), data));
}
// Parses "base64([hmac]scope:time)".
AuthScope SecurityManager::ParseAccessToken(const std::string& token,
base::Time* time) const {
chromeos::Blob decoded;
if (!chromeos::data_encoding::Base64Decode(token, &decoded) ||
decoded.size() <= kSha256OutputSize)
return AuthScope::kNone;
chromeos::SecureBlob data(decoded.begin() + kSha256OutputSize, decoded.end());
decoded.resize(kSha256OutputSize);
if (decoded != HmacSha256(secret_, data))
return AuthScope::kNone;
return SplitTokenData(data.to_string(), time);
}
std::set<PairingType> SecurityManager::GetPairingTypes() const {
return pairing_modes_;
}
std::set<CryptoType> SecurityManager::GetCryptoTypes() const {
std::set<CryptoType> result{CryptoType::kSpake_p224};
if (is_security_disabled_)
result.insert(CryptoType::kNone);
return result;
}
bool SecurityManager::IsValidPairingCode(const std::string& auth_code) const {
if (is_security_disabled_)
return true;
chromeos::Blob auth_decoded;
if (!chromeos::data_encoding::Base64Decode(auth_code, &auth_decoded))
return false;
for (const auto& session : confirmed_sessions_) {
if (auth_decoded ==
HmacSha256(chromeos::SecureBlob{session.second->GetKey()},
chromeos::SecureBlob{session.first})) {
pairing_attemts_ = 0;
block_pairing_until_ = base::Time{};
return true;
}
}
LOG(ERROR) << "Attempt to authenticate with invalide code.";
return false;
}
bool SecurityManager::StartPairing(PairingType mode,
CryptoType crypto,
std::string* session_id,
std::string* device_commitment,
chromeos::ErrorPtr* error) {
if (!CheckIfPairingAllowed(error))
return false;
if (std::find(pairing_modes_.begin(), pairing_modes_.end(), mode) ==
pairing_modes_.end()) {
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kInvalidParams,
"Pairing mode is not enabled");
return false;
}
std::string code;
switch (mode) {
case PairingType::kEmbeddedCode:
CHECK(!embedded_code_path_.empty());
if (embedded_code_.empty())
embedded_code_ = LoadEmbeddedCode(embedded_code_path_);
if (embedded_code_.empty()) { // File is not created yet.
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kDeviceBusy,
"Embedded code is not ready");
return false;
}
code = embedded_code_;
break;
case PairingType::kUltrasound32:
case PairingType::kAudible32: {
code = base::RandBytesAsString(4);
break;
}
case PairingType::kPinCode:
code = base::StringPrintf("%04i", base::RandInt(0, 9999));
break;
default:
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kInvalidParams,
"Unsupported pairing mode");
return false;
}
std::unique_ptr<KeyExchanger> spake;
switch (crypto) {
case CryptoType::kSpake_p224:
spake.reset(new Spakep224Exchanger(code));
break;
case CryptoType::kNone:
if (is_security_disabled_) {
spake.reset(new UnsecureKeyExchanger(code));
break;
}
// Fall through...
default:
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kInvalidParams, "Unsupported crypto");
return false;
}
// Allow only a single session at a time for now.
while (!pending_sessions_.empty())
ClosePendingSession(pending_sessions_.begin()->first);
std::string session;
do {
session = base::GenerateGUID();
} while (confirmed_sessions_.find(session) != confirmed_sessions_.end() ||
pending_sessions_.find(session) != pending_sessions_.end());
std::string commitment = spake->GetMessage();
pending_sessions_.emplace(session, std::move(spake));
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(base::IgnoreResult(&SecurityManager::ClosePendingSession),
weak_ptr_factory_.GetWeakPtr(), session),
base::TimeDelta::FromMinutes(kPairingExpirationTimeMinutes));
*session_id = session;
*device_commitment = chromeos::data_encoding::Base64Encode(commitment);
LOG(INFO) << "Pairing code for session " << *session_id << " is " << code;
// TODO(vitalybuka): Handle case when device can't start multiple pairing
// simultaneously and implement throttling to avoid brute force attack.
if (!on_start_.is_null()) {
on_start_.Run(session, mode,
chromeos::string_utils::GetStringAsBytes(code));
}
return true;
}
bool SecurityManager::ConfirmPairing(const std::string& session_id,
const std::string& client_commitment,
std::string* fingerprint,
std::string* signature,
chromeos::ErrorPtr* error) {
auto session = pending_sessions_.find(session_id);
if (session == pending_sessions_.end()) {
chromeos::Error::AddToPrintf(
error, FROM_HERE, errors::kDomain, errors::kUnknownSession,
"Unknown session id: '%s'", session_id.c_str());
return false;
}
CHECK(!certificate_fingerprint_.empty());
chromeos::Blob commitment;
if (!chromeos::data_encoding::Base64Decode(client_commitment, &commitment)) {
ClosePendingSession(session_id);
chromeos::Error::AddToPrintf(
error, FROM_HERE, errors::kDomain, errors::kInvalidFormat,
"Invalid commitment string: '%s'", client_commitment.c_str());
return false;
}
if (!session->second->ProcessMessage(
std::string(commitment.begin(), commitment.end()), error)) {
ClosePendingSession(session_id);
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kCommitmentMismatch,
"Pairing code or crypto implementation mismatch");
return false;
}
std::string key = session->second->GetKey();
VLOG(3) << "KEY " << base::HexEncode(key.data(), key.size());
*fingerprint =
chromeos::data_encoding::Base64Encode(certificate_fingerprint_);
chromeos::Blob cert_hmac =
HmacSha256(chromeos::SecureBlob(session->second->GetKey()),
certificate_fingerprint_);
*signature = chromeos::data_encoding::Base64Encode(cert_hmac);
confirmed_sessions_.emplace(session->first, std::move(session->second));
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(base::IgnoreResult(&SecurityManager::CloseConfirmedSession),
weak_ptr_factory_.GetWeakPtr(), session_id),
base::TimeDelta::FromMinutes(kSessionExpirationTimeMinutes));
ClosePendingSession(session_id);
return true;
}
bool SecurityManager::CancelPairing(const std::string& session_id,
chromeos::ErrorPtr* error) {
bool confirmed = CloseConfirmedSession(session_id);
bool pending = ClosePendingSession(session_id);
if (pending) {
CHECK_GE(pairing_attemts_, 1);
--pairing_attemts_;
}
CHECK(!confirmed || !pending);
if (confirmed || pending)
return true;
chromeos::Error::AddToPrintf(error, FROM_HERE, errors::kDomain,
errors::kUnknownSession,
"Unknown session id: '%s'", session_id.c_str());
return false;
}
void SecurityManager::RegisterPairingListeners(
const PairingStartListener& on_start,
const PairingEndListener& on_end) {
CHECK(on_start_.is_null() && on_end_.is_null());
on_start_ = on_start;
on_end_ = on_end;
}
bool SecurityManager::CheckIfPairingAllowed(chromeos::ErrorPtr* error) {
if (is_security_disabled_)
return true;
if (block_pairing_until_ > base::Time::Now()) {
chromeos::Error::AddTo(error, FROM_HERE, errors::kDomain,
errors::kDeviceBusy, "Too many pairing attempts");
return false;
}
if (++pairing_attemts_ >= kMaxAllowedPairingAttemts) {
LOG(INFO) << "Pairing blocked for" << kPairingBlockingTimeMinutes
<< "minutes.";
block_pairing_until_ = base::Time::Now();
block_pairing_until_ +=
base::TimeDelta::FromMinutes(kPairingBlockingTimeMinutes);
}
return true;
}
bool SecurityManager::ClosePendingSession(const std::string& session_id) {
// The most common source of these session_id values is the map containing
// the sessions, which we're about to clear out. Make a local copy.
const std::string safe_session_id{session_id};
const size_t num_erased = pending_sessions_.erase(safe_session_id);
if (num_erased > 0 && !on_end_.is_null())
on_end_.Run(safe_session_id);
return num_erased != 0;
}
bool SecurityManager::CloseConfirmedSession(const std::string& session_id) {
return confirmed_sessions_.erase(session_id) != 0;
}
} // namespace privetd