VP2 rotation fix, NumBlok mini toggle, HW keyboard auto-hide, audit
ViewPager2 rotation phantom (Google Issue 175796502):
- KeyboardView.onSizeChanged rebuilds buildPages() on any width
transition including initial 0→actual layout, not just on resize.
The first buildPages() from setup() runs at width=0 (view still
detached), VP2's internal RecyclerView can't reliably re-anchor to
the real dimensions that arrive later, leaving a fractional scroll
offset that leaks the neighbor page on the right edge. A second
buildPages() once the real size is known produces a clean result.
- KeyboardView.lastModifierStates caches CTRL/ALT/SHIFT state so it
survives the rebuild.
- Works around an unfixed VP2 1.1.0 bug for non-FragmentStateAdapter
usage. Fresh launches and all rotation paths confirmed clean on S23.
NumBlok toggle on mini numpad:
- KeyDefinition gains numLabel/numAction optional fields
- KeyAction gains ToggleNumBlock data object
- LayoutParser understands "numLabel"/"numAction" JSON and the
"toggle_numblock" action type
- KeyboardPageView.numBlockActive swaps label rendering; toggle key
gets LOCKED-style amber glow while active
- TerminalKeyboard.numBlockActive state; handleKeyAction swaps
action→numAction; attachMiniTo carries state across rotation rebuilds
- Mini last row: Num 0 \ (base) → Num Ins ~ (numblock)
- Rows 1-3 map to PC-keypad nav: Home/↑/PgUp, ←/Esc/→, End/↓/PgDn
- QuickBarCustomizer serializer/deserializer learned toggle_numblock
Hardware keyboard auto-hide:
- MainActivity derives hasHwKeyboard from LocalConfiguration.current.
keyboard != KEYBOARD_NOKEYS && hardKeyboardHidden != HARDKEYBOARDHIDDEN_YES
- LaunchedEffect(hasHwKeyboard) { ckbHidden = hasHwKeyboard } seeds
the toggle on every HW kb connect/disconnect
- In HW kb mode the kebab Show/Hide controls both CKB and QuickBar as
a pair: val qbShownByUser = if (hasHwKeyboard) !ckbHidden else
quickBarVisible
- Normal mode unchanged (QB follows its own quickBarVisible pref)
Number row dropdown:
- KeyboardSettingsDialog now shows all four options (top/left/right/
hidden) in both orientations. Previously left/right were filtered
out in portrait, preventing the user from setting the mini mode
without first rotating into landscape.
- Portrait-override on the effective numberRowMode stays in place
(mini is landscape-only by design — documented in CLAUDE.md and
auto-memory).
Audit fixes:
- KeyboardPageView: removed key.hint!! by capturing into a local
hintText val so smart-cast flows through the render branches
- SSHKeyLoader.loadEd25519FromPkcs8: require(seed.size == 32) so a
malformed PKCS#8 blob fails fast with a clear message instead of
crashing deep inside EdDSAPrivateKeySpec
- vault_crypto.cpp nativeEncrypt: secure_zero(ptPtr, ptLen) before
ReleaseByteArrayElements on all three paths (success + two error
paths), matching the existing key-handling pattern for plaintext
defence-in-depth through the JNI boundary
Docs:
- TECHNICAL.md: NumBlok, VP2 rebuild workaround, HW keyboard auto-hide,
portrait mini override notes
- CLAUDE.md: lib-terminal-keyboard module description updated with
the same invariants
- TODO.md: recently-completed items through 2026-04-06
- Audit.md: investigation log including false positives for future
reference
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f6f0e5e078
commit
e243b8e1e5
19 changed files with 278 additions and 56 deletions
64
Audit.md
Normal file
64
Audit.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Audit — 2026-04-06
|
||||
|
||||
Comprehensive audit across all modules after the NumBlok / HW keyboard / VP2 rotation work in this session. Focus: security, bugs, dead code, file size, code quality. Debug infrastructure (TRACE logs, DevConfig receiver, SessionRecorder) is intentionally preserved and gated on the `dev` flavor.
|
||||
|
||||
Parallel scan by module (lib-terminal-keyboard, lib-ssh, lib-terminal-view, lib-vault-crypto, app). Many reported findings were false positives on re-inspection — logged below for clarity.
|
||||
|
||||
## Fixes applied
|
||||
|
||||
### `lib-terminal-keyboard/src/main/kotlin/com/roundingmobile/keyboard/view/KeyboardPageView.kt`
|
||||
- **Removed `key.hint!!` on lines 201/208.** Captured `key.hint` into a local `hintText` val at the top of the render branches so Kotlin's smart-cast flows through the `if (hintText != null && hasBelowHint)` / `if (hintText != null && hasCornerHint)` checks. Drop-in safer replacement for the two `!!` assertions — no runtime behavior change.
|
||||
|
||||
### `lib-ssh/src/main/java/com/roundingmobile/ssh/SSHKeyLoader.kt`
|
||||
- **Added `require(seed.size == 32)` in `loadEd25519FromPkcs8`.** A malformed PKCS#8 blob with a non-32-byte OCTET STRING would previously crash inside `EdDSAPrivateKeySpec(seed, …)` with a cryptic stack. Now it fails fast with a clear message: `"Ed25519 seed must be exactly 32 bytes, got $size"`.
|
||||
|
||||
### `lib-vault-crypto/src/main/cpp/vault_crypto.cpp`
|
||||
- **Zero plaintext JNI buffer before `ReleaseByteArrayElements` in `nativeEncrypt`.** The existing code was already zeroing the key buffer (`secure_zero(keyPtr, keyLen)`) before release with `JNI_ABORT`, but leaking the plaintext buffer through the same path. Added three `secure_zero(ptPtr, ptLen)` calls — one on the normal success path, two on error paths (urandom failure, keyPtr acquisition failure). This matches the key-handling pattern and closes a defence-in-depth gap for sensitive plaintext passing through encrypt. On Android ART, `GetByteArrayElements` returns a JVM-managed copy, so zeroing does not affect the caller's input array.
|
||||
|
||||
## Findings that were NOT real issues (documented to avoid re-flagging)
|
||||
|
||||
### `lib-terminal-keyboard/TerminalKeyboard.kt`
|
||||
- "Comment syntax error `/ NumBlock…` on lines 173/814" — **false positive**. Both lines use valid `//` comments; the agent misread the indentation.
|
||||
- "`_rawJson!!` on line 1086" — **false positive**. Guarded by the enclosing `if (kl == null && _rawJson != null)` check; `!!` is safe.
|
||||
- "Race condition on `numBlockActive` between UI and touch threads" — **false positive**. Both `handleKeyAction` (toggle) and `KeyboardPageView` rendering (read) run on the main thread.
|
||||
|
||||
### `lib-terminal-keyboard/KeyboardView.kt`
|
||||
- "Rapid rebuilds via `onSizeChanged` could lose state" — **not a real issue**. The rebuild is idempotent: it re-creates the ViewPager2 and re-applies cached `lastModifierStates`. Debouncing would hide legitimate resize events.
|
||||
|
||||
### `lib-terminal-keyboard/QuickBarView.kt`
|
||||
- "`VelocityTracker.recycle()` on line 747 is unreachable after return" — **false positive**. The recycles on 731 and 747 are in two separate `if`/`else` branches; each is reachable within its branch.
|
||||
- "Empty catches around color parsing" — **intentional**. These are graceful fallbacks for user-provided custom color strings; failing silently to the theme default is the desired UX.
|
||||
|
||||
### `lib-terminal-keyboard/gesture/KeyRepeatHandler.kt`
|
||||
- "CoroutineScope not cancelled if `destroy()` never called" — **not a real issue**. `TerminalKeyboard.detach()` calls `keyRepeatHandler.destroy()`, and `detach()` is called from the `DisposableEffect(keyboard)` onDispose in `MainActivity`.
|
||||
|
||||
### `lib-ssh/SftpSession.kt`
|
||||
- "`sortEntries` sorts dirs by name even in `size` mode" — **intentional**. Directories in SFTP do not have a meaningful file size (usually 0 or filesystem-block size); alphabetical ordering is the sensible fallback.
|
||||
- "Dispatcher leak on `connect()` failure" — **false positive**. `SftpSessionManager.openSftpSession` wraps the `connect()` call in try/catch and calls `sftpSession.close()` on failure (line 49), which shuts down the dispatcher.
|
||||
|
||||
### `lib-ssh/SSHSession.kt`
|
||||
- "Password in `String`" — **known limitation** documented in the existing code. Strings are immutable in the JVM and cannot be reliably zeroed; the app converts them to `CharArray` at the earliest opportunity in `MainViewModel`/`TerminalService` and zeros those. Not a regression.
|
||||
- "Swallowed cleanup exceptions" — **intentional**. Cleanup paths swallow to avoid cascading failures that could mask the original disconnect reason.
|
||||
|
||||
### `lib-vault-crypto/aes256gcm.c`
|
||||
- "Table-based AES vulnerable to cache-timing side channels" — **not in threat model**. The app runs in its own Android process; there is no co-located untrusted code attempting cache-timing attacks against the vault crypto. Using a constant-time software AES would add complexity without addressing a real attack vector.
|
||||
- "GHASH not constant-time" — same rationale.
|
||||
|
||||
### `lib-vault-crypto/VaultCrypto.kt`
|
||||
- "Plaintext ByteArray parameter not zeroed by the Kotlin API" — **caller's responsibility**. The Kotlin API is a thin JNI wrapper; the caller owns the input ByteArray and is expected to zero it after use if it contains secrets. Documenting this would be useful (follow-up doc task), but the code itself is not buggy.
|
||||
|
||||
### `lib-terminal-view/ScreenBuffer.kt`
|
||||
- "`screenRow()` returns `buf[0]` for out-of-bounds" — **defensive**, not a bug. Parser callers should never hit this path; the fallback prevents a crash in unexpected cases.
|
||||
|
||||
### `app/ui/MainActivity.kt`, `MainViewModel.kt`, `SessionTabBar.kt`, `EditConnectionScreen.kt`, `KeyboardSettingsDialog.kt`, `QuickBarCustomizer.kt`
|
||||
- All clean per the app-module agent. HW keyboard detection, theme picker, NumBlok serialization all correct.
|
||||
|
||||
### `app/src/dev/DevConfig.kt`
|
||||
- "Hardcoded test credentials" — **dev flavor only**, stripped from prod. Used for the internal test SSH servers (one.jima.cat, 10.10.0.39). Not a leak.
|
||||
|
||||
## Build verification
|
||||
|
||||
- `./gradlew assembleDevDebug` — **BUILD SUCCESSFUL** (v0.0.31)
|
||||
- Native `vault_crypto.cpp` recompiled cleanly for all ABIs (armeabi-v7a, arm64-v8a, x86, x86_64) with the added `secure_zero` calls.
|
||||
- Deployed to `duero:/mnt/master/ssh-workbench.v0.0.31.dev.dbg.2026-04-06.apk`.
|
||||
- S23 was disconnected at deploy time, so no install/smoke test on device. Previous v0.0.30 is still installed on S23; the audit changes are all defence-in-depth tightening and should be transparent at runtime.
|
||||
|
|
@ -30,8 +30,8 @@ android {
|
|||
defaultConfig {
|
||||
minSdk = 27
|
||||
targetSdk = 36
|
||||
versionCode = 20
|
||||
versionName = "0.0.20"
|
||||
versionCode = 31
|
||||
versionName = "0.0.31"
|
||||
|
||||
applicationId = "com.roundingmobile.sshworkbench"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ package com.roundingmobile.sshworkbench
|
|||
|
||||
// Auto-generated — do not edit
|
||||
object BuildTimestamp {
|
||||
const val TIME = "2026-04-06 11:23:39"
|
||||
const val TIME = "2026-04-06 13:12:27"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ import androidx.compose.ui.geometry.Offset
|
|||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
|
@ -756,17 +755,14 @@ private fun KeyboardTab(
|
|||
)
|
||||
}
|
||||
|
||||
// Number row mode — mini numpad (left/right) only available in landscape
|
||||
// Number row mode — all four options available in both orientations
|
||||
SectionLabel(stringResource(R.string.number_row))
|
||||
val isLandscape = LocalConfiguration.current.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
val allNumberRowOptions = listOf(
|
||||
val numberRowOptions = listOf(
|
||||
"top" to R.string.number_row_top,
|
||||
"left" to R.string.number_row_left,
|
||||
"right" to R.string.number_row_right,
|
||||
"hidden" to R.string.number_row_hidden
|
||||
)
|
||||
val numberRowOptions = if (isLandscape) allNumberRowOptions
|
||||
else allNumberRowOptions.filter { it.first != "left" && it.first != "right" }
|
||||
LanguageDropdown(
|
||||
options = numberRowOptions.map { it.first to stringResource(it.second) },
|
||||
selected = numberRowMode,
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ private fun serializeAction(action: KeyAction): JSONObject {
|
|||
is KeyAction.ToggleMod -> { obj.put("type", "toggle_mod"); obj.put("mod", action.mod) }
|
||||
is KeyAction.Macro -> { obj.put("type", "macro"); obj.put("text", action.text) }
|
||||
is KeyAction.SwitchPage -> { obj.put("type", "switch_page"); obj.put("pageId", action.pageId) }
|
||||
is KeyAction.ToggleNumBlock -> { obj.put("type", "toggle_numblock") }
|
||||
is KeyAction.None -> { obj.put("type", "none") }
|
||||
}
|
||||
return obj
|
||||
|
|
@ -215,6 +216,7 @@ private fun deserializeAction(obj: JSONObject): KeyAction =
|
|||
"esc_seq" -> KeyAction.EscSeq(obj.getString("seq"))
|
||||
"combo" -> { val m = obj.getJSONArray("mod"); KeyAction.Combo((0 until m.length()).map { m.getString(it) }, obj.getString("key")) }
|
||||
"toggle_mod" -> KeyAction.ToggleMod(obj.getString("mod"))
|
||||
"toggle_numblock" -> KeyAction.ToggleNumBlock
|
||||
"macro" -> KeyAction.Macro(obj.getString("text"))
|
||||
"switch_page" -> KeyAction.SwitchPage(obj.getString("pageId"))
|
||||
else -> KeyAction.None
|
||||
|
|
|
|||
|
|
@ -297,11 +297,17 @@ class MainActivity : AppCompatActivity() {
|
|||
val isTablet = LocalContext.current.resources.configuration.smallestScreenWidthDp >= 600
|
||||
val sameSizeBoth by mainViewModel.terminalPrefs.keyboardSameSizeBoth.collectAsStateWithLifecycle(!isTablet)
|
||||
val isLandscape = LocalConfiguration.current.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
// Hardware keyboard detection — Configuration updates live when a BT/USB keyboard
|
||||
// connects or disconnects. When one is attached we default-hide the CKB and QB so
|
||||
// the user sees more terminal. Manual overrides via the kebab menu still work.
|
||||
val hasHwKeyboard = LocalConfiguration.current.keyboard != android.content.res.Configuration.KEYBOARD_NOKEYS &&
|
||||
LocalConfiguration.current.hardKeyboardHidden != android.content.res.Configuration.HARDKEYBOARDHIDDEN_YES
|
||||
val keyboardHeightPercent = if (isLandscape && !sameSizeBoth) keyboardHeightLandscape else keyboardHeightPortrait
|
||||
val keyboardLanguage by mainViewModel.terminalPrefs.keyboardLanguage.collectAsStateWithLifecycle("en")
|
||||
val showKeyHints by mainViewModel.terminalPrefs.showKeyHints.collectAsStateWithLifecycle(true)
|
||||
val numberRowModePref by mainViewModel.terminalPrefs.numberRowMode.collectAsStateWithLifecycle("left")
|
||||
// Mini numpad (left/right) only in landscape — portrait falls back to "top"
|
||||
// Mini numpad is landscape-only by design — portrait always falls back to "top".
|
||||
// NEVER remove this override to "fix" unrelated rotation bugs.
|
||||
val numberRowMode = if (!isLandscape && (numberRowModePref == "left" || numberRowModePref == "right")) "top" else numberRowModePref
|
||||
val showPageIndicators by mainViewModel.terminalPrefs.showPageIndicators.collectAsStateWithLifecycle(true)
|
||||
val cqbSize by mainViewModel.terminalPrefs.quickBarSize.collectAsStateWithLifecycle(42)
|
||||
|
|
@ -325,8 +331,17 @@ class MainActivity : AppCompatActivity() {
|
|||
val sessionNavStyle by mainViewModel.terminalPrefs.sessionNavStyle.collectAsStateWithLifecycle("top_bar")
|
||||
val isDrawerMode = sessionNavStyle == "drawer"
|
||||
|
||||
// Temporary hide state — tapping terminal or navigating to terminal resets it
|
||||
// Temporary hide state — tapping terminal or navigating to terminal resets it.
|
||||
// When a hardware keyboard is connected, this flag also gates the QuickBar
|
||||
// (see the QB AndroidView update block below) and defaults to true so CKB+QB
|
||||
// start hidden; user can reveal both via the kebab "Show keyboard" entry.
|
||||
var ckbHidden by remember { mutableStateOf(false) }
|
||||
// Reset the ckbHidden state whenever the HW keyboard attach/detach changes:
|
||||
// HW kb connected → hide both by default; HW kb disconnected → show CKB again.
|
||||
// User's manual show/hide between transitions is preserved.
|
||||
androidx.compose.runtime.LaunchedEffect(hasHwKeyboard) {
|
||||
ckbHidden = hasHwKeyboard
|
||||
}
|
||||
// Keyboard settings dialog state (opened from CKB gear menu)
|
||||
var showKbSettings by remember { mutableStateOf(false) }
|
||||
var showAqbSettings by remember { mutableStateOf(false) }
|
||||
|
|
@ -665,7 +680,11 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
val isTerminalTab = tabTypes[appState.activeSessionId] != TabType.SFTP
|
||||
val qbHidden = qbPosition == "none"
|
||||
container.visibility = if (quickBarVisible && showTerminal && isTerminalTab && !qbHidden) View.VISIBLE else View.GONE
|
||||
// When a HW keyboard is active, the kebab Show/Hide toggle
|
||||
// (ckbHidden) controls both the CKB and the QB as a pair.
|
||||
// Otherwise the QB follows its own quickBarVisible preference.
|
||||
val qbShownByUser = if (hasHwKeyboard) !ckbHidden else quickBarVisible
|
||||
container.visibility = if (qbShownByUser && showTerminal && isTerminalTab && !qbHidden) View.VISIBLE else View.GONE
|
||||
// Set orientation on the actual QuickBarView
|
||||
keyboard.quickBarView?.setOrientation(
|
||||
if (isVerticalQb) com.roundingmobile.keyboard.model.QuickBarOrientation.VERTICAL
|
||||
|
|
@ -825,6 +844,9 @@ class MainActivity : AppCompatActivity() {
|
|||
},
|
||||
update = { container ->
|
||||
val isTermTab = tabTypes[appState.activeSessionId] != TabType.SFTP
|
||||
// ckbHidden gates CKB regardless of HW kb — the LaunchedEffect
|
||||
// above seeds it to hasHwKeyboard so the default is hidden when
|
||||
// a HW keyboard is connected, but user can still show via kebab.
|
||||
container.visibility = if (isCustomKeyboard && !ckbHidden && showTerminal && isTermTab) android.view.View.VISIBLE else android.view.View.GONE
|
||||
keyboard.setPageIndicatorVisible(showPageIndicators)
|
||||
val screenHeight = container.resources.displayMetrics.heightPixels
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
> Package: `com.roundingmobile.sshworkbench`
|
||||
> Developer: Rounding Mobile Technologies S.L.
|
||||
> Min SDK: 27 (Android 8.1) | Target SDK: 36 (Android 16)
|
||||
> Last updated: 2026-04-03
|
||||
> Last updated: 2026-04-06
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -207,6 +207,10 @@ Single Activity host for the entire app. Three-layer Box architecture:
|
|||
- **Layer 2**: NavHost (AnimatedVisibility) — ConnectionListScreen, EditConnectionScreen, SettingsScreen, KeyManagerScreen
|
||||
- Binds to TerminalService, delegates all state management to MainViewModel
|
||||
|
||||
**Hardware keyboard auto-hide** — `MainActivity` derives `hasHwKeyboard` from `LocalConfiguration.current.keyboard != KEYBOARD_NOKEYS && hardKeyboardHidden != HARDKEYBOARDHIDDEN_YES`. Compose re-observes Configuration live when a BT/USB keyboard connects or disconnects (even with `configChanges` set). A `LaunchedEffect(hasHwKeyboard) { ckbHidden = hasHwKeyboard }` seeds the `ckbHidden` state on every transition, so both the CKB and the QuickBar default-hide when a hardware keyboard is present. The kebab menu's "Show keyboard" toggle controls **both** the CKB and the QuickBar together in HW keyboard mode — the QuickBar `update` block uses `val qbShownByUser = if (hasHwKeyboard) !ckbHidden else quickBarVisible`. In normal (no HW kb) mode the QuickBar follows its own `quickBarVisible` pref unchanged.
|
||||
|
||||
**Portrait mini override** — `MainActivity` computes `numberRowMode` as `if (!isLandscape && (pref == "left" || pref == "right")) "top" else pref`. The mini numpad is **landscape-only by design**; portrait always falls back to `top`. Do not remove this override to "fix" unrelated rotation bugs — the mini-in-portrait scenario has been intentionally disabled.
|
||||
|
||||
#### `MainViewModel`
|
||||
Central ViewModel for the single-Activity architecture:
|
||||
- `TabType` enum (`TERMINAL`, `SFTP`) and `SftpTabInfo` data class for SFTP tab metadata
|
||||
|
|
@ -687,6 +691,8 @@ TerminalKeyboard (entry point, Builder)
|
|||
└── ModifierStateManager (CTRL/ALT/SHIFT state machine)
|
||||
```
|
||||
|
||||
**VP2 rotation-phantom workaround** — `KeyboardView.onSizeChanged` rebuilds the ViewPager2 (via `buildPages()`) on **any** width transition including the initial `0 → actual` layout pass. The first `buildPages()` happens from `TerminalKeyboard.attachTo()` → `kv.setup()` while the view is still detached (width=0), so VP2's internal RecyclerView scroll offset is unreliable; a second `buildPages()` after the real layout arrives produces a clean result. `KeyboardView.lastModifierStates` caches the current CTRL/ALT/SHIFT state between rebuilds so visual state survives. This works around [Google Issue Tracker 175796502](https://issuetracker.google.com/issues/175796502) which is still unfixed in ViewPager2 1.1.0 for non-`FragmentStateAdapter` usage.
|
||||
|
||||
### Data Models
|
||||
|
||||
**`KeyboardLayout`** — parsed from JSON:
|
||||
|
|
@ -703,9 +709,11 @@ TerminalKeyboard (entry point, Builder)
|
|||
}
|
||||
```
|
||||
|
||||
**`KeyDefinition`**: `id`, `label`, `action` (KeyAction), `w/h` (proportional), `style`, `repeatable`, `longPress` (variants), `visible`.
|
||||
**`KeyDefinition`**: `id`, `label`, `action` (KeyAction), `w/h` (proportional), `style`, `repeatable`, `longPress` (variants), `visible`, `numLabel` / `numAction` (alternates shown/dispatched when mini NumBlock mode is active).
|
||||
|
||||
**`KeyAction`** (sealed): `Char(primary, shift?)`, `Bytes(values)`, `EscSeq(seq)`, `Combo(mod, key)`, `ToggleMod(mod)`, `Macro(text)`, `SwitchPage(pageId)`, `None`.
|
||||
**`KeyAction`** (sealed): `Char(primary, shift?)`, `Bytes(values)`, `EscSeq(seq)`, `Combo(mod, key)`, `ToggleMod(mod)`, `ToggleNumBlock`, `Macro(text)`, `SwitchPage(pageId)`, `None`.
|
||||
|
||||
**NumBlok (mini numpad only)**: The mini numpad's last row contains a `toggle_numblock` key. Tapping it flips `TerminalKeyboard.numBlockActive`; while active, `KeyboardPageView.resolveDisplayLabel` returns `key.numLabel` instead of `key.label`, `handleKeyAction` dispatches `key.numAction` instead of `key.action`, and the toggle key itself gets a LOCKED-style amber glow. Mini rows 1-3 become PC-keypad nav (`Home/↑/PgUp`, `←/Esc/→`, `End/↓/PgDn`); `m0` → `Ins`; `m_bslash` → `~`. State is carried across rotation rebuilds via `attachMiniTo` copying `numBlockActive` into the new `KeyboardPageView`. See `project_numblock_mini.md` in auto-memory.
|
||||
|
||||
**`KeyEvent`** (sealed): `Character`, `Bytes`, `EscapeSequence`, `Combo`, `Macro`, `Special`.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@
|
|||
- ~~Vault import in-place~~ — VaultImportSheet hosted in KeysVaultScreen, no popBackStack
|
||||
- ~~Clickable radio/checkbox rows~~ — VaultExportSheet, VaultImportSheet rows tappable on text
|
||||
- ~~configChanges~~ — orientation|screenSize|smallestScreenSize|screenLayout prevents activity recreation on rotation
|
||||
- ~~Theme picker default label~~ — EditConnection + tab bar dialogs now show actual global theme in `Default (…)` label; tab bar dialog uses all 20 themes with scrollable Default option
|
||||
- ~~Drawer back button~~ — Back in terminal pane closes drawer first if drawer is open
|
||||
- ~~Mini numpad 15% width~~ — `widthPercent` 10 → 15 in all three `layout_qwerty*.json`
|
||||
- ~~VP2 rotation phantom fix~~ — `KeyboardView.onSizeChanged` rebuilds VP2 on any width transition including initial 0→actual layout; `lastModifierStates` cached for re-apply (Google Issue 175796502 workaround)
|
||||
- ~~Number row dropdown in portrait~~ — `KeyboardSettingsDialog` now shows all 4 options (top/left/right/hidden) regardless of orientation
|
||||
- ~~NumBlok toggle on mini numpad~~ — `KeyDefinition.numLabel`/`numAction` fields, `KeyAction.ToggleNumBlock`; mini last row has `Num 0 \` which becomes PC-keypad nav when toggled
|
||||
- ~~Hardware keyboard auto-hide~~ — `Configuration.keyboard` detection; CKB + QB hidden by default when BT/USB keyboard connects; kebab Show/Hide toggles both as a pair in HW kb mode
|
||||
- ~~Audit 2026-04-06~~ — `KeyboardPageView` smart-cast `!!` removal, `SSHKeyLoader` Ed25519 seed length require, `vault_crypto.cpp` plaintext `secure_zero` before `ReleaseByteArrayElements` in `nativeEncrypt`
|
||||
|
||||
## Open
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ object SSHKeyLoader {
|
|||
*/
|
||||
private fun loadEd25519FromPkcs8(info: PrivateKeyInfo): KeyProvider {
|
||||
val seed = ASN1OctetString.getInstance(info.parsePrivateKey()).octets
|
||||
require(seed.size == 32) { "Ed25519 seed must be exactly 32 bytes, got ${seed.size}" }
|
||||
val edSpec = EdDSANamedCurveTable.getByName("Ed25519")
|
||||
val privSpec = EdDSAPrivateKeySpec(seed, edSpec)
|
||||
val privKey = EdDSAPrivateKey(privSpec)
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ class TerminalKeyboard private constructor(
|
|||
val page = KeyboardPage(id = "mini", name = "Mini", rows = mini.rows)
|
||||
val pv = KeyboardPageView(context, page, theme)
|
||||
pv.setOnTouchListener { _, event -> handlePageTouch(pv, event) }
|
||||
pv.numBlockActive = numBlockActive // carry over across re-attach
|
||||
container.addView(pv, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
|
@ -810,6 +811,8 @@ class TerminalKeyboard private constructor(
|
|||
}
|
||||
|
||||
private fun resolveHintLabel(key: KeyDefinition): String {
|
||||
// Mini NumBlock mode: show the nav label instead of the digit
|
||||
if (numBlockActive && key.numLabel != null) return key.numLabel
|
||||
val action = key.action
|
||||
if (action is KeyAction.Char && action.shift != null) {
|
||||
val shiftState = modifierManager.states.value[Modifier.SHIFT]
|
||||
|
|
@ -822,8 +825,12 @@ class TerminalKeyboard private constructor(
|
|||
|
||||
// --- Action resolution ---
|
||||
|
||||
/** NumBlock mode flag — toggled by a ToggleNumBlock key, controls the mini numpad's alternate action/label. */
|
||||
private var numBlockActive: Boolean = false
|
||||
|
||||
private fun handleKeyAction(key: KeyDefinition) {
|
||||
val action = key.action
|
||||
// If NumBlock is active and the key has a numAction, use that instead of the base action.
|
||||
val action = if (numBlockActive && key.numAction != null) key.numAction else key.action
|
||||
val activeModifiers = modifierManager.activeModifiers()
|
||||
val shiftActive = Modifier.SHIFT in activeModifiers
|
||||
|
||||
|
|
@ -870,6 +877,10 @@ class TerminalKeyboard private constructor(
|
|||
is KeyAction.SwitchPage -> {
|
||||
keyboardView?.switchToPage(action.pageId)
|
||||
}
|
||||
is KeyAction.ToggleNumBlock -> {
|
||||
numBlockActive = !numBlockActive
|
||||
miniPageView?.numBlockActive = numBlockActive
|
||||
}
|
||||
is KeyAction.None -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ sealed class KeyAction {
|
|||
/** Switch keyboard page. */
|
||||
data class SwitchPage(val pageId: String) : KeyAction()
|
||||
|
||||
/** Toggle the mini numpad's NumBlok mode (keys 1-9 become PC keypad nav keys). */
|
||||
data object ToggleNumBlock : KeyAction()
|
||||
|
||||
/** No action — spacer/decorative key. */
|
||||
data object None : KeyAction()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ data class KeyDefinition(
|
|||
val repeatable: Boolean = false,
|
||||
val longPress: List<String>? = null,
|
||||
val menuItems: List<MenuItem>? = null,
|
||||
val visible: Boolean = true
|
||||
val visible: Boolean = true,
|
||||
/** Alternate label shown when the mini numpad's NumBlok mode is active. */
|
||||
val numLabel: String? = null,
|
||||
/** Alternate action emitted when NumBlok is active (overrides `action`). */
|
||||
val numAction: KeyAction? = null
|
||||
) {
|
||||
/** Computed pixel bounds after layout pass — used for hit testing and drawing */
|
||||
var bounds: RectF = RectF()
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ object LayoutParser {
|
|||
repeatable = obj.optBoolean("repeatable", false),
|
||||
longPress = parseStringList(obj.optJSONArray("longPress")),
|
||||
menuItems = parseMenuItems(obj.optJSONArray("menuItems")),
|
||||
visible = obj.optBoolean("visible", true)
|
||||
visible = obj.optBoolean("visible", true),
|
||||
numLabel = obj.optString("numLabel", null),
|
||||
numAction = obj.optJSONObject("numAction")?.let { parseAction(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +120,7 @@ object LayoutParser {
|
|||
)
|
||||
}
|
||||
"toggle_mod" -> KeyAction.ToggleMod(obj.getString("mod"))
|
||||
"toggle_numblock" -> KeyAction.ToggleNumBlock
|
||||
"macro" -> KeyAction.Macro(obj.getString("text"))
|
||||
"switch_page" -> KeyAction.SwitchPage(obj.getString("pageId"))
|
||||
"none" -> KeyAction.None
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ class KeyboardPageView(
|
|||
var modifierStates: Map<Modifier, ModifierState> = emptyMap()
|
||||
set(value) { field = value; invalidate() }
|
||||
|
||||
/** NumBlock mode — when true, keys with a numLabel/numAction render and dispatch those instead. */
|
||||
var numBlockActive: Boolean = false
|
||||
set(value) { field = value; invalidate() }
|
||||
|
||||
// Paint objects reused across draws
|
||||
private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE; strokeWidth = 1f }
|
||||
|
|
@ -166,14 +170,19 @@ class KeyboardPageView(
|
|||
} else if (modState == ModifierState.LOCKED) {
|
||||
canvas.drawRoundRect(rect, radiusPx, radiusPx, lockedPaint)
|
||||
}
|
||||
// NumBlock toggle key: LOCKED-style amber glow when active
|
||||
if (key.action is KeyAction.ToggleNumBlock && numBlockActive) {
|
||||
canvas.drawRoundRect(rect, radiusPx, radiusPx, lockedPaint)
|
||||
}
|
||||
|
||||
// Main label — show uppercase when Shift is active
|
||||
labelPaint.color = theme.resolveKeyText(key.style)
|
||||
labelPaint.isFakeBoldText = pressed
|
||||
val displayLabel = resolveDisplayLabel(key)
|
||||
val fm = labelPaint.fontMetrics
|
||||
val hasCornerHint = key.hint != null && key.hintColor != 0
|
||||
val hasBelowHint = key.hint != null && key.hintColor == 0
|
||||
val hintText = key.hint // capture once so smart-cast works through the render branches
|
||||
val hasCornerHint = hintText != null && key.hintColor != 0
|
||||
val hasBelowHint = hintText != null && key.hintColor == 0
|
||||
val labelY = if (hasBelowHint) {
|
||||
// Label + hint below: label in upper portion, hint below
|
||||
val totalH = -fm.ascent + fm.descent + hintPaint.textSize
|
||||
|
|
@ -186,27 +195,29 @@ class KeyboardPageView(
|
|||
canvas.drawText(displayLabel, rect.centerX(), labelY, labelPaint)
|
||||
|
||||
// Hint label
|
||||
if (hasBelowHint) {
|
||||
if (hintText != null && hasBelowHint) {
|
||||
hintPaint.color = theme.keyHintText
|
||||
val hintFm = hintPaint.fontMetrics
|
||||
val hintY = labelY + fm.descent + (-hintFm.ascent)
|
||||
canvas.drawText(key.hint!!, rect.centerX(), hintY, hintPaint)
|
||||
} else if (hasCornerHint) {
|
||||
canvas.drawText(hintText, rect.centerX(), hintY, hintPaint)
|
||||
} else if (hintText != null && hasCornerHint) {
|
||||
// Corner hint: top-right of key, smaller font
|
||||
hintPaint.color = key.hintColor
|
||||
hintPaint.textAlign = Paint.Align.RIGHT
|
||||
val hintFm = hintPaint.fontMetrics
|
||||
val hintY = rect.top + paddingPx - hintFm.ascent
|
||||
canvas.drawText(key.hint!!, rect.right - paddingPx, hintY, hintPaint)
|
||||
canvas.drawText(hintText, rect.right - paddingPx, hintY, hintPaint)
|
||||
hintPaint.textAlign = Paint.Align.CENTER // restore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label to display, respecting Shift state.
|
||||
* When Shift is ARMED or LOCKED, character keys show their uppercase/shift variant.
|
||||
* Returns the label to display, respecting NumBlock and Shift state.
|
||||
* When NumBlock is active and the key has a numLabel, it wins over the base label and Shift.
|
||||
* When Shift is ARMED/LOCKED, character keys show their uppercase/shift variant.
|
||||
*/
|
||||
private fun resolveDisplayLabel(key: KeyDefinition): String {
|
||||
if (numBlockActive && key.numLabel != null) return key.numLabel
|
||||
val action = key.action
|
||||
if (action is KeyAction.Char && action.shift != null) {
|
||||
val shiftState = modifierStates[Modifier.SHIFT]
|
||||
|
|
|
|||
|
|
@ -189,7 +189,54 @@ class KeyboardView(context: Context) : FrameLayout(context) {
|
|||
fun getPageCount(): Int = realPageCount
|
||||
fun getCurrentPageIndex(): Int = currentRealIndex
|
||||
|
||||
/** Cached modifier states so we can re-apply them after a full VP2 rebuild in onSizeChanged. */
|
||||
private var lastModifierStates: Map<Modifier, ModifierState> = emptyMap()
|
||||
|
||||
/**
|
||||
* Google Issue Tracker 175796502 — after the ViewPager2's width changes (rotation
|
||||
* when Activity has configChanges=orientation, split-screen resize, etc.) its
|
||||
* internal RecyclerView ends up at a fractional scroll offset, leaving the neighbor
|
||||
* page bleeding in from the side. Confirmed still unfixed in VP2 1.1.0.
|
||||
*
|
||||
* Softer workarounds tried and confirmed ineffective:
|
||||
* - setCurrentItem(currentItem, false) — no-op (VP2 early-returns when item==current)
|
||||
* - LinearLayoutManager.scrollToPositionWithOffset(currentItem, 0) — runs, but
|
||||
* VP2's own post-measure logic and the cached RV children override it; phantom
|
||||
* stays on screen (verified on S23 with logcat proving the call fires).
|
||||
*
|
||||
* Nuclear fix: fully rebuild the ViewPager2 from scratch. Same code path as fresh
|
||||
* launch, which we know produces a correct layout. Cost is a single extra layout
|
||||
* pass on rotation — invisible to the user.
|
||||
*/
|
||||
/**
|
||||
* Rebuild the ViewPager2 on every width change, including the initial 0→actual layout
|
||||
* pass. The first buildPages() (triggered from setup()) happens before the view has
|
||||
* real dimensions (w=0), so VP2 is created with zero-width pages. When a size later
|
||||
* arrives, VP2 does not reliably re-anchor its scroll offset — the neighbor page
|
||||
* leaks in from the side (Google Issue Tracker 175796502, still unfixed in VP2 1.1.0
|
||||
* for non-FragmentStateAdapter usage). A second buildPages() at the real dimensions
|
||||
* produces a clean layout regardless of VP2's broken resize path.
|
||||
*
|
||||
* The cost is one extra VP2 construction on startup and on every genuine resize —
|
||||
* cheap (3 pages, Canvas-drawn). The previous KeyboardView instance's modifier states
|
||||
* are re-applied from the cached `lastModifierStates` field.
|
||||
*/
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (w > 0 && w != oldw && layout != null) {
|
||||
val savedIdx = currentRealIndex
|
||||
post {
|
||||
buildPages()
|
||||
if (lastModifierStates.isNotEmpty()) {
|
||||
for (pv in allPageViews) pv.modifierStates = lastModifierStates
|
||||
}
|
||||
if (savedIdx > 0) switchToPage(savedIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setModifierStates(states: Map<Modifier, ModifierState>) {
|
||||
lastModifierStates = states
|
||||
for (pv in allPageViews) {
|
||||
pv.modifierStates = states
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,22 +205,35 @@
|
|||
"widthPercent": 15,
|
||||
"rows": [
|
||||
{ "keys": [
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" } }
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" },
|
||||
"numLabel": "End", "numAction": { "type": "esc_seq", "seq": "[F" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" },
|
||||
"numLabel": "↓", "numAction": { "type": "esc_seq", "seq": "[B" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" },
|
||||
"numLabel": "PgDn", "numAction": { "type": "esc_seq", "seq": "[6~" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" } }
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" },
|
||||
"numLabel": "←", "numAction": { "type": "esc_seq", "seq": "[D" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" },
|
||||
"numLabel": "Esc", "numAction": { "type": "bytes", "value": [27] } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" },
|
||||
"numLabel": "→", "numAction": { "type": "esc_seq", "seq": "[C" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" } }
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" },
|
||||
"numLabel": "Home", "numAction": { "type": "esc_seq", "seq": "[H" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" },
|
||||
"numLabel": "↑", "numAction": { "type": "esc_seq", "seq": "[A" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" },
|
||||
"numLabel": "PgUp", "numAction": { "type": "esc_seq", "seq": "[5~" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" } }
|
||||
{ "id": "m_numblock", "label": "Num", "action": { "type": "toggle_numblock" }, "style": "modifier" },
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" },
|
||||
"numLabel": "Ins", "numAction": { "type": "esc_seq", "seq": "[2~" } },
|
||||
{ "id": "m_bslash", "label": "\\", "action": { "type": "char", "primary": "\\", "shift": "|" },
|
||||
"numLabel": "~", "numAction": { "type": "char", "primary": "~" } }
|
||||
]}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -206,22 +206,35 @@
|
|||
"widthPercent": 15,
|
||||
"rows": [
|
||||
{ "keys": [
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" } }
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" },
|
||||
"numLabel": "End", "numAction": { "type": "esc_seq", "seq": "[F" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" },
|
||||
"numLabel": "↓", "numAction": { "type": "esc_seq", "seq": "[B" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" },
|
||||
"numLabel": "PgDn", "numAction": { "type": "esc_seq", "seq": "[6~" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" } }
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" },
|
||||
"numLabel": "←", "numAction": { "type": "esc_seq", "seq": "[D" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" },
|
||||
"numLabel": "Esc", "numAction": { "type": "bytes", "value": [27] } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" },
|
||||
"numLabel": "→", "numAction": { "type": "esc_seq", "seq": "[C" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" } }
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" },
|
||||
"numLabel": "Home", "numAction": { "type": "esc_seq", "seq": "[H" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" },
|
||||
"numLabel": "↑", "numAction": { "type": "esc_seq", "seq": "[A" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" },
|
||||
"numLabel": "PgUp", "numAction": { "type": "esc_seq", "seq": "[5~" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" } }
|
||||
{ "id": "m_numblock", "label": "Num", "action": { "type": "toggle_numblock" }, "style": "modifier" },
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" },
|
||||
"numLabel": "Ins", "numAction": { "type": "esc_seq", "seq": "[2~" } },
|
||||
{ "id": "m_bslash", "label": "\\", "action": { "type": "char", "primary": "\\", "shift": "|" },
|
||||
"numLabel": "~", "numAction": { "type": "char", "primary": "~" } }
|
||||
]}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -208,22 +208,35 @@
|
|||
"widthPercent": 15,
|
||||
"rows": [
|
||||
{ "keys": [
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" } }
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" },
|
||||
"numLabel": "End", "numAction": { "type": "esc_seq", "seq": "[F" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" },
|
||||
"numLabel": "↓", "numAction": { "type": "esc_seq", "seq": "[B" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" },
|
||||
"numLabel": "PgDn", "numAction": { "type": "esc_seq", "seq": "[6~" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" } }
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" },
|
||||
"numLabel": "←", "numAction": { "type": "esc_seq", "seq": "[D" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" },
|
||||
"numLabel": "Esc", "numAction": { "type": "bytes", "value": [27] } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" },
|
||||
"numLabel": "→", "numAction": { "type": "esc_seq", "seq": "[C" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" } }
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" },
|
||||
"numLabel": "Home", "numAction": { "type": "esc_seq", "seq": "[H" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" },
|
||||
"numLabel": "↑", "numAction": { "type": "esc_seq", "seq": "[A" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" },
|
||||
"numLabel": "PgUp", "numAction": { "type": "esc_seq", "seq": "[5~" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" } }
|
||||
{ "id": "m_numblock", "label": "Num", "action": { "type": "toggle_numblock" }, "style": "modifier" },
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" },
|
||||
"numLabel": "Ins", "numAction": { "type": "esc_seq", "seq": "[2~" } },
|
||||
{ "id": "m_bslash", "label": "\\", "action": { "type": "char", "primary": "\\", "shift": "|" },
|
||||
"numLabel": "~", "numAction": { "type": "char", "primary": "~" } }
|
||||
]}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ Java_com_roundingmobile_vaultcrypto_VaultCrypto_nativeEncrypt(
|
|||
jbyte *keyPtr = env->GetByteArrayElements(key, nullptr);
|
||||
if (!keyPtr) {
|
||||
LOGE("Failed to get key bytes");
|
||||
secure_zero(ptPtr, ptLen);
|
||||
env->ReleaseByteArrayElements(plaintext, ptPtr, JNI_ABORT);
|
||||
return nullptr;
|
||||
}
|
||||
|
|
@ -149,6 +150,7 @@ Java_com_roundingmobile_vaultcrypto_VaultCrypto_nativeEncrypt(
|
|||
LOGE("Failed to read /dev/urandom");
|
||||
if (urandom) fclose(urandom);
|
||||
secure_zero(keyPtr, keyLen);
|
||||
secure_zero(ptPtr, ptLen);
|
||||
env->ReleaseByteArrayElements(plaintext, ptPtr, JNI_ABORT);
|
||||
env->ReleaseByteArrayElements(key, keyPtr, JNI_ABORT);
|
||||
return nullptr;
|
||||
|
|
@ -173,6 +175,7 @@ Java_com_roundingmobile_vaultcrypto_VaultCrypto_nativeEncrypt(
|
|||
secure_zero(tag, sizeof(tag));
|
||||
secure_zero(nonce, sizeof(nonce));
|
||||
secure_zero(keyPtr, keyLen);
|
||||
secure_zero(ptPtr, ptLen); // plaintext in the JNI copy — defence-in-depth
|
||||
env->ReleaseByteArrayElements(plaintext, ptPtr, JNI_ABORT);
|
||||
env->ReleaseByteArrayElements(key, keyPtr, JNI_ABORT);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue