mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-16 08:31:35 +10:00
emdashes begone (#10847)
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
// M22 (audit): refuse to engage APPROTECT on silicon revisions where the
|
||||
// debug-port lockout is publicly known to be bypassable. nRF52840 build
|
||||
// codes AAB0..AAF0 are all affected by the SWD glitching attack documented
|
||||
// in LimitedResults' nRF52-series research — i.e. every nRF52840 currently
|
||||
// in LimitedResults' nRF52-series research - i.e. every nRF52840 currently
|
||||
// in shipping Meshtastic hardware. Engaging APPROTECT on these revisions
|
||||
// gives the operator a false sense of security AND irreversibly blocks
|
||||
// legitimate SWD-based dev/recovery: the worst of both. Detect-and-skip
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* reflash. The DFU bootloader path keeps working for routine app
|
||||
* updates because the bootloader doesn't need SWD.
|
||||
* - The only way to clear APPROTECT is an SWD-side `nrfjprog --recover`
|
||||
* (CTRL-AP ERASEALL), which wipes the entire chip — bootloader,
|
||||
* application, LittleFS, and the encrypted DEK — destroying all
|
||||
* (CTRL-AP ERASEALL), which wipes the entire chip - bootloader,
|
||||
* application, LittleFS, and the encrypted DEK - destroying all
|
||||
* on-device state in the process. That destructive coupling is the
|
||||
* point: an attacker cannot clear APPROTECT to extract user data
|
||||
* without also wiping the data they were trying to read.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
|
||||
// Common includes — available for all platform implementations
|
||||
// Common includes - available for all platform implementations
|
||||
#include "EncryptedStorage.h"
|
||||
#include "FSCommon.h"
|
||||
#include "RTC.h"
|
||||
@@ -53,14 +53,14 @@ static bool kekDerived = false;
|
||||
static uint8_t ephemeralKek[AES_KEY_SIZE];
|
||||
static bool ephemeralKekDerived = false;
|
||||
|
||||
// Data Encryption Key — loaded from /prefs/.dek, never stored in plaintext
|
||||
// Data Encryption Key - loaded from /prefs/.dek, never stored in plaintext
|
||||
static uint8_t dek[AES_KEY_SIZE];
|
||||
static bool dekLoaded = false;
|
||||
|
||||
// Reason the device is currently locked (set in initLocked / readAndConsumeToken)
|
||||
static const char *lockReason = "not_provisioned";
|
||||
|
||||
// Token state after successful unlock — exposed via getBootsRemaining() / getValidUntilEpoch()
|
||||
// Token state after successful unlock - exposed via getBootsRemaining() / getValidUntilEpoch()
|
||||
static uint8_t s_bootsRemaining = 0;
|
||||
static uint32_t s_validUntilEpoch = 0;
|
||||
|
||||
@@ -73,7 +73,7 @@ static uint32_t s_validUntilEpoch = 0;
|
||||
static uint32_t s_sessionMaxMs = 0;
|
||||
static uint32_t s_sessionStartedMs = 0;
|
||||
|
||||
// Backoff state — seconds remaining before next passphrase attempt is allowed
|
||||
// Backoff state - seconds remaining before next passphrase attempt is allowed
|
||||
static uint32_t s_backoffSecondsRemaining = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -159,7 +159,7 @@ static void readBackoff(uint8_t &attempts, uint8_t &bootsSinceFail, uint32_t &la
|
||||
auto f = FSCom.open(BACKOFF_FILENAME, FILE_O_READ);
|
||||
if (!f) {
|
||||
// Fresh device (no provision yet) OR an attacker deleted the
|
||||
// file. Caller resolves the ambiguity via isProvisioned() —
|
||||
// file. Caller resolves the ambiguity via isProvisioned() -
|
||||
// see bumpBootsSinceFailOnBoot and the unlock backoff gate.
|
||||
return;
|
||||
}
|
||||
@@ -217,7 +217,7 @@ static void writeBackoff(uint8_t attempts, uint8_t bootsSinceFail, uint32_t last
|
||||
}
|
||||
|
||||
// Called once per boot from initLocked(). Skip the bump on a fresh
|
||||
// (un-provisioned) device — there's no backoff file to MAC against yet and
|
||||
// (un-provisioned) device - there's no backoff file to MAC against yet and
|
||||
// readBackoff would return kBackoffMaxAttempts which would be wrong here.
|
||||
static void bumpBootsSinceFailOnBoot()
|
||||
{
|
||||
@@ -489,11 +489,11 @@ static bool loadDEK()
|
||||
|
||||
const uint8_t *storedHmac = buf + DEK_SIZE - HMAC_SIZE;
|
||||
if (!constTimeEq(expectedHmac.data(), storedHmac, HMAC_SIZE)) {
|
||||
LOG_ERROR("EncryptedStorage: DEK HMAC mismatch — wrong passphrase or tampered file");
|
||||
LOG_ERROR("EncryptedStorage: DEK HMAC mismatch - wrong passphrase or tampered file");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrypt DEK into a local candidate — only write to global dek[] on success so that
|
||||
// Decrypt DEK into a local candidate - only write to global dek[] on success so that
|
||||
// a wrong passphrase attempt does not destroy the live DEK in RAM.
|
||||
meshtastic_security::ZeroizingBuffer<AES_KEY_SIZE> dekCandidate;
|
||||
nRFCrypto.begin();
|
||||
@@ -566,7 +566,7 @@ static bool saveDEK()
|
||||
// Write file: magic(4)+nonce(13)+encDEK(16)+hmac(32) = 65 bytes
|
||||
// H12 (audit): atomic write via SafeFile. Power-loss between remove()
|
||||
// and write() previously left a missing or partial DEK file, which
|
||||
// bricked the device — the encrypted protos can't be decrypted with
|
||||
// bricked the device - the encrypted protos can't be decrypted with
|
||||
// no DEK on flash. SafeFile writes a tmp file, reads it back to verify
|
||||
// a content hash, then atomically renames over the target. Crash before
|
||||
// rename → old DEK stays in place; crash after rename → new DEK is on
|
||||
@@ -631,7 +631,7 @@ static bool computeMonoHmac(const uint8_t body[MONO_BODY_SIZE], uint8_t out[HMAC
|
||||
}
|
||||
|
||||
// Read the persisted max-counter-seen value. Missing/short/MAC-fail
|
||||
// returns 0 — the safe default that lets the next token write succeed and
|
||||
// returns 0 - the safe default that lets the next token write succeed and
|
||||
// re-seed the file. Unlike the backoff file, missing-here is not a tamper
|
||||
// signal: a fresh device or a device whose .tokmono got wiped (e.g. via
|
||||
// factory-erase) legitimately has no counter file.
|
||||
@@ -707,7 +707,7 @@ static bool writeMonoCounter(uint32_t counter)
|
||||
* @param sessionMaxSeconds Uptime-based session cap per boot (0 = no cap).
|
||||
* Persisted in the token so token-auto-unlock at
|
||||
* cold boot inherits the same limit. Reboot
|
||||
* starts a fresh session window — combined with
|
||||
* starts a fresh session window - combined with
|
||||
* bootsRemaining, gives a hard exposure ceiling
|
||||
* bootsRemaining * sessionMaxSeconds.
|
||||
*/
|
||||
@@ -858,7 +858,7 @@ static bool readAndConsumeToken()
|
||||
nRFCrypto.end();
|
||||
|
||||
if (!hmacOk || !constTimeEq(computedHmac.data(), buf.data() + TOKEN_BODY_SIZE, HMAC_SIZE)) {
|
||||
LOG_ERROR("EncryptedStorage: Token HMAC failed — tampered or wrong device, deleting");
|
||||
LOG_ERROR("EncryptedStorage: Token HMAC failed - tampered or wrong device, deleting");
|
||||
concurrency::LockGuard g(spiLock);
|
||||
FSCom.remove(TOKEN_FILENAME);
|
||||
lockReason = "token_hmac_fail";
|
||||
@@ -916,7 +916,7 @@ static bool readAndConsumeToken()
|
||||
|
||||
// Check time expiry. A wall-clock TTL (validUntilEpoch != 0) needs a
|
||||
// currently valid RTC to verify. getValidTime() returns 0 unless we
|
||||
// actually have an RTC source — getTime() would return a boot-relative
|
||||
// actually have an RTC source - getTime() would return a boot-relative
|
||||
// count, which an attacker can reset by power-cycling with no RTC sync.
|
||||
//
|
||||
// If the wall-clock TTL is set but we can't verify it right now:
|
||||
@@ -1037,11 +1037,11 @@ void initLocked()
|
||||
return;
|
||||
}
|
||||
|
||||
// Not unlocked — log the device state for the operator
|
||||
// Not unlocked - log the device state for the operator
|
||||
if (isProvisioned()) {
|
||||
LOG_WARN("EncryptedStorage: Device LOCKED — reason: %s", lockReason);
|
||||
LOG_WARN("EncryptedStorage: Device LOCKED - reason: %s", lockReason);
|
||||
} else {
|
||||
LOG_WARN("EncryptedStorage: Device NOT PROVISIONED — operator must set passphrase");
|
||||
LOG_WARN("EncryptedStorage: Device NOT PROVISIONED - operator must set passphrase");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1160,7 +1160,7 @@ bool provisionPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8_
|
||||
|
||||
// H4 (audit): seed an attempts=0 backoff sentinel so the file is
|
||||
// present post-provision. From this point on, a missing backoff file
|
||||
// means an attacker deleted it — readBackoff returns max-attempts and
|
||||
// means an attacker deleted it - readBackoff returns max-attempts and
|
||||
// forces a full backoff window. Without this, the very first failed
|
||||
// attempt would find a missing file and have to choose between fresh
|
||||
// (no penalty, attacker bypass) and tamper (legitimate user locked
|
||||
@@ -1184,8 +1184,8 @@ bool unlockWithPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exponential backoff. Three independent enforcement layers — any one
|
||||
// triggers a block — so the policy is robust whether RTC is valid, never
|
||||
// Exponential backoff. Three independent enforcement layers - any one
|
||||
// triggers a block - so the policy is robust whether RTC is valid, never
|
||||
// synced, or being spoofed within reasonable bounds:
|
||||
//
|
||||
// 1. Within-boot: millis() since the last failure on THIS boot. RAM-only,
|
||||
@@ -1228,7 +1228,7 @@ bool unlockWithPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8
|
||||
}
|
||||
}
|
||||
|
||||
// (3) reboot-count fallback. Always enforced — closes the bypass
|
||||
// (3) reboot-count fallback. Always enforced - closes the bypass
|
||||
// where an attacker reboots between attempts (which resets millis
|
||||
// and may leave the RTC unsynced). Conservative: assume ~5 s per
|
||||
// reboot cycle, so require ceil(delay / 5) boots to elapse.
|
||||
@@ -1269,7 +1269,7 @@ bool unlockWithPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8
|
||||
// H3 (audit): RESERVE the attempt slot on disk BEFORE running the HMAC
|
||||
// verify. The previous design wrote the failure record only after a
|
||||
// failed verify, so pulling power between verify and the write left
|
||||
// attempts unchanged — an attacker could glitch the chip mid-call to
|
||||
// attempts unchanged - an attacker could glitch the chip mid-call to
|
||||
// bypass the counter. Pre-incrementing means the attempt is durably
|
||||
// recorded regardless of what happens to the chip during verify; the
|
||||
// success path then writes attempts=0 to clear the reservation.
|
||||
@@ -1292,15 +1292,15 @@ bool unlockWithPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8
|
||||
s_backoffSecondsRemaining);
|
||||
};
|
||||
|
||||
// Load the DEK (verifies HMAC — wrong passphrase will fail here)
|
||||
// Load the DEK (verifies HMAC - wrong passphrase will fail here)
|
||||
if (!loadDEK()) {
|
||||
LOG_ERROR("EncryptedStorage: DEK load failed — wrong passphrase?");
|
||||
LOG_ERROR("EncryptedStorage: DEK load failed - wrong passphrase?");
|
||||
kekDerived = false;
|
||||
onFailure();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Passphrase correct — clear the reserved attempt by writing an
|
||||
// Passphrase correct - clear the reserved attempt by writing an
|
||||
// attempts=0 sentinel (NOT removing the file; see clearBackoff note).
|
||||
clearBackoff();
|
||||
|
||||
@@ -1328,13 +1328,13 @@ void lockNow()
|
||||
secureWipeKeys();
|
||||
s_sessionMaxMs = 0;
|
||||
s_sessionStartedMs = 0;
|
||||
LOG_INFO("EncryptedStorage: Device locked — token deleted, DEK and KEK material zeroed");
|
||||
LOG_INFO("EncryptedStorage: Device locked - token deleted, DEK and KEK material zeroed");
|
||||
}
|
||||
|
||||
void secureWipeKeys()
|
||||
{
|
||||
// M11 (audit): callable from fault handlers. Do NOT take spiLock and do
|
||||
// NOT log — interrupt-context safety. Just zero every byte of key
|
||||
// NOT log - interrupt-context safety. Just zero every byte of key
|
||||
// material that the rest of the module might leave in BSS.
|
||||
meshtastic_security::secure_zero(dek, sizeof(dek));
|
||||
dekLoaded = false;
|
||||
@@ -1362,7 +1362,7 @@ bool isUnlocked()
|
||||
bool isLockdownActive()
|
||||
{
|
||||
// Lockdown is "active" iff the device has been provisioned with a
|
||||
// passphrase — i.e. a DEK exists on flash. A lockdown-capable build
|
||||
// passphrase - i.e. a DEK exists on flash. A lockdown-capable build
|
||||
// (MESHTASTIC_LOCKDOWN) that has never been provisioned, or that has
|
||||
// been disabled via disableLockdown(), is NOT active and behaves like
|
||||
// stock firmware: plaintext storage, no redaction, normal logging.
|
||||
@@ -1407,7 +1407,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
|
||||
|
||||
// MED-1: snapshot DEK before any lock acquisition so a concurrent lockNow() cannot
|
||||
// zero dek[] mid-operation. If lockNow() races and zeros dek[] before we copy, the
|
||||
// snapshot will be zero and the HMAC will fail — a secure failure mode.
|
||||
// snapshot will be zero and the HMAC will fail - a secure failure mode.
|
||||
uint8_t dekSnapshot[AES_KEY_SIZE];
|
||||
memcpy(dekSnapshot, dek, AES_KEY_SIZE);
|
||||
|
||||
@@ -1416,7 +1416,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
|
||||
size_t fileSize = 0;
|
||||
|
||||
// MED-3: hold spiLock only for file I/O; release it before any crypto operation.
|
||||
// spiLock is a non-recursive binary semaphore — re-entry from the same task deadlocks.
|
||||
// spiLock is a non-recursive binary semaphore - re-entry from the same task deadlocks.
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
auto f = FSCom.open(filename, FILE_O_READ);
|
||||
@@ -1466,7 +1466,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
|
||||
meshtastic_security::secure_zero(dekSnapshot, sizeof(dekSnapshot));
|
||||
return false;
|
||||
}
|
||||
} // spiLock released here — MED-3
|
||||
} // spiLock released here - MED-3
|
||||
|
||||
// Parse header (outside spiLock)
|
||||
size_t pos = 0;
|
||||
@@ -1491,7 +1491,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
|
||||
const uint8_t *ciphertext = fileBuf.get() + pos;
|
||||
const uint8_t *storedHmac = fileBuf.get() + fileSize - HMAC_SIZE;
|
||||
|
||||
// M2 (audit): HMAC now covers the full on-disk header — magic +
|
||||
// M2 (audit): HMAC now covers the full on-disk header - magic +
|
||||
// plaintext_len in addition to the nonce + ciphertext that the original
|
||||
// design covered. Without this, the 4-byte magic and 4-byte plaintext_len
|
||||
// bytes are integrity-protected only by the equality check
|
||||
@@ -1533,13 +1533,13 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
|
||||
// length derived from the file size. For AES-CTR the two are always equal in a legitimate
|
||||
// file; a mismatch means the header field was tampered independently of the ciphertext.
|
||||
if (plaintextLen != ciphertextLen) {
|
||||
LOG_ERROR("EncryptedStorage: plaintextLen (%d) != ciphertextLen (%d) in %s — header tampered", plaintextLen,
|
||||
LOG_ERROR("EncryptedStorage: plaintextLen (%d) != ciphertextLen (%d) in %s - header tampered", plaintextLen,
|
||||
(uint32_t)ciphertextLen, filename);
|
||||
meshtastic_security::secure_zero(dekSnapshot, sizeof(dekSnapshot));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrypt using dekSnapshot — MED-1
|
||||
// Decrypt using dekSnapshot - MED-1
|
||||
if (plaintextLen > outBufSize) {
|
||||
LOG_ERROR("EncryptedStorage: Output buffer too small for %s (%d > %d)", filename, plaintextLen, outBufSize);
|
||||
meshtastic_security::secure_zero(dekSnapshot, sizeof(dekSnapshot));
|
||||
@@ -1607,7 +1607,7 @@ bool encryptAndWrite(const char *filename, const uint8_t *plaintext, size_t plai
|
||||
|
||||
// M2 (audit): HMAC covers the full header (magic + plaintext_len) in
|
||||
// addition to nonce + ciphertext. See readAndDecrypt for the rationale.
|
||||
// Must match the read side exactly — keep both updates in lockstep.
|
||||
// Must match the read side exactly - keep both updates in lockstep.
|
||||
uint32_t magicForMac = MAGIC;
|
||||
uint32_t plaintextLenForMac = (uint32_t)plaintextLen;
|
||||
size_t hmacDataLen = 4 + NONCE_SIZE + 4 + ciphertextLen;
|
||||
@@ -1665,7 +1665,7 @@ bool encryptAndWrite(const char *filename, const uint8_t *plaintext, size_t plai
|
||||
|
||||
bool migrateFile(const char *filename)
|
||||
{
|
||||
// L-2: Precondition — spiLock must NOT be held by the calling task when this function
|
||||
// L-2: Precondition - spiLock must NOT be held by the calling task when this function
|
||||
// is called. Both isEncrypted() and encryptAndWrite() (called internally) acquire
|
||||
// spiLock; since it is a non-recursive binary semaphore, re-entry deadlocks the task.
|
||||
#ifdef FSCom
|
||||
@@ -1692,13 +1692,13 @@ bool migrateFile(const char *filename)
|
||||
|
||||
// M25 (audit): refuse to allocate a buffer for an attacker-injected
|
||||
// oversized file. The legitimate ceiling is the largest proto file
|
||||
// we ever write — comfortably under 64 KiB on every supported
|
||||
// we ever write - comfortably under 64 KiB on every supported
|
||||
// variant. Anything significantly larger is either corrupt or
|
||||
// hostile (e.g. DFU file inject); reading it into RAM would OOM
|
||||
// the device.
|
||||
constexpr size_t kMigrateMaxFileSize = 64 * 1024;
|
||||
if (fileSize > kMigrateMaxFileSize) {
|
||||
LOG_ERROR("EncryptedStorage: refusing to migrate %s — size %u exceeds %u-byte cap", filename, (unsigned)fileSize,
|
||||
LOG_ERROR("EncryptedStorage: refusing to migrate %s - size %u exceeds %u-byte cap", filename, (unsigned)fileSize,
|
||||
(unsigned)kMigrateMaxFileSize);
|
||||
f.close();
|
||||
return false;
|
||||
@@ -1734,7 +1734,7 @@ bool migrateFile(const char *filename)
|
||||
bool migrateFileToPlaintext(const char *filename)
|
||||
{
|
||||
// Inverse of migrateFile: decrypt an encrypted file and rewrite it as
|
||||
// plaintext, atomically. Idempotent — a file that is already plaintext
|
||||
// plaintext, atomically. Idempotent - a file that is already plaintext
|
||||
// is a no-op success, which is what makes the disable flow re-runnable
|
||||
// after a power-loss crash.
|
||||
#ifdef FSCom
|
||||
@@ -1743,7 +1743,7 @@ bool migrateFileToPlaintext(const char *filename)
|
||||
return true;
|
||||
}
|
||||
if (!dekLoaded) {
|
||||
LOG_ERROR("EncryptedStorage: cannot revert %s — not unlocked", filename);
|
||||
LOG_ERROR("EncryptedStorage: cannot revert %s - not unlocked", filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1779,7 +1779,7 @@ bool migrateFileToPlaintext(const char *filename)
|
||||
}
|
||||
|
||||
// Write the plaintext over the encrypted file. SafeFile (non-encrypted
|
||||
// direct write — NOT encryptAndWrite) atomically replaces it.
|
||||
// direct write - NOT encryptAndWrite) atomically replaces it.
|
||||
SafeFile sf(filename, /*fullAtomic=*/true);
|
||||
sf.write(plaintext.get(), plaintextLen);
|
||||
if (!sf.close()) {
|
||||
@@ -1807,7 +1807,7 @@ void removeLockdownArtifacts()
|
||||
secureWipeKeys();
|
||||
s_sessionMaxMs = 0;
|
||||
s_sessionStartedMs = 0;
|
||||
LOG_INFO("EncryptedStorage: lockdown artifacts removed — device is no longer in lockdown");
|
||||
LOG_INFO("EncryptedStorage: lockdown artifacts removed - device is no longer in lockdown");
|
||||
}
|
||||
|
||||
} // namespace EncryptedStorage
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
* (the DEK file itself is HMAC'd with KEK; only proto files use HMAC(DEK))
|
||||
*
|
||||
* Ephemeral KEK (FICR-only, no passphrase) -> wraps DEK in the unlock token only.
|
||||
* Unlock token (/prefs/.unlock_token) — valid for N boots and/or M hours after provisioning.
|
||||
* Unlock token (/prefs/.unlock_token) - valid for N boots and/or M hours after provisioning.
|
||||
*
|
||||
* Boot flow:
|
||||
* 1. initLocked() — derive ephemeral KEK, try unlock token
|
||||
* 1. initLocked() - derive ephemeral KEK, try unlock token
|
||||
* 2a. Token valid → UNLOCKED (DEK in RAM, all encrypted files accessible)
|
||||
* 2b. No token, no DEK file → NOT PROVISIONED (operator must call provisionPassphrase)
|
||||
* 2c. No token, DEK file exists → LOCKED (operator must call unlockWithPassphrase)
|
||||
@@ -52,7 +52,7 @@
|
||||
* [1B] boots_remaining
|
||||
* [4B] valid_until_epoch (LE uint32, 0 = no time limit)
|
||||
* [4B] session_max_seconds (LE uint32, 0 = no session limit)
|
||||
* [4B] monotonic_counter (LE uint32) — see /prefs/.tokmono
|
||||
* [4B] monotonic_counter (LE uint32) - see /prefs/.tokmono
|
||||
* [32B] HMAC-SHA256(ephemeralKEK, all above fields)
|
||||
* Total: 78 bytes.
|
||||
*
|
||||
@@ -80,7 +80,7 @@ namespace EncryptedStorage
|
||||
// File format constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static constexpr uint32_t MAGIC = 0x4D454E43; // "MENC" — encrypted proto files
|
||||
static constexpr uint32_t MAGIC = 0x4D454E43; // "MENC" - encrypted proto files
|
||||
static constexpr size_t NONCE_SIZE = 13;
|
||||
static constexpr size_t HMAC_SIZE = 32;
|
||||
static constexpr size_t HEADER_SIZE = 4 + NONCE_SIZE + 4; // magic+nonce+plaintext_len
|
||||
@@ -116,7 +116,7 @@ void initLocked();
|
||||
* save it wrapped with the passphrase-mixed KEK, and create an unlock token.
|
||||
*
|
||||
* @param passphrase Raw passphrase bytes (need not be NUL-terminated)
|
||||
* @param passphraseLen Length in bytes (1–32; matches the proto private_key field size)
|
||||
* @param passphraseLen Length in bytes (1-32; matches the proto private_key field size)
|
||||
* @param bootsRemaining Token valid for this many boots (default TOKEN_DEFAULT_BOOTS)
|
||||
* @param validUntilEpoch Absolute Unix timestamp after which token expires (0 = no time limit)
|
||||
* @param sessionMaxSeconds Per-boot uptime cap on the unlocked session (0 = no cap).
|
||||
@@ -131,7 +131,7 @@ bool provisionPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8_
|
||||
* unwrap the stored DEK, and create a fresh unlock token.
|
||||
*
|
||||
* @param passphrase Raw passphrase bytes
|
||||
* @param passphraseLen Length in bytes (1–32; matches the proto private_key field size)
|
||||
* @param passphraseLen Length in bytes (1-32; matches the proto private_key field size)
|
||||
* @param bootsRemaining New token valid for this many boots
|
||||
* @param validUntilEpoch Absolute Unix timestamp after which token expires (0 = no time limit)
|
||||
* @param sessionMaxSeconds Per-boot uptime cap on the unlocked session (0 = no cap).
|
||||
@@ -188,7 +188,7 @@ bool migrateFileToPlaintext(const char *filename);
|
||||
* Final step of disabling lockdown: remove the DEK, unlock token,
|
||||
* monotonic-counter, and backoff files, then wipe the in-RAM keys.
|
||||
* Call this ONLY after every encrypted file has been reverted to plaintext
|
||||
* via migrateFileToPlaintext() — deleting the DEK first would make any
|
||||
* via migrateFileToPlaintext() - deleting the DEK first would make any
|
||||
* remaining encrypted file permanently unreadable. After this returns,
|
||||
* isProvisioned()/isLockdownActive() are false. APPROTECT is NOT touched
|
||||
* (its lockout is permanent on silicon where it engaged).
|
||||
@@ -198,15 +198,15 @@ void removeLockdownArtifacts();
|
||||
/**
|
||||
* Returns a short string describing why the device is locked (set during initLocked()).
|
||||
* Useful for client-side diagnostics. Examples:
|
||||
* "token_missing" — no unlock token file found
|
||||
* "token_wrong_size" — token file exists but is corrupt
|
||||
* "token_bad_magic" — wrong magic bytes
|
||||
* "token_hmac_fail" — HMAC mismatch (tampered or wrong device)
|
||||
* "token_boots_zero" — boot count exhausted
|
||||
* "token_expired" — TTL expired
|
||||
* "token_dek_fail" — DEK decrypt failed
|
||||
* "not_provisioned" — no DEK file; needs first provisioning
|
||||
* "ok" — unlocked successfully via token
|
||||
* "token_missing" - no unlock token file found
|
||||
* "token_wrong_size" - token file exists but is corrupt
|
||||
* "token_bad_magic" - wrong magic bytes
|
||||
* "token_hmac_fail" - HMAC mismatch (tampered or wrong device)
|
||||
* "token_boots_zero" - boot count exhausted
|
||||
* "token_expired" - TTL expired
|
||||
* "token_dek_fail" - DEK decrypt failed
|
||||
* "not_provisioned" - no DEK file; needs first provisioning
|
||||
* "ok" - unlocked successfully via token
|
||||
*/
|
||||
const char *getLockReason();
|
||||
|
||||
@@ -230,7 +230,7 @@ uint32_t getBackoffSecondsRemaining();
|
||||
// Combined hard cap: bootsRemaining * sessionMaxSeconds total exposure.
|
||||
//
|
||||
// Uptime (not wall-clock) by design: an attacker pulling the RTC backup
|
||||
// battery and spoofing GPS to roll the clock back cannot defeat this —
|
||||
// battery and spoofing GPS to roll the clock back cannot defeat this -
|
||||
// we never read getValidTime() for session enforcement. The check only
|
||||
// engages when sessionMaxSeconds is non-zero, so 0 = unlimited (the
|
||||
// existing token-only behavior, suitable for tower/infra nodes).
|
||||
@@ -239,7 +239,7 @@ uint32_t getBackoffSecondsRemaining();
|
||||
/// maxSeconds = 0 disables the timer for this session.
|
||||
void setSession(uint32_t maxSeconds);
|
||||
|
||||
/// True if a session timer is set and has elapsed. Idempotent — call
|
||||
/// True if a session timer is set and has elapsed. Idempotent - call
|
||||
/// from the main loop on a low-frequency tick.
|
||||
bool isSessionExpired();
|
||||
|
||||
@@ -248,7 +248,7 @@ bool isSessionExpired();
|
||||
uint32_t getSessionRemainingSeconds();
|
||||
|
||||
/// Consume one boot from the on-flash token (the rollback ledger) and
|
||||
/// re-arm the session timer in place — no reboot. Called from the main
|
||||
/// re-arm the session timer in place - no reboot. Called from the main
|
||||
/// loop when a session expires AND there is still budget. Decrements
|
||||
/// bootsRemaining on flash (delete-and-rewrite of the token file, or
|
||||
/// outright deletion if the new count is 0). Returns the new boot
|
||||
|
||||
@@ -35,7 +35,7 @@ bool shouldRedactDisplay()
|
||||
{
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// Lockdown not active (capable build, never provisioned or disabled):
|
||||
// never redact the display — behave like stock firmware.
|
||||
// never redact the display - behave like stock firmware.
|
||||
if (!EncryptedStorage::isLockdownActive())
|
||||
return false;
|
||||
if (!EncryptedStorage::isUnlocked())
|
||||
|
||||
@@ -26,17 +26,17 @@ namespace meshtastic_security
|
||||
* storage-locked: the device stays fully functional on the mesh,
|
||||
* only the display is gated. The latch is set by lockScreen() when
|
||||
* the stock idle timeout powers the screen off (hooked in
|
||||
* Screen::setOn) — so it reuses config.display.screen_on_secs
|
||||
* Screen::setOn) - so it reuses config.display.screen_on_secs
|
||||
* rather than running a second timer. It is cleared only by
|
||||
* unlockScreen(), called from PhoneAPI's lockdown_auth handler when
|
||||
* a client authenticates with the passphrase over any transport.
|
||||
* Button/joystick input can wake the backlight but does NOT clear
|
||||
* the latch — the woken screen shows the LOCKED frame, not content.
|
||||
* the latch - the woken screen shows the LOCKED frame, not content.
|
||||
* This closes the "operator walked away from an unlocked device"
|
||||
* leak without conflating it with the storage-lock security state.
|
||||
*
|
||||
* The latch starts TRUE at boot so a token-auto-unlocked cold boot
|
||||
* comes up redacted — otherwise an attacker holding a screen-locked
|
||||
* comes up redacted - otherwise an attacker holding a screen-locked
|
||||
* device could power-cycle it (RAM latch resets) to recover a
|
||||
* content screen. After any boot, the operator must authenticate
|
||||
* from a client to reveal content.
|
||||
@@ -45,7 +45,7 @@ namespace meshtastic_security
|
||||
* - graphics/Screen.cpp (OLED via OLEDDisplayUi): GATED, renders a centered
|
||||
* "LOCKED" + battery when shouldRedactDisplay() is true.
|
||||
*
|
||||
* KNOWN GAPS — these renderers still leak content under lockdown
|
||||
* KNOWN GAPS - these renderers still leak content under lockdown
|
||||
* - graphics/InkHUD/ (e-ink rich UI on supported boards)
|
||||
* - graphics/niche/ (TFT niche graphics)
|
||||
* - meshtastic/device-ui (T-Deck/TFT, separate submodule)
|
||||
|
||||
Reference in New Issue
Block a user