Commit Graph

108 Commits

Author SHA1 Message Date
r-zakarya e85f46f501 feat(android): copy from the list, framed entries, one radius everywhere
The cross moves inside the search field, where it belonged. My first attempt failed with a TLabel, whose HitTest is false by default, and I concluded too quickly that no child of a TEdit can take a touch. A TRectangle takes it.

Buttons drop the pill and take the fields' radius: a stadium beside a 14-radius field put two shape vocabularies on one line, and consistency is worth more here than fidelity to the reference.

List entries get their own block, like the detail fields - on a gradient a bare line of text has no readable edge. And each row carries User and Pass buttons, so the commonest gesture no longer requires opening the entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:44:52 +01:00
r-zakarya d22089900c fix(android): the edit fields were still shaped like pills
A radius of 24 kept them fully rounded even after the fill was hollowed out, so a text field still read as a command. They now take 14, the same as the row buttons - rounded, unmistakably rectangular.

The search field loses the stadium shape of Android's global search along with the others, since one style serves every edit. Its magnifier is what identifies it now. Giving it back its own shape means cloning the style entry, which is worth doing only if the shape is missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:26:37 +01:00
r-zakarya 4347dba8d9 feat(android): the system bars join the gradient
The app sat in a frame that did not belong to it: a grey status bar above, a WHITE navigation bar below, under a blue background. The reference bathes both in the same gradient, and that is what makes an app look like it occupies the screen rather than being placed on it.

Each bar takes one end of the gradient - top colour above, bottom colour below - so the continuity is exact. The FLAG_SECURE condition moved from the uses clause into the body of SecureScreen, since the same JNI imports now serve the bars, which must work in debug too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:23:27 +01:00
r-zakarya 9bc005696d fix(android): a field should look recessed, a button raised
They carried the same fill and the same pill, so the password field looked like a command. A field now holds 10 per cent white against a button's 18 and a main action's 30 - hollow against raised, which is the only cue left once borders are gone.

The settings panel also reclaimed the top of the screen, above the title: created hidden, FMX moves it to the head of the stack when it becomes visible. Its position is now reasserted each time it appears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:18:16 +01:00
r-zakarya 2e2ded1146 fix(android): the Copy buttons were ellipses, not pills
A fixed radius of 26 on a button only 40 tall makes the corners meet, and the shape stops being a rounded rectangle at all. The radius is now recomputed from the height whenever it changes - the height is set after creation, which is why a constant could never be right.

And the small row buttons take a moderate radius rather than the full pill: they are low and wide, and a stadium shape read as an oval. The fully rounded form stays where the reference puts it, on the main actions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:06:09 +01:00
r-zakarya 27a7a322d6 feat(android): the palette taken from the phone's global search
Not the desktop's slate any more: a blue gradient behind, blocks that are only translucent white laid over it, no borders at all, wide radii, white text. Depth comes from transparency, which is why the frames are gone - a hairline reads as a desktop application immediately.

There is no coloured accent in the reference either: primary and secondary differ by how much white they carry, 30 against 18 per cent. The 2FA code goes white, carried by its size. Nothing is uppercase - the second plane is marked by colour and size alone.

Three compiler traps on the way, all worth knowing: TBrush needs FMX.Graphics in the INTERFACE clause, a fresh TGradient already runs top to bottom, and TForm already owns a parameterless PaintBackground that shadowed the unit's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:47:44 +01:00
r-zakarya 1609b4d328 docs(android): Gitea as the transport, instead of hosting the endpoint
The Contents API maps term for term onto what is already written: it returns the file with its sha and demands that sha to write, answering 409 on a stale one - exactly the roles of the ETag and the 412. The desktop's bounded reread-remerge-repush loop would work unchanged.

It also settles the one remaining obstacle: nothing to host, no certificate, outside access for free. Records the calls, and why the answer is a private Gitea rather than GitHub - git history never forgets, so every past snapshot stays readable under whatever sync password was current then. Survivable on your own disk, not on a third party's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 11:36:43 +01:00
r-zakarya 40475b0204 docs(android): two open questions on the interface
The theme still is not the one from the global search - the blue and the pill were taken, not the spacing or the density. And a larger one: whether there should be Delphi controls at all, when the desktop shows no native controls either and serves the same HTML in a WebView.

