blob: 9e3c5a42cf0e27e4ff43a9e2495c8536d2ff6bed [file]
/*
* Copyright (c) 2021-2026 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file
* This file defines the CHIP message counters of remote nodes.
*
*/
#pragma once
#include <array>
#include <bitset>
#include <utility>
#include <variant>
#include <lib/support/Span.h>
namespace chip {
namespace Transport {
class PeerMessageCounter
{
public:
static constexpr size_t kChallengeSize = 8;
static constexpr uint32_t kInitialSyncValue = 0;
PeerMessageCounter() : mSyncState(NotSynced{}) {}
PeerMessageCounter(const PeerMessageCounter &) = default;
PeerMessageCounter(PeerMessageCounter &&) = default;
PeerMessageCounter & operator=(const PeerMessageCounter &) = default;
PeerMessageCounter & operator=(PeerMessageCounter &&) = default;
~PeerMessageCounter() { Reset(); }
void Reset() { mSyncState = NotSynced{}; }
bool IsSynchronizing() const { return std::holds_alternative<SyncInProcess>(mSyncState); }
bool IsSynchronized() const { return std::holds_alternative<Synced>(mSyncState); }
void SyncStarting(FixedByteSpan<kChallengeSize> challenge)
{
VerifyOrDie(std::holds_alternative<NotSynced>(mSyncState));
SyncInProcess sip;
::memcpy(sip.mChallenge.data(), challenge.data(), kChallengeSize);
mSyncState = std::move(sip);
}
void SyncFailed() { Reset(); }
CHIP_ERROR VerifyChallenge(uint32_t counter, FixedByteSpan<kChallengeSize> challenge)
{
if (!std::holds_alternative<SyncInProcess>(mSyncState))
{
return CHIP_ERROR_INCORRECT_STATE;
}
SyncInProcess & sip = std::get<SyncInProcess>(mSyncState);
if (::memcmp(sip.mChallenge.data(), challenge.data(), kChallengeSize) != 0)
{
return CHIP_ERROR_INVALID_ARGUMENT;
}
mSyncState = Synced{};
std::get<Synced>(mSyncState).mMaxCounter = counter;
std::get<Synced>(mSyncState).mWindow.reset();
return CHIP_NO_ERROR;
}
/**
* @brief Implementation of spec 4.5.4.2
*
* For encrypted messages of Group Session Type, any arriving message with a counter in the range
* [(max_message_counter + 1) to (max_message_counter + 2^31 - 1)] (modulo 2^32) SHALL be considered
* new, and cause the max_message_counter value to be updated. Messages with counters from
* [(max_message_counter - 2^31) to (max_message_counter - MSG_COUNTER_WINDOW_SIZE - 1)] (modulo 2^
* 32) SHALL be considered duplicate. Message counters within the range of the bitmap SHALL be
* considered duplicate if the corresponding bit offset is set to true.
*
*/
CHIP_ERROR VerifyGroup(uint32_t counter) const
{
if (!std::holds_alternative<Synced>(mSyncState))
{
return CHIP_ERROR_INCORRECT_STATE;
}
Position pos = ClassifyWithRollover(counter);
return VerifyPositionEncrypted(pos, counter);
}
CHIP_ERROR VerifyOrTrustFirstGroup(uint32_t counter)
{
if (std::holds_alternative<NotSynced>(mSyncState))
{
SetCounter(counter);
return CHIP_NO_ERROR;
}
if (std::holds_alternative<Synced>(mSyncState))
{
return VerifyGroup(counter);
}
VerifyOrDie(false);
return CHIP_ERROR_INTERNAL;
}
/**
* @brief
* With the group counter verified and the packet MIC also verified by the secure key, we can trust the packet and adjust
* counter states.
*
* @pre counter has been verified via VerifyGroup or VerifyOrTrustFirstGroup
*/
void CommitGroup(uint32_t counter) { CommitWithRollover(counter); }
CHIP_ERROR VerifyEncryptedUnicast(uint32_t counter) const
{
if (!std::holds_alternative<Synced>(mSyncState))
{
return CHIP_ERROR_INCORRECT_STATE;
}
Position pos = ClassifyWithoutRollover(counter);
return VerifyPositionEncrypted(pos, counter);
}
/**
* @brief
* With the counter verified and the packet MIC also verified by the secure key, we can trust the packet and adjust
* counter states.
*
* @pre counter has been verified via VerifyEncryptedUnicast
*/
void CommitEncryptedUnicast(uint32_t counter) { CommitWithoutRollover(counter); }
CHIP_ERROR VerifyUnencrypted(uint32_t counter)
{
if (std::holds_alternative<NotSynced>(mSyncState))
{
SetCounter(counter);
return CHIP_NO_ERROR;
}
if (std::holds_alternative<Synced>(mSyncState))
{
Position pos = ClassifyWithRollover(counter);
return VerifyPositionUnencrypted(pos, counter);
}
VerifyOrDie(false);
return CHIP_ERROR_INTERNAL;
}
/**
* @brief
* With the unencrypted counter verified we can trust the packet and adjust
* counter states.
*
* @pre counter has been verified via VerifyUnencrypted
*/
void CommitUnencrypted(uint32_t counter) { CommitWithRollover(counter); }
void SetCounter(uint32_t value)
{
mSyncState = Synced{};
std::get<Synced>(mSyncState).mMaxCounter = value;
std::get<Synced>(mSyncState).mWindow.reset();
}
uint32_t GetCounter() const { return std::get<Synced>(mSyncState).mMaxCounter; }
private:
// Counter position indicator with respect to our current max counter.
enum class Position
{
BeforeWindow,
InWindow,
MaxCounter,
FutureCounter,
};
// Classify an incoming counter value's position. Must be used only if
// the peer is synchronized.
Position ClassifyWithoutRollover(uint32_t counter) const
{
auto & synced = std::get<Synced>(mSyncState);
if (counter > synced.mMaxCounter)
{
return Position::FutureCounter;
}
return ClassifyNonFutureCounter(counter);
}
/**
* Classify an incoming counter value's position for the cases when counters
* are allowed to roll over. Must be used only if the peer is
* synchronized.
*
* This can be used as the basis for implementing section 4.5.4.2 in the
* spec:
*
* For encrypted messages of Group Session Type, any arriving message with a counter in the range
* [(max_message_counter + 1) to (max_message_counter + 2^31 - 1)] (modulo 2^32) SHALL be considered
* new, and cause the max_message_counter value to be updated. Messages with counters from
* [(max_message_counter - 2^31) to (max_message_counter - MSG_COUNTER_WINDOW_SIZE - 1)] (modulo 2^
* 32) SHALL be considered duplicate. Message counters within the range of the bitmap SHALL be
* considered duplicate if the corresponding bit offset is set to true.
*/
Position ClassifyWithRollover(uint32_t counter) const
{
auto & synced = std::get<Synced>(mSyncState);
uint32_t counterIncrease = counter - synced.mMaxCounter;
constexpr uint32_t futureCounterWindow = (static_cast<uint32_t>(1 << 31)) - 1;
if (counterIncrease >= 1 && counterIncrease <= futureCounterWindow)
{
return Position::FutureCounter;
}
return ClassifyNonFutureCounter(counter);
}
/**
* Classify a counter that's known to not be future counter. This works
* identically whether we are doing rollover or not.
*/
Position ClassifyNonFutureCounter(uint32_t counter) const
{
auto & synced = std::get<Synced>(mSyncState);
if (counter == synced.mMaxCounter)
{
return Position::MaxCounter;
}
uint32_t offset = synced.mMaxCounter - counter;
if (offset <= CHIP_CONFIG_MESSAGE_COUNTER_WINDOW_SIZE)
{
return Position::InWindow;
}
return Position::BeforeWindow;
}
/**
* Given an encrypted (group or unicast) counter position and the counter
* value, verify whether we should accept it.
*/
CHIP_ERROR VerifyPositionEncrypted(Position position, uint32_t counter) const
{
auto & synced = std::get<Synced>(mSyncState);
switch (position)
{
case Position::FutureCounter:
return CHIP_NO_ERROR;
case Position::InWindow: {
uint32_t offset = synced.mMaxCounter - counter;
if (synced.mWindow.test(offset - 1))
{
return CHIP_ERROR_DUPLICATE_MESSAGE_RECEIVED;
}
return CHIP_NO_ERROR;
}
default: {
// Equal to max counter, or before window.
return CHIP_ERROR_DUPLICATE_MESSAGE_RECEIVED;
}
}
}
/**
* Given an unencrypted counter position and value, verify whether we should
* accept it.
*/
CHIP_ERROR VerifyPositionUnencrypted(Position position, uint32_t counter) const
{
auto & synced = std::get<Synced>(mSyncState);
switch (position)
{
case Position::MaxCounter:
return CHIP_ERROR_DUPLICATE_MESSAGE_RECEIVED;
case Position::InWindow: {
uint32_t offset = synced.mMaxCounter - counter;
if (synced.mWindow.test(offset - 1))
{
return CHIP_ERROR_DUPLICATE_MESSAGE_RECEIVED;
}
return CHIP_NO_ERROR;
}
default: {
// Future counter or before window; all of these are accepted. The
// before-window case is accepted because the peer may have reset
// and is using a new randomized initial value.
return CHIP_NO_ERROR;
}
}
}
void CommitWithRollover(uint32_t counter)
{
Position pos = ClassifyWithRollover(counter);
CommitWithPosition(pos, counter);
}
void CommitWithoutRollover(uint32_t counter)
{
Position pos = ClassifyWithoutRollover(counter);
CommitWithPosition(pos, counter);
}
/**
* Commit a counter value that is known to be at the given position with
* respect to our max counter.
*/
void CommitWithPosition(Position position, uint32_t counter)
{
auto & synced = std::get<Synced>(mSyncState);
switch (position)
{
case Position::InWindow: {
uint32_t offset = synced.mMaxCounter - counter;
synced.mWindow.set(offset - 1);
break;
}
case Position::MaxCounter: {
// Nothing to do
break;
}
default: {
// Since we are committing, this becomes a new max-counter value.
uint32_t shift = counter - synced.mMaxCounter;
synced.mMaxCounter = counter;
if (shift > CHIP_CONFIG_MESSAGE_COUNTER_WINDOW_SIZE)
{
synced.mWindow.reset();
}
else
{
synced.mWindow <<= shift;
synced.mWindow.set(shift - 1);
}
break;
}
}
}
// Synthetic type for "not synced" state (std::monostate)
using NotSynced = std::monostate;
struct SyncInProcess
{
std::array<uint8_t, kChallengeSize> mChallenge;
};
struct Synced
{
/*
* Past <-- --> Future
* MaxCounter - 1
* |
* v
* | <-- mWindow -->|
* |[n]| ... |[0]|
*/
uint32_t mMaxCounter = 0; // The most recent counter we have seen
std::bitset<CHIP_CONFIG_MESSAGE_COUNTER_WINDOW_SIZE> mWindow;
};
std::variant<NotSynced, SyncInProcess, Synced> mSyncState;
};
} // namespace Transport
} // namespace chip