diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index d37b77af32..c373d96880 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -599,6 +599,8 @@ lockdown_passphrase
lockdown_passphrases_do_not_match
lockdown_session_boots_remaining
lockdown_session_expires
+lockdown_session_minutes
+lockdown_session_minutes_help
lockdown_session_no_time_limit
lockdown_set_passphrase
lockdown_show_passphrase
diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
index 6075c536ae..6f28dc3ed4 100644
--- a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
+++ b/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt
@@ -72,7 +72,9 @@ fun MainScreen() {
val lockdownState by viewModel.lockdownState.collectAsStateWithLifecycle()
LockdownDialog(
lockdownState = lockdownState,
- onSubmit = { passphrase, boots, hours -> viewModel.sendLockdownUnlock(passphrase, boots, hours) },
+ onSubmit = { passphrase, boots, hours, sessionMinutes ->
+ viewModel.sendLockdownUnlock(passphrase, boots, hours, sessionMinutes * SECONDS_PER_MINUTE)
+ },
onDisconnect = { viewModel.setDeviceAddress("n") },
)
// Auto-disconnect when firmware acknowledges Lock Now
@@ -145,3 +147,5 @@ private fun AndroidAppVersionCheck(viewModel: UIViewModel) {
}
}
}
+
+private const val SECONDS_PER_MINUTE = 60
diff --git a/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl b/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
index c4b099b813..ae07820378 100644
--- a/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
+++ b/core/api/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
@@ -206,7 +206,7 @@ interface IMeshService {
void requestRebootOta(in int requestId, in int destNum, in int mode, in byte []hash);
/// Send a lockdown passphrase to authenticate with a TAK-locked device
- void sendLockdownUnlock(in String passphrase, in int bootTtl, in int hourTtl);
+ void sendLockdownUnlock(in String passphrase, in int bootTtl, in int hourTtl, in int maxSessionSeconds);
/// Send a Lock Now command to the connected TAK-enabled device
void sendLockNow();
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
index f35b41caac..d8578f61de 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
@@ -375,7 +375,7 @@ class CommandSenderImpl(
}
}
- override fun sendLockdownPassphrase(passphrase: String, boots: Int, hours: Int) {
+ override fun sendLockdownPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int) {
val validUntilEpoch =
if (hours > 0) {
(nowMillis / MILLIS_PER_SECOND + hours.toLong() * SECONDS_PER_HOUR).toInt()
@@ -387,6 +387,7 @@ class CommandSenderImpl(
passphrase = passphrase.encodeToByteArray().toByteString(),
boots_remaining = boots.coerceAtLeast(0),
valid_until_epoch = validUntilEpoch,
+ max_session_seconds = maxSessionSeconds.coerceAtLeast(0),
)
sendLockdownAdmin(AdminMessage(lockdown_auth = lockdownAuth))
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImpl.kt
index 179d14496d..4ff43721a1 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/LockdownCoordinatorImpl.kt
@@ -56,6 +56,8 @@ class LockdownCoordinatorImpl(
@Volatile private var pendingHours: Int = 0
+ @Volatile private var pendingMaxSessionSeconds: Int = 0
+
override fun onConnect() {
serviceRepository.setSessionAuthorized(false)
resetTransientState()
@@ -108,7 +110,12 @@ class LockdownCoordinatorImpl(
if (stored != null) {
Logger.i { "Lockdown: Auto-unlocking with stored passphrase" }
wasAutoAttempt = true
- commandSender.sendLockdownPassphrase(stored.passphrase, stored.boots, stored.hours)
+ commandSender.sendLockdownPassphrase(
+ stored.passphrase,
+ stored.boots,
+ stored.hours,
+ stored.maxSessionSeconds,
+ )
return
}
}
@@ -126,7 +133,13 @@ class LockdownCoordinatorImpl(
// Only save on manual submit — auto-unlock already has a stored passphrase.
if (deviceAddress != null && passphrase != null) {
try {
- passphraseStore.savePassphrase(deviceAddress, passphrase, pendingBoots, pendingHours)
+ passphraseStore.savePassphrase(
+ deviceAddress,
+ passphrase,
+ pendingBoots,
+ pendingHours,
+ pendingMaxSessionSeconds,
+ )
Logger.i { "Lockdown: Saved passphrase for device" }
} catch (e: Exception) {
Logger.e(e) { "Lockdown: Failed to persist passphrase (session still unlocked)" }
@@ -174,14 +187,15 @@ class LockdownCoordinatorImpl(
}
}
- override fun submitPassphrase(passphrase: String, boots: Int, hours: Int) {
+ override fun submitPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int) {
pendingPassphrase = passphrase
pendingBoots = boots
pendingHours = hours
+ pendingMaxSessionSeconds = maxSessionSeconds
wasAutoAttempt = false
wasLockNow = false
serviceRepository.setLockdownState(LockdownState.None)
- commandSender.sendLockdownPassphrase(passphrase, boots, hours)
+ commandSender.sendLockdownPassphrase(passphrase, boots, hours, maxSessionSeconds)
}
override fun lockNow() {
@@ -195,5 +209,6 @@ class LockdownCoordinatorImpl(
pendingPassphrase = null
pendingBoots = LockdownPassphraseStore.DEFAULT_BOOTS
pendingHours = 0
+ pendingMaxSessionSeconds = 0
}
}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
index b9026dac71..74b7d3b6d0 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
@@ -404,8 +404,8 @@ class MeshActionHandlerImpl(
}
}
- override fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
- lockdownCoordinator.submitPassphrase(passphrase, bootTtl, hourTtl)
+ override fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {
+ lockdownCoordinator.submitPassphrase(passphrase, bootTtl, hourTtl, maxSessionSeconds)
}
override fun handleSendLockNow() {
diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt
index e2c207ccbe..284dbe3bb5 100644
--- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt
+++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/RadioController.kt
@@ -341,7 +341,7 @@ interface RadioController {
fun setDeviceAddress(address: String)
/** Submits a lockdown passphrase to authenticate with a TAK-locked device. */
- suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int)
+ suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int = 0)
/** Sends a Lock Now command to the connected TAK-enabled device. */
suspend fun sendLockNow()
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt
index 3c01bbd377..a3d1128ac6 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt
@@ -85,7 +85,12 @@ interface CommandSender {
fun requestNeighborInfo(requestId: Int, destNum: Int)
/** Sends a lockdown passphrase to authenticate with a TAK-locked device. */
- fun sendLockdownPassphrase(passphrase: String, boots: Int = 0, hours: Int = 0)
+ fun sendLockdownPassphrase(
+ passphrase: String,
+ boots: Int = 0,
+ hours: Int = 0,
+ maxSessionSeconds: Int = 0,
+ )
/** Sends a Lock Now command to immediately lock a TAK-enabled device. */
fun sendLockNow()
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownCoordinator.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownCoordinator.kt
index 67f5f4974d..18e17ccf1b 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownCoordinator.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownCoordinator.kt
@@ -43,7 +43,7 @@ interface LockdownCoordinator {
fun handleLockdownStatus(status: LockdownStatus)
/** Submits a passphrase to authenticate with the locked device. */
- fun submitPassphrase(passphrase: String, boots: Int, hours: Int)
+ fun submitPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int = 0)
/** Sends a Lock Now command to the connected device. */
fun lockNow()
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownPassphraseStore.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownPassphraseStore.kt
index a544d05715..b024bd24f1 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownPassphraseStore.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/LockdownPassphraseStore.kt
@@ -16,8 +16,19 @@
*/
package org.meshtastic.core.repository
-/** Stored passphrase entry with associated TTL parameters. */
-data class StoredPassphrase(val passphrase: String, val boots: Int, val hours: Int) {
+/**
+ * Stored passphrase entry with associated TTL parameters.
+ *
+ * @param maxSessionSeconds Per-boot uptime cap, in seconds. 0 = unlimited.
+ * Non-zero is firmware-side enforcement: the device revokes auth and reboots
+ * after this many seconds of uptime even if the boot-count TTL is still valid.
+ */
+data class StoredPassphrase(
+ val passphrase: String,
+ val boots: Int,
+ val hours: Int,
+ val maxSessionSeconds: Int = 0,
+) {
init {
require(passphrase.isNotEmpty()) { "passphrase must not be empty" }
}
@@ -35,7 +46,13 @@ interface LockdownPassphraseStore {
fun getPassphrase(deviceAddress: String): StoredPassphrase?
/** Saves the passphrase and TTL parameters for the given device address. */
- fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int)
+ fun savePassphrase(
+ deviceAddress: String,
+ passphrase: String,
+ boots: Int,
+ hours: Int,
+ maxSessionSeconds: Int = 0,
+ )
/** Clears the stored passphrase for the given device address. */
fun clearPassphrase(deviceAddress: String)
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt
index 4c6b58af88..19fd1f3577 100644
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt
@@ -118,7 +118,7 @@ interface MeshActionHandler {
fun handleUpdateLastAddress(deviceAddr: String?)
/** Submits a lockdown passphrase to authenticate with a TAK-locked device. */
- fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int)
+ fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int = 0)
/** Sends a Lock Now command to the connected TAK-enabled device. */
fun handleSendLockNow()
diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index 20b92cbd37..c9df41bdfa 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -623,6 +623,8 @@
Passphrases do not match
Session: %1$d reboots remaining
Expires %1$s
+ Session cap (minutes)
+ Per-boot uptime cap. 0 = unlimited.
No time limit
Set Passphrase
Show
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt
index 041a884449..a811bd0e2a 100644
--- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt
@@ -221,8 +221,8 @@ class AndroidRadioControllerImpl(
context.startForegroundService(intent)
}
- override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
- serviceRepository.meshService?.sendLockdownUnlock(passphrase, bootTtl, hourTtl)
+ override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {
+ serviceRepository.meshService?.sendLockdownUnlock(passphrase, bootTtl, hourTtl, maxSessionSeconds)
}
override suspend fun sendLockNow() {
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt
index 731e53aba9..0c908ae2cd 100644
--- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt
@@ -61,23 +61,36 @@ class LockdownPassphraseStoreImpl(app: Application) : LockdownPassphraseStore {
val passphrase = p.getString("${key}_passphrase", null) ?: return null
val boots = p.getInt("${key}_boots", LockdownPassphraseStore.DEFAULT_BOOTS)
val hours = p.getInt("${key}_hours", 0)
- return StoredPassphrase(passphrase, boots, hours)
+ val maxSessionSeconds = p.getInt("${key}_maxSessionSeconds", 0)
+ return StoredPassphrase(passphrase, boots, hours, maxSessionSeconds)
}
- override fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int) {
+ override fun savePassphrase(
+ deviceAddress: String,
+ passphrase: String,
+ boots: Int,
+ hours: Int,
+ maxSessionSeconds: Int,
+ ) {
val p = requirePrefs()
val key = sanitizeKey(deviceAddress)
p.edit()
.putString("${key}_passphrase", passphrase)
.putInt("${key}_boots", boots)
.putInt("${key}_hours", hours)
+ .putInt("${key}_maxSessionSeconds", maxSessionSeconds)
.apply()
}
override fun clearPassphrase(deviceAddress: String) {
val p = requirePrefs()
val key = sanitizeKey(deviceAddress)
- p.edit().remove("${key}_passphrase").remove("${key}_boots").remove("${key}_hours").apply()
+ p.edit()
+ .remove("${key}_passphrase")
+ .remove("${key}_boots")
+ .remove("${key}_hours")
+ .remove("${key}_maxSessionSeconds")
+ .apply()
}
private fun sanitizeKey(address: String): String = address.replace(":", "_")
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt
index 19e80684e7..a8c80c35b8 100644
--- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt
@@ -402,8 +402,18 @@ class MeshService : Service() {
router.actionHandler.handleRequestRebootOta(requestId, destNum, mode, hash)
}
- override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) = toRemoteExceptions {
- router.actionHandler.handleSendLockdownUnlock(passphrase.orEmpty(), bootTtl, hourTtl)
+ override fun sendLockdownUnlock(
+ passphrase: String?,
+ bootTtl: Int,
+ hourTtl: Int,
+ maxSessionSeconds: Int,
+ ) = toRemoteExceptions {
+ router.actionHandler.handleSendLockdownUnlock(
+ passphrase.orEmpty(),
+ bootTtl,
+ hourTtl,
+ maxSessionSeconds,
+ )
}
override fun sendLockNow() = toRemoteExceptions { router.actionHandler.handleSendLockNow() }
diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt
index e88b451051..0f0e54ee2a 100644
--- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt
+++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/testing/FakeIMeshService.kt
@@ -126,7 +126,7 @@ open class FakeIMeshService : IMeshService.Stub() {
override fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
- override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
+ override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {}
override fun sendLockNow() {}
}
diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/DirectRadioControllerImpl.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/DirectRadioControllerImpl.kt
index 86b7a0398d..318ee7981e 100644
--- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/DirectRadioControllerImpl.kt
+++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/DirectRadioControllerImpl.kt
@@ -235,8 +235,8 @@ class DirectRadioControllerImpl(
radioInterfaceService.setDeviceAddress(address)
}
- override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
- actionHandler.handleSendLockdownUnlock(passphrase, bootTtl, hourTtl)
+ override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {
+ actionHandler.handleSendLockdownUnlock(passphrase, bootTtl, hourTtl, maxSessionSeconds)
}
override suspend fun sendLockNow() {
diff --git a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt
index 8bbf4eab9a..868f72a75c 100644
--- a/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt
+++ b/core/service/src/jvmMain/kotlin/org/meshtastic/core/service/LockdownPassphraseStoreImpl.kt
@@ -71,9 +71,15 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore {
}
}
- override fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int) {
+ override fun savePassphrase(
+ deviceAddress: String,
+ passphrase: String,
+ boots: Int,
+ hours: Int,
+ maxSessionSeconds: Int,
+ ) {
val key = masterKey ?: error("Lockdown: Cannot save passphrase - keystore unavailable")
- val plaintext = serialize(passphrase, boots, hours)
+ val plaintext = serialize(passphrase, boots, hours, maxSessionSeconds)
val encrypted = encrypt(key, plaintext)
entryFile(deviceAddress).writeBytes(encrypted)
}
@@ -114,24 +120,42 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore {
// region Serialization (simple line-based to avoid adding kotlinx-serialization dependency)
- private fun serialize(passphrase: String, boots: Int, hours: Int): ByteArray =
- "$boots\n$hours\n$passphrase".encodeToByteArray()
+ // Format v2: "boots\nhours\nmaxSessionSeconds\npassphrase" (4 lines).
+ // Backward-compat: legacy 3-line entries (no maxSessionSeconds) decode with maxSessionSeconds=0.
+ private fun serialize(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int): ByteArray =
+ "$boots\n$hours\n$maxSessionSeconds\n$passphrase".encodeToByteArray()
@Suppress("ReturnCount")
private fun deserialize(plaintext: ByteArray): StoredPassphrase? {
val text = plaintext.decodeToString()
- val lines = text.split("\n", limit = 3)
- if (lines.size < SERIALIZED_LINE_COUNT) {
+ // Try v2 (4-line) format first.
+ val v2 = text.split("\n", limit = 4)
+ if (v2.size == SERIALIZED_LINE_COUNT_V2) {
+ val boots = v2[0].toIntOrNull()
+ val hours = v2[1].toIntOrNull()
+ val maxSession = v2[2].toIntOrNull()
+ if (boots != null && hours != null && maxSession != null) {
+ return StoredPassphrase(
+ passphrase = v2[3],
+ boots = boots,
+ hours = hours,
+ maxSessionSeconds = maxSession,
+ )
+ }
+ }
+ // Fall back to v1 (3-line, no maxSessionSeconds).
+ val v1 = text.split("\n", limit = 3)
+ if (v1.size < SERIALIZED_LINE_COUNT_V1) {
Logger.w { "Lockdown: Invalid passphrase entry format" }
return null
}
- val boots = lines[0].toIntOrNull()
- val hours = lines[1].toIntOrNull()
+ val boots = v1[0].toIntOrNull()
+ val hours = v1[1].toIntOrNull()
if (boots == null || hours == null) {
Logger.w { "Lockdown: Invalid passphrase entry metadata" }
return null
}
- return StoredPassphrase(passphrase = lines[2], boots = boots, hours = hours)
+ return StoredPassphrase(passphrase = v1[2], boots = boots, hours = hours)
}
// endregion
@@ -172,6 +196,7 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore {
private const val AES_KEY_BITS = 256
private const val GCM_TAG_BITS = 128
private const val BYTE_MASK = 0xFF
- private const val SERIALIZED_LINE_COUNT = 3
+ private const val SERIALIZED_LINE_COUNT_V1 = 3
+ private const val SERIALIZED_LINE_COUNT_V2 = 4
}
}
diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLockdownCoordinator.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLockdownCoordinator.kt
index 40a318c359..9091242d3b 100644
--- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLockdownCoordinator.kt
+++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeLockdownCoordinator.kt
@@ -27,6 +27,7 @@ class FakeLockdownCoordinator : LockdownCoordinator {
var lastPassphrase: String? = null
var lastBoots: Int? = null
var lastHours: Int? = null
+ var lastMaxSessionSeconds: Int? = null
var lockNowCalled = false
override fun onConnect() {
@@ -45,10 +46,11 @@ class FakeLockdownCoordinator : LockdownCoordinator {
lastStatus = status
}
- override fun submitPassphrase(passphrase: String, boots: Int, hours: Int) {
+ override fun submitPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int) {
lastPassphrase = passphrase
lastBoots = boots
lastHours = hours
+ lastMaxSessionSeconds = maxSessionSeconds
}
override fun lockNow() {
diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt
index 036e9148a6..859fe07c17 100644
--- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt
+++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt
@@ -162,7 +162,7 @@ class FakeRadioController :
lastSetDeviceAddress = address
}
- override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {}
+ override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {}
override suspend fun sendLockNow() {}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
index 7aed6d7ad9..84dee5df94 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt
@@ -141,8 +141,13 @@ class UIViewModel(
val lockdownState = serviceRepository.lockdownState
val lockdownTokenInfo = serviceRepository.lockdownTokenInfo
- fun sendLockdownUnlock(passphrase: String, bootTtl: Int = DEFAULT_BOOT_TTL, hourTtl: Int = 0) {
- viewModelScope.launch { radioController.sendLockdownUnlock(passphrase, bootTtl, hourTtl) }
+ fun sendLockdownUnlock(
+ passphrase: String,
+ bootTtl: Int = DEFAULT_BOOT_TTL,
+ hourTtl: Int = 0,
+ maxSessionSeconds: Int = 0,
+ ) {
+ viewModelScope.launch { radioController.sendLockdownUnlock(passphrase, bootTtl, hourTtl, maxSessionSeconds) }
}
fun sendLockNow() {
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt
index 1ec56fb7ce..6a83edf108 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/lockdown/LockdownDialog.kt
@@ -57,6 +57,8 @@ import org.meshtastic.core.resources.lockdown_incorrect_passphrase
import org.meshtastic.core.resources.lockdown_lock_reason
import org.meshtastic.core.resources.lockdown_passphrase
import org.meshtastic.core.resources.lockdown_passphrases_do_not_match
+import org.meshtastic.core.resources.lockdown_session_minutes
+import org.meshtastic.core.resources.lockdown_session_minutes_help
import org.meshtastic.core.resources.lockdown_set_passphrase
import org.meshtastic.core.resources.lockdown_show_passphrase
import org.meshtastic.core.resources.lockdown_submit
@@ -75,7 +77,7 @@ import org.meshtastic.core.ui.icon.VisibilityOff
@Composable
fun LockdownDialog(
lockdownState: LockdownState,
- onSubmit: (passphrase: String, boots: Int, hours: Int) -> Unit,
+ onSubmit: (passphrase: String, boots: Int, hours: Int, sessionMinutes: Int) -> Unit,
onDisconnect: () -> Unit,
) {
val shouldShow =
@@ -93,6 +95,7 @@ fun LockdownDialog(
var passwordVisible by rememberSaveable { mutableStateOf(false) }
var boots by rememberSaveable { mutableIntStateOf(LockdownPassphraseStore.DEFAULT_BOOTS) }
var hours by rememberSaveable { mutableIntStateOf(0) }
+ var sessionMinutes by rememberSaveable { mutableIntStateOf(0) }
val isProvisioning = lockdownState is LockdownState.NeedsProvision
val title =
@@ -208,10 +211,20 @@ fun LockdownDialog(
modifier = Modifier.weight(1f),
)
}
+ Spacer(modifier = Modifier.height(SPACING_DP.dp))
+ OutlinedTextField(
+ value = sessionMinutes.toString(),
+ onValueChange = { str -> str.toIntOrNull()?.let { sessionMinutes = it.coerceAtLeast(0) } },
+ label = { Text(stringResource(Res.string.lockdown_session_minutes)) },
+ supportingText = { Text(stringResource(Res.string.lockdown_session_minutes_help)) },
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.fillMaxWidth(),
+ )
}
},
confirmButton = {
- TextButton(onClick = { onSubmit(passphrase, boots, hours) }, enabled = isValid) {
+ TextButton(onClick = { onSubmit(passphrase, boots, hours, sessionMinutes) }, enabled = isValid) {
Text(stringResource(Res.string.lockdown_submit))
}
},