APK : le clair est permis par Android et trie dans TSyncClient.RefusalReason
(IsLocalHost : RFC 1918, 127.x, .local, .lan, nom sans point) - chacun pointe
vers son propre WebDAV sans IP connue a la compilation ; vers Internet, HTTPS.
PC : Authorization Basic envoye d'emblee, plus de 401 anonyme ni de PUT du
coffre envoye deux fois.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jeton "utilisateur:mot-de-passe" sur une URL ni Gitea ni GitHub = WebDAV,
authentification Basic. Fichier lu et ecrit tel quel ; l'ETag fait le sha
(If-None-Match, If-Match, 412 -> conflit). Clair autorise vers l'IP fixe du
poste seulement. Eprouve contre le WsgiDAV local : 304, PUT 204, puis
synchro du poste. Docs : CLAUDE.md, android-client/README.md (page eveillee
comprise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md : interrupteur Copy / Fill de la recherche rapide et pourquoi le
tray copie seulement ; QR lu mais pas de 2FA ; nom par defaut data.json,
messages de commit neutres, 8 s / 3 essais sur le reseau.
README Android : edition de l'entree entiere en un commit, oeil, icone depuis
la galerie, scan 2FA ; verification a l'ouverture, depot vide = creation ; les
pieges 'view' qui defocalisait les champs et 'toast' qui etait un <div>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
GET in 80 ms, decryption in 3893 ms, entries and trash counted, second call answers 304. try-open.js opens a container with Node so a failure can be attributed: a GCM tag that does not verify means wrong password OR altered file, and the app cannot tell which.
Measured on device: Android derives the same key as WebCrypto on a non-ASCII password, so PBEKeySpec does encode as UTF-8 - the doubt that justified the Pascal implementation was unfounded. It is barely faster though, 3869 ms against 4139 ms: PBKDF2 gets no hardware acceleration.
So 4 seconds per container is incompressible, and the iteration count belongs to the desktop format. The answer is not to speed up the derivation but to stop repeating it: derive once per snapshot, then keep the vault locally under an Android Keystore key. Written up in android-client/README.md.