Worth deciding before investing further in the current interface: every display fix made today would be thrown away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 20:00:04 +01:00
r-zakarya 8f9ebb8d47 feat(android): blue instead of cyan, everywhere at once
Cyan came from the desktop app. On the phone the reference is the system: its global search and its own apps are blue, and an app should look like the place it runs.

Accent #3B82F6 on the same slate, with white on the accent since blue is darker than cyan. The launcher icon and the launch screen follow - a cyan padlock in front of a blue interface would have read as a different app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:55:06 +01:00
r-zakarya ceaba331d8 feat(android): the search field takes the shape Android uses for search
I had followed the desktop capture - a squared field with slightly rounded corners - without thinking of the search bar the phone itself uses. On a phone that pill is the reference, and the app should look like where it runs.

Edits become pills; the memo keeps its sober corners. A magnifier is drawn inside the field, as a circle and a handle rather than a glyph, since no font is guaranteed across devices. Drawing inside a TEdit works - it is only the clickable that does not, which is why the clear cross stays beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:38:37 +01:00
r-zakarya aba873b9cc feat(android): follow the desktop's look, and a cross to clear the search
Section captions go uppercase, small and muted, the way the desktop app labels TWO-FACTOR (TOTP) or CUSTOM FIELDS - the eye jumps between captions without confusing them for values. Each field sits in its own bordered box: it is the border that separates values, not the spacing. Buttons take the same thin border instead of a solid grey block.

The 2FA code turns cyan and gains its countdown bar, so a glance says whether there is time to copy it. The seconds leave the text - the bar says it better.

And a cross clears the search, shown only when there is something to clear. A label rather than a button: inside a field, a cross is touched, not commanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:19:22 +01:00
r-zakarya e128b85658 docs(android): the two safety rules, as they actually stand now 2026-08-15 18:53:47 +01:00
r-zakarya 773e47330f fix(android): ask for the device credential alone when no biometric is enrolled
The re-seal never ran and left no trace, because a branch not taken logs nothing. It logs now, verdict and reason, whether or not anything happens - the absence of a line was itself the thing that cost time.

And the verdict may have been wrong: asking canAuthenticate for BIOMETRIC_STRONG together with DEVICE_CREDENTIAL can fail outright on a device with a pattern and no enrolled fingerprint, instead of keeping what exists. It now falls back to the credential alone, which is all the key needs, and AuthenticateThen presents whichever set answered - offering something else would draw the same refusal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:35:30 +01:00
r-zakarya d472dd4ef8 fix(android): encryption cannot see a dead key, so stop pretending it can
The retry I added yesterday never fired, and could not: the public key is
rebuilt through KeyFactory, so it is an ordinary RSA key with no link to the
Keystore. It encrypts perfectly well while its private half is permanently
invalidated. The re-seal therefore sealed the cache with a key nothing would
ever open again - the app asked for the sync password instead of the pattern.

Invalidation is observable at exactly one moment: reading with the private
key. LoadVault now drops the alias when the system names the cause, so the
next write mints a fresh pair instead of sealing another unopenable file.

And the caller that re-seals a plain cache asks for a fresh key outright:
protection reappearing on the device is precisely the event that invalidated
the old one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:21:50 +01:00
r-zakarya e4a50df64d fix(android): protection catches up with the cache, and a dead key gets replaced
After the screen lock was removed and put back, the app stopped asking for the
pattern. Two gaps, both mine.

The cache had been rewritten while the device had no lock, so under the plain
key - and nothing ever moves it back. Protection was decided at write time
only, and a write happens only when a new snapshot arrives; a 304 left the
file in the clear indefinitely. It is now re-sealed on open, where the
plaintext is already in hand.

And the invalidated RSA key was never replaced. Removing a lock, or enrolling
a new fingerprint, permanently invalidates a protected key while its alias
survives. EnsureWrapKey only created the pair when the alias was absent, so it
kept reusing a dead key: every encryption failed and every write fell back to
the plain key, forever. It now deletes and regenerates on a second attempt.

