Skip to content

Commit

Permalink
Implement lockout for door lock (#21408)
Browse files Browse the repository at this point in the history
* [#19989] Implement lockout for door lock

* Update auto-generated files
  • Loading branch information
Morozov-5F authored and pull[bot] committed Aug 7, 2023
1 parent 6f461f1 commit 1068480
Show file tree
Hide file tree
Showing 8 changed files with 839 additions and 176 deletions.
4 changes: 4 additions & 0 deletions examples/chef/common/stubs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#include <app-common/zap-generated/callback.h>
#include <app-common/zap-generated/cluster-id.h>
#include <app-common/zap-generated/command-id.h>

// Include door lock callbacks only when the server is enabled
#ifdef EMBER_AF_PLUGIN_DOOR_LOCK_SERVER
#include <app/clusters/door-lock-server/door-lock-server.h>

bool emberAfPluginDoorLockOnDoorLockCommand(chip::EndpointId endpointId, const chip::Optional<chip::ByteSpan> & pinCode,
Expand All @@ -19,3 +22,4 @@ bool emberAfPluginDoorLockOnDoorUnlockCommand(chip::EndpointId endpointId, const
err = DlOperationError::kUnspecified;
return true;
}
#endif /* EMBER_AF_PLUGIN_DOOR_LOCK_SERVER */
6 changes: 5 additions & 1 deletion examples/lock-app/linux/src/LockEndpoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,10 @@ bool LockEndpoint::setLockState(DlLockState lockState, const Optional<chip::Byte
{
ChipLogDetail(Zcl, "Lock App: PIN code is not specified, setting door lock state to \"%s\" [endpointId=%d]",
lockStateToString(lockState), mEndpointId);

mLockState = lockState;
DoorLockServer::Instance().SetLockState(mEndpointId, mLockState);

return true;
}

Expand Down Expand Up @@ -418,13 +421,14 @@ bool LockEndpoint::setLockState(DlLockState lockState, const Optional<chip::Byte
return false;
}
}

ChipLogDetail(
Zcl,
"Lock App: specified PIN code was found in the database, setting door lock state to \"%s\" [endpointId=%d,userIndex=%u]",
lockStateToString(lockState), mEndpointId, userIndex);

mLockState = lockState;
DoorLockServer::Instance().SetLockState(mEndpointId, mLockState);

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,7 @@ emberAfPluginDoorLockSetSchedule(chip::EndpointId endpointId, uint8_t holidayInd
{
return DlStatus::kFailure;
}

void __attribute__((weak))
emberAfPluginDoorLockLockoutStarted(chip::EndpointId endpointId, chip::System::Clock::Timestamp lockoutEndTime)
{}
140 changes: 113 additions & 27 deletions src/app/clusters/door-lock-server/door-lock-server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ void DoorLockServer::InitServer(chip::EndpointId endpointId)
ChipLogError(Zcl, "[InitDoorLockServer] Unable to set the Lock State attribute to null [status=%d]", status);
}
SetActuatorEnabled(endpointId, true);

for (auto & ep : mEndpointCtx)
{
ep.lockoutEndTimestamp = ep.lockoutEndTimestamp.zero();
ep.wrongCodeEntryAttempts = 0;
}
}

bool DoorLockServer::SetLockState(chip::EndpointId endpointId, DlLockState newLockState)
Expand Down Expand Up @@ -179,6 +185,67 @@ bool DoorLockServer::SetPrivacyModeButton(chip::EndpointId endpointId, bool isEn
return SetAttribute(endpointId, Attributes::EnablePrivacyModeButton::Id, Attributes::EnablePrivacyModeButton::Set, isEnabled);
}

bool DoorLockServer::TrackWrongCodeEntry(chip::EndpointId endpointId)
{
auto endpointContext = getContext(endpointId);
if (nullptr == endpointContext)
{
ChipLogError(Zcl, "Failed to get endpoint index for cluster [endpoint=%d]", endpointId);
return false;
}

uint8_t wrongCodeEntryLimit = 0xFF;
auto status = Attributes::WrongCodeEntryLimit::Get(endpointId, &wrongCodeEntryLimit);
if (EMBER_ZCL_STATUS_SUCCESS == status)
{
if (++endpointContext->wrongCodeEntryAttempts >= wrongCodeEntryLimit)
{
emberAfDoorLockClusterPrintln("Too many wrong code entry attempts, engaging lockout [endpoint=%d,wrongCodeAttempts=%d]",
endpointId, endpointContext->wrongCodeEntryAttempts);
engageLockout(endpointId);
}
}
else if (EMBER_ZCL_STATUS_UNSUPPORTED_ATTRIBUTE != status)
{
ChipLogError(Zcl, "Failed to read Wrong Code Entry Limit attribute, status=0x%x", to_underlying(status));
return false;
}
return true;
}

bool DoorLockServer::engageLockout(chip::EndpointId endpointId)
{
uint8_t lockoutTimeout;

auto endpointContext = getContext(endpointId);
if (nullptr == endpointContext)
{
ChipLogError(Zcl, "Failed to get endpoint index for cluster [endpoint=%d]", endpointId);
return false;
}

auto status = Attributes::UserCodeTemporaryDisableTime::Get(endpointId, &lockoutTimeout);
if (EMBER_ZCL_STATUS_UNSUPPORTED_ATTRIBUTE == status)
{
return false;
}
if (EMBER_ZCL_STATUS_SUCCESS != status)
{
ChipLogError(Zcl, "Unable to read the UserCodeTemporaryDisableTime attribute [status=%d]", to_underlying(status));
return false;
}

endpointContext->wrongCodeEntryAttempts = 0;
endpointContext->lockoutEndTimestamp =
chip::System::SystemClock().GetMonotonicTimestamp() + chip::System::Clock::Seconds32(lockoutTimeout);

emberAfDoorLockClusterPrintln("Lockout engaged [endpointId=%d,lockoutTimeout=%d]", endpointId, lockoutTimeout);

emberAfPluginDoorLockLockoutStarted(endpointId, endpointContext->lockoutEndTimestamp);

return true;
}

bool DoorLockServer::GetAutoRelockTime(chip::EndpointId endpointId, uint32_t & autoRelockTime)
{
return GetAttribute(endpointId, Attributes::AutoRelockTime::Id, Attributes::AutoRelockTime::Get, autoRelockTime);
Expand Down Expand Up @@ -3136,28 +3203,50 @@ CHIP_ERROR DoorLockServer::sendClusterResponse(chip::app::CommandHandler * comma
return err;
}

EmberAfDoorLockEndpointContext * DoorLockServer::getContext(chip::EndpointId endpointId)
{
auto index = emberAfFindClusterServerEndpointIndex(endpointId, ::Id);
if (index != 0xFFFF)
{
return &mEndpointCtx[index];
}
return nullptr;
}

bool DoorLockServer::HandleRemoteLockOperation(chip::app::CommandHandler * commandObj,
const chip::app::ConcreteCommandPath & commandPath, DlLockOperationType opType,
RemoteLockOpHandler opHandler, const Optional<ByteSpan> & pinCode)
{
VerifyOrDie(DlLockOperationType::kLock == opType || DlLockOperationType::kUnlock == opType);
VerifyOrDie(nullptr != opHandler);

DlLockState newLockState = (DlLockOperationType::kLock == opType) ? DlLockState::kLocked : DlLockState::kUnlocked;
EndpointId endpoint = commandPath.mEndpointId;
DlOperationError reason = DlOperationError::kUnspecified;
uint16_t pinUserIdx = 0;
uint16_t pinCredIdx = 0;
bool credentialsOk = false;
bool success = false;
EndpointId endpoint = commandPath.mEndpointId;
DlOperationError reason = DlOperationError::kUnspecified;
uint16_t pinUserIdx = 0;
uint16_t pinCredIdx = 0;
bool success = false;
bool sendEvent = true;

auto currentTime = chip::System::SystemClock().GetMonotonicTimestamp();

EmberAfDoorLockEndpointContext * endpointContext;

VerifyOrExit(RemoteOperationEnabled(endpoint), reason = DlOperationError::kUnspecified);

// appclusters.pdf 5.3.4.1:
// When the PINCode field is provided an invalid PIN will count towards the WrongCodeEntryLimit and the
// UserCodeTemporaryDisableTime will be triggered if the WrongCodeEntryLimit is exceeded. The lock SHALL ignore any attempts
// to lock/unlock the door until the UserCodeTemporaryDisableTime expires.
// TODO: check whether UserCodeTemporaryDisableTime expired or not.
endpointContext = getContext(endpoint);
VerifyOrExit(nullptr != endpointContext, ChipLogError(Zcl, "Failed to get endpoint index for cluster [endpoint=%d]", endpoint));
if (endpointContext->lockoutEndTimestamp >= currentTime)
{
emberAfDoorLockClusterPrintln("Rejecting unlock command -- lockout is in action [endpoint=%d,lockoutEnd=%u,currentTime=%u]",
endpoint, static_cast<unsigned>(endpointContext->lockoutEndTimestamp.count()),
static_cast<unsigned>(currentTime.count()));
sendEvent = false;
goto exit;
}

if (pinCode.HasValue())
{
Expand All @@ -3180,12 +3269,6 @@ bool DoorLockServer::HandleRemoteLockOperation(chip::app::CommandHandler * comma
"Unable to perform remote lock operation: user is disabled [endpoint=%d, lock_op=%d, userIndex=%d]", endpoint,
to_underlying(opType), pinUserIdx);
});

// [EM]: I don't think we should prevent door lock/unlocking if we couldn't find credential associated with user. I
// think if the app thinks that PIN is correct the door should be unlocked.
//
// [DV]: let app decide on PIN correctness, we will fail only if 'opHandler' returns false.
credentialsOk = true;
}
else
{
Expand All @@ -3202,27 +3285,30 @@ bool DoorLockServer::HandleRemoteLockOperation(chip::app::CommandHandler * comma
EMBER_ZCL_STATUS_UNSUPPORTED_ATTRIBUTE == status || EMBER_ZCL_STATUS_SUCCESS == status,
ChipLogError(Zcl, "Failed to read Require PIN For Remote Operation attribute, status=0x%x", to_underlying(status)));
}
credentialsOk = !requirePin;
// If the PIN is required but not provided we should exit
VerifyOrExit(!requirePin, {
reason = DlOperationError::kInvalidCredential;
emberAfDoorLockClusterPrintln("Checking credentials failed: PIN is not provided when it is required");
});
}

// TODO: increase WrongCodeEntryLimit if credentialsOk == false.
// TODO: If limit is exceeded, lock remote operations for UserCodeTemporaryDisableTime.
VerifyOrExit(credentialsOk, {
reason = DlOperationError::kInvalidCredential;
emberAfDoorLockClusterPrintln("Checking credentials failed: either PIN is invalid or not provided");
});

// credentials check succeeded, try to lock/unlock door
success = opHandler(endpoint, pinCode, reason);
VerifyOrExit(success, /* reason is set by the above call */);

// door locked, set cluster attribute
VerifyOrDie(SetLockState(endpoint, newLockState, DlOperationSource::kRemote));

if (!success && reason == DlOperationError::kInvalidCredential)
{
TrackWrongCodeEntry(endpoint);
}
// The app should trigger the lock state change as it may take a while before the lock actually locks/unlocks
exit:
// Send command response
emberAfSendImmediateDefaultResponse(success ? EMBER_ZCL_STATUS_SUCCESS : EMBER_ZCL_STATUS_FAILURE);

// Most of the time we want to send the lock operation event but sometimes (when the lockout is active) we don't want it.
if (!sendEvent)
{
return success;
}

// Send LockOperation/LockOperationError event
LockOpCredentials foundCred[] = { { DlCredentialType::kPin, pinCredIdx } };
LockOpCredentials * credList = nullptr;
Expand Down
23 changes: 23 additions & 0 deletions src/app/clusters/door-lock-server/door-lock-server.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <app/CommandHandler.h>
#include <app/ConcreteCommandPath.h>
#include <app/util/af.h>
#include <app/util/config.h>

#ifndef DOOR_LOCK_SERVER_ENDPOINT
#define DOOR_LOCK_SERVER_ENDPOINT 1
Expand Down Expand Up @@ -65,6 +66,12 @@ static constexpr size_t DOOR_LOCK_USER_NAME_BUFFER_SIZE =
struct EmberAfPluginDoorLockCredentialInfo;
struct EmberAfPluginDoorLockUserInfo;

struct EmberAfDoorLockEndpointContext
{
chip::System::Clock::Timestamp lockoutEndTimestamp;
int wrongCodeEntryAttempts;
};

/**
* @brief Door Lock Server Plugin class.
*/
Expand Down Expand Up @@ -109,6 +116,8 @@ class DoorLockServer
bool SetOneTouchLocking(chip::EndpointId endpointId, bool isEnabled);
bool SetPrivacyModeButton(chip::EndpointId endpointId, bool isEnabled);

bool TrackWrongCodeEntry(chip::EndpointId endpointId);

bool GetAutoRelockTime(chip::EndpointId endpointId, uint32_t & autoRelockTime);
bool GetNumberOfUserSupported(chip::EndpointId endpointId, uint16_t & numberOfUsersSupported);
bool GetNumberOfPINCredentialsSupported(chip::EndpointId endpointId, uint16_t & numberOfPINCredentials);
Expand Down Expand Up @@ -348,6 +357,10 @@ class DoorLockServer

bool RemoteOperationEnabled(chip::EndpointId endpointId) const;

EmberAfDoorLockEndpointContext * getContext(chip::EndpointId endpointId);

bool engageLockout(chip::EndpointId endpointId);

static CHIP_ERROR sendClusterResponse(chip::app::CommandHandler * commandObj,
const chip::app::ConcreteCommandPath & commandPath, EmberAfStatus status);

Expand Down Expand Up @@ -513,6 +526,8 @@ class DoorLockServer

EmberEventControl AutolockEvent; /**< for automatic relock scheduling */

std::array<EmberAfDoorLockEndpointContext, EMBER_AF_DOOR_LOCK_CLUSTER_SERVER_ENDPOINT_COUNT> mEndpointCtx;

static DoorLockServer instance;
};

Expand Down Expand Up @@ -954,3 +969,11 @@ bool emberAfPluginDoorLockGetCredential(chip::EndpointId endpointId, uint16_t cr
bool emberAfPluginDoorLockSetCredential(chip::EndpointId endpointId, uint16_t credentialIndex, chip::FabricIndex creator,
chip::FabricIndex modifier, DlCredentialStatus credentialStatus,
DlCredentialType credentialType, const chip::ByteSpan & credentialData);

/**
* @brief This callback is called when the Door Lock server starts the lockout so the app could be notified about it.
*
* @param endpointId ID of the endpoint that contains the door lock to be locked out.
* @param lockoutEndTime Monotonic time of when lockout ends.
*/
void emberAfPluginDoorLockLockoutStarted(chip::EndpointId endpointId, chip::System::Clock::Timestamp lockoutEndTime);
Loading

0 comments on commit 1068480

Please sign in to comment.