Replace the plaintext FileAuthTokenStore binding with platform-specific
encrypted stores behind the same AuthTokenStore interface, wired via a new
expect/actual authTokenStoreModule.
Android: EncryptedSharedPrefsAuthTokenStore backed by EncryptedSharedPreferences
with a Keystore-backed AES256_GCM master key (androidx.security:security-crypto).
Desktop: EncryptedFileAuthTokenStore writes the token-map JSON as AES/GCM/NoPadding
to the config directory. The key is derived (PBKDF2WithHmacSHA256) from the OS user
name and home dir plus a static salt, with no key file on disk, so a copied token
file is useless on another machine or user. A random 12-byte IV is prepended per
write and owner-only POSIX perms are applied best-effort. Decryption failure is
treated as no tokens rather than crashing. This guards against casual disk
scraping and off-machine copies, not same-user local malware that can re-derive
the key.
iOS: still uses the plaintext file store pending a Keychain-backed implementation
(TODO marker in the iOS binding).
Migration: a legacy plaintext auth_tokens.json from an intermediate build is
imported into the encrypted store and deleted on first access; existing encrypted
tokens win on key collision so a stale plaintext entry cannot clobber a fresh
session.
Every project-scoped client API built its request path by raw string
interpolation of projectName, e.g. "/api/project/$userId/$projectName/begin_sync".
That string reached the shared url() builder whose only path handling was
pathSegments = path.split("/"). Because the split ran on the already-interpolated
string, a projectName containing "/" was split into extra discrete path segments
and a ".." survived as a literal traversal dot-segment, so the outbound request
could target a different endpoint than the {userId}/{projectName}/{action}
template intended (e.g. a malicious sync server returning a project named
"p/../../../api/account/test_auth").
The shared ProjectNameValidator permits "/", "\" and "." (they are encoded to
disk-safe lookalikes only when used as a directory name), so a server-supplied
project name persists verbatim and then injects into every subsequent
project-scoped request under the same host with the bearer token attached.
Fix: each dynamic value is now percent-encoded into a single opaque path
segment via String.encodeUrlPathSegment() before interpolation, and the sink
sets encodedPath directly. Embedded "/" becomes %2F so it cannot create extra
segments, and an all-dots segment is encoded to %2E so a ".." name cannot act as
a traversal segment. The validator is intentionally left unchanged: tightening
it to reject "/" or "." would break syncing for already-valid existing project
names, so encoding is the backward-compatible fix and the on-disk
encodeForFilename behavior is untouched.
Adds a MockEngine test asserting a malicious projectName collapses to a single
encoded segment in the outbound URL across ProjectDataApi, ServerProjectApi and
WritingActivityApi.
Mark the scene-tree state types @Immutable/@Stable and move them onto
kotlinx.collections.immutable so Compose can skip recomposition when the
tree is unchanged: TreeValue.children becomes ImmutableList, SceneSummary
.hasDirtyBuffer a PersistentSet (sourced as such from SceneContentRepository),
and SceneList.State.archivedScenes an ImmutableList. Also cache ImmutableTree
.nodeIndex/hashCode lazily and gate compose-compiler stability reports behind
the composeCompilerReports property.
* Harden onSceneBufferUpdate to reduce from oldState
Read the scene summary from the getAndUpdate lambda's oldState argument
instead of a snapshot captured before the CAS, so the reducer stays a pure
function of its input and composes correctly if buffer updates ever run off
the main dispatcher.
Restores the "store projects in public storage" feature, gated to F-Droid builds (the required MANAGE_EXTERNAL_STORAGE permission is disallowed on Google Play).
- Expose the build channel at runtime via BuildConfig.FDROID in the common module.
- Declare the storage permissions only in src/fdroid/AndroidManifest.xml, swapped in for F-Droid builds.
- Restore the storage-location toggle + file-access UI, gated on BuildConfig.FDROID; reconcile the toggle with the real location on open.
- Build the GitHub release APK as the F-Droid flavor.
- Extract the directory move into a tested FileSystem.moveDirectory() helper (fixes the same-path data-loss crash; runs off the UI thread).
- Read the fdroid flag consistently across settings.gradle.kts and module scripts.
- Document the F-Droid build flag in DEVELOPMENT.md.
Delete the in-tree de.jonasbroeckmann.kzip ZIP format implementation
(~700 lines across commonMain/jvmMain/iosMain) and the kompress.core
dependency, in favor of the published no.synth:kmp-zip 0.11.3 library
plus its okio adapter.
ZipUtils now wraps zipTo/unzipFrom for directory operations and uses
the synchronous ZipInputStream(ByteArray) constructor for the in-memory
extract path, removing the previous write-temp-file workaround. The
skipHiddenFiles parameter is dropped (production callers already passed
false) and the corresponding tests are removed.
Co-authored-by: Claude <noreply@anthropic.com>
iOS now runs the same shared Compose UI as Android and Desktop. The
existing Decompose component graph (IosRoot, ProjectSelection,
ProjectRoot) is reused; the Swift app shrinks to an AppDelegate, a
Koin bootstrap, and a UIViewControllerRepresentable that hands off to
a Kotlin ComposeUIViewController. The SwiftUI starter under
/ios/ios/ui and /ios/ios/DecomposeHelpers is deleted.
Hammer.framework moves from :common to :composeUi so the framework
ships the Compose entry point; Xcode's Run Script invokes
:composeUi:embedAndSignAppleFrameworkForXcode. iosArm64 and
iosSimulatorArm64 targets are declared with libbacktrace source-info
so K/N crashes report file:line. 17 expects across composeUi get iOS
actuals (file pickers via filekit, image loading via Coil3,
LanguageUtil via NSLocale.preferredLanguages, etc).
The adaptive nav scaffolds — bottom bar at compact width, side nav
rail otherwise — are extracted from the Android activities into
shared ProjectSelectScaffold and ProjectRootScaffold in
composeUi/commonMain, along with their Modifier helpers and the
close-confirm dialogs. Android activities are now thin shells that
delegate to these; iOS calls the same scaffolds, so phone/tablet
layout is unified across all three platforms.
App icon and CFBundleDisplayName set so the home screen reads
"Hammer" with the brand logo instead of the blueprint placeholder.
* Update moko.resources to v0.24.0
* Fixed breaking changes for Moko
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Adam Brown <adamwbrown@gmail.com>