Measured on device: header 1 before, 3 after the first launch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:11:48 +01:00
r-zakarya fa424a3bc7 docs(android): section 1 is complete, six checks out of six
Removing the screen lock permanently invalidates every Keystore key that requires authentication, so the cache was unreadable for good and re-downloading was the only way out. The automatic open kept the cache and reported; only the explicit tap cleared it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 18:03:09 +01:00
r-zakarya bc89ec0010 i18n(android): the interface speaks English, like the desktop app
It was in French because the conversation is, and I never asked. The desktop
app the client mirrors says Add field, Attach file, Show previous versions -
someone moving between the two would notice the seam.

Every visible string moved: buttons, statuses, detail row captions, log lines
and the biometric prompt, plus the messages the modules surface through
LastStoreError and LastBiometricError.

Code comments stay in French. They address the author, not the user, and that
is already the language of CLAUDE.md and much of the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:59:31 +01:00
r-zakarya e8e305b6fd feat(android): recolour the two native controls, and hide the log by default
Dark.style has no transparent template for an edit or a memo, so a StyleLookup at them fell back silently to the normal one and its grey background - the search field and the log were the last two things clashing with a painted interface. Since the style is vendored anyway, make-style.ps1 rebuilds it from Delphi's with those two backgrounds set to the palette. Reproducible, so a palette change does not quietly lose the patch.

The log is hidden now: it only serves diagnosis and it was taking a third of the screen at every launch. A Journal button beside the server settings brings it back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:43:52 +01:00
r-zakarya e5eb91cd3f feat(android): paint the interface instead of wearing a style
None of the styles Delphi ships is meant for a phone - grey gradients, sharp
corners, thick frames, all from the desktop era. And the day showed that
anything rendered through a style template may not exist in the style that is
actually loaded: the list row template, the search prompt and the detail row
each vanished in turn.

So the buttons are rectangles we draw: slate at rest, cyan for the one action
that matters, rounded, no gradient, with a press feedback a TRectangle does
not have on its own - without it you cannot tell whether the tap landed. The
form's own fill carries the background, the list drops its frame, labels take
the palette.

A style stays loaded underneath, as a base only: edits, list and memo are
native controls we are not going to rewrite, and with no style at all they
would render light.

Palette lives in PMA.Theme, the same slate and cyan as the desktop app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:55:54 +01:00
r-zakarya 25dc524585 feat(android): the launch screen shows the padlock, not Delphi's flame
The launcher icon and the launch screen are different resources. On Android 12+ the theme points windowSplashScreenAnimatedIcon at splash_vector, which was Delphi's logo, over a #FFF background - a white flash before a dark app, every single launch.

Same padlock, drawn at 288 units with its content inside the central 192 the system guarantees. splash_background goes slate, in the light variant too since the app has only one theme. For Android 11 and earlier, which have no SplashScreen API and merely paint windowBackground, splash_image_def composes the background and the logo itself.

One source for the vector, copied onto the density and night variants by repack: four identical files in the repo would eventually diverge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:21:26 +01:00
r-zakarya fdeb870541 chore: track the launcher icons, the blanket *.png rule was swallowing them
The rule exists for screenshots dropped in the repo root during dev. Launcher icons are content: they regenerate from make-icons.ps1, but tracking them means not having to know that - nor depending on System.Drawing to build an APK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:12:08 +01:00
r-zakarya 202b93c246 feat(android): a padlock icon instead of Delphi's default
Cyan on slate, the accent the desktop app already uses. Adaptive vectors for
Android 8+ - background covering the full 108 units since the launcher applies
its own mask, foreground kept inside the central 66 that no mask can crop -
plus a monochrome layer for Android 13 themed icons, where the keyhole has to
be a hole in the path rather than a shape in another colour.

PNGs for older launchers come from make-icons.ps1, same drawing, and carry
their own rounded square since nothing masks them.

