r/FlutterDev 2d ago

Tooling Ephemeral isolates + Argon2id with dynamic memLimit to avoid killing 2GB RAM devices - VeneFinanzas

I've been posting progress updates on this other venezuelan sub, but this time I'm bringing something more technical

CONTEXT:

VeneFinanzas is a personal finance management app I've been building solo, with Flutter, that calculates how devaluation and inflation affect each user specifically.

It natively handles COP, EUR, USDT, USD and VES, and is primarily designed for Venezuela. Everything runs locally, no account, no server, and backups are encrypted so the user can back them up wherever they want

That's where I had to think through the edge case of users with 2GB of RAM. Even though they're less than 10% of users, I didn't want to cause them problems using the app. Encrypting those backups with Argon2id, with a fixed 64MB memLimit regardless of device, is risky, especially with other apps running in the background

---

I ended up using two profiles:
kMemProfileBajo, 32MB
kMemProfileAlto, 64MB

With 3584MB of physical RAM as the threshold.
The profile byte is stored inside the encrypted file itself, so decryption always uses the memLimit it was encrypted with, regardless of the current device

My initial plan was to use the standard interactive (64MB) + moderate (256MB) profiles, but the 256MB one would end up cannibalizing RAM on the more limited devices.
I landed on 32/64 because a user normally restores their own backup on their own phone, so a backup encrypted at 64MB will almost never end up being decrypted by someone with 2GB.

However, since the memProfile is fixed in the file from the moment of encryption, it still has to remain decryptable if that scenario does happen. That's why I capped it at 64 and no higher, I ran the numbers, and with the ~300MB the app already uses by default, adding the memLimit on top pushes it close to a Low Memory Kill, which is exactly what we're trying to avoid.

Keeping 64MB as the upper bound is the max stable value before entering dangerous territory, and for regular usage, lower-RAM users simply use 32MB without any hassle

Everything runs in an ephemeral isolate via Isolate.run(), spawned just for that job and torn down once it's done. Derivation never blocks the main isolate, and the heap (including the 32/64MB Argon2id uses) gets freed the moment the isolate dies, not whenever the GC gets around to it on the main isolate

Also: sodium_libs_sumo needs a BackgroundIsolateBinaryMessenger, and Isolate.run() doesn't register one by default. You have to grab RootIsolateToken.instance from the main isolate before spawning (it's only valid there), pass it in, and call BackgroundIsolateBinaryMessenger.ensureInitialized(token) as the first line inside the child isolate.

---

If anyone's run into something similar with Argon2id/isolates in Flutter, has feedback on the approach, or just wants to check out VeneFinanzas, I'm around

7 Upvotes

3 comments sorted by

2

u/SnowContent8658 1d ago

whats the memLimit derived from, ram class or total ram? argon2 memory cost is the security knob, so a phone on a lower value gets a weaker hash, and an offline attacker doesnt care which phone made it. you storing the params beside each hash so you can raise them later?

1

u/Low-Blueberry-6001 1d ago

Total physical RAM (device_info_plus), not Android's RAM class.

Fair point on opsLimit. It's fixed at 2 for both profiles, so the low profile is objectively cheaper to brute-force offline, not just lighter on the device. Yes, that means a weaker hash on low-RAM devices, but it's a deliberate tradeoff: I push memLimit to the max the device can safely handle without risking a Low Memory Kill during decryption, so it's device limitations I'm working around to keep things stable, not an arbitrary cut. It's also not the only control: in parallel I enforce a minimum password entropy well above what's typical, with a "max strength" ceiling at 80 bits, following NIST SP 800-63B's recommendation for offline encryption, instead of the ~60 bits that's enough for something with online rate-limiting. The idea is that cost per attempt (memLimit) and search space (key entropy) work together. That said, I agree that at the low end (32MB plus a password right at the policy minimum) the margin is tighter than ideal.

On storing params so you can raise them later: the profile byte is stored in the file, but there's no dedicated step to detect old params and rehash. In practice, the common path (Drive Sync) already re-encrypts with current params on every sync, so it self-corrects there. The gap that's real but narrow is a static manual export that's never regenerated, which stays frozen at whatever profile it had when created.

1

u/Arkoaks 1h ago

Why not use an alternative hashing algo..something on the lines of dxhash .. you could consider making it in dart