Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
*.bin
*.hex
*.map
/release/

# Device backups and private MeshCore state
/device-spiffs-extracted/
/device-spiffs*/
/device-*.bin
/settings.dat
*.mcb

# IDE
.idea/
Expand Down
64 changes: 64 additions & 0 deletions create-settings-backup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
param(
[Parameter(Mandatory = $true)]
[string]$Source,

[Parameter(Mandatory = $true)]
[string]$Destination
)

$ErrorActionPreference = 'Stop'

Add-Type -TypeDefinition @'
public static class MeshCoreCrc32
{
public static uint Compute(byte[] data)
{
uint crc = 0xFFFFFFFFu;
foreach (byte value in data)
{
crc ^= value;
for (int bit = 0; bit < 8; bit++)
{
crc = (crc >> 1) ^ (0xEDB88320u & (uint)-(int)(crc & 1u));
}
}
return ~crc;
}
}
'@

$sourcePath = (Resolve-Path -LiteralPath $Source).Path
$payload = [System.IO.File]::ReadAllBytes($sourcePath)
if ($payload.Length -lt 1 -or $payload.Length -gt 512) {
throw "Settings payload must contain between 1 and 512 bytes; found $($payload.Length)."
}

$destinationPath = [System.IO.Path]::GetFullPath($Destination)
if ([System.IO.File]::Exists($destinationPath)) {
throw "Refusing to overwrite existing backup: $destinationPath"
}

$stream = [System.IO.File]::Open($destinationPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
try {
$magic = [byte[]](0x4D, 0x43, 0x50, 0x53) # MCPS
$version = [System.BitConverter]::GetBytes([uint16]1)
$length = [System.BitConverter]::GetBytes([uint16]$payload.Length)
$crc = [System.BitConverter]::GetBytes([MeshCoreCrc32]::Compute($payload))

$stream.Write($magic, 0, $magic.Length)
$stream.Write($version, 0, $version.Length)
$stream.Write($length, 0, $length.Length)
$stream.Write($crc, 0, $crc.Length)
$stream.Write($payload, 0, $payload.Length)
$stream.Flush($true)
}
finally {
$stream.Dispose()
}

[pscustomobject]@{
SourceBytes = $payload.Length
BackupBytes = (Get-Item -LiteralPath $destinationPath).Length
Crc32 = ('{0:X8}' -f [MeshCoreCrc32]::Compute($payload))
Destination = $destinationPath
}
54 changes: 54 additions & 0 deletions docs/encrypted-sd-backup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Encrypted SD backup and restore

The Cardputer ADV companion firmware can save an authenticated, encrypted
snapshot of its complete SPIFFS partition to a microSD card. The snapshot
includes node and radio settings, channels and channel secrets, contacts,
messages, the node identity/private key, and other SPIFFS-managed state. UI
brightness and theme preferences are included separately inside the same
encrypted archive.

## Create a backup

1. Insert a FAT-formatted microSD card.
2. Open **Settings > SD Backup > Encrypted backup**.
3. Enter a passphrase of at least eight characters.
4. Enter the same passphrase again to confirm it.

The firmware writes the archive to `/meshcore/full-backup.mcb`. An existing
archive is replaced only after the new temporary file has been written
successfully.

Keep both the archive and passphrase secure. The passphrase cannot be recovered,
and the archive contains the node identity and channel secrets needed to assume
the backed-up node's identity.

## Restore a backup

1. Insert the microSD card containing `/meshcore/full-backup.mcb`.
2. Open **Settings > SD Backup > Full restore**.
3. Enter the backup passphrase.

The firmware authenticates the complete archive and checks that its recorded
SPIFFS partition size matches the device before changing flash. A wrong
passphrase, modified archive, or incompatible partition layout is rejected.
After a successful restore, the device restarts automatically.

Restore replaces the complete SPIFFS snapshot. Use an archive created with the
same partition layout and keep a separate copy of important backups.

## Archive protection

- AES-256-GCM provides encryption and tamper detection.
- PBKDF2-HMAC-SHA-256 derives the key from the passphrase using 100,000
iterations and a random 16-byte salt.
- Each archive uses a random 12-byte nonce and a 16-byte authentication tag.
- The passphrase and derived-key buffers are cleared after use.

The identity/private key is encrypted, not hashed. A one-way hash could verify
data but could not restore the identity.

## M5Launcher installation

When installing a firmware update through M5Launcher, do not install or replace
SPIFFS if you want to preserve the live settings already on the device. The
backup archive itself remains on the microSD card.
Loading