They are copied over Delphi's in repack-apk.ps1, for the same reason as
libsk4d.so: the Android_LauncherIcon and Android_AdaptiveIcon properties ought
to be enough, and nothing deploys them here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:11:31 +01:00
r-zakarya b5ccf63a85 feat(android): Skia renders the text, so Arabic reads properly
An Arabic attachment name displayed scrambled. Not a missed setting: FMX's original text engine does not shape complex scripts - no bidirectional reordering, no letter joining. Separating the size from the name limited the damage without removing it. Skia carries HarfBuzz and does the work.

GlobalUseSkia goes before Application.Initialize; the canvas is chosen at init and switching later does not reclaim what is already built. The price is libsk4d.so, 25 MB raw, and an APK from 13 to 22 MB.

Deploy rebuilds library\\lib just before calling aapt, so a hand-placed library disappears. A proper DeployFile entry was tried first - MSBuild ignores it, as it ignores every other one in this project. Hence repack-apk.ps1, which redoes aapt, zipalign and signing on the folder already prepared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:44:43 +01:00
r-zakarya 49efccdd59 feat(android): settings live on the device, dark theme, and the app is called Coffre
URL and token are no longer constants. They sit in settings.bin, sealed with the no-auth Keystore AES key - deliberately weaker than the vault: they must be readable to rebuild a vault that no longer exists, and a bearer token only opens ENCRYPTED data, worthless without the sync password that stays in the guarded vault. Retyping 64 hex characters after every cache purge was the real risk: the risk of writing them down elsewhere. They are saved only after a fetch that worked - proven settings, not typed ones.

The token and sync password moved to the gitignored local include, as first-run defaults only. Tracked source now carries no secret, and leaving them empty is a viable configuration: the app asks.

Theme: Delphi's Dark.style, copied into the repo and linked as a resource rather than deployed as a file - this project's deployment puts nothing beside the app, which is the same hole that forces the manifest and dex staging. A failure to load is never fatal; the default look is ugly but works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:33:56 +01:00
r-zakarya 861d721513 docs(android): plan caught up with what the device confirmed 2026-08-15 15:23:04 +01:00
r-zakarya 7229028060 feat(android): show attachments, without carrying their bytes
One attachment took the cache from 13 to 174 KB: the content travels base64 in the snapshot and is decrypted in memory with the rest. TVault reads name, type and size and DROPS content_b64 - holding it for the whole vault while nothing displays it would be paying for nothing.

The detail now says a document exists. Saving it is what remains, and what blocks that is not code but where a file lands on Android 10+. When that is settled, the content re-reads from the raw JSON.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:05:08 +01:00
r-zakarya 7939d268f3 feat(android): mark the clipboard sensitive, and lock after 3 min idle
Since Android 13 the system shows a preview of whatever was just copied, at the bottom of the screen. For a password that is precisely what we are trying to avoid: it appears in clear, outside the app, for several seconds. The flag that suppresses it is a ClipDescription extra, which IFMXClipboardService does not expose - hence going straight to ClipboardManager. Note Jcontent_ClipboardManager, not JClipboardManager: the latter is the deprecated android.text one, which knows only bare text and has no ClipData to carry an extra.

Backgrounding already locks; the idle timer covers the phone left face-up on a table with the app open. It samples timestamps every 15 s rather than counting seconds - a wake-up per second would cost battery for no precision that matters.

Attachments dropped from the plan: this vault has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:58:27 +01:00
r-zakarya 0e39cf7e56 docs(android): the expired auth window no longer costs the cache 2026-08-15 14:37:56 +01:00
r-zakarya f3a8631440 docs(android): the cache chain is verified on device
Write with no prompt, read under the device credential, cache surviving a cold start with no derivation, and a silent 304. Records the three traps that hid behind it, none of which the compiler could see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:28:58 +01:00
r-zakarya c4d2bdde73 fix(android): OAEP masks with SHA-1, which is the only MGF1 the key allows
The read failed with IllegalBlockSizeException and, in logcat, KeymasterUtils status -28. setDigests() authorises the MESSAGE digest only; the MGF1 digest is a separate authorisation whose default is SHA-1 and stays SHA-1 unless setMgf1Digests is called (API 31+). Asking for MGF1-SHA-256 at decrypt time therefore hands the hardware a key that refuses.

