mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feature: Add TAK passphrase lock/unlock support
Implement the client-side TAK passphrase authentication flow for
devices running TAK-locked firmware.
Key components:
- TakPassphraseStore: per-device passphrase persistence using
EncryptedSharedPreferences (Android Keystore AES-256-GCM), with
boot and hour TTL fields stored alongside the passphrase
- TakLockHandler: orchestrates the full lock/unlock lifecycle —
auto-unlock on reconnect using stored credentials, passphrase
submission, token info parsing, and backoff/failure handling
- MeshCommandSender: sendTakPassphrase() and sendTakLockNow() build
plain local packets that bypass PKC signing and session_passkey;
hour TTL is encoded as an absolute Unix epoch as required by firmware
- ServiceRepository: TakLockState sealed class (None, Locked,
NeedsProvision, Unlocked, LockNowAcknowledged, UnlockFailed,
UnlockBackoff), TakTokenInfo (boots remaining + expiry epoch), and
sessionAuthorized flag
- TakUnlockDialog: Compose dialog for passphrase entry, shown on
Locked and NeedsProvision states; onDismissRequest is a no-op to
prevent race conditions with firmware response timing; cancel
disconnects the user and navigates to the Connections tab
- Lock Now (Security settings): immediately disconnects the client
after informing firmware, purges cached config, navigates away
without showing a passphrase dialog
- ConnectionsScreen: suppress "region unset" prompt while the device
is TAK-locked, since pre-auth config is zeroed/redacted and would
lead the user to a blank LoRa settings screen
- AIDL: sendTakUnlock() and sendTakLockNow() wired through
MeshService → MeshActionHandler → TakLockHandler
- Security settings: "Lock Now (TAK)" button and token info display
showing boots remaining and expiry date
This commit is contained in:
parent
986c60ce88
commit
e7ba8e8497
26 changed files with 753 additions and 8 deletions
|
|
@ -189,4 +189,10 @@ interface IMeshService {
|
|||
* hash is the 32-byte firmware SHA256 hash (optional, can be null)
|
||||
*/
|
||||
void requestRebootOta(in int requestId, in int destNum, in int mode, in byte []hash);
|
||||
|
||||
/// Send TAK unlock passphrase to the device
|
||||
void sendTakUnlock(in String passphrase, in int bootTtl, in int hourTtl);
|
||||
|
||||
/// Lock the device with TAK lock immediately
|
||||
void sendTakLockNow();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e2d1873a6fef7b4b63525cdd014790298d80bef8
|
||||
Subproject commit bc63a57f9e5dba8a7c90ee0bd4a9840862d61f6d
|
||||
|
|
@ -28,9 +28,34 @@ import kotlinx.coroutines.withTimeoutOrNull
|
|||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed class TakLockState {
|
||||
data object None : TakLockState()
|
||||
data object Locked : TakLockState()
|
||||
data object NeedsProvision : TakLockState()
|
||||
data object Unlocked : TakLockState()
|
||||
/** Lock Now ACK received — client should disconnect immediately, no dialog. */
|
||||
data object LockNowAcknowledged : TakLockState()
|
||||
/** Wrong passphrase — retry immediately. */
|
||||
data object UnlockFailed : TakLockState()
|
||||
/** Too many attempts — must wait [backoffSeconds] before retrying. */
|
||||
data class UnlockBackoff(val backoffSeconds: Int) : TakLockState()
|
||||
}
|
||||
|
||||
/**
|
||||
* TAK session token metadata parsed from the TAK_UNLOCKED:boots=N:until=EPOCH: notification.
|
||||
*
|
||||
* @param bootsRemaining Number of reboots before the token expires.
|
||||
* @param expiryEpoch Unix epoch seconds; 0 means no time-based expiry.
|
||||
*/
|
||||
data class TakTokenInfo(
|
||||
val bootsRemaining: Int,
|
||||
val expiryEpoch: Long,
|
||||
)
|
||||
|
||||
sealed class RetryEvent {
|
||||
abstract val packetId: Int
|
||||
abstract val attemptNumber: Int
|
||||
|
|
@ -159,6 +184,38 @@ class ServiceRepository @Inject constructor() {
|
|||
_serviceAction.send(action)
|
||||
}
|
||||
|
||||
// TAK lock state
|
||||
private val _takLockState: MutableStateFlow<TakLockState> = MutableStateFlow(TakLockState.None)
|
||||
val takLockState: StateFlow<TakLockState>
|
||||
get() = _takLockState
|
||||
|
||||
fun setTakLockState(state: TakLockState) {
|
||||
_takLockState.value = state
|
||||
}
|
||||
|
||||
fun clearTakLockState() {
|
||||
_takLockState.value = TakLockState.None
|
||||
}
|
||||
|
||||
// TAK token info (boots remaining + expiry) from the most recent TAK_UNLOCKED notification
|
||||
private val _takTokenInfo: MutableStateFlow<TakTokenInfo?> = MutableStateFlow(null)
|
||||
val takTokenInfo: StateFlow<TakTokenInfo?>
|
||||
get() = _takTokenInfo
|
||||
|
||||
fun setTakTokenInfo(info: TakTokenInfo?) {
|
||||
_takTokenInfo.value = info
|
||||
}
|
||||
|
||||
// True once TAK passphrase is accepted for this BLE connection; false on disconnect.
|
||||
private val _sessionAuthorized: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val sessionAuthorized: StateFlow<Boolean>
|
||||
get() = _sessionAuthorized
|
||||
|
||||
fun setSessionAuthorized(authorized: Boolean) {
|
||||
_sessionAuthorized.value = authorized
|
||||
}
|
||||
|
||||
|
||||
// Retry management
|
||||
private val _retryEvents = MutableStateFlow<RetryEvent?>(null)
|
||||
val retryEvents: StateFlow<RetryEvent?>
|
||||
|
|
|
|||
|
|
@ -120,4 +120,8 @@ open class FakeIMeshService : IMeshService.Stub() {
|
|||
override fun requestTelemetry(requestId: Int, destNum: Int, type: Int) {}
|
||||
|
||||
override fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
|
||||
|
||||
override fun sendTakUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
|
||||
override fun sendTakLockNow() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,4 +117,8 @@ open class FakeIMeshService : IMeshService.Stub() {
|
|||
override fun requestTelemetry(requestId: Int, destNum: Int, type: Int) {}
|
||||
|
||||
override fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
|
||||
|
||||
override fun sendTakUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
|
||||
override fun sendTakLockNow() {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue