mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-08 22:29:03 -04:00
Lockdown's core promise is that an unauthenticated local connection sees nothing sensitive. Nothing asserted it. The gates in PhoneAPI::getFromRadio() are #ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL, and no native env defined that, so every one of them compiled out of the test build. Across 84 suites the only lockdown coverage was two cases in test_stream_api, both on classifyLocalAdminPacket. Adds [env:coverage-lockdown], which turns the lockdown flags on, and test_lockdown_redaction, which drives a want_config handshake and checks what actually reaches the client: - security config: no private key, no public key, no admin keys - bluetooth: fixed_pin zeroed - network: wifi_psk withheld - device metadata: whole struct wiped - LoRa: reduced to the public whitelist - region, preset, channel_num and hop_limit survive; tx_power, override_frequency and the gain flag do not - MQTT module config: broker username and password withheld - channels: PSK and name emptied, and the handshake still reaches config_complete rather than stalling - node DB: other nodes withheld (the DB is populated first, or "no nodes were sent" would be true of an empty one and prove nothing) - inbound: an unauthorized client cannot inject mesh traffic but can still deliver lockdown_auth - refusing that too would make a locked node permanently unreachable - with lockdown inactive, nothing is redacted at all: a capable build that was never provisioned must behave like stock firmware Every case asserts both directions. A redaction test that only checks the redacted side passes just as happily against a build that returns nothing to anybody, which is not the property we want. Verified by mutation: stubbing getAdminAuthorized() to always return true fails all eight redaction cases and leaves the other two passing. State is reset in setUp/tearDown rather than at the end of each test, because Unity longjmps out of a failing assertion - inline cleanup would be skipped and one red test would poison every test after it. No nRF52 crypto is involved. The real EncryptedStorage hard-errors off ARCH_NRF52, so the env drops it and links a stub in the suite directory that keeps the one semantic the tests need: isLockdownActive() means "a DEK file exists", which a test drives by creating /prefs/.dek. Wired into CI through SPECIAL_ENVS in bin/test-shards.py, which gives the env its own shard. The suite also lands in the general pool under plain [env:coverage], where the flags are absent - it compiles to a single ignored case there rather than breaking that shard. Both paths verified.
142 lines
2.6 KiB
C++
142 lines
2.6 KiB
C++
/*
|
|
* Native stand-in for EncryptedStorage.
|
|
*
|
|
* The real implementation is nRF52-only by design - it hard-errors off ARCH_NRF52 because it
|
|
* needs CC310 - so [env:coverage-lockdown] excludes it from the build and links this instead.
|
|
* The redaction tests do not exercise crypto: what they need is for isLockdownActive() and
|
|
* isUnlocked() to be steerable, so the compile-time gates in PhoneAPI::getFromRadio() are
|
|
* reachable. Everything else is a no-op that fails safe.
|
|
*
|
|
* isLockdownActive() keeps the real semantics - "a DEK file exists" - so tests drive it by
|
|
* creating and removing /prefs/.dek, exactly as the firmware decides it.
|
|
*/
|
|
|
|
#include "configuration.h"
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
|
|
#include "FSCommon.h"
|
|
#include "security/EncryptedStorage.h"
|
|
#include <cstring>
|
|
|
|
namespace EncryptedStorage
|
|
{
|
|
|
|
static const char *kDekFilename = "/prefs/.dek";
|
|
static bool s_unlocked = true; // storage unlocked; per-connection auth is the gate under test
|
|
static const char *s_lockReason = "ok";
|
|
|
|
void initLocked() {}
|
|
|
|
bool isProvisioned()
|
|
{
|
|
#ifdef FSCom
|
|
return FSCom.exists(kDekFilename);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool isLockdownActive()
|
|
{
|
|
return isProvisioned();
|
|
}
|
|
|
|
bool isUnlocked()
|
|
{
|
|
return s_unlocked;
|
|
}
|
|
|
|
bool provisionPassphrase(const uint8_t *, size_t, uint8_t, uint32_t, uint32_t)
|
|
{
|
|
s_unlocked = true;
|
|
return true;
|
|
}
|
|
|
|
bool unlockWithPassphrase(const uint8_t *, size_t, uint8_t, uint32_t, uint32_t)
|
|
{
|
|
s_unlocked = true;
|
|
return true;
|
|
}
|
|
|
|
void lockNow()
|
|
{
|
|
s_unlocked = false;
|
|
}
|
|
|
|
void secureWipeKeys()
|
|
{
|
|
s_unlocked = false;
|
|
}
|
|
|
|
void removeLockdownArtifacts()
|
|
{
|
|
#ifdef FSCom
|
|
FSCom.remove(kDekFilename);
|
|
#endif
|
|
}
|
|
|
|
const char *getLockReason()
|
|
{
|
|
return s_lockReason;
|
|
}
|
|
|
|
uint8_t getBootsRemaining()
|
|
{
|
|
return TOKEN_DEFAULT_BOOTS;
|
|
}
|
|
|
|
uint32_t getValidUntilEpoch()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
uint32_t getBackoffSecondsRemaining()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
void setSession(uint32_t) {}
|
|
|
|
bool isSessionExpired()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint8_t consumeSessionBoot()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// File helpers: the tests use plaintext prefs, so report "not encrypted" and pass writes
|
|
// through unchanged rather than pretending to encrypt.
|
|
bool isEncrypted(const char *)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool readAndDecrypt(const char *, uint8_t *, size_t, size_t &outLen)
|
|
{
|
|
outLen = 0;
|
|
return false;
|
|
}
|
|
|
|
bool encryptAndWrite(const char *, const uint8_t *, size_t, bool)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool migrateFile(const char *)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool migrateFileToPlaintext(const char *)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
} // namespace EncryptedStorage
|
|
|
|
#endif // MESHTASTIC_ENCRYPTED_STORAGE
|