Encryption did not complain, because the rebuilt public key is served by the software provider, which accepts any MGF1. So we produced perfectly valid ciphertext that the hardware would not read - the worst shape of bug, since it only surfaces on the next launch. Both sides now pass SHA-256 for the message and SHA-1 for the mask; SHA-1 there weakens nothing, a mask generation function needs no collision resistance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:17:26 +01:00
r-zakarya 8dc0840e03 fix(android): declare FMX's biometric activity, and stop LoadVault from deleting the cache
The prompt raised ActivityNotFoundException: FMX does not show BiometricPrompt from our NativeActivity, it launches its own BiometricFragmentActivity, which lives in fmx.dex.jar but has to be declared in the manifest. Given a translucent framework theme - it hosts the system dialog and draws nothing itself, and the generated resources contain no AppCompat.

Then the read failed and the cache vanished with no trace. LoadVault sorted 'key lost' from 'not authenticated' on the exception message and deleted the file when it guessed the former - so a wrong guess destroyed the cache and the only record of why. It now reports through LastStoreError and deletes nothing; the caller has the log and the context. OpenLocalNow shows the reason, after ShowUnlock rather than before, which was overwriting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:11:20 +01:00
r-zakarya 1dc4f3bdb6 feat(android): wrap the cache key with an asymmetric Keystore pair
Measured on device: android.security.keystore.UserNotAuthenticatedException on every write. An AES key created with setUserAuthenticationRequired demands a recent authentication to ENCRYPT as well as to decrypt, and the write follows a silent sync with no prompt. The very first write only succeeded because the phone had just been unlocked - a test that passes, then stops passing.

Now an RSA pair: the public half wraps a throwaway AES key with no prompt, the private half - the guarded one - unwraps it. Authentication is required at read time and nowhere else. Two documented traps handled: the public key returned by the Keystore carries the private key's constraints, so it is rebuilt from its encoded form; and that rebuild changes provider, so the OAEP parameters are passed explicitly on both sides - the Keystore defaults MGF1 to SHA-1 where the transformation name suggests SHA-256.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:00:35 +01:00
r-zakarya 09048410e1 fix(android): report the biometric failure instead of destroying the cache
The launch-time auto-open received boUnavailable and wiped the cache, seconds after the write path had proved the device could authenticate. The diagnosis was wrong and the reaction irreversible. An automatic action now never clears the vault: it logs the cause and leaves the user on the unlock screen. Only an explicit tap on Open, followed by a clean refusal from the system, may clear it. PMA.Biometric keeps the reason in LastBiometricError, and a Java-layer exception now maps to boFailed rather than boUnavailable - an exception says nothing about the device. The log memo also wraps: it was cutting off exactly the end of the message, which is where the cause is named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 13:42:07 +01:00
r-zakarya 5187da3070 docs(android): the adb reverse tunnel dies with the USB link 2026-08-15 13:00:28 +01:00
r-zakarya 57a295ad36 fix(android): set the biometric strengths before asking what the device can do
A pattern lock reported "no authentication here", so the cache was written
under the plain key.

FMX builds its authenticator mask from FBiometricStrengths, and that field
starts as the EMPTY SET - the constructor never initialises it. On API 30+
CanAuthenticate therefore calls canAuthenticate(0), which never answers
"available", whatever the device can actually do. Asking before setting the
strengths cannot succeed.

Both entry points now go through AcquireService, which sets Strong +
DeviceCredential and only then hands back the service. DeviceCredential is
the point: a phone with a pattern or a PIN and no enrolled fingerprint is
the common case, not an edge one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:48:43 +01:00
r-zakarya 4258e9b559 fix(android): USE_BIOMETRIC, and never lock the cache behind a door we cannot open
Opening the vault threw java.lang.SecurityException: USE_BIOMETRIC is not in
Delphi default permission list, and CanAuthenticate raises instead of
answering false. So the fault showed up neither at compile time nor at
startup, but on the first unlock - after the cache had already been written
under an auth-protected key.

