ya-webadb shipped v3.0.0-beta.3 on Sep 17
(yume-chan/ya-webadb@96182a6b).
The headline changes are two fixes that bit real users on real devices
between beta.2 and beta.3: adb push no longer races with its own
final response, and the PKCS#8 RSA private-key parser stopped assuming
2048-bit / 65537 keys and now walks the DER structure properly. The
former matters for anyone uploading files (which on webadb.online is
the File Manager’s upload button and the Screencast pipeline’s
helper-APK install). The latter matters for anyone whose stored
credential is the slightly uncommon DER encoding where the private
exponent d has a leading zero byte.
We are still pinning [email protected] on webadb.online — we
held off the bump after beta.2 because the screenrecord-fallback
work was still landing and we didn’t want to chase two moving targets
at once. After this release stabilizes for a couple of weeks we will
schedule the bump. None of the beta.3 fixes are blockers for us, but
the push-completion one is on the short list for the next webadb
release — we have an open ticket to revisit the File Manager upload
path after a user reported silent truncation on a Xiaomi 14 Pro.
What actually changed in beta.3
The diff betweenv3.0.0-beta.2
andv3.0.0-beta.3
is six commits. Sorted by impact:
| Commit | Title | Why it matters |
|---|---|---|
69fffaf0 |
fix(adb): wait for sync send v1 completion (#869) |
The biggest behavioral change. Replaces a pipeTo(...).catch(NOOP) that returned a writable whose close() ignored the pipe’s outcome. |
45198b86 |
chore: update dependencies |
Mechanical lockfile refresh, ~720 lines of pnpm-lock.yaml churn, no API changes. |
f92c641d |
fix(adb): correctly parse private keys whose DER encoded private exponent is not 256 bytes long (#865) |
Replaces hardcoded offsets (38, 303) with a real DER walker. |
340d3fe0 |
fix(adb): prevent unhandled disconnect rejection (#863) |
banner.ts now attaches the disconnect promise rejection handler so adb’s TCP transport doesn’t trigger Node’s unhandledRejection. |
5fd3bcae |
fix(fetch-scrcpy-server): remove vulnerable dependencies |
Strips two transitive deps that were pulling in known-vulnerable versions. |
96182a6b |
chore: release v3.0.0-beta.3 |
The release itself. |
#869 — why adb push was racing its own ACK
The old sendV1 in libraries/adb/src/service/sync/request/push.ts
returned a writable that was, structurally, this:
1 | const pipe = distributeStream.readable.pipeTo(sendStream).catch(NOOP); |
The pipe here is distributeStream.readable.pipeTo(sendStream) —
the actual wire transport. When you call writer.close(), the upstream
side of the pipe finishes, which means sendStream finishes writing
its last packet and transitions to reading the server’s OKAY
response. The new code wraps that into a Promise.allSettled againstwriter.close() via a new stream primitive calledDelayedCloseWritableStream, defined inlibraries/stream-extra/src/delayed-close-writable.ts:
1 | export class DelayedCloseWritableStream<T> extends WritableStream<T> { |
The semantic shift is in the name: the writable’s close() waits for
the downstream side of the pipe to actually complete. In the old
code, await pipe would resolve as soon as pipeTo settled — and on
a slow device, that meant resolving before sendStream had read
the server’s OKAY frame. The caller of push() saw a resolved
promise, started its next operation, and on devices where the
transport can’t keep two sync sessions open cleanly, the last packet
of the upload got dropped on the floor. Silent truncation is the
worst kind of push bug because the file appears to have succeeded —
you only notice when the SHA-256 of what you pushed doesn’t match the
SHA-256 of what you pulled back.
The same race existed in sendV2 and the compressed variant. All three
paths now use DelayedCloseWritableStream. The SuppressedError /AggregateError chains are there because the pipe can fail in three
ways simultaneously (writer close rejects, pipe rejects with a
different reason, abort rejects), and the older .catch(NOOP) lost
the distinction.
For webadb’s File Manager upload — we callsession.adb.sync.write(...) against a target path on the device —
this fix is the difference between “upload says it succeeded” and
“upload actually succeeded”. We didn’t lose data in production
because we always hash-verify against the device’s own readback, but
the verification step was masking the race rather than fixing it.
#865 — the hardcoded-offset RSA parser
This was the most pleasant of the beta.3 commits to read because the
old code was so confidently wrong:
1 | // These values are correct only if |
The comment is honest about its own assumptions, which is more than
most hand-rolled DER parsers do. The bug: PKCS#8 DER encodes INTEGERs
with a leading 0x00 byte if the high bit is set (so the value
stays positive when read as a signed two’s-complement bigint). For
the private exponent d of a 2048-bit RSA key, that leading-zero
condition depends on the exact key generated — about half of all
2048-bit keys have the leading zero on d. When it does, the actual
DER-encoded length is RsaPrivateKeyDLength + 1 = 257 bytes, not 256,
and the hardcoded offset 303 points one byte too early into the
length field of the next element. The old code then either threw
(“RSA private key is too short”) or, worse, parsed a wrong d and
got a signature verification failure at runtime.
The new code inlibraries/adb/src/daemon/crypto.ts
is a real (small) DER walker:
1 | function derHeader( |
The walker handles the multi-byte length form (length & 0x80 →
read N more length bytes), the leading-zero INTEGER encoding, and
the nested OCTET STRING → RSAPrivateKey structure. The getBigUint
helper also picked up a fix for the case where length % 8 != 0 —
the old version assumed every input was an exact multiple of 64 bits
and silently shifted in garbage from the next field. That was the
“first test case” bug from thecrypto.spec.ts
additions.
For webadb this is directly relevant: our credential storage usesTangoLocalStorage to persist the AdbWebCryptoCredentialManager‘s
PKCS#8 RSA private key (base64-encoded) and re-parse it on every
session connect via session.credentials.parse(privateKey). We’ve
seen roughly one user per ~200 connects report “credentials parse
failed” on first connect after a browser profile migration, which
fits the “leading zero on d“ symptom. We never reproduced it
locally because window.crypto.subtle.generateKey() forRSASSA-PKCS1-v1_5 with modulusLength: 2048 and publicExponent: new Uint8Array([1, 0, 1]) is deterministic across our Chromium
versions and happens to produce keys without the leading zero ond. The fix doesn’t change the wire format or our storage format —
it just means parsing is now correct on the keys we didn’t
generate ourselves (older Android adb-key files, third-party
credential managers, etc.).
#863 — disconnect rejection, and why it was an unhandledRejection
adb-daemon-tcp‘s banner reader was returning the disconnect promise
without attaching a rejection handler, which meant that when the
server closed the socket during the banner handshake,
Node’s unhandledRejection event fired (and on newer Node, the
process crashed with the default --unhandled-rejections=throw
policy). The fix in libraries/adb/src/banner.ts is small — attach a
handler — but it also caught a deeper bug inlibraries/adb/src/server/client.ts where the disconnect promise was
created before any handler could be attached. The new code creates
the disconnect promise lazily inside a getter so the first listener
is guaranteed to attach.
Web-side this doesn’t bite us — browser environments don’t haveunhandledRejection semantics that crash the tab, and we already
have a try/catch around the entire connection setup. But it’s a
real fix for anyone using ya-webadb in a CLI or a long-running
Node-based adb tunnel.
Why we’re holding the bump at beta.1
Three reasons:
- The screenrecord / file-poll pipeline (post
f92c641d
in our tree, not ya-webadb’s) is the most fragile subsystem in
webadb. Any ya-webadb bump touches theshellProtocol.spawn()return shape, and we have severallib/screencast/*.tsfiles that pin the v3.0.0-beta.1 return
type. Bumping tobeta.2orbeta.3requires re-reading those
files line-by-line against the new types — we’ve done it forbeta.2locally and it’s safe, but we want one more beta cycle
of “no breaking API changes” before shipping. beta.2introducedAdbSyncErrorwith a brand check — we
don’t rely on the brand message yet, but we will when we add
properadb pullerror reporting to the File Manager. Better to
bump once and pick up both the error branding and theDelayedCloseWritableStreampush fix together.beta.3‘s dependency refresh is sizable (~720 lines of lock
churn). We prefer to let it sit for a few days upstream so any
“oops, that dep update broke a transitive” issues surface before
we pin it.
We will bump to beta.3 (or beta.4 if one ships) as part of the
next webadb.online release. The File Manager upload path is the
direct beneficiary of #869 — we already hash-verify uploads, but
the new primitive means the close-then-verify sequence is
race-free rather than race-mitigated.
What this means for webadb.online
Three concrete items, none user-visible on their own:
- File Manager upload reliability improves when we adopt
DelayedCloseWritableStream. The race insendV1was rare in
our telemetry (we’d estimate <0.5% of uploads on slow devices
based on the symptom report) but real. The fix moves it from
“rare bug we work around” to “race the library guarantees
won’t happen.” - Stored credential parsing becomes more robust for users
migrating between browser profiles or restoring adb keys from
older Android installs. We may also be able to drop one
try/catch inlib/use-adb.tsthat currently catches the
“RSA private key is too short” error and falls back to
re-prompting the user. fetch-scrcpy-serverdep cleanup is a transitive win. The
removed packages were not in our direct dependency tree (we
don’t usefetch-scrcpy-serverourselves — we ship our own
scrcpy fallback in the Screencast panel) but any future
switch to that library is now less likely to drag in a
known-vulnerable transitive.
We will follow up with a separate post when we actually bump the
dependency and exercise the new code paths on real devices. For
now, [email protected] is the recommended version for new
projects and we’ll be on it within the next release cycle.