3 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
737a3e5d5d |
security(sms): encrypt scheduled payloads, report real send status (E3, E7)
Some checks failed
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Closes the two Phase E items I had left open and documented as open.
E3 -- scheduled message bodies were plaintext on disk.
Pending schedules must outlive the process so SmsAlarmReceiver can
send them when the alarm fires and so they survive a reboot, so
recipient and body go to SharedPreferences. MODE_PRIVATE is the right
primitive -- the file is UID-scoped -- but the contents were in the
clear, readable by anything running as the same UID and swept into
cloud backup by default. Same asset class as the inbox
(THREAT_MODEL.md T-I4).
Adds SmsScheduleCrypto: AES-256-GCM, fresh IV per value, key generated
inside the platform AndroidKeyStore and non-exportable. An attacker
with the prefs file but not the keystore gets ciphertext.
Deliberately NOT androidx.security.EncryptedSharedPreferences: that is
a Gradle dependency, and this crate compiles its Java with bare javac
against android.jar (see build.rs), so using it would mean a Gradle
build or a vendored jar. AndroidKeyStore and javax.crypto are both in
android.jar and give the property that matters.
The key is deliberately NOT user-authentication-bound: an alarm fires
while the device may be locked and the receiver must decrypt with no
user present. This protects against another app and against an
extracted backup, which is the threat in scope -- not against someone
holding an unlocked handset.
Fails CLOSED. If the keystore is unavailable, encrypt returns null and
schedule_sms errors rather than writing plaintext. A row that cannot
be decrypted -- wrong key after a reinstall, tampering, or written by
an older build -- is treated exactly like a missing row and skipped;
sending a garbled body would be worse than not sending.
E7 -- "sent" was a guess.
Both the sentIntent and deliveryIntent arguments were null, so nothing
could report back. send_sms returning Ok meant "the JNI call
returned", not that the radio accepted the message and certainly not
that it arrived -- and the UI rendered that as a tick. A send rejected
for no service, no SIM or a throttled radio was indistinguishable from
a delivered one.
Adds send_sms_tracked, which attaches real PendingIntents and returns
a correlating token, plus SmsSentReceiver to collect the platform
result and SendOutcome/SendReport to express it: Sent (radio accepted)
is now a different value from Delivered (handset acknowledged), and
failures carry the RESULT_ERROR_* code.
Three details worth recording:
- multipart takes ArrayList<PendingIntent>, one entry per part, so
the intent is repeated part_count times. Passing null here, as
before, meant no status for exactly the messages most likely to
fail: the long ones.
- the request code is derived from (token, kind), or the two intents
collide and the delivery report overwrites the send report.
- the broadcast is package-scoped and the receiver registered
NOT_EXPORTED, so another app cannot forge a delivery report.
If the receiver class is unavailable the send still goes out with null
intents: losing the status report is much better than losing the
message.
Also fixes A5 on the scheduled path. SmsAlarmReceiver still called
sendTextMessage directly, so a scheduled message over 160 GSM-7
characters -- or 70 with any emoji -- was silently truncated by the
carrier. It now divides and sends multipart, as the Rust send path has
since Phase A.
CI: two gates, both negative-tested by reverting the fix and confirming
they fail. One asserts schedule_sms never writes request.recipient or
request.body directly; the other asserts the send path still passes
sent/delivery intents in both the single-part and multipart calls.
THREAT_MODEL.md T-I4 and the delivery-confirmation row move from open to
fixed, with the residual risk stated: callers may still use the
untracked send_sms, which remains honest about meaning only "handed to
the platform".
Tests: robius-sms 25 -> 28.
Verified: 13/13 CI checks, clippy -D warnings clean on host and
aarch64-linux-android, both new Java classes javac-compile and dex.
NOT verified on a device. The keystore round-trip, the broadcast
delivery and the token correlation all need an emulator or handset;
there is still no CI runner on this repo.
|
||
|
|
0ed64c0435 |
fix(sms): send long messages whole, and confirm before bulk (A5, A7, A8)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
A5 -- long messages were silently truncated.
send_text_message() called SmsManager.sendTextMessage
unconditionally. That API is only defined for a body that fits one
PDU: 160 GSM-7 characters, or 70 once anything forces UCS-2. Past
that the behaviour is carrier- and OEM-dependent -- silent
truncation, silent failure, or an exception -- and the UI reported
"Sent" regardless, because a JNI call returning cleanly only means
the call was made.
Now uses divideMessage() + sendMultipartTextMessage() when the body
needs more than one part, so the recipient's handset reassembles it.
Adds segment_count(), which computes encoding and segment count on
any host so it is unit-testable and usable by UI. Correct GSM-7
modelling matters here and is not intuitive:
- the extended characters ^ { } \ [ ] ~ | € cost TWO septets each
- concatenation costs 7 septets per part, so the limit drops from
160 to 153 (and 70 to 67 for UCS-2)
- non-BMP characters such as emoji are surrogate pairs and cost two
UTF-16 units
- e/a/o/n/u with diacritics ARE in GSM-7. My first version of the
boundary test assumed é forced UCS-2 and failed; Swahili, French
and German text still bills at the 160-char rate. Arabic and
emoji do not.
A7 -- bulk send had no confirmation.
One tap on "Send SMS to Selected", one tap on Send, and N real,
billed, irreversible messages went out. For a feature whose purpose
is mass-messaging a scraped business directory, that is a
financial-harm defect rather than a UX gap.
Send now arms a confirmation quoting SEGMENTS, not messages, because
segments are what the user pays for and the number is surprising: one
emoji in the body forces UCS-2 and can turn "200 messages" into 800
paid segments. Editing the body or the recipient list re-arms the
prompt, so a confirmation cannot be inherited by different content.
A8 -- SCHEDULE_EXACT_ALARM was neither requested nor documented.
Android 12+ refuses exact alarms without it, and it is a
special-access permission: a runtime prompt cannot grant it, the user
must enable "Alarms & reminders" in Settings. Without it scheduled
sends are subject to Doze batching. Documented in the crate docs and
README, along with two things that were not written down anywhere:
Ok from send_sms means "handed to the platform", NOT delivered (both
PendingIntents are null, so nothing can report back); and callers are
billed per segment.
Verified: robius-sms 15 tests pass (8 new for segmentation), nigig-sms
15 pass, clippy -D warnings clean on host and aarch64-linux-android,
nigig-sms clippy ratchet holds at 50, gates and supply-chain green.
|
||
| e9616c3288 | added missing crates |