The permission is the shallow half. The deep half is that SaveVault was
called with a hard-coded True: we asked for a key guarded by a prompt that
could not be shown, producing a file that was unreadable forever. It now
receives BiometricAvailable, evaluated on the main thread before the worker
starts.

BiometricAvailable and AuthenticateThen also swallow what comes up from the
Java layer and map it to boUnavailable. The caller has a path for every
TBioOutcome and none for an exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:09:29 +01:00
r-zakarya 6d31bf83d2 docs(android): the res tree comes from any IDE build, not the 32-bit one 2026-08-15 11:54:29 +01:00
r-zakarya 4805c9ac31 docs(android): building and installing the APK without the IDE 2026-08-15 11:52:44 +01:00
r-zakarya 9a4170c44f feat(android): folder filter, icons, sort, template, updated_at, FLAG_SECURE
Works through the plan's §2 and §3 items that need no hardware and no
platform decision.

- allowBackup="false". The private directory holds vault.bin; a cloud
  backup has no business in a password manager's path, even though what
  would leave is encrypted by a key that stays in the chip.
- FLAG_SECURE, Release builds only - in debug it also blocks development
  screenshots. Complementary to hiding on WillBecomeInactive rather than
  redundant: that one covers the recents preview, the flag also covers
  manual captures, screen recording and casting to an external display.
- List sorted by name (snapshot order is DB order, arbitrary to the eye),
  entry icons decoded once per vault rather than on every keystroke, and a
  folder dropdown whose selection survives a background refresh. No tag
  filter: text search already matches tags.
- template and updated_at read and shown in the detail. updated_at is also
  step 1 of the write path - nothing is arbitrable without it.

Android code is now type-checked without deploying: dccaarm64 reaches the
link step and fails only on the NDK's ldandroid.exe. A JNI declaration that
does not compile no longer waits for an APK to say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:23:29 +01:00
r-zakarya 3bb6f6f903 feat(android): show first, sync after
The local vault now opens by itself at launch and the GET runs behind it,
silently. A 304 shows nothing; a network failure shows nothing either -
offline is a normal state, and an alert on every launch only teaches people
to dismiss alerts. Only a new snapshot manifests, by refreshing the list.

That silence is what puts the sync password inside the Keystore-encrypted
file next to the vault: without it a new snapshot cannot be opened, so every
edit made on the desktop would raise a password prompt at launch. The file
already holds every password in plaintext; this one adds almost nothing to
its exposure. A typed password still wins over the stored one, which is how
you fix one that no longer works.

Adopting a snapshot while the app is already open rebuilds the list in place
instead of navigating - mandatory even from the detail screen, since list
rows carry indices into the old vault and a tap would open the wrong entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:14:44 +01:00
r-zakarya ba0d3c1d3c docs(android): plan of the remaining work 2026-08-15 10:56:49 +01:00
r-zakarya 20ba2438df feat(android): TOTP codes and custom fields in the detail screen
The detail screen showed site, username and password and silently dropped
everything else - custom fields, notes, 2FA. It is now built per entry
rather than from a fixed template: every row is a caption, a value, and
whether it is secret, so reveal and copy exist once instead of once per
kind of field.

Two rows stay special on purpose. A note's body is stored in the password
column, so it gets its own caption and no mask. The 2FA code is the only
live row - refreshed every second, placed first, since it is what you pull
a phone out for.

PMA.Totp takes bare base32 or an otpauth:// URI; both travel in snapshots
depending on how the entry was created. Checked against all six RFC 6238
vectors, including T=20000000000, which overflows 32 bits.

The rows hold plaintext, so clearing them is part of locking, not of
navigating away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 10:35:09 +01:00
r-zakarya 0ddfe5c5fd feat(crypto): wire AES-GCM and PBKDF2 to Windows CNG
The viewer compiled for Win64 but died on first use: AesGcmDecrypt existed
only as JNI, so the desktop build downloaded 27 KB and then refused it.
Windows has the same primitive in bcrypt.dll - no DLL to ship, no OpenSSL,
and BCryptDeriveKeyPBKDF2 takes bytes, so none of the char[] encoding doubt
that keeps the Pascal loop around on Android.

This exists so the client can be exercised on the desktop; deploying an APK
to watch a list redraw is a slow way to build a UI.

selftest.dpr replaces the crypto-vector button the viewer dropped: same
known-answer vector, console, exit code = failures. Measured here - Pascal
1670 ms, CNG 528 ms, identical keys on the non-ASCII password.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 05:59:36 +01:00
r-zakarya bf7aac478b feat(android): read-only viewer - unlock, search, detail
Replaces the crypto test bench with the three screens the client actually
needs. PMA.Vault holds the decrypted snapshot and answers the search;
mainApk builds unlock/list/detail in code rather than three .fmx files.

Two safety rules live in the UI because nowhere else can hold them: the
plaintext is dropped when the app backgrounds (screen hidden on
WillBecomeInactive, which precedes the recents-preview capture), and the
clipboard is cleared after 45 s only if it still holds what we put there.

The search deliberately ignores the password field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:19:47 +01:00
r-zakarya 4effc3611d feat(android): prefill the sync password from a local, untracked include
Asked for, but it opens the whole vault once the container is downloaded, so it does not belong in a versioned file - even in a repo with no remote. It lives in PMA.LocalConfig.inc, gitignored, with a template checked in so a fresh clone knows what to create.
2026-08-15 01:04:08 +01:00
r-zakarya 3cbfd54c26 fix(android): KeyStore.load takes a LoadStoreParameter, not an Object
The one-argument overload is load(LoadStoreParameter). Declaring JObject made the bridge look for a method that does not exist - reported at runtime as 'method not found', with nothing at compile time. Same class of mistake as generateSecret earlier: the JNI signature is built from the declared types, so an approximate type is a silent break.
2026-08-15 00:56:22 +01:00
r-zakarya 60b806536a fix(android): the prompt field is CancelButtonText, not NegativeButtonText
NegativeButtonText is the Android API name; FMX's record exposes CancelButtonText.
2026-08-15 00:51:24 +01:00
r-zakarya 1e7477cdd6 fix(android): overload both Cipher.init declarations, restore CRLF line endings
Delphi refuses a second declaration when the first is not marked overload, so the three-argument init was reported as too many parameters. Also adds Androidapi.Jni to silence the non-inlined TJavaArray.Length hints.

My ASCII normalisation had rewritten the sources in LF, which RAD Studio flags on every unit; a .gitattributes rule now pins CRLF for Pascal sources so git does not undo it.
2026-08-15 00:49:37 +01:00
r-zakarya 9649dac7b2 feat(android): protect the local vault with biometric or device credential
The Keystore key now requires authentication, valid for 30 s afterwards - short enough that an unattended phone is not exploitable, long enough to decrypt and display. Time-bound rather than a per-use CryptoObject: BiometricPrompt's callback would need a Java class compiled into a jar, while FMX already bridges it.

Two aliases, because the auth constraint is fixed when the key is created and cannot be added later. The file header records which key wrote it, so a read knows whether to authenticate first.

A device with neither fingerprint nor lock screen cannot hold such a key, so SaveVault falls back to the plain key rather than giving up the cache. And a failed authentication no longer wipes the cache: the vault is intact, it is the user who cancelled - erasing it would cost a download and 4 s of derivation for a dismissed prompt.
2026-08-15 00:42:50 +01:00
r-zakarya 5fdb7f914e feat(android): keep the decrypted vault locally under an Android Keystore key
Opening a container costs ~4 s and the iteration count belongs to the desktop format, so the fix is to stop repeating it: derive once per snapshot, then read a local AES file which the hardware accelerates. The key is generated inside the Keystore and never leaves it - the file alone is useless, even rooted.

ETag and vault are encrypted together on purpose: an ETag outliving an unreadable vault would make the server answer 304 with nothing to show. A failed read wipes the cache rather than guessing.

Also normalises every Delphi source to pure ASCII: Delphi reads a BOM-less file as ANSI, so accented comments came back as mojibake, and each PowerShell rewrite added another layer.
2026-08-15 00:37:59